Inbound HTTP, portable. One path, one handler fn, every host.
Deliberately minimal. The use case is a JSON-RPC endpoint (toolnexus SPEC §7B/§7C): a request comes in on ONE path, a string body goes in, a string body comes out. There is no routing table, no middleware, no static files, no websockets, no TLS — every one of those multiplies the surface that has to agree across four hosts, and none of them is needed.
(def h (server/serve (fn [req] {:status 200 :body (:body req)})
{:port 0}))
(server/port h) ; => 54321
(server/stop! h) ; => nil, idempotent
handler : request-map -> response-map request {:method :post :path "/" :headers {"content-type" "…"} :body "…"} response {:status 200 :headers {…} :body "…"} ; :status defaults to 200, ; :headers to {}, :body to ""
Normalised across hosts: :method is always a lower-case keyword,
:headers keys are always lower-case strings with a single string value
(first value wins on repeats), :body is always a string — never nil.
Host support (measured 2026-07-27, see the porting notes at the bottom):
| host | serve | :port 0 | stop! | :host |
|---|---|---|---|---|
| jvm | yes | yes | yes | yes |
| cljgo | yes | yes | yes | no |
| glojure | yes | NO | yes | yes |
| let-go | yes | NO | NO | yes |
Where a host cannot do something it throws a named, actionable error at the point of use rather than pretending.
Inbound HTTP, portable. One path, one handler fn, every host.
Deliberately minimal. The use case is a JSON-RPC endpoint (toolnexus
SPEC §7B/§7C): a request comes in on ONE path, a string body goes in, a
string body comes out. There is no routing table, no middleware, no
static files, no websockets, no TLS — every one of those multiplies the
surface that has to agree across four hosts, and none of them is needed.
(def h (server/serve (fn [req] {:status 200 :body (:body req)})
{:port 0}))
(server/port h) ; => 54321
(server/stop! h) ; => nil, idempotent
handler : request-map -> response-map
request {:method :post :path "/" :headers {"content-type" "…"} :body "…"}
response {:status 200 :headers {…} :body "…"} ; :status defaults to 200,
; :headers to {}, :body to ""
Normalised across hosts: `:method` is always a lower-case keyword,
`:headers` keys are always lower-case strings with a single string value
(first value wins on repeats), `:body` is always a string — never nil.
Host support (measured 2026-07-27, see the porting notes at the bottom):
| host | serve | :port 0 | stop! | :host |
|---------|-------|---------|-------|-------|
| jvm | yes | yes | yes | yes |
| cljgo | yes | yes | yes | no |
| glojure | yes | NO | yes | yes |
| let-go | yes | NO | NO | yes |
Where a host cannot do something it throws a named, actionable error at
the point of use rather than pretending.(port handle)The port the server is actually bound to. The reason :port 0 is worth
having: on the JVM and cljgo this is the OS-assigned port.
The port the server is actually bound to. The reason `:port 0` is worth having: on the JVM and cljgo this is the OS-assigned port.
(serve handler {:keys [port host path] :or {port 0 host "127.0.0.1" path "/"}})Start an HTTP server that sends every request on path to handler.
opts: {:port 8080 ; 0 = pick a free port, where the host allows it :host "127.0.0.1" :path "/"} ; catch-all; see the caveat below
Returns an opaque handle for port and stop!.
The default :path of "/" is a prefix/catch-all on all four hosts and is
the only value that behaves identically everywhere — let-go's http/serve
takes no pattern at all, so a non-"/" path is NOT enforced there. The
handler always receives :path, so filter in the handler if it matters.
Start an HTTP server that sends every request on `path` to `handler`.
opts: {:port 8080 ; 0 = pick a free port, where the host allows it
:host "127.0.0.1"
:path "/"} ; catch-all; see the caveat below
Returns an opaque handle for `port` and `stop!`.
The default `:path` of "/" is a prefix/catch-all on all four hosts and is
the only value that behaves identically everywhere — let-go's `http/serve`
takes no pattern at all, so a non-"/" path is NOT enforced there. The
handler always receives `:path`, so filter in the handler if it matters.(stop! handle)Shut the server down. Returns nil. Idempotent — the second call is a no-op, so teardown can be unconditional.
Throws on let-go, every time, because there is nothing to stop there; the flag is only set once the underlying shutdown actually succeeded, so an unsupported host stays loudly unsupported rather than going quiet after the first attempt.
Shut the server down. Returns nil. Idempotent — the second call is a no-op, so teardown can be unconditional. Throws on let-go, every time, because there is nothing to stop there; the flag is only set once the underlying shutdown actually succeeded, so an unsupported host stays loudly unsupported rather than going quiet after the first attempt.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |