Skip to main content

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

main.rank
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 a Runtime::ExecutionContext<Routes> and returns an HTTP::Response (or an inlined object with status and body).

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.

FieldTypeDescription
methodstringHTTP method of the request
pathstringRequest path
paramsObjectNamed path parameters (narrowed per route)
queryObjectParsed query parameters (narrowed per route)
bodyT | nullParsed request body, or null
headersObjectDeclared request headers (narrowed per route)
cookiesObjectDeclared cookies (narrowed per route)
authPrincipal | nullResolved auth principal when the route declares auth
ctxObjectRequest metadata (see below)

ctx fields:

FieldTypeDescription
ctx.request_idstringUnique identifier for the request
ctx.time_receivedstringRFC 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],
}
FieldRequiredDescription
statusyesHTTP status code
bodyyesResponse body value. For json and yaml, this may be an ordinary document value or a top-level std::Emit<T> descriptor.
formatnoSerialization format. Inherits from pub config when omitted. Default: json
headersnoAdditional response headers
cookiesnoCookies to set on the response

Response formats

  • json — serializes body as JSON. The default. Top-level std::Emit<T> bodies are realized first.
  • yaml — serializes body as YAML. Top-level std::Emit<T> bodies are realized first.
  • text — writes body as a plain string. Requires body to be a string.

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:

FieldTypeDefaultDescription
allowedOrigins[string][]CORS allowed origins. * permits all origins
allowCredentialsboolfalseSet Access-Control-Allow-Credentials: true
defaultResponseFormatjson|yaml|textjsonFormat used when a route response omits format
maxRequestBodyBytesnumber1048576 (1 MB)Maximum inbound body size
requestTimeoutMsnumber30000Per-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:

FieldTypeDescription
in`header` | `query`Where to read the key
namestringHeader or query parameter name
prefixstringToken prefix to strip before passing to verify (e.g. `Bearer `)
verify|HTTP::ApiKeyToken| -> Principal | HTTP::ResponseValidation 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

Dockerfile
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.

bootstrap
#!/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}"
template.yaml (SAM)
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.