Skip to content

bri.http

The Ring contract on stdlib routing: a handler is a fn of request-map → response-map; middleware is handler → handler; routes are data on Go 1.22+ ServeMux pattern strings. There is no router of our own and no hidden call graph — the adapter only invokes what you handed it.

If you haven’t built an app yet, the tutorial gets you from cljgo new to a running server first.

;; request ;; response
{:request-method :get {:status 200
:uri "/users/7" :headers {"content-type" "..."}
:query-string "page=2" :body "..."} ; string, or
:headers {"accept" "text/html"} ; map/vector → JSON
:params {:id "7"} ; {name} segments, AS STRINGS
:query-params {:page "2"}
:body "..."
:session {...} ; from the signed cookie (middleware)
:json {...} ; parsed JSON body (middleware)
:form-params {...}} ; parsed form body (middleware)

:params bind as strings; the blessed, visible coercion is

(http/param! req :id :int) ; :string :int :uuid :keyword

Failure throws :http/bad-param — the funnel maps it to a 400. No error-handling code in the handler.

(def routes
[["POST /signup" #'signup]
["GET /users/{id}" #'show-user]
["GET /static/" (http/dir "public")]
["GET /health" (http/health {})]])
  • Patterns are the stdlib’s own syntax (METHOD /path, {name} segments, {$} for exact-root, trailing / for subtree).
  • #'var handlers deref per request — re-def at the REPL updates the live server. A plain fn works but is not live; cljgo dev warns.
  • (http/group "/admin" [require-admin] routes) prefixes a group and wraps extra middleware.
  • (http/dir "public") serves static files (Go’s http.FileServer).
(http/serve routes {:port (:port cfg)
:drain [workers]}) ; blocks; SIGTERM drains
  • Pings anything in :ping before accepting traffic.
  • Production timeouts are default on (read/write/idle).
  • SIGTERM/Ctrl-C: in-flight requests finish (deadline), then each handle in :drain is invoked — shutdown wiring is on the page, never an ambient registry.
  • Tests/REPL: {:block? false} returns {:port n :stop (fn)}.

Middleware: the safe stack is what you didn’t type

Section titled “Middleware: the safe stack is what you didn’t type”

Omitting :middleware applies (http/defaults):

[access-log (recover) sessions negotiate csrf] ; outermost first

It is a plain vector of {:name kw :wrap fn} entries — inspect it, conj onto it, (http/without stack :csrf) to remove by name. cljgo routes prints the effective stack; dev mode warns when a custom stack lacks recover or csrf.

  • access-log — one line per request.
  • recover — THE error funnel (below).
  • sessions — signed cookies (HMAC-SHA256; key from APP_SESSION_KEY, or per-process random in dev). Read as :session; attach one with (http/start-session res {...}). (APP_SESSION_KEY is env-only — see bri.config.)
  • negotiate (:json in the stack) — JSON bodies → :json, form bodies → :form-params; map/vector response bodies → JSON; string bodies default to text/html.
  • csrf — gates session-bearing mutating requests on the token bri.html/form mints (or the x-csrf-token header). Sessionless requests pass: the documented API posture — a JSON curl with no cookie has nothing to forge.

Errors: one blessed surface, one documented funnel

Section titled “Errors: one blessed surface, one documented funnel”

App handlers use ! forms and let the funnel answer. The funnel’s mapping is shipped data:

:bri/error in ex-data status
:http/bad-param 400
:cast/invalid 422
:db/not-found 404
:db/constraint 409
anything else 500

Override rows: (http/recover {:error-map {:app/teapot 418}}) in a custom stack. Error bodies name the kind; messages appear in dev only.

Result values cross the http boundary only through the visible bridge:

(defn signup [req]
(http/render
(let? [input (validate (:json req))
user (create input)]
(http/created user))))

(ok resp) → the response; (err e) → the funnel (an err payload map may carry :bri/error to pick its row). A handler that returns a bare Result without http/render is a loud 500 explaining exactly that — never a silently laundered status.

The in-process client runs the same mount + middleware path with no socket:

(http/request routes {:method "GET" :path "/users/7"})
(http/request routes {:method "POST" :path "/e"
:headers {"content-type" "application/json"}
:body "{\"a\":1}"})

The adapter is a thin shim over net/http (pkg/bri). When you outgrow the blessed path, mount your own patterns — routes are just data and the mux is the stdlib’s.

  • bri.html — render the pages your handlers return; how forms carry the CSRF token
  • bri.config — where :port and APP_SESSION_KEY come from
  • Tutorial — the whole app on one page, end to end
  • Go interop — for when you reach past the shim into net/http itself

Questions or feedback on this page? Comment below with your GitHub account — comments are public and live in the project's GitHub Discussions.