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 both 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 |
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 both 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 |
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 both hosts and is
the only value that behaves identically everywhere — cljgo's 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 both hosts and is
the only value that behaves identically everywhere — cljgo's `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.
Shut the server down. Returns nil. Idempotent — the second call is a no-op, so teardown can be unconditional.
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 |