Building APIs with Ring and Compojure

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.

Start With the Boundary Contract

An HTTP API boundary has four recurring responsibilities:

  • route matching
  • request decoding and validation
  • domain invocation
  • response shaping

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.

Keep Routes Thin

The most important API design rule is simple: route handlers should not become the whole application.

Good route handlers usually:

  • read route or query parameters
  • call validation or coercion code
  • invoke domain functions
  • turn results into HTTP responses

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.

A Small Example

 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.

Validate Before Domain Logic

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:

  • required fields checked consistently
  • malformed bodies rejected predictably
  • type and shape errors mapped to stable 400 responses
  • domain functions called only after the HTTP payload is trustworthy enough

That 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.

Ring Still Owns the Boundary

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:

  • validation
  • serialization discipline
  • error handling
  • domain architecture

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.

Response Consistency Matters Early

Small APIs often drift because every route invents its own response style. Decide early:

  • whether resource bodies are wrapped or plain
  • how errors are shaped
  • which status codes mean validation, conflict, not-found, and auth failure
  • whether IDs, timestamps, and pagination fields follow one naming convention

The smaller the stack, the more important it is to make these choices explicit instead of depending on framework convention to rescue them later.

Keep Persistence Behind a Cleaner Boundary

One common source of route bloat is letting handlers talk directly to every storage concern:

  • database queries
  • transaction decisions
  • message publication
  • cache invalidation

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.

Handler Design for Growth

Compojure remains a good fit when route declarations stay readable and the rest of the application is split cleanly:

  • routes describe the web surface
  • service or domain namespaces hold business rules
  • repository or persistence namespaces own storage boundaries
  • middleware handles shared concerns

That split is what keeps the API maintainable once it grows beyond a handful of endpoints.

Think About Retries and Idempotency Early

Even simple APIs eventually encounter:

  • client retries after timeouts
  • duplicate submissions
  • downstream partial failures
  • users refreshing a form or replaying a request

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.

Where APIs Usually Go Wrong

API code becomes hard to maintain when:

  • routes contain business logic directly
  • parameter coercion is inconsistent
  • error responses vary arbitrarily between endpoints
  • persistence logic leaks into every handler

The more your routes look like orchestration shells around cleaner domain functions, the easier the codebase is to test and evolve.

When Ring and Compojure Are a Good Fit

This stack works well when:

  • the API surface is moderate
  • the team wants straightforward Ring compatibility
  • route definitions should stay simple and readable
  • heavy framework machinery would not buy much

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.

Practical Heuristics

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.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026