Learn how to build straightforward HTTP APIs on Ring and Compojure, keep route handlers thin, and separate routing concerns from validation, domain logic, and persistence.
Compojure: A routing library in the Ring ecosystem that lets you define routes with macros while still returning ordinary Ring handlers.
Ring gives you the request/response contract. Compojure gives you a convenient routing layer on top of that contract. It is still useful for straightforward API construction, even though many newer Clojure teams also consider data-driven routers such as Reitit for larger systems.
An HTTP API boundary has four recurring responsibilities:
The mistake is letting one route handler own all four in an ad hoc way.
Once those responsibilities are separated, API code becomes easier to test and easier to change. Routes can stay as web-boundary shells instead of slowly becoming the place where all validation, policy, and persistence logic accumulates.
The most important API design rule is simple: route handlers should not become the whole application.
Good route handlers usually:
They should not also contain the entire business policy and persistence story.
One good review question is: “If this route disappeared, where would the business logic still live?” If the answer is “nowhere,” the route is carrying too much.
1(ns myapp.api
2 (:require [compojure.core :refer [defroutes GET POST]]
3 [ring.util.response :as resp]
4 [myapp.orders :as orders]))
5
6(defroutes app-routes
7 (GET "/orders/:id" [id]
8 (if-some [order (orders/find-order id)]
9 (resp/response order)
10 (-> (resp/response {:error "not found"})
11 (resp/status 404))))
12
13 (POST "/orders" request
14 (let [payload (:body-params request)
15 created (orders/create-order! payload)]
16 (-> (resp/response created)
17 (resp/status 201)))))
This is enough structure for many APIs: route declaration, request extraction, domain call, and response shaping.
One of the easiest ways to degrade an API is to let every handler validate payloads differently. Even on a small Ring/Compojure stack, the application benefits from a clear validation step:
400 responsesThat keeps the route layer from turning into a mix of parsing, coercion, and business rules.
Validation also protects API evolution. When every route hand-rolls payload assumptions, compatibility bugs multiply as clients and handlers drift.
Even when using Compojure, the API still lives on Ring’s core contract. That matters because middleware, testing, authentication, and response shaping all continue to build on the same request/response model.
Compojure is mainly about route matching and handler composition. It is not a replacement for:
That matters in larger systems because teams sometimes overestimate what the router is supposed to solve. The router should make the surface readable. It should not become the application’s policy engine.
Small APIs often drift because every route invents its own response style. Decide early:
The smaller the stack, the more important it is to make these choices explicit instead of depending on framework convention to rescue them later.
One common source of route bloat is letting handlers talk directly to every storage concern:
That can work for very small services, but it becomes harder to reason about once failures, retries, or multiple side effects enter the picture. Handlers are usually easier to maintain when they call a narrower application or domain boundary instead of orchestrating all persistence concerns directly.
Compojure remains a good fit when route declarations stay readable and the rest of the application is split cleanly:
That split is what keeps the API maintainable once it grows beyond a handful of endpoints.
Even simple APIs eventually encounter:
The boundary should decide which operations are safely repeatable and which ones need stronger duplicate protection. This is one of the places where API design and domain design meet directly.
API code becomes hard to maintain when:
The more your routes look like orchestration shells around cleaner domain functions, the easier the codebase is to test and evolve.
This stack works well when:
If the system needs richer data-driven route metadata, coercion, or structured OpenAPI-oriented workflows, the team may want additional tooling beyond classic Compojure.
That is a growth decision, not a sign of failure. Ring and Compojure remain useful when the API surface stays understandable and the surrounding boundaries are disciplined.
Use Ring and Compojure to keep the HTTP boundary explicit and lightweight. Let routes describe the web surface. Let domain namespaces own the business logic. Standardize validation and error handling early so the API does not drift into route-by-route inconsistency.