# HTTP Server
Deft's HTTP server is built on top of the TCP primitives and runs on
the same epoll event loop. Each request is handled by a coroutine, so
handlers can `await` async operations (database queries, upstream
HTTP calls, timers) without blocking other connections.
Routers are built from standard maps wrapped with `http/router`:
def app %{...} |> [http/router]
## Hello, Server
def app %{ routes @{ %{ path "/" method :GET handler { "Hello, World!" } } } } |> [http/router] http/serve $app :8080
Run with `deft server.dft`, then `curl http://localhost:8080/`.
## Router Construction
A router is a map with these optional keys:
def app %{ prefix "/api" # optional path prefix routes @{ %{ path "/" method :GET handler root-handler } %{ path "/users/:id" method :GET handler get-user } %{ path "/users" method :POST handler create-user } %{ path "/files/*rest" method :GET handler get-file } } statics @{ %{ path "/assets" dir "./public" } } middleware @{ {req res} { echo "request: $req~method $req~path" } } } |> [http/router]
### `routes`
A list of route maps. Each route has:
- `path` — URL pattern. `:name` captures a path segment into
`$req~params~name`. `*name` captures the rest of the path (zero or
more segments).
- `method` — `:GET`, `:POST`, `:PUT`, `:PATCH`, `:DELETE`, `:HEAD`.
- `handler` — a function `{req res}` or a closure.
### `statics`
A list of static-file directories:
%{ path "/assets" dir "./public" }
Requests under `/assets/` are served from files under `./public/`.
Use `--watch` on `http/serve` to log filesystem changes during
development.
### `prefix`
A path prefix applied to every route and static in the router:
def api %{ prefix "/api/v1" routes @{ %{ path "/users" method :GET handler list-users } # GET /api/v1/users } } |> [http/router]
### `middleware`
A list of handler fns invoked in order before the matched route.
Middleware receives `{req res}` and can short-circuit by writing a
response.
## Request and Response
Handlers receive `%Request{}` and `%Response{}` tagged maps.
def get-user {|req res| @doc "Fetch a user by id." set id $req~params~id # captured from :id in the path set user [load-user-from-db $id] if $user { http/json $res $user } else { http/status $res 404 http/html $res "not found" } }
### Request fields
| Field | Type | Notes |
|---|---|---|
| `:method` | keyword | `:GET`, `:POST`, etc. |
| `:path` | string | URL path |
| `:headers` | map | Lowercased header names → values |
| `:params` | map | Path captures (`:id` → `$req~params~id`) |
| `:query` | map | Query string parsed |
| `:body` | string | Request body |
### Response helpers
| Function | Purpose |
|---|---|
| `[http/respond $res $body $content-type]` | Write response body |
| `[http/json $res $data]` | JSON response (auto-encodes) |
| `[http/html $res $html]` | HTML response |
| `[http/status $res $code]` | Set status code |
| `[http/header $res "name" "value"]` | Set response header |
| `[http/redirect $res "/url" ?code]` | Redirect (default 302) |
## Implicit vs Explicit Response
Two ways to send a response:
**Implicit** — return the body from the handler:
%{ path "/" method :GET handler { "Hello, World!" } }
A bare string return becomes a 200 OK with the string as the body.
**Explicit** — write via response helpers:
def get-user {|req res| http/status $res 200 http/header $res "Content-Type" "application/json" http/respond $res [json/stringify $user] "application/json" }
Use explicit when you need to set status, headers, or a specific
content type.
## Async Handlers
Handlers run as coroutines on the event loop. Each request spawns a
coroutine; the connection stays alive until the coroutine completes.
Use `await` for any async operation:
def proxy {|req res| @doc "Proxy to an upstream service." set backend [await [http/get [str "http://127.0.0.1:3001" $req~path]]] http/status $res $backend~status http/respond $res $backend~body "application/json" } def api %{ routes @{ %{ path "/api/*rest" method :GET handler $proxy } } } |> [http/router]
def slow {|req res| @doc "Sleep 2 seconds, non-blocking for other connections." sleep 2000 http/html $res "<h1>done</h1>" }
Synchronous handlers continue to work identically — they run to
completion in a single event loop tick.
## Keep-Alive
By default connections close after each response. To enable HTTP/1.1
keep-alive:
http/header $res "Connection" "keep-alive"
## HTML Templating
`[html ... \html]` blocks produce an HTML string. The full templating
system — interpolation forms, escaping, partials, defining your own
template blocks, and `[sql ... \sql]` — is documented in
[Templating](templating).
def render-user {|user| [html <div class="user"> <h1>$user~name</h1> <p>Age: $user~age</p> </div> \html] } def handler {|req res| http/html $res [render-user $alice] }
## Static Files
def app %{ statics @{ %{ path "/assets" dir "./public" } %{ path "/downloads" dir "/var/files" } } routes @{ %{ path "/" method :GET handler { [fs/read "./public/index.html"] } } } } |> [http/router]
Add `--watch` to log changes:
http/serve $app :8080 --watch
## Middleware
Middleware fires in order before the matched route. Each middleware
fn receives `{req res}`:
def logger {|req res| @doc "Log every request." echo "$req~method $req~path" } def auth {|req res| @doc "Require an Authorization header." if ![get $req~headers "authorization"] { http/status $res 401 http/html $res "auth required" return } } def api %{ middleware @{ $logger $auth } routes @{ %{ path "/secure" method :GET handler { "secret" } } } } |> [http/router]
### Async middleware
Middleware runs as coroutines — the same machinery as route handlers —
so `[await ...]` works inside them. A middleware that awaits a database
lookup suspends only its own request; other connections keep running:
def load-user {|req res| @doc "Resolve the Authorization token to a user before the handler runs." def token [get $req~headers "authorization"] if token { set user [await [sqlite/query $db "SELECT * FROM users WHERE token = ?" @{ $token }]] if ![empty? $user] { [http/header $res "X-User" $user~first~name] } } } def api %{ middleware @{ $load-user } routes @{ %{ path "/me" method :GET handler { "profile" } } } } |> [http/router]
A middleware that returns `false` rejects the request — the response
state is flushed as-is and neither downstream middleware nor the route
handler runs. A middleware that returns early (after writing a response)
has the same effect: it prevents downstream middleware and the route
handler from running.
## Multi-Dispatch Pattern
For non-trivial handlers, define the logic separately from the
HTTP adapter:
# Pure logic — testable without HTTP def load-user {|id^string| [db-query "SELECT * FROM users WHERE id = ?" @{ $id }] } # HTTP adapter def get-user {|req res| set user [load-user $req~params~id] http/json $res $user } def api %{ routes @{ %{ path "/users/:id" method :GET handler $get-user } } } |> [http/router]
This pattern (logic in `def`, HTTP shape in handler) keeps handlers
small and lets you unit-test the business logic without spinning up
a server.
## Examples
### JSON API
def api %{ prefix "/api/v1" routes @{ %{ path "/users" method :GET handler list-users } %{ path "/users/:id" method :GET handler get-user } %{ path "/users" method :POST handler create-user } } } |> [http/router] def users-db @{} def list-users {|req res| http/json $res $users-db } def get-user {|req res| set id $req~params~id set u [get $users-db $id] if $u { http/json $res $u } else { http/status $res 404 http/json $res %Err{ :message "not found" } } } def create-user {|req res| set data [json/parse $req~body] set id $data~id set users-db [assoc $users-db $id $data] http/status $res 201 http/json $res $data } http/serve $api :8080
### SPA with static + fallback
def app %{ statics @{ %{ path "/assets" dir "./dist/assets" } } routes @{ %{ path "/*rest" method :GET handler { [fs/read "./dist/index.html"] } } } } |> [http/router]
## Server Lifecycle
def srv [http/serve $app :8080] # ... later ... http/stop $srv
`http/stop` is graceful — in-flight requests finish before the server
exits.
## Cluster Mode
For production workloads, the App Server runs the same app in N worker
processes, each binding the same port with `SO_REUSEPORT` — the kernel
load-balances accepts with no application-level proxy hop. This is the
App Server; see [App Server](app-server) for cluster deployment,
reloads, and cross-worker state.
## Quick Reference
| Form | Purpose |
|---|---|
| `def name %{ ... } \|> [http/router]` | Build a router |
| `[http/serve $router :port]` | Start server |
| `[http/serve $router :port --watch]` | Watch static dirs |
| `[http/stop $server]` | Stop server |
| `[http/respond $res $body $content-type]` | Write response |
| `[http/json $res $data]` | JSON response |
| `[http/html $res $html]` | HTML response |
| `[http/status $res $code]` | Set status |
| `[http/header $res "name" "value"]` | Set header |
| `[http/redirect $res "/url" ?code]` | Redirect |
| Cluster stdlib functions | See [App Server](app-server) |