Clojure/REST (pt. 2)

Malcolm Sparks

@malcolmsparks

Let's review

The good points

  • Simple & easy
  • Idiomatic
  • Good as an example
  • Exercises many parts of REST
  • Stateful, resource-based

No content negotiation

  • Only produces HTML (in UTF-8)
  • Only speaks English
  • No support for compression (e.g. gzip)
  • No machine-readable API

Inefficient

  • No cache-control headers
  • Doesn't support If-Modified-Since (dates)
  • Doesn't support If-Not-Match (etags)
    • Responsive UIs need fast server responses
    • om.next
    • What about your upstream systems?
                       /------- Other system 1
om.next               /
Browser <--------- Server -------- Other system 2
                      \
                       \------ Other system 3

Vulnerable to attack

  • No security headers
  • Ad-hoc parameter validation

No service metadata

  • No data about what the service does (e.g. Swagger, RAML)
  • No HEAD, OPTIONS or TRACE methods

Blocking I/O

  • Unlike Play, Ratpack, Vert.x, Dropwizard, etc.

Ad-hoc

  • 'Hand-crafted'
  • Hypermedia links are hand-coded
  • Services can't be (easily) stamped-out
  • Consequences?
    • Lots of similar yet inconsistent (and buggy) code
    • Business logic coupled to infrastructure
    • 'YAGNI' mentality to REST

What are our options?

  • Functional model
    • Function composition (Ring middleware)
  • Execution model
    • Liberator
  • Resource model
    • Define resource using a Clojure map

yada

yada

API

(def handler (yada data))

features

  • Parameter validation (and coercion)
  • Automatic full HTTP compliance
    • correct HTTP method semantics, response codes
    • content negotiation, allow, vary
    • conditional requests, entity tags
    • custom methods/media-types
    • security headers, CORS
    • range-requests, transfer-encoding, multipart
  • Automatic Swagger spec. generation
  • Async 'on demand'
    • database trips, SSE, streaming downloads/uploads

The phonebook index

{:description "Phonebook index"
 :properties {…}
 :methods {:get {…}
           :post {…}}

GET

{:get
 {:parameters {:query {(s/optional-key :q) String}}

  :produces [{:media-type #{"text/html" "application/json;q=0.9"}
              :charset "UTF-8"}]

  :handler
  (fn [ctx]
    (let [q (get-in ctx [:parameters :query :q])
          entries (if q
                    (db/search-entries db q)
                    (db/get-entries db))]
      (case (get-in ctx [:response :representation :media-type :name])
        "text/html" (html/index-html entries @*routes q)
        entries)))}}

POST

{:post
 {:parameters
  {:form {:surname String :firstname String :phone [String]}}

  :consumes
  [{:media-type "application/x-www-form-urlencoded"
    :charset "UTF-8"}]

  :handler
  (fn [ctx]
    (let [id (db/add-entry db (get-in ctx [:parameters :form]))]
      (-> (:response ctx)
          (assoc :status 303)
          (update :headers merge
            {"location"
             (bidi/path-for @*routes ::entry :entry id)}))))}}

The phonebook entry

{:description "Phonebook entry"
 :parameters {:path {:entry Long}}
 :properties (fn [ctx] {:last-modified …
                        :version …})
 :produces {:media-type #{"text/html"
                          "application/json;q=0.8"}
            :charset "UTF-8"}
 :methods {:get {…}
           :put {…}
           :delete {…}}}

Phonebook entry GET

{:get
 {:handler
  (fn [ctx]
    (when-let [entry (db/get-entry db
                       (get-in ctx [:parameters :path :entry]))]
      (case (get-in ctx [:response :representation :media-type :name])
        "text/html" (html/entry-html entry)
        entry)))}}

Phonebook entry DELETE

{:delete
 {:handler
  (fn [ctx]
    (let [id (get-in ctx [:parameters :path :entry])]
      (db/delete-entry db id)))}}

Phonebook entry PUT

[:button {:onclick (format "phonebook.update('%s')" entry)} "Update"]
update: function(url) {
    x = new XMLHttpRequest()
    x.open("PUT", url)
    // FormData is built-in, sends multipart/form-data
    x.send(new FormData(document.getElementById("entry")))
}

Phonebook entry PUT (pt. 2)

{:put
 {:consumes [{:media-type #{"multipart/form-data"}}]

  :parameters
  {:form {:surname String
          :firstname String
          :phone [String]
          :photo java.io.File}}

  :handler
  (fn [ctx]
    (let [entry (get-in ctx [:parameters :path :entry])
          form (get-in ctx [:parameters :form])]
      (db/update-entry db entry form)))}}

References