Skip to content

Routes & Params

A route binds an HTTP method and path to a handler body. Path segments prefixed with : are typed parameters; a trailing *name is a wildcard capturing the rest of the path.

route  := 'route' METHOD STRING params? ('->' Type)? '{' stmt* '}'
params := '(' param (',' param)* ')'
param  := 'query'? NAME ':' Type ('=' Expr)?     # path (default) or query source

Typed path parameters

(id: int) binds and parses the :id path segment; a value that fails to parse becomes a 400. Wildcards bind a string.

weld
route GET "/users/:id" (id: int) {
    respond "user id = {id}\n"
}

route GET "/files/*path" {
    respond "serving file: {path}\n"
}

Query parameters

Prefix a param with query to read it from the query string. Give it a default with =, or make it optional with T? and coalesce with ??:

weld
route GET "/users/:id/posts/:post" (id: int, post: int, query q: string?) {
    respond "user {id}, post {post}, filter={q ?? "none"}\n"
}

route GET "/greet/:name" (name: string, query loud: bool = false) {
    let greeting = "Hello, " + name
    respond "{greeting} (loud={loud})\n"
}

Statements in a handler

Inside a route body you can set the status, the content type, and respond:

weld
route GET "/" {
    type "text/html"          # set Content-Type
    respond "<h1>Weld API</h1>\n"
}

route GET "/health" {
    status 200                # set status code
    respond "OK\n"
}

route GET "/teapot" {
    status 418
    respond "I'm a little teapot\n"
}
  • respond Expr — send the value (a record/list becomes JSON, a string is sent as text).
  • status Expr — set the HTTP status code.
  • type STRING — set the Content-Type header.
  • emit Expr — stream a chunk (see below).

Response headers & cookies

Inside a route handler (or a decorator) you can set arbitrary response headers and cookies — useful for CORS, caching, and cookie sessions. These are not available in a hub or alongside emit streaming.

  • header name value — set an arbitrary response header, e.g. a CORS or caching header, or a raw Set-Cookie.
  • cookie name value — set a Set-Cookie with secure session defaults (Path=/; HttpOnly; SameSite=Lax). For full control (Max-Age, Secure) write the header yourself with header "Set-Cookie" "…".
  • request_cookie(name) -> string? — read a cookie off the request (companion to request_header(name)).
weld
route GET "/api/data" -> string {
    header "Access-Control-Allow-Origin" "*"
    header "Cache-Control" "max-age=60"
    respond "some data"
}

route GET "/login/:user" (user: string) -> string {
    cookie "session" "token-for-{user}"
    respond "logged in as {user}"
}

route GET "/me" -> string {
    let token = request_cookie("session") else { fail 401 "not logged in" }
    respond "session={token}"
}

Form bodies & file uploads

A handler can read HTML form fields and multipart file uploads off the request body. Each returns an optional (null when absent), so pair it with ?? or an else:

  • form_field(name) -> string? — a field from a urlencoded or multipart/form-data body (the encoding is auto-detected).
  • upload(name) -> string? — a multipart file part's raw bytes (binary-safe).
  • upload_filename(name) -> string? — an uploaded file's filename.
weld
route POST "/contact" -> string {
    let name = form_field("name") ?? "anonymous"
    let message = form_field("message") else { fail 400 "message is required" }
    respond "thanks {name}, we received {len(message)} chars"
}

route POST "/upload" -> string {
    let file = upload("file") else { fail 400 "no file part named 'file'" }
    let filename = upload_filename("file") ?? "upload.bin"
    respond "stored {filename} ({len(file)} bytes)"
}

For inputs too large to buffer, stream the body with for chunk in request_body() — each chunk is a slice of raw bytes, read in constant memory (the whole body is never held at once). Such a route can't also declare a JSON body parameter.

weld
route POST "/measure" -> string {
    var bytes = 0
    for chunk in request_body() {
        bytes = bytes + len(chunk)
    }
    respond "received {bytes} bytes"
}

To stream typed records instead of raw bytes, declare the body as Stream<T> and for over it — each record is JSON-decoded one at a time off the socket (constant memory, no MAX_BODY_SIZE limit), accepting a JSON array or NDJSON. body rows: [T] buffers the whole list; body rows: Stream<T> streams it — the loop body is the same. See Bulk import.

Interpolation

{expr} inside a string evaluates a type-checked expression ("sum={a + b}", "hi {name}"); a doubled brace (two { or two }) is a literal brace. Strings support the escapes \n \t \r \" \\ \0 and \u{HEX} (a Unicode codepoint, e.g. "\u{2603}" → ☃), and # starts a line comment.

weld
route GET "/add/:a/:b" (a: int, b: int) {
    respond "sum={a + b}, quotient={a / b}\n"
}

Streaming responses

Use emit instead of respond to stream a response chunk by chunk (HTTP chunked transfer encoding) — the full body is never held in memory; each chunk is flushed as it is produced:

weld
route GET "/stream" {
    type "text/plain"
    for x in [10, 20, 30] { emit "value = {x}\n" }
}

type, header, and cookie set before the first emit are written into the chunked reply's head, so you can stream a proper download. Read a file chunk-by-chunk with read_file_stream to serve arbitrarily large files in constant memory:

weld
route GET "/download/:name" (name: string) -> string {
    type "application/octet-stream"
    header "content-disposition" "attachment; filename=\"{name}\""
    for chunk in read_file_stream("assets/{name}") { emit chunk }   # nothing emitted if it can't open
}

A respond before the first emit is a terminal early bail-out — it sends a normal buffered reply and returns, so you can validate first (if bad { status 400 respond "…" }) and stream only on the happy path.

Static files

serve BASE SUB sends the file at BASE/SUB — content type inferred from the extension, 404 if missing, 403 on a .. traversal attempt. Pair it with a wildcard route so the remaining path is the subpath:

weld
route GET "/assets/*path" (path: string) {
    serve "public" path                 # serves ./public/<path>
}