HTTP Services in Clojure

Priyatam Mudivarti

Principal, Facjure LLC

Why?

Abstract HTTP

Write idiomatic code

Run on any web server

Bare Metal

Easy to Test

Live Coding

I've written webservices in 5 languages.

 

I find Clojure to be 10x simpler

HTTP services in Clojure (Ring)

I just write functions,

higher-order functions, with two data structures

No More framework abstractions

Data

Functions

Abstractions

Clojure

in 5 mins

(def a-vector [1 2 3 4 5])

(def a-nested-map 
  {:customer-id 1e6
   :preferences {:nickname "Bob"
                 :avatar "http://en.gravatar.com/userimage/0/0.jpg"}
                 :services {:alerts {:daily true}}})

(def complex-map 
  {[1 2] :one-two 
   [3 4] :three-four})

(def a-set 
  #{:cat :dog :bird})

Clojure 101: data structures

;; Unlike JavaScript there is no hoisting in ClojureScript. ClojureScript has lexical scoping.

(def some-x 1)

(let [some-x 2]
  some-x)


;; functions parameters
(let [fns (loop [i 0 ret []]
            (if (< i 10)
              (recur (inc i) 
              (conj ret (fn [] i)))
              ret))]
  (map #(%) fns))

;; destructuring
(def point {:x 5 :y 7})

(let [{:keys [x y] :as the-point} point]
  (println "x:" x "y:" y "point:" the-point))

Clojure 101: variables & scope

; simple function
(defn add [a b]
  (+ a b))

;; function with multiple args and default values
(defn another-function
  ([x] (defaults x :default))
  ([x y] [x y]))

;; closure
(let [a 1e3]
  (defn foo []
    (* a a))
  (defn bar []
    (+ (foo) a)))


;; higher order functions
(map inc [0 1 2 3 4 5 6 7 8 9])
(filter even? (range 10))
(reduce + (range 100))
(partition 2 [:a 1 :b 2 :c 3 :d 4 :e 5])

;; loops
(for [x (range 1 10)
      y (range 1 10)
      :when (and (zero? (rem x y))
                 (even? (quot x y)))]
  [x y])

Clojure 101: functions

That's it.

can we build http services with these concepts?

;; handler
(defn my-ip [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body (:remote-addr request)})

;; middleware
(defn wrap-content-type [handler content-type]
  (fn [request]
    (let [response (handler request)]
      (assoc-in response [:headers "Content-Type"] content-type))))

(defn wrap-content-type [handler content-type]
  (fn [request]
    (let [response (handler request)]
      (assoc-in response [:headers "Content-Type"] content-type))))

(defn json-response [data & [status]]
  {:status (or status 200)
   :headers {"Content-Type" "application/edn"}
   :body (pr-str data)})

(def app
  (wrap-content-type my-ip "text/html"))

(defn serve [request]
  (app request))

Ring - Concepts

;; Routes

(defroutes routes
  (GET "/" [] (str "Server is running"))
  (GET "/ws" [] handle-websocket)
  (context
      "/api" []
      (GET "/languages" []
           (let [prog (get-in db/data [:languages])]
             (when prog
               (edn-response prog))))
      (GET "/languages/:id/:type" [id type]
           (let [stats (filter #(and
                                 (= (:id %) id) (= (:type %) type))
                               (get-in db/data [:stats]))]
             (when stats
               (response stats)))))))

Routes

(def app
  (->
   (handler/site router/routes)
   (wrap-json-body)
   (wrap-json-response)
   (wrap-edn-params)
   (wrap-defaults api-defaults)
   (wrap-cors
    :access-control-allow-origin #".+"
    :access-control-allow-methods [:get :post :put :delete :head]
    :access-control-allow-headers ["Content-Type" "X-Requested-With"])))

(defn -main [& _]
  (let [port (Integer. (or (System/getenv "SERVER_PORT") 8000))]
    (server/run-server app {:port port :join? false})
    (log/info "Server started and listening at port..." port)))

Server

(defresource list-resource
  :available-media-types ["application/json"]
  :allowed-methods [:get :post]
  :known-content-type? #(check-content-type % ["application/json"])
  :malformed? #(parse-json % ::data)
  :post! #(let [id (str (inc (rand-int 100000)))]
            (dosync (alter entries assoc id (::data %)))
            {::id id})
  :post-redirect? true
  :location #(build-entry-url (get % :request) (get % ::id))
  :handle-ok #(map (fn [id] (str (build-entry-url (get % :request) id)))
                   (keys @entries)))

REST?

Demos

Thank you!

References

 

https://github.com/priyatam/ring-micro

 

Clojure HTTP Services

By Priyatam Mudivarti

Clojure HTTP Services

HTTP/RESt Services in Clojure

  • 414