A web framework for Carp with routing, middleware, WebSocket support, JSON integration, and concurrent connection handling via kqueue/epoll.
(load "git@github.com:carpentry-org/web@0.4.0")Define handler functions and register them with defserver:
(load "git@github.com:carpentry-org/web@0.4.0")
(defn hello [req params]
(Response.text @"Hello, world!"))
(defn greet [req params]
(let [name (Map.get params "name")]
(Response.text (fmt "Hello, %s!" &name))))
(defserver "0.0.0.0" 8080
(GET "/" hello)
(GET "/hello/:name" greet))A handler is (Fn [&Request &(Map String String)] Response). It receives
the parsed HTTP request and a map of path parameters captured by :name
segments in the route pattern. Middleware before-hooks can add entries to
this map for downstream use.
Routes are matched in registration order. Pattern segments starting with
: capture the corresponding path segment. A * as the last segment
captures the rest of the path:
(GET "/users/:id" get-user)
(GET "/api/*" api-catch-all) ; params has "*" = "the/rest"
(GET "/users/:id/*" user-sub) ; both :id and * capturedUnmatched requests go through the error handler.
Before-hooks run before route dispatch, after-hooks run after. Both receive the params map, so hooks can annotate it for downstream use.
(defn require-auth [req params]
(if (has-token req)
(Maybe.Nothing) ; continue
(Maybe.Just (Response.text @"denied")))) ; short-circuit
(defn add-header [req params resp]
(Response.with-header resp @"X-Server" @"carp"))
(defserver "0.0.0.0" 3000
(before require-auth)
(after add-header)
(GET "/" hello))(defserver "0.0.0.0" 3000
(CORS.configure @"http://localhost:5173")
(before CORS.before-hook)
(after CORS.after-hook)
(GET "/api/data" handler))(defn login [req params]
(-> (Response.text @"logged in")
(Response.set-simple-cookie @"session" @"abc123")))set-simple-cookie uses Path=/, HttpOnly, SameSite=Lax. For full
control, use Response.set-cookie with a Cookie value from the http
library.
(load "git@github.com:carpentry-org/simplelog@<version>")
(defserver "0.0.0.0" 3000
(SimpleLog.install Log.INFO)
(before log-before)
(after log-after)
(GET "/" hello))Prints GET /path 200 3ms for each request. Works with any log backend
(simplelog, filelog, or your own).
(Response.text @"plain text")
(Response.html @"<h1>hi</h1>")
(Response.json &json-value)
(Response.file "path/to/file.pdf")
(Response.not-found)
(Response.bad-request)
(Response.redirect @"/other")
(Response.with-header resp @"X-Custom" @"value")
(Response.with-status resp 201 @"Created")(defserver "0.0.0.0" 3000
(GET "/api/health" health-check)
(static "public"))Serves public/index.html at /. Paths with .. return 404. Content
types are inferred from file extensions.
(defn my-errors [req code msg]
(Response.html (fmt "<h1>%d %s</h1>" code &msg)))
(defserver "0.0.0.0" 3000
(errors my-errors)
(GET "/" hello))The error handler receives the request, status code, and reason phrase.
(defn handle-login [req params]
(let [form (Form.decode-request req)]
(Response.text (fmt "hello %s" &(Map.get &form "username")))))Form.decode decodes application/x-www-form-urlencoded bodies. Handles
+ as space and percent-encoding.
(defn stream [req params]
(Response.chunked 200 @"text/plain"
&[@"chunk one\n" @"chunk two\n"]))The chunks are pre-encoded with Transfer-Encoding: chunked framing. The
client can start processing before the full response arrives.
Register WebSocket routes with (WS pattern handler). The handler
receives a WSEvent (Connect, Message, or Close), the path parameters,
and a WebSocket handle for sending messages:
(defn echo [event params ws]
(match-ref event
(WSEvent.Connect) (WebSocket.send ws @"connected")
(WSEvent.Message msg) (WebSocket.send ws (fmt "echo: %s" msg))
(WSEvent.Close) ()))
(defserver "0.0.0.0" 3000
(GET "/api/data" api-handler)
(WS "/ws/echo" echo))The upgrade handshake (RFC 6455) is handled automatically. Once upgraded, the connection uses WebSocket framing over the same non-blocking event loop. Text frames, ping/pong, and close frames are supported. Binary frames are not yet supported.
The server uses kqueue (macOS) or epoll (Linux) in a single-threaded, non-blocking event loop. HTTP keep-alive is supported. WebSocket and HTTP connections share the same event loop. Large responses drain across multiple writable events without stalling other connections.
carp -x test/web.carp
carp -x test/websocket.carp
Have fun!