~ / track C / clojure ecosystem
Web stack: Ring and Reitit
IntermediateA Clojure web app is just a function from request map to response map. That's the entire Ring contract. Routing, middleware, content-negotiation — everything else is composition on top of that function. The modern stack is Ring (the protocol) + Reitit (data-driven routing) + Jetty/http-kit/Aleph (the server).
Ring in one example
(defn handler [request]
{:status 200
:headers {"Content-Type" "text/plain"}
:body "hello"})
(require '[ring.adapter.jetty :as jetty])
(jetty/run-jetty handler {:port 3000 :join? false})
That's a full Clojure web server. Request is a map (:request-method,
:uri, :headers, :body...), response is a map (:status,
:headers, :body). Nothing more.
Middleware = function composition
Middleware is a function handler → handler:
(defn wrap-logging [handler]
(fn [req]
(println :req (:uri req))
(let [resp (handler req)]
(println :resp (:status resp))
resp)))
(def app
(-> handler
wrap-logging
(ring.middleware.json/wrap-json-body {:keywords? true})
ring.middleware.json/wrap-json-response))
-> ([[threading-macros]]) reads top-to-bottom: every request flows
through wrap-logging first, then wrap-json-body, then the handler;
the response unwinds back through the same middleware in reverse. Pure
function composition.
Reitit — data-driven routing
(require '[reitit.ring :as ring])
(def app
(ring/ring-handler
(ring/router
[["/" {:get (fn [_] {:status 200 :body "home"})}]
["/users/:id"
{:get (fn [{:keys [path-params]}]
{:status 200 :body (str "user " (:id path-params))})
:post {:parameters {:body {:name string?}}
:handler (fn [_] {:status 201})}}]])))
Routes are data — a vector of [path data]. That data is just a
map; the framework reads :get, :post, etc. Because routes are data
you can:
- Inspect them at runtime (
reitit.core/match-by-path) - Generate OpenAPI docs from them automatically
- Apply per-route middleware via
:middlewarein the data - Coerce parameters by attaching specs/malli schemas (
:parameters)
Coercion
(require '[reitit.ring.coercion :as rrc]
'[reitit.coercion.malli])
(def app
(ring/ring-handler
(ring/router
["/echo"
{:post {:parameters {:body [:map [:name :string]]}
:handler (fn [{{:keys [body]} :parameters}]
{:status 200 :body body})}}]
{:data {:coercion reitit.coercion.malli/coercion
:middleware [rrc/coerce-request-middleware
rrc/coerce-response-middleware]}})))
If the request body fails the [[malli]] schema, Reitit returns 400 with
an explanation before your handler runs. Same idea with clojure.spec.
Comparison: Compojure vs Reitit
| Compojure | Reitit | |
|---|---|---|
| Style | Macro DSL — defroutes, GET, POST | Plain data (vectors and maps) |
| Reflection | Limited (macro-defined) | Full — routes are introspectable values |
| Coercion | Manual | First-class (spec/malli) |
| Docs | None built-in | OpenAPI/Swagger from the data |
| Performance | Slower routing | Hash-trie based, very fast |
| Default for new code | Less common today | Modern default |
Compojure is still common in older codebases. Reitit wins greenfield projects because routes-as-data composes with everything else.
The full request lifecycle
client → Jetty (Java thread) → Ring middleware stack
↓
router (Reitit)
↓
coercion (malli/spec)
↓
handler (your fn)
↓
response map
↓
middleware unwinds → client
Each step is just (fn [req] resp). The whole app is one function you
can call directly from tests — no HTTP needed.
Servers compared
| Server | Concurrency model | Notes |
|---|---|---|
Jetty (ring-jetty-adapter) | Thread-per-request | Most common; battle-tested |
| http-kit | Async / event-loop | Very fast for many idle connections (WebSockets, SSE) |
| Aleph | Netty-based, async | Manifold deferreds; lower-level, very fast |
Sun's HttpServer (ring-httpserver) | Threads | Tiny footprint; toy use |
Default to Jetty unless you need long-polling/WebSockets at scale.
Try it
Real-world
| Library | Use |
|---|---|
ring/ring-core, ring/ring-jetty-adapter | The base contract + a server |
metosin/reitit | Routing + coercion + OpenAPI |
metosin/malli | Schemas for coercion ([[malli]]) |
metosin/muuntaja | Format negotiation (JSON, EDN, Transit) |
ring-cors | CORS middleware |
buddy-auth, clj-jwt | Auth middleware |
aleph | Netty server with Manifold for streaming/WebSocket |
http-kit | Async server with built-in WebSocket support |
clj-http / hato | HTTP client (for calling other services) |
Check yourself
? quiz
In Ring, what type is a middleware?
Exercise
Write a Reitit app with two routes:
GET /health→{:status 200 :body "ok"}.POST /addaccepting{:a int :b int}(use malli coercion); responds with{:sum (+ a b)}.
Now add a middleware wrap-timing that measures wall-clock time per
request and adds an X-Elapsed-Ms header to the response. Wire it
globally via :middleware in the router options. Verify a failing
malli coercion (e.g. {:a "x"}) returns 400 without invoking your
handler.