Web Server
Sema includes a built-in HTTP server powered by axum, with data-driven routing, middleware as function composition, SSE streaming, and WebSocket support. The server runs on a background thread with a Tokio runtime while keeping all Sema evaluation single-threaded — the same model as Node.js.
Quick Start
(define (handler req)
(http/ok {:message "Hello from Sema!"}))
(http/serve handler {:port 3000})$ curl http://localhost:3000
{"message":"Hello from Sema!"}Serving
http/serve
Start an HTTP server. Takes a handler function and an optional options map. The handler receives a request map and returns a response map. This function blocks — it becomes the server's run loop.
(http/serve handler)
(http/serve handler {:port 3000})
(http/serve handler {:port 8080 :host "127.0.0.1"})| Option | Default | Description |
|---|---|---|
:port | 3000 | TCP port to bind |
:host | "0.0.0.0" | Address to bind to |
The handler is any function (request-map -> response-map). This can be a plain function, a router, or a middleware-wrapped stack.
Routing
http/router
Create a handler function from a list of route definitions. Each route is a vector of [method pattern handler].
(define routes
[[:get "/" handle-home]
[:get "/users/:id" handle-user]
[:post "/users" handle-create]
[:any "/echo" handle-echo]])
(define app (http/router routes))
(http/serve app {:port 3000})Supported methods: :get, :post, :put, :patch, :delete, :any (matches all methods), and :ws (WebSocket upgrade).
Routes are matched top-to-bottom — first match wins. Unmatched routes return 404.
Path Parameters
Use :param syntax to capture path segments. Extracted values appear in the request's :params map.
;; Route: [:get "/users/:id" handle-user]
;; Request: GET /users/42
(define (handle-user req)
(let ((id (:id (:params req))))
(http/ok {:user-id id})))
; => {"user-id":"42"}Multiple parameters work as expected:
[:get "/users/:uid/posts/:pid" handler]
;; GET /users/1/posts/99 → {:uid "1" :pid "99"}Wildcard Routes
Use * to capture the rest of the path.
[:get "/files/*" handle-files]
;; GET /files/docs/readme.md → {:* "docs/readme.md"}Request Map
Every handler receives a request map with the following fields:
{:method :get ; HTTP method as keyword
:path "/users/42" ; Request path
:headers {"content-type" "application/json" ...} ; Headers (string keys)
:query {:search "term" :page "1"} ; Query params (keyword keys)
:params {:id "42"} ; Route params (keyword keys)
:body "{\"name\": \"Ada\"}" ; Raw body string
:json {:name "Ada"}} ; Parsed JSON body (if applicable)The :json field is automatically populated when the request has Content-Type: application/json.
Accessing Request Data
;; Method
(:method req) ; => :get
;; Path
(:path req) ; => "/users/42"
;; A specific header
(get (:headers req) "authorization") ; => "Bearer ..."
;; Query parameter
(:page (:query req)) ; => "2"
;; Route parameter
(:id (:params req)) ; => "42"
;; JSON body field
(:name (:json req)) ; => "Ada"Response Map
Handlers return a response map with :status, :headers, and :body:
{:status 200
:headers {"content-type" "application/json"}
:body "{\"message\": \"ok\"}"}You can construct these by hand, but the response helpers below are more convenient.
Response Helpers
http/ok
Return 200 with a JSON-encoded body.
(http/ok {:message "success"})
; => {:status 200 :headers {"content-type" "application/json"} :body "{\"message\":\"success\"}"}
(http/ok [1 2 3])
; => {:status 200 :body "[1,2,3]" ...}http/created
Return 201 with a JSON-encoded body.
(http/created {:id 42 :name "Ada"})http/no-content
Return 204 with an empty body.
(http/no-content)http/not-found
Return 404 with a JSON-encoded body.
(http/not-found {:error "User not found"})http/error
Return a custom status code with a JSON-encoded body.
(http/error 422 {:errors ["Invalid email" "Name required"]})
(http/error 503 {:error "Service unavailable"})http/redirect
Return a 302 redirect to a URL.
(http/redirect "https://example.com/login")http/html
Return 200 with Content-Type: text/html.
(http/html "<h1>Hello</h1><p>Welcome to Sema.</p>")http/text
Return 200 with Content-Type: text/plain.
(http/text "OK")Middleware
Middleware in Sema is just function composition — a function that takes a handler and returns a new handler. No special framework needed.
Writing Middleware
;; Logging middleware
(define (with-logging handler)
(fn (req)
(let ((resp (handler req)))
(println (:method req) (:path req) "->" (:status resp))
resp)));; CORS middleware
(define (with-cors handler)
(fn (req)
(let ((resp (handler req)))
(assoc resp :headers
(merge (or (:headers resp) {})
{"access-control-allow-origin" "*"
"access-control-allow-methods" "GET, POST, PUT, DELETE"})))));; Auth middleware
(define (with-auth handler)
(fn (req)
(let ((token (get (:headers req) "authorization")))
(if token
(handler req)
(http/error 401 {:error "Unauthorized"})))))Composing Middleware
Stack middleware by nesting function calls. The outermost middleware runs first.
(define app
(with-logging
(with-cors
(with-auth
(http/router routes)))))
(http/serve app {:port 3000})Or use the threading macro for a cleaner pipeline:
(define app
(-> (http/router routes)
with-auth
with-cors
with-logging))SSE Streaming
http/stream
Return a Server-Sent Events stream. Takes a handler function that receives a send callback.
(define (handle-events req)
(http/stream
(fn (send)
(send "connected")
(sleep 1000)
(send "update 1")
(sleep 1000)
(send "update 2"))))The stream stays open as long as the handler is running. When the handler returns, the stream closes.
;; Route it like any other handler
(define routes
[[:get "/events" handle-events]])$ curl -N http://localhost:3000/events
data: connected
data: update 1
data: update 2Streaming LLM Responses
SSE is particularly useful for streaming LLM completions to the browser:
(define (handle-chat req)
(http/stream
(fn (send)
(let ((prompt (:prompt (:json req))))
;; Stream each token as an SSE event
(llm/chat {:prompt prompt
:stream (fn (token) (send token))})))))WebSocket
http/websocket
Handle bidirectional WebSocket connections. Takes a handler function that receives a connection map with :send, :recv, and :close functions.
(define (handle-ws conn)
(let ((msg ((:recv conn))))
(when msg
((:send conn) (string-append "echo: " msg))
(handle-ws conn))))The connection map:
| Key | Description |
|---|---|
:send | (send message) — Send a string to the client |
:recv | (recv) — Block until a message arrives, nil on close |
:close | (close) — Close the connection |
WebSocket Routes
Use the :ws method in the router:
(define routes
[[:get "/api/status" handle-status]
[:ws "/ws/chat" handle-ws]])
(http/serve (http/router routes) {:port 3000})Chat Room Example
(define clients (atom '()))
(define (broadcast msg)
(for-each (fn (send) (send msg))
@clients))
(define (handle-ws conn)
;; Add this client's send function to the list
(swap! clients (fn (lst) (cons (:send conn) lst)))
;; Read loop
(let loop ((msg ((:recv conn))))
(when msg
(broadcast msg)
(loop ((:recv conn))))))
(define routes
[[:ws "/chat" handle-ws]])
(http/serve (http/router routes) {:port 3000})Complete Examples
REST API
A JSON API with CRUD operations, middleware, and error handling.
;; In-memory data store
(define db (atom {}))
(define next-id (atom 0))
(define (gen-id)
(swap! next-id (fn (n) (+ n 1)))
@next-id)
;; Handlers
(define (list-users _)
(http/ok (vals @db)))
(define (get-user req)
(let ((id (:id (:params req)))
(user (get @db id)))
(if user
(http/ok user)
(http/not-found {:error "User not found"}))))
(define (create-user req)
(let ((data (:json req))
(id (str (gen-id)))
(user (assoc data :id id)))
(swap! db (fn (d) (assoc d id user)))
(http/created user)))
(define (delete-user req)
(let ((id (:id (:params req))))
(swap! db (fn (d) (dissoc d id)))
(http/no-content)))
;; Middleware
(define (with-json-errors handler)
(fn (req)
(let ((resp (handler req)))
(if (map? resp) resp
(http/error 500 {:error "Internal server error"})))))
(define (with-cors handler)
(fn (req)
(let ((resp (handler req)))
(assoc resp :headers
(merge (or (:headers resp) {})
{"access-control-allow-origin" "*"
"access-control-allow-methods" "GET, POST, DELETE"})))))
;; Routes
(define routes
[[:get "/users" list-users]
[:get "/users/:id" get-user]
[:post "/users" create-user]
[:delete "/users/:id" delete-user]])
;; Start
(define app
(-> (http/router routes)
with-json-errors
with-cors))
(http/serve app {:port 3000})LLM-Powered API
An API endpoint that uses Sema's built-in LLM primitives to generate responses.
(define (handle-summarize req)
(let ((text (:text (:json req))))
(if text
(http/ok {:summary (llm/chat (str "Summarize this:\n\n" text))})
(http/error 400 {:error "Missing 'text' field"}))))
(define (handle-extract req)
(let ((text (:text (:json req))))
(http/ok (llm/extract text {:name "string"
:date "string"
:amount "number"}))))
(define routes
[[:post "/summarize" handle-summarize]
[:post "/extract" handle-extract]
[:get "/health" (fn (_) (http/ok {:status "up"}))]])
(http/serve (http/router routes) {:port 3000})HTML Application
Serve dynamic HTML pages.
(define (page title body)
(http/html
(str "<!DOCTYPE html><html><head><title>" title "</title>"
"<style>body{font-family:sans-serif;max-width:800px;margin:0 auto;padding:2rem}</style>"
"</head><body>" body "</body></html>")))
(define (handle-home _)
(page "Home" "<h1>Welcome</h1><p>Built with Sema.</p>"))
(define (handle-greet req)
(let ((name (or (:name (:params req)) "world")))
(page "Greeting" (str "<h1>Hello, " name "!</h1>"))))
(define routes
[[:get "/" handle-home]
[:get "/greet/:name" handle-greet]])
(http/serve (http/router routes) {:port 3000})Architecture Notes
- Single-threaded evaluation: All Sema code runs on the main thread. HTTP I/O runs on a background Tokio runtime. Requests are bridged via channels.
- Concurrency model: Requests are processed sequentially by the evaluator. For LLM-backed services (where each request takes 1–5s of LLM latency), this is fine. For high-throughput APIs, consider a reverse proxy.
- Graceful shutdown: Ctrl+C breaks the channel and the server exits cleanly.
- Sandbox-aware:
http/serverequires theNETWORKcapability when running in sandbox mode.
See Also
- HTTP Client & JSON — outbound HTTP requests and JSON encoding/decoding
- LLM Primitives — building LLM-powered endpoints
- Key-Value Store — persistent storage for server state