Web Services with Ring
Prerequisites: Higher-order functions, EDN and JSON
Ring is the foundation of nearly every Clojure web application. Its model is just data and functions:
- A request is a map (
:request-method,:uri,:headers,:body, ...). - A handler is a function from a request map to a response map
(
:status,:headers,:body). - Middleware is a function that wraps a handler to add behavior.
Handlers are plain functions
Because handlers are ordinary functions, you can call and test them without a server:
(require '[clojure.data.json :as json])
(def users (atom {1 {:id 1 :name "Ada"}}))
(defn json-response [status body]
{:status status
:headers {"Content-Type" "application/json"}
:body (json/write-str body)})
(defn handler [{:keys [request-method uri]}]
(cond
(and (= request-method :get) (= uri "/users"))
(json-response 200 (vals @users))
(and (= request-method :get) (re-matches #"/users/\d+" uri))
(if-let [u (get @users (parse-long (subs uri 7)))]
(json-response 200 u)
(json-response 404 {:error "not found"}))
:else (json-response 404 {:error "no route"})))
(defn wrap-logging [handler]
(fn [request]
(let [response (handler request)]
(println (name (:request-method request)) (:uri request) "->" (:status response))
response)))
(def app (wrap-logging handler))
(println (:body (app {:request-method :get :uri "/users"})))
(println (:body (app {:request-method :get :uri "/users/1"})))
(println (:status (app {:request-method :get :uri "/users/9"})))
get /users -> 200
[{"id":1,"name":"Ada"}]
get /users/1 -> 200
{"id":1,"name":"Ada"}
get /users/9 -> 404
404
wrap-logging is middleware: it takes a handler and returns a new
handler. Real applications stack middleware for JSON parsing, sessions,
authentication, CORS, and error handling.
Running it with Jetty
;; illustrative — deps: ring/ring-core, ring/ring-jetty-adapter 1.15.5
(ns inventory.server
(:require [ring.adapter.jetty :refer [run-jetty]]
[ring.middleware.params :refer [wrap-params]]))
(defn -main [& _]
(run-jetty (-> app wrap-params) {:port 3000 :join? false})
(println "Listening on http://localhost:3000"))
run-jetty serves the handler; with :join? false it returns the
server object, so you can .stop it from the REPL and restart with new
code — no process restart.
Routing with Reitit
Hand-written cond routing gets unwieldy. Reitit describes routes
as data:
;; illustrative — deps: metosin/reitit 0.11.0
(require '[reitit.ring :as ring])
(def app
(ring/ring-handler
(ring/router
[["/users" {:get list-users :post create-user}]
["/users/:id" {:get get-user :delete delete-user}]])
(ring/create-default-handler)))
Path parameters arrive in (:path-params request). Reitit also
integrates coercion with spec or Malli, OpenAPI documentation, and
interceptors. Compojure is an older, macro-based router you'll see
in many existing projects.
The rest of the stack
- Muuntaja or ring-json — JSON request/response bodies.
- Hiccup — HTML as vectors:
[:p {:class "note"} "Hello"]. - Integrant, Component, or mount — manage the lifecycle of stateful parts (server, database pool) so the REPL can start and stop the whole system.
- http-kit and Aleph — alternative servers (async, WebSockets).
- Pedestal, Biff, and Kit — larger frameworks/templates if you want batteries included.