HTTP Server
Rank programs can run as long-lived HTTP servers. The rank serve command compiles the entry module and starts a listener that evaluates pub main for each incoming request.
Use this page when you want typed HTTP routes, request narrowing, and provider-backed handlers inside rank serve. If you only need generated files or manifests, start with Introduction or Examples instead.
For deployment-oriented server examples, see Serve — Docker, Serve — Lambda, and S3 Logs.
Quick start
use std::HTTP
use std::Runtime
HealthRoute = HTTP::Route {
method: `GET`,
path: `/health`
}
Routes = HealthRoute
pub main = |req: Runtime::ExecutionContext<Routes>| -> HTTP::Response {
return {
status: 200,
body: { ok: true }
}
}
rank serve .
# Listening on http://127.0.0.1:3000
curl http://127.0.0.1:3000/health
# {"ok":true}
The compiled program must export:
pub main— a function that accepts aRuntime::ExecutionContext<Routes>and returns anHTTP::Response(or an inlined object withstatusandbody).
Defining routes
Routes are named type aliases that describe a single HTTP endpoint.
use std::HTTP
GetUsersRoute = HTTP::Route {
method: `GET`,
path: `/users`
}
Union them together to form the route set:
Routes = GetUsersRoute | CreateUserRoute | DeleteUserRoute
Pass Routes as the type argument to Runtime::ExecutionContext<Routes> so the type checker can narrow the context inside each match arm.
Supported methods
Any standard HTTP method string: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`.
Literal paths
IndexRoute = HTTP::Route { method: `GET`, path: `/` }
AdminRoute = HTTP::Route { method: `GET`, path: `/admin/dashboard` }
Path parameters
Use the match prefix with {name:Type} placeholders to declare typed path parameters:
use std::Regex
@constraint(cond: |id| Regex::matches(id, re`^[a-z0-9_-]+$`) )
UserId = string
UserRoute = HTTP::Route {
method: `GET`,
path: match`/users/{id:UserId}`
}
Inside the handler the params field is narrowed to { id: UserId }:
pub main = |req: Runtime::ExecutionContext<UserRoute>| -> HTTP::Response {
return {
status: 200,
body: { id: req.params.id }
}
}
Query parameters
Declare the expected query schema with an Object type. All fields are optional unless explicitly required:
SearchRoute = HTTP::Route {
method: `GET`,
path: `/search`,
query: Object {
q: string,
limit?: number,
offset?: number
}
}
Inside the handler req.query is narrowed to the declared schema. Optional fields have type T | null when not present.
Request body
Declare the body schema for routes that accept a payload:
CreateUserRoute = HTTP::Route {
method: `POST`,
path: `/users`,
body: Object {
name: string,
email: string,
active?: bool
}
}
req.body is the parsed, type-checked body. For routes without a body field, req.body is null.
Request headers and cookies
Declare the headers or cookies your handler needs to read:
InspectRoute = HTTP::Route {
method: `GET`,
path: `/inspect`,
headers: Object {
accept: string,
x-trace-id?: string
},
cookies: Object {
session: string
}
}
Fields declared in headers and cookies are validated on every inbound request. Missing required fields produce a 400 response before the handler runs.
The execution context
Runtime::ExecutionContext<Routes> is the type of the value passed to pub main. Its fields depend on which arm of the route union is active.
| Field | Type | Description |
|---|---|---|
method | string | HTTP method of the request |
path | string | Request path |
params | Object | Named path parameters (narrowed per route) |
query | Object | Parsed query parameters (narrowed per route) |
body | T | null | Parsed request body, or null |
headers | Object | Declared request headers (narrowed per route) |
cookies | Object | Declared cookies (narrowed per route) |
auth | Principal | null | Resolved auth principal when the route declares auth |
ctx | Object | Request metadata (see below) |
ctx fields:
| Field | Type | Description |
|---|---|---|
ctx.request_id | string | Unique identifier for the request |
ctx.time_received | string | RFC 3339 timestamp when the request arrived |
Narrowing with match
When Routes is a union, use match to narrow req to a specific route's context:
Routes = HealthRoute | StatusRoute | UserRoute
pub main = |req: Runtime::ExecutionContext<Routes>| -> HTTP::Response {
return match req {
HealthRoute => { status: 200, body: { ok: true } },
StatusRoute => { status: 200, body: { verbose: req.query.verbose } },
UserRoute => { status: 200, body: { id: req.params.id } },
}
}
The compiler enforces that all arms are covered. Each arm receives a fully narrowed context — req.params.id is only accessible inside the UserRoute arm.
Responses
pub main returns an HTTP::Response (or an inline object with the same shape):
HTTP::Response = Object {
status: number,
body: unknown,
format?: `json` | `yaml` | `text`,
headers?: Object { ...: string },
cookies?: [HTTP::SetCookie],
}
| Field | Required | Description |
|---|---|---|
status | yes | HTTP status code |
body | yes | Response body value. For json and yaml, this may be an ordinary document value or a top-level std::Emit<T> descriptor. |
format | no | Serialization format. Inherits from pub config when omitted. Default: json |
headers | no | Additional response headers |
cookies | no | Cookies to set on the response |
Response formats
json— serializesbodyas JSON. The default. Top-levelstd::Emit<T>bodies are realized first.yaml— serializesbodyas YAML. Top-levelstd::Emit<T>bodies are realized first.text— writesbodyas a plain string. Requiresbodyto be astring.
This is especially useful with Mutation::commit(...): the handler can return commit-aware output directly in HTTP::Response.body, and the projected receipt metadata is realized before the server serializes the response.
pub main = |req: Runtime::ExecutionContext<Routes>| -> HTTP::Response {
return match req {
JsonRoute => { status: 200, body: { ok: true } },
TextRoute => { status: 200, format: `text`, body: `pong` },
}
}
Setting cookies
Use HTTP::SetCookie in the cookies list to set response cookies:
HTTP::SetCookie = Object {
name: string,
value: string,
http_only?: bool,
secure?: bool,
same_site?: `Lax` | `Strict` | `None`,
path?: string,
domain?: string,
max_age?: number,
expires?: string,
}
pub main = |req: Runtime::ExecutionContext<Routes>| -> HTTP::Response {
return {
status: 200,
body: { ok: true },
cookies: [
{
name: `session`,
value: `tok_abc123`,
http_only: true,
secure: true,
same_site: `Lax`
}
]
}
}
Server configuration
Export a pub config binding to control server-level behavior. It must be an object literal or a zero-argument function:
pub config = {
allowedOrigins: [`https://app.example.com`],
allowCredentials: true,
defaultResponseFormat: `json`,
maxRequestBodyBytes: 1048576,
requestTimeoutMs: 30000
}
Or as a zero-argument function (useful when the config depends on computed values):
pub config = || {
defaultResponseFormat: `json`
}
HTTP::AppConfig fields:
| Field | Type | Default | Description |
|---|---|---|---|
allowedOrigins | [string] | [] | CORS allowed origins. * permits all origins |
allowCredentials | bool | false | Set Access-Control-Allow-Credentials: true |
defaultResponseFormat | json|yaml|text | json | Format used when a route response omits format |
maxRequestBodyBytes | number | 1048576 (1 MB) | Maximum inbound body size |
requestTimeoutMs | number | 30000 | Per-request timeout in milliseconds |
Authentication
API key auth
Use HTTP::ApiKeyAuth<Principal> to protect a route with an API key. The verify function receives the raw token and returns either the resolved principal or an error response:
use std::Env
use std::HTTP
use std::Runtime
AuthEnv = Object { API_KEY: string, ...: string }
env = Env<AuthEnv> {}
Principal = Object {
client_id: string,
scopes: [string]
}
AdminRoute = HTTP::Route {
method: `GET`,
path: `/admin`,
auth: HTTP::ApiKeyAuth<Principal> {
in: `header`,
name: `x-api-key`,
prefix: `Bearer `,
verify: |token: HTTP::ApiKeyToken| {
return token.matches(env.API_KEY)
? { client_id: `internal`, scopes: [`admin`] }
: { status: 401, body: { code: `invalid_key` } }
}
}
}
Routes = AdminRoute
pub main = |req: Runtime::ExecutionContext<Routes>| -> HTTP::Response {
return {
status: 200,
body: {
client: req.auth.client_id,
scopes: req.auth.scopes
}
}
}
HTTP::ApiKeyAuth<Principal> fields:
| Field | Type | Description |
|---|---|---|
in | `header` | `query` | Where to read the key |
name | string | Header or query parameter name |
prefix | string | Token prefix to strip before passing to verify (e.g. `Bearer `) |
verify | |HTTP::ApiKeyToken| -> Principal | HTTP::Response | Validation function |
HTTP::ApiKeyToken has one method:
token.matches(secret: string) -> bool
matches performs a constant-time comparison to prevent timing attacks. Return an HTTP::Response with a non-2xx status from verify to reject the request.
When auth succeeds, req.auth inside the handler is narrowed to Principal. The verify function runs before the handler and its result is injected into the context.
Built-in endpoints
The server exposes a readiness endpoint at /.well-known/rank/ready that is not part of the application route table. It returns 200 OK once the runtime is ready to accept traffic. Use this path for load balancer health checks and Lambda Web Adapter READINESS_CHECK_PATH configuration.
Deployment
Plain Docker
FROM node:22-slim
WORKDIR /app
RUN npm install -g @rank-lang/cli
COPY rank.toml main.rank ./
RUN rank sync .
EXPOSE 3000
CMD ["rank", "serve", ".", "--host", "0.0.0.0", "--port", "3000"]
docker build -t my-rank-app .
docker run --rm -p 3000:3000 my-rank-app
Running rank sync . during the build step caches all registry dependencies so the container starts without making outbound package requests.
AWS Lambda (Lambda Web Adapter)
Use a provided.al2023 custom runtime and attach the Lambda Web Adapter as a layer — no container image required.
#!/bin/sh
export PATH="$LAMBDA_TASK_ROOT/bin:$LAMBDA_TASK_ROOT/node_modules/.bin:$PATH"
exec rank serve "$LAMBDA_TASK_ROOT" --host 0.0.0.0 --port "${PORT:-8080}"
Resources:
RankApp:
Type: AWS::Serverless::Function
Properties:
Runtime: provided.al2023
Handler: bootstrap
Layers:
- !Sub arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:24
Environment:
Variables:
PORT: '8080'
READINESS_CHECK_PATH: /.well-known/rank/ready
sam build --use-container
sam deploy --guided
Lambda calls the bootstrap executable directly. The Lambda Web Adapter layer translates Lambda invocations into HTTP requests and forwards them to rank serve on PORT. READINESS_CHECK_PATH points to the built-in readiness endpoint so the adapter waits until the server is listening before forwarding traffic.
See the Serve Docker and Serve Lambda examples for complete runnable projects.
rank serve reference
See the rank serve CLI reference for the full list of flags including --host, --port, --dev, --trace-eval, --emit-sensitive, --allow-http-host, --allow-env, and --shutdown-grace-ms. For trace output examples and debugging workflow, see Debugging.