Skip to content
AgileLogics
8 min read

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.