Examples
Every example on this page is a complete, compileable .weld file from the repository's examples/ directory — not a fragment. Each is shown in full, explained, and paired with the requests that exercise it. Compile and run any of them with:
./weld examples/<name>.weld --emit bin -O ReleaseFast && ./<name>
# then, in another shell:
curl localhost:8080/...Examples that talk to an external service (Postgres, an upstream, gRPC) read their endpoints from environment variables — set those first (shown per example).
Basics
hello.weld — routes, status, content type
The smallest useful service: static routes that set a content type and status and return text.
route GET "/" {
type "text/html"
respond "<h1>Welcome to Weld</h1>\n"
}
route GET "/hello" {
respond "Hello, world!\n"
}
route GET "/health" {
status 200
respond "OK\n"
}
route GET "/teapot" {
status 418
respond "I'm a little teapot\n"
}$ curl -i localhost:8080/ # Content-Type: text/html
$ curl localhost:8080/hello # Hello, world!
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/teapot # 418calc.weld — expressions, bindings, interpolation
Everything inside {…} is a real, type-checked expression: arithmetic, comparisons, &&/||, let bindings, string concatenation, and a defaulted query parameter.
route GET "/add/:a/:b" (a: int, b: int) {
respond "sum={a + b} diff={a - b} prod={a * b} quot={a / b} mod={a % b}\n"
}
route GET "/cmp/:a/:b" (a: int, b: int) {
let bigger = a > b
respond "a>b: {bigger}, equal: {a == b}, in_range: {a > 0 && a < 100}\n"
}
route GET "/greet/:name" (name: string, query loud: bool = false) {
let greeting = "Hello, " + name
respond "{greeting}{loud}\n"
}$ curl localhost:8080/add/7/3 # sum=10 diff=4 prod=21 quot=2 mod=1
$ curl localhost:8080/cmp/9/4 # a>b: true, equal: false, in_range: true
$ curl 'localhost:8080/greet/Ada?loud=true' # Hello, Adatrueapi.weld — typed params, wildcards, optional query
Typed path parameters (a bad int yields 400), an optional query param defaulted with ??, and a trailing wildcard that captures the remainder of the path.
route GET "/" {
type "text/html"
respond "<h1>Weld API</h1>\n"
}
# :id is typed int; {id} interpolates it (bad int -> 400)
route GET "/users/:id" (id: int) {
respond "user id = {id}\n"
}
# two int params plus an optional query param, defaulted with ??
route GET "/users/:id/posts/:post" (id: int, post: int, query q: string?) {
respond "user {id}, post {post}, filter={q ?? "none"}\n"
}
# trailing wildcard captures the remainder of the path
route GET "/files/*path" {
respond "serving file: {path}\n"
}
route GET "/health" {
status 200
respond "OK\n"
}$ curl localhost:8080/users/7/posts/99?q=recent # user 7, post 99, filter=recent
$ curl localhost:8080/files/docs/readme.md # serving file: docs/readme.md
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/users/abc # 400constants.weld — top-level let constants
Module-scoped let bindings are evaluated once at compile time and used by bare name from any route or function; a later one can build on an earlier one. Great for config defaults — a query parameter can even default to a constant.
# Top-level `let` — module-scoped constants, evaluated at compile time and
# usable by bare name from any route or fn. A `let` may reference an earlier one.
let siteName = "Weld Demo"
let pageSize: int = 20
let maxPageSize = pageSize * 5
let taxRate = 0.08
type Page = { site: string, size: int, max: int }
fn label(n: int) -> string {
return "{siteName}: showing {n} of up to {maxPageSize}"
}
route GET "/config" -> Page {
respond Page { site: siteName, size: pageSize, max: maxPageSize }
}
route GET "/list" (query size: int = pageSize) -> string {
# `size` is a query param defaulting to the pageSize constant.
respond label(size)
}
route GET "/tax" -> string {
respond "tax rate is {taxRate}"
}$ curl localhost:8080/config # {"site":"Weld Demo","size":20,"max":100}
$ curl localhost:8080/list # Weld Demo: showing 20 of up to 100
$ curl 'localhost:8080/list?size=5' # Weld Demo: showing 5 of up to 100
$ curl localhost:8080/tax # tax rate is 0.08Types & data
users.weld — records and automatic JSON
Return a record or a list of records and Weld encodes it as JSON (application/json); return a string and it is sent as text. A route's -> T is the response contract.
type User = {
id: int,
name: string,
email: string?, # optional field -> JSON null when absent
}
# Returns a single User, encoded as JSON automatically
route GET "/users/:id" (id: int) -> User {
respond User { id: id, name: "Ada Lovelace", email: "ada@example.com" }
}
# Returns a JSON array of Users
route GET "/users" -> [User] {
respond [
User { id: 1, name: "Ada", email: "ada@example.com" },
User { id: 2, name: "Alan Turing", email: null },
]
}
# Field access; returns a plain-text string
route GET "/users/:id/name" (id: int) -> string {
let u = User { id: id, name: "Grace Hopper", email: null }
respond u.name
}$ curl localhost:8080/users/42
{"id":42,"name":"Ada Lovelace","email":"ada@example.com"}
$ curl localhost:8080/users/1/name # Grace Hoppercrud.weld — typed JSON request bodies
A body x: T parameter parses the JSON request body into a typed record (invalid JSON → 400).
type NewUser = { name: string, email: string? }
type User = { id: int, name: string, email: string? }
# The JSON request body is parsed into a typed NewUser (bad JSON -> 400)
route POST "/users" (body input: NewUser) -> User {
respond User { id: 1, name: input.name, email: input.email }
}
route GET "/users/:id" (id: int) -> User {
respond User { id: id, name: "Ada Lovelace", email: "ada@example.com" }
}$ curl -X POST localhost:8080/users -d '{"name":"Ada","email":"ada@x.com"}'
{"id":1,"name":"Ada","email":"ada@x.com"}store.weld — in-memory CRUD with state
state holds server-global, lock-guarded values that persist across requests. Values assigned into state are deep-copied into a durable allocator, so request-scoped strings survive.
type NewUser = { name: string, email: string? }
type User = { id: int, name: string, email: string? }
state db {
var users: [User] = []
var nextId: int = 1
}
# Create: parse the body, assign an id, persist it
route POST "/users" (body input: NewUser) -> User {
let user = User { id: db.nextId, name: input.name, email: input.email }
db.users = push(db.users, user)
db.nextId = db.nextId + 1
status 201
respond user
}
# List everything
route GET "/users" -> [User] {
respond db.users
}
# Count
route GET "/count" {
respond "count={len(db.users)}\n"
}$ curl -X POST localhost:8080/users -d '{"name":"Ada"}' # {"id":1,...}
$ curl -X POST localhost:8080/users -d '{"name":"Alan"}' # {"id":2,...}
$ curl localhost:8080/count # count=2enums.weld — proto-style enums
Each variant has a number (required) and an optional string label; a hidden Unspecified = 0 is the fallback for unknown inputs. Role(x) reinterprets a number or string into the enum; int(r) / str(r) go back.
enum Role {
# Unspecified = 0 : "unspecified" is implicit
Admin = 1 : "admin",
User = 2 : "user",
}
type Account = {
id: int,
role: Role, # enum field -> JSON string
}
# Enum in a JSON record; Role.Admin is a variant reference
route GET "/accounts/:id" (id: int) -> Account {
respond Account { id: id, role: Role.Admin }
}
# Reinterpret a path string into an enum, then back to int/string
route GET "/role/:name" (name: string) {
let r = Role(name) # string -> enum (unknown -> Unspecified)
respond "name={name} number={int(r)} string={str(r)}\n"
}
# Reinterpret a number into an enum
route GET "/role-num/:n" (n: int) {
let r = Role(n)
respond "number={n} -> {str(r)}\n"
}$ curl localhost:8080/accounts/1 # {"id":1,"role":"admin"}
$ curl localhost:8080/role/admin # name=admin number=1 string=admin
$ curl localhost:8080/role/nope # name=nope number=0 string=unspecifiedsumtypes.weld — tagged unions & exhaustive match
A type is a record or a sum — a tagged union with positional payloads. Construct a variant qualified (Shape.Circle(2.0)); match it unqualified, binding the payload; a responded sum auto-encodes as {"tag":…,"values":[…]}.
type Shape =
| Circle(float)
| Rect(float, float)
| Empty
fn area(s: Shape) -> float {
return match s {
Circle(r) => 3.14159 * r * r,
Rect(w, h) => w * h,
Empty => 0.0,
}
}
fn describe(s: Shape) -> string {
return match s {
Circle(r) => "circle r={r}",
Rect(w, h) => "rect {w}x{h}",
Empty => "empty",
}
}
route GET "/area/circle/:r" (r: float) -> float { respond area(Shape.Circle(r)) }
route GET "/area/rect/:w/:h" (w: float, h: float) -> float { respond area(Shape.Rect(w, h)) }
route GET "/describe/rect/:w/:h" (w: float, h: float) -> string { respond describe(Shape.Rect(w, h)) }
# Build a shape by name and return it as JSON (exercises the tagged-union encoding).
route GET "/shape/:kind/:x" (kind: string, x: float) -> Shape {
let s = match kind {
"circle" => Shape.Circle(x),
_ => Shape.Empty,
}
respond s
}$ curl localhost:8080/area/rect/3/4 # 12
$ curl localhost:8080/shape/circle/2.0 # {"tag":"Circle","values":[2]}
$ curl localhost:8080/shape/none/0 # {"tag":"Empty","values":[]}Control flow & abstraction
features.weld — functions, for/var, match, indexing
User fns, builtins inside interpolation, var + assignment, for, push, list indexing, and match over both an enum and a string.
enum Role { Admin = 1 : "admin", User = 2 }
fn double(x: int) -> int {
return x * 2
}
fn greet(name: string) -> string {
return "Hello, " + upper(name)
}
# user functions + builtins inside interpolation
route GET "/calc/:n" (n: int) {
let d = double(n)
respond "double={d} greet={greet("ada")} len={len("hello")}\n"
}
# for + var + assignment + push + indexing
route GET "/sum/:n" (n: int) {
var total = 0
var xs = [1, 2, 3]
xs = push(xs, n)
for x in xs {
total = total + x
}
respond "total={total} count={len(xs)} first={xs[0]}\n"
}
# match on an enum
route GET "/role/:name" (name: string) {
let r = Role(name)
let label = match r {
Role.Admin => "an admin",
Role.User => "a user",
_ => "unknown",
}
respond "role={str(r)} label={label}\n"
}
# match on a string
route GET "/hi/:lang" (lang: string) {
let g = match lang {
"es" => "Hola",
"fr" => "Bonjour",
_ => "Hello",
}
respond "{g}, {name(lang)}!\n"
}
fn name(lang: string) -> string {
return match lang {
"es" => "amigo",
"fr" => "ami",
_ => "friend",
}
}$ curl localhost:8080/calc/5 # double=10 greet=Hello, ADA len=5
$ curl localhost:8080/sum/4 # total=10 count=4 first=1
$ curl localhost:8080/hi/es # Hola, amigo!iterator.weld — methods, mutation, custom iterators
A method is fn Type.name(self, …); mut self mutates the receiver in place. Any type with a next(mut self) -> T? method is iterable by for.
type Counter = { n: int }
fn Counter.inc(mut self) { # mutates the receiver
self.n = self.n + 1
}
fn Counter.value(self) -> int { # reads it (by value)
return self.n
}
# An iterator that yields the numbers `from..to` (exclusive), one call at a time.
type Range = { cur: int, to: int }
fn Range.next(mut self) -> int? {
if self.cur >= self.to {
return null # null signals "finished"
}
let v = self.cur
self.cur = self.cur + 1
return v
}
# Uses both: a Counter counts how many odd numbers Range yields in [0, n).
route GET "/odds/:n" (n: int) {
var odds = Counter { n: 0 }
let r = Range { cur: 0, to: n }
for x in r {
if x % 2 != 0 {
odds.inc()
}
}
respond "there are {odds.value()} odd numbers in 0..{n}"
}$ curl localhost:8080/odds/10 # there are 5 odd numbers in 0..10collections.weld — list builtins & indexed iteration
slice, concat, push, pop, reverse, and contains are built-in list operations; for i, x in xs yields an index alongside each element, and an empty [] on the right of ?? infers its element type from the left. The higher-order ops (map, filter, reduce) and sort/insert/removeAt are methods too, so they chain.
# List builtins, indexed iteration, and empty-literal inference.
fn capLast(xs: [string], n: int) -> [string] {
# slice(list, start, end) replaces a hand-rolled drop loop.
if len(xs) <= n { return xs }
return slice(xs, len(xs) - n, len(xs))
}
route GET "/tags" -> string {
var tags = ["intro", "weld", "demo", "lists"]
tags = concat(tags, ["extra"]) # join two lists
tags = push(tags, "last") # append one element
tags = capLast(tags, 4) # keep the final 4
var recent = reverse(tags) # newest first
recent = pop(recent) # drop the newest
var out = ""
for i, t in recent { # indexed iteration
out = out + "{i}:{t} "
}
respond "has-weld={contains(tags, "weld")} list={out}"
}
route GET "/merge" (query extra: string?) -> string {
# An empty `[]` takes its element type from the `??` left side.
var base: [string]? = null
var items = base ?? []
items = push(items, extra ?? "none")
respond "count={len(items)} first={items[0]}"
}
route GET "/stats" -> string {
let xs = [5, 3, 8, 1, 9, 2]
# Collection ops are also methods, so they chain: filter -> map -> sort.
let bigs = xs.filter(x => x > 3).map(x => x * 10) # [50, 80, 90]
let total = xs.reduce(0, (acc, x) => acc + x) # 28
let ranked = xs.sort() # [1, 2, 3, 5, 8, 9]
let edited = xs.insert(0, 0).removeAt(3) # insert 0 at front, drop index 3
respond "n_big={len(bigs)} total={total} min={ranked[0]} max={ranked[len(ranked) - 1]} edited0={edited[0]}"
}$ curl localhost:8080/tags # has-weld=false list=0:last 1:extra 2:lists
$ curl 'localhost:8080/merge?extra=hi' # count=1 first=hi
$ curl localhost:8080/merge # count=1 first=none
$ curl localhost:8080/stats # n_big=3 total=28 min=1 max=9 edited0=0generics.weld — generic types, methods, interfaces
type Name<T> is generic (arguments inferred at use); <T: Iface> constrains a parameter to an interface, checked structurally. Everything monomorphizes to Zig comptime generics.
type Box<T> = { value: T }
fn Box<T>.get(self) -> T { return self.value }
fn Box<T>.replace(mut self, v: T) { self.value = v }
type Pair<A, B> = { first: A, second: B }
# An interface is a named set of methods; `Self` is the implementing type.
interface Show {
fn show(self) -> string
}
type Point = { x: int, y: int }
fn Point.show(self) -> string { return "({self.x}, {self.y})" } # Point satisfies Show
# `T: Show` requires the argument to implement Show — a compile error otherwise.
# Inside the generic body we can call the constrained interface method *through*
# the type parameter (`self.item.show()`), which dispatches to the concrete type.
type Labeled<T: Show> = { item: T }
fn Labeled<T: Show>.render(self) -> string {
return "[" + self.item.show() + "]"
}
route GET "/demo" {
var b = Box { value: 41 } # Box<int>, inferred
b.replace(b.get() + 1)
let p = Pair { first: "answer", second: 42 } # Pair<string, int>
let lbl = Labeled { item: Point { x: 3, y: 4 } }
respond "box={b.get()} pair={p.first}/{p.second} labeled={lbl.render()}"
}$ curl localhost:8080/demo # box=42 pair=answer/42 labeled=[(3, 4)]hashmap.weld — a generic HashMap written in Weld
The payoff of the whole language: a fully generic HashMap<K: Key, V> — generics, an interface bound, interface dispatch through the type parameter (key.hash()), a generic static constructor, expected-type inference, mutable arrays, and the hash builtin — with no built-in map involved.
interface Key {
fn hash(self) -> int
fn equals(self, other: Self) -> bool
}
type Entry<K: Key, V> = { key: K?, val: V?, used: bool }
type HashMap<K: Key, V> = { slots: [Entry<K, V>] }
# A generic static constructor (no `self`). Its K, V are inferred from the call
# site's expected type — `HashMap.new(16)` below.
fn HashMap<K: Key, V>.new(cap: int) -> HashMap<K, V> {
return HashMap { slots: filled(cap, Entry { key: null, val: null, used: false }) }
}
fn HashMap<K: Key, V>.set(mut self, key: K, value: V) {
let cap = len(self.slots)
var i = ((key.hash() % cap) + cap) % cap
for _probe in self.slots {
let e = self.slots[i]
if !e.used { self.slots[i] = Entry { key: key, val: value, used: true } return }
let ek = e.key else { return }
if ek.equals(key) { self.slots[i] = Entry { key: key, val: value, used: true } return }
i = (i + 1) % cap
}
}
fn HashMap<K: Key, V>.get(self, key: K) -> V? {
let cap = len(self.slots)
var i = ((key.hash() % cap) + cap) % cap
for _probe in self.slots {
let e = self.slots[i]
if !e.used { return null }
let ek = e.key else { return null }
if ek.equals(key) { return e.val }
i = (i + 1) % cap
}
return null
}
# A concrete key type: any record that implements `hash` and `equals` works.
type Point = { x: int, y: int }
fn Point.hash(self) -> int { return hash(self.x) * 31 + hash(self.y) }
fn Point.equals(self, other: Point) -> bool { return self.x == other.x && self.y == other.y }
route GET "/lookup/:x/:y" (x: int, y: int) {
var m: HashMap<Point, string> = HashMap.new(16) # K, V inferred here
m.set(Point { x: 1, y: 2 }, "origin-ish")
m.set(Point { x: 3, y: 4 }, "somewhere")
respond "({x},{y}) -> {m.get(Point { x: x, y: y }) ?? "(not found)"}"
}$ curl localhost:8080/lookup/3/4 # (3,4) -> somewhere
$ curl localhost:8080/lookup/9/9 # (9,9) -> (not found)errors.weld — typed errors
A fn declares an error type with -> T ! E (E is a sum type); it fails with a variant and the fallible T! carries that typed value, so a caller can match on it. An unhandled typed error auto-maps to a 500 JSON body.
type LookupError =
| NotFound
| Invalid(string)
# Returns a name, or fails with a *typed* error the caller can inspect.
fn lookup(id: int) -> string ! LookupError {
if id < 0 { fail LookupError.Invalid("id must be non-negative") }
if id > 100 { fail LookupError.NotFound }
return "user-{id}"
}
route GET "/user/:id" (id: int) -> string {
# Handle the typed error: pick a status and message per variant.
let name = lookup(id) else e {
let code = match e { NotFound => 404, Invalid(_) => 400 }
let msg = match e { NotFound => "no such user", Invalid(reason) => "bad request: {reason}" }
status code
respond msg
}
respond "found {name}"
}
# Without `else`, an unhandled typed error becomes a 500 with the error as JSON.
route GET "/raw/:id" (id: int) -> string {
respond try lookup(id)
}$ curl localhost:8080/user/5 # found user-5
$ curl -s -w ' [%{http_code}]\n' localhost:8080/user/200 # no such user [404]
$ curl localhost:8080/raw/-1 # {"tag":"Invalid","values":["id must be non-negative"]}tasks.weld — background work with every
An every N { } block runs its body on a fixed interval off the request path — ideal for periodic cleanup like expiring sessions or sweeping stale entries. It shares state (under the same lock as handlers) but has no request or response. Units are ms, s, m, h.
# tasks.weld — background work with `every N { }`.
#
# An `every` block runs its body on a fixed interval off the request path — for
# periodic cleanup like expiring sessions or sweeping stale entries. It can read
# and write `state` (guarded by the same lock as handlers) and broadcast to hubs,
# but has no request/response (no respond/status/header/fail). Units: ms, s, m, h.
# (Not supported with the experimental --green runtime yet.)
type Session = { user: string, expires: int }
state store {
var sessions: [string: Session] = [:]
}
let sessionTtlMs = 1800000 # 30 minutes
route GET "/login/:user" (user: string) -> string {
let sid = uuid()
store.sessions[sid] = Session { user: user, expires: now_ms() + sessionTtlMs }
respond sid
}
route GET "/session/:sid" (sid: string) -> string {
let s = store.sessions[sid] else { fail 401 "no such session" }
respond "user={s.user} expires_in_ms={s.expires - now_ms()}"
}
route GET "/stats" -> string {
respond "active_sessions={len(store.sessions)}"
}
# Every minute, drop sessions whose deadline has passed. Runs on its own thread;
# holds the state lock only for the sweep.
every 1m {
let now = now_ms()
var live: [string: Session] = [:]
for id, s in store.sessions {
if s.expires > now { live[id] = s }
}
store.sessions = live
}$ SID=$(curl -s localhost:8080/login/alice) # a fresh session id (uuid)
$ curl "localhost:8080/session/$SID" # user=alice expires_in_ms=1799xxx
$ curl localhost:8080/stats # active_sessions=1
# a minute after a session's 30-min TTL lapses, the sweep drops itStreaming & files
stream.weld — chunked streaming with emit
emit streams a response chunk by chunk (HTTP chunked transfer); the full body is never buffered — each chunk is flushed as it is produced.
route GET "/stream" {
type "text/plain"
emit "starting...\n"
for x in [10, 20, 30] {
emit "value = {x}\n"
}
emit "done\n"
}$ curl -N localhost:8080/stream # lines arrive one at a timestreaming.weld — constant-memory request bodies
for chunk in request_body() reads the request body in fixed-size chunks straight off the socket, so the whole thing is never buffered — uploads far larger than MAX_BODY_SIZE cost only constant memory. A streaming route can't also bind a JSON body parameter (that needs the whole body); read what you need from the chunks instead.
# streaming.weld — consume a large request body in constant memory.
#
# `for chunk in request_body()` reads the body in fixed-size chunks straight from
# the socket; each `chunk` is a slice of raw bytes and the whole body is never
# buffered. This handles uploads far larger than the buffered-body limit
# (MAX_BODY_SIZE) — the handler's memory use is constant regardless of size.
#
# A streaming route can't also declare a JSON `body` parameter (that needs the
# whole body); read what you need from the chunks instead.
route POST "/measure" -> string {
var bytes = 0
var chunks = 0
for chunk in request_body() {
bytes = bytes + len(chunk)
chunks = chunks + 1
}
respond "received {bytes} bytes in {chunks} chunk(s)"
}$ head -c 10000000 /dev/zero | curl --data-binary @- localhost:8080/measure
# received 10000000 bytes in N chunk(s)static.weld — serving static files
serve BASE SUB sends BASE/SUB with a content type inferred from the extension (404 if missing, 403 on a .. traversal). Pair it with a wildcard route.
route GET "/" -> string {
respond "see /assets/... for static files"
}
route GET "/assets/*path" (path: string) {
serve "public" path # serves ./public/<path>
}$ mkdir -p public && echo 'body{color:teal}' > public/site.css
$ curl -i localhost:8080/assets/site.css # Content-Type: text/css
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/assets/../secret # 403Requests & responses
headers.weld — response headers & cookies
header name value sets any response header; cookie name value emits a Set-Cookie with secure defaults (Path=/; HttpOnly; SameSite=Lax), and request_cookie reads one back. Together they express CORS, cache policy, and cookie sessions without hand-writing header strings.
# headers.weld — response headers & cookies: CORS, caching, and cookie sessions.
#
# `header name value` sets an arbitrary response header; `cookie name value` sets a
# Set-Cookie with secure defaults (Path=/; HttpOnly; SameSite=Lax). request_cookie
# reads one back. Together these express CORS, cache policy, and cookie-based auth
# without touching a raw header string.
type Claims = { sub: string, exp: int }
let secret = "change-me-in-prod"
# CORS + caching on a plain JSON-ish response, set with `header`.
route GET "/api/data" -> string {
header "Access-Control-Allow-Origin" "*"
header "Cache-Control" "max-age=60"
respond "some data"
}
# Log in: issue the session JWT as an HttpOnly cookie the browser sends back
# automatically. `header "Set-Cookie" …` would give full control (Max-Age, Secure).
route GET "/login/:user" (user: string) -> string {
let claims = Claims { sub: user, exp: now_ms() / 1000 + 3600 }
cookie "session" jwt_sign(claims, secret)
respond "logged in as {user}"
}
# A later request carries the cookie; read + verify it. A missing/invalid token
# falls back to a 401 via `try` (jwt_verify) or the `else` on the optional.
route GET "/me" -> string {
let token = request_cookie("session") else { fail 401 "not logged in" }
let claims = try jwt_verify(Claims, token, secret)
respond "you are {claims.sub}"
}
# Clear the session cookie by overwriting it with an immediate expiry.
route GET "/logout" -> string {
header "Set-Cookie" "session=; Path=/; Max-Age=0"
respond "logged out"
}$ curl -i localhost:8080/api/data # Access-Control-Allow-Origin: *; Cache-Control: max-age=60
$ curl -i localhost:8080/login/alice # Set-Cookie: session=<jwt>; Path=/; HttpOnly; SameSite=Lax
$ curl --cookie 'session=<jwt>' localhost:8080/me # you are alice
$ curl -i localhost:8080/logout # Set-Cookie: session=; Path=/; Max-Age=0forms.weld — form bodies, uploads & required config
form_field reads a field from either a urlencoded or multipart/form-data body; upload / upload_filename pull a file part from a multipart body. Each returns an optional, so pair it with ?? or else. require "VAR" refuses to boot unless the env var is set.
# forms.weld — HTML form bodies, file uploads, and required config.
#
# `require "VAR"` refuses to boot unless the env var is set (no more silent "").
# form_field reads a field from a urlencoded OR multipart/form-data body;
# upload / upload_filename read a file part from a multipart body. All return
# an optional (null when absent), so pair them with `??` or `else`.
require "APP_SECRET"
# Handles a classic HTML <form method=post> (application/x-www-form-urlencoded)
# as well as a multipart submission — form_field covers both encodings.
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"
}
# A multipart file upload with an accompanying text field.
route POST "/upload" -> string {
let file = upload("file") else { fail 400 "no file part named 'file'" }
let filename = upload_filename("file") ?? "upload.bin"
let note = form_field("note") ?? ""
respond "stored {filename} ({len(file)} bytes), note: {note}"
}$ APP_SECRET=dev ./forms & # require refuses to boot without it
$ curl --data 'name=Ada&message=hello' localhost:8080/contact
# thanks Ada, we received 5 chars
$ curl -F file=@photo.png -F note=hi localhost:8080/upload
# stored photo.png (NNN bytes), note: hiIntegrations
db.weld — native Postgres
A database block holds typed SQL querys, called db.name(args) (fallible). The driver speaks the Postgres v3 wire protocol directly — no libpq. $1, $2… bind from the typed params; columns map positionally onto the return record.
type User = { id: int, name: string, email: string? }
database db {
url env("DATABASE_URL") # e.g. postgres://user@host:5432/dbname
query findUser(id: int) "SELECT id, name, email FROM users WHERE id = $1" -> User
query listUsers() "SELECT id, name, email FROM users ORDER BY id" -> [User]
}
route GET "/users/:id" (id: int) -> User {
let u = db.findUser(id) else e {
status 404
respond "user {id} not found ({e.message})\n"
}
respond u
}
route GET "/users" -> [User] {
respond try db.listUsers()
}$ export DATABASE_URL='postgres://user:pass@127.0.0.1:5432/mydb'
$ ./weld examples/db.weld --emit bin -O ReleaseFast && ./db &
$ curl localhost:8080/users/1See Databases for auth, pooling, and cardinality (T / T? / [T]).
transaction.weld — atomic writes
transaction db { … } pins one pooled connection, wraps the body in BEGIN/COMMIT, and rolls back automatically if any query fails or a fail fires. Respond after the block.
type Account = { id: int, balance: int }
database bank {
url env("DATABASE_URL")
query balanceOf(id: int) "SELECT id, balance FROM accounts WHERE id = $1" -> Account
query debit(id: int, amt: int) "UPDATE accounts SET balance = balance - $2 WHERE id = $1 RETURNING id, balance" -> Account
query credit(id: int, amt: int) "UPDATE accounts SET balance = balance + $2 WHERE id = $1 RETURNING id, balance" -> Account
}
# Move `amt` from one account to another. Both updates commit together or not at all.
route POST "/transfer/:from/:to/:amt" (from: int, to: int, amt: int) -> Account {
var result = Account { id: 0, balance: 0 }
transaction bank {
let src = try bank.balanceOf(from)
if src.balance < amt { fail 400 "insufficient funds" } # rolls back
let _ = try bank.debit(from, amt)
result = try bank.credit(to, amt)
}
respond result
}
route GET "/balance/:id" (id: int) -> Account {
respond try bank.balanceOf(id)
}$ curl -X POST localhost:8080/transfer/1/2/50 # both updates commit, or neithersqlite.weld — embedded SQLite
driver sqlite selects the bundled SQLite engine instead of Postgres — the url is a file path, opened in WAL mode on first use. Queries, positional ? parameters, RETURNING, and transaction blocks are identical to the Postgres driver; only the connection layer differs. The schema is a tracked migration, applied automatically at startup. See Databases for the full comparison.
# sqlite.weld — an embedded SQL database with the bundled SQLite engine.
#
# `driver sqlite` selects the SQLite backend (Postgres is the default). The `url` is
# the database file path; it is opened in WAL mode on first use. Queries, positional
# `?` parameters, `RETURNING`, and `transaction` blocks all work exactly like the
# Postgres driver — only the connection layer differs. Run with `DB_PATH=app.db`.
#
# The schema lives in a `migration` (not a hand-run query): it is applied once,
# before serving, inside a transaction, and recorded in `_weld_migrations`.
type Todo = { id: int, title: string, done: int }
database db {
driver sqlite
url env("DB_PATH")
migration "0001_todos" {
up "CREATE TABLE todos(id INTEGER PRIMARY KEY, title TEXT, done INTEGER DEFAULT 0)"
down "DROP TABLE todos"
}
query add(title: string) "INSERT INTO todos(title) VALUES (?) RETURNING id, title, done" -> Todo
query complete(id: int) "UPDATE todos SET done = 1 WHERE id = ? RETURNING id, title, done" -> Todo
query all() "SELECT id, title, done FROM todos ORDER BY id" -> [Todo]
}
route POST "/todos/:title" (title: string) -> Todo {
respond try db.add(title)
}
route POST "/todos/:id/done" (id: int) -> Todo {
respond try db.complete(id)
}
route GET "/todos" -> [Todo] {
respond try db.all()
}$ export DB_PATH=app.db
$ ./weld examples/sqlite.weld --emit bin -O ReleaseFast && ./sqlite &
# migration 0001_todos applied on startup
$ curl -X POST localhost:8080/todos/buy-milk # {"id":1,"title":"buy-milk","done":0}
$ curl -X POST localhost:8080/todos/1/done # {"id":1,"title":"buy-milk","done":1}
$ curl localhost:8080/todos # [{"id":1,"title":"buy-milk","done":1}]gateway.weld — HTTP upstreams
An upstream calls an external REST API with typed requests/responses (std.http.Client). Calls are fallible; try propagates (auto 502), else handles.
type User = { id: int, name: string, email: string? }
upstream backend {
base env("BACKEND_URL") # e.g. http://127.0.0.1:9099
header "Accept" "application/json"
call getUser(id: int) GET "/users/{id}" -> User
call listUsers() GET "/users" -> [User]
}
# propagate failures automatically (try -> 502 on upstream error)
route GET "/proxy/:id" (id: int) -> User {
respond try backend.getUser(id)
}
route GET "/all" -> [User] {
let users = try backend.listUsers()
respond users
}
# handle failures explicitly with `else`
route GET "/safe/:id" (id: int) -> User {
let u = backend.getUser(id) else e {
status 502
respond "upstream unavailable (status={e.status}): {e.message}\n"
}
respond u
}$ export BACKEND_URL='http://127.0.0.1:9099'
$ curl localhost:8080/proxy/1httpsapi.weld — HTTPS upstreams (real TLS)
An https:// base does a real TLS handshake and verifies the server certificate against the system CA bundle — no configuration.
type DogImage = { message: string, status: string }
upstream dogs {
base "https://dog.ceo"
call random() GET "/api/breeds/image/random" -> DogImage
}
# GET /random-dog -> proxies the upStream<JSON> straight through.
route GET "/random-dog" -> DogImage {
respond try dogs.random()
}$ ./weld examples/httpsapi.weld --emit bin -O ReleaseFast && ./httpsapi &
$ curl localhost:8080/random-dog # proxies live JSON from dog.ceo over TLSgrpc.weld — gRPC over native HTTP/2
Protobuf messages are records with @field-numbers; a grpc block declares rpcs (unary and all three streaming modes). Server/bidi streams pair naturally with emit.
type HelloRequest = { name: string @1, times: int @2 }
type HelloReply = { message: string @1, length: int @2 }
grpc Greeter {
addr env("GRPC_ADDR") # host:port, e.g. 127.0.0.1:50051
rpc SayHello(HelloRequest) -> HelloReply
rpc SayHelloStream(HelloRequest) -> Stream<HelloReply> # server-streaming
rpc SumLengths(Stream<HelloRequest>) -> HelloReply # client-streaming
rpc Chat(Stream<HelloRequest>) -> Stream<HelloReply> # bidirectional
}
# HTTP route that fans out to the gRPC backend and returns the reply as JSON
route GET "/hello/:name" (name: string) -> HelloReply {
respond try Greeter.SayHello(HelloRequest { name: name, times: 1 })
}
# gRPC server-stream -> HTTP chunked stream: one reply message -> one chunk
route GET "/hellostream/:name" (name: string) {
type "text/plain"
for reply in Greeter.SayHelloStream(HelloRequest { name: name, times: 3 }) {
emit "{reply.message}\n"
}
}
# client-streaming: send several requests, get a single aggregate reply back
route GET "/sum" -> HelloReply {
respond try Greeter.SumLengths([
HelloRequest { name: "ada", times: 1 },
HelloRequest { name: "grace", times: 1 },
HelloRequest { name: "turing", times: 1 },
])
}
# bidirectional: stream requests up, stream replies down -> HTTP chunks
route GET "/chat" {
type "text/plain"
for reply in Greeter.Chat([
HelloRequest { name: "one", times: 1 },
HelloRequest { name: "two", times: 1 },
HelloRequest { name: "three", times: 1 },
]) {
emit "{reply.message}\n"
}
}$ export GRPC_ADDR='127.0.0.1:50051'
$ curl localhost:8080/hello/ada
$ curl -N localhost:8080/hellostream/ada # one chunk per streamed replyhub.weld — a WebSocket hub with auth & events
A hub is a WebSocket endpoint at /<HubName>. connect runs once (query params carry the JWT and the room to join); methods are id-correlated request/response calls; any method can push events to a filtered client set.
type Claims = { sub: string, role: string }
# Any record is an event payload; the event name on the wire is the type name.
type ChatMsg = { from: string, text: string }
type Notice = { text: string }
hub Chat {
# Runs once, on connect. `fail` rejects the connection; `identify` records this
# connection's identity (for direct messages); `join` adds it to a room.
connect (query token: string, query room: string) injects (user: Claims) {
user = try jwt_verify(Claims, token, env("JWT_SECRET"))
identify(user.sub)
join(room)
Chat.clients.filter(c => c.subscribed(room))
.send(Notice { text: "{user.sub} joined" })
}
# Broadcast to everyone in a room (fire-and-forget; arrives as an event).
method say(room: string, text: string) {
Chat.clients.filter(c => c.subscribed(room))
.send(ChatMsg { from: user.sub, text: text })
}
# A direct message: target a single connection by its JWT identity.
method dm(to: string, text: string) {
Chat.clients.filter(c => c.identity == to)
.send(ChatMsg { from: user.sub, text: text })
}
# Admin-only broadcast, filtered by the injected role.
method announce(text: string) -> string {
if user.role != "admin" { fail 403 "admins only" }
Chat.clients.filter(c => c.user.role != "admin")
.send(Notice { text: text })
return "sent"
}
# A plain request/response method still works alongside events.
method echo(text: string) -> string {
return "you ({user.sub}) said: {text}"
}
}Connect to ws://host/Chat?token=<jwt>&room=<name>. The generated client gives you a typed ChatHub class. See WebSocket Hubs.
sessions.weld — issuing auth: bcrypt & JWT
The issuing side of authentication (auth.weld below shows the verifying side). password_hash / password_verify are bcrypt; jwt_sign / jwt_verify are HS256 and honor the standard exp claim, so an expired token is rejected 401 automatically. random_bytes + hex_encode mint opaque tokens.
# sessions.weld — issuing side of auth with the crypto builtins.
# (auth.weld shows the verifying side: a decorator that checks a Bearer token.)
#
# password_hash / password_verify use bcrypt; jwt_sign / jwt_verify are HS256 and
# honor the standard `exp` claim — a request with an expired token is rejected
# 401 automatically. random_bytes + hex_encode mint opaque tokens.
type Credentials = { username: string, password: string }
type Claims = { sub: string, exp: int }
type Session = { token: string }
# The bcrypt hash per user; persisted in state, guarded by the state lock.
state accounts {
var hashes: [string: string] = [:]
}
# In a real service this comes from the environment, not source.
let secret = "change-me-in-prod"
let sessionTtl = 3600
route POST "/register" (body creds: Credentials) -> Session {
accounts.hashes[creds.username] = password_hash(creds.password)
let claims = Claims { sub: creds.username, exp: now_ms() / 1000 + sessionTtl }
respond Session { token: jwt_sign(claims, secret) }
}
route POST "/login" (body creds: Credentials) -> Session {
let stored = accounts.hashes[creds.username] ?? ""
if !password_verify(creds.password, stored) {
fail 401 "invalid credentials"
}
let claims = Claims { sub: creds.username, exp: now_ms() / 1000 + sessionTtl }
respond Session { token: jwt_sign(claims, secret) }
}
# Present the token as `?token=…`; an expired or tampered token fails `try` -> 401.
route GET "/me" (query token: string) -> string {
let claims = try jwt_verify(Claims, token, secret)
respond "you are {claims.sub}"
}
# An opaque random token (24 bytes -> 48 hex chars), e.g. for password resets.
route GET "/opaque-token" -> string {
respond hex_encode(random_bytes(24))
}$ curl -X POST localhost:8080/register -d '{"username":"ada","password":"s3cret"}'
# {"token":"<jwt>"}
$ curl -X POST localhost:8080/login -d '{"username":"ada","password":"s3cret"}'
# {"token":"<jwt>"}
$ curl "localhost:8080/me?token=<jwt>" # you are ada
$ curl localhost:8080/opaque-token # 48 hex charsauth.weld — decorators, JWT, injected variables
A decorator runs before the handler; it can fail to reject and assign the bindings in its injects clause, which become typed variables in the handler. Decorators stack.
type Claims = { sub: string, role: string }
# `injects (user: Claims)` exposes a `user` binding to any route this decorates.
decorator require_role(role: string) injects (user: Claims) {
# request_header returns string?; `else { ... }` runs when it is absent.
let header = request_header("Authorization") else { fail 401 "missing authorization" }
let token = trimPrefix(header, "Bearer ")
# jwt_verify(Type, token, secret) HS256-verifies and decodes the payload.
user = try jwt_verify(Claims, token, env("JWT_SECRET"))
if user.role != role { fail 403 "forbidden" }
}
@require_role("admin")
route GET "/admin" -> Claims {
respond user # `user` is in scope, typed as Claims
}
@require_role("member")
route GET "/me" {
respond "you are {user.sub} ({user.role})"
}
route GET "/" {
respond "public — no decorator"
}$ export JWT_SECRET=devsecret
$ curl localhost:8080/admin # 401 (no token)
$ curl -H "Authorization: Bearer $TOKEN" localhost:8080/me # you are ada (member)ratelimit.weld — rate limiting written in Weld
No built-in primitive: a [string: Bucket] map in state, keyed by client IP, holds a fixed-window counter — composed from decorators, state, a map, now_ms, and fail.
type Bucket = { count: int, window_start: int }
state limiter {
var buckets: [string: Bucket] = [:]
}
decorator rate_limit(max: int, window_ms: int) {
let ip = client_ip()
let now = now_ms()
# `m[key]` yields an optional (null if absent); `??` supplies a fresh bucket.
let b = limiter.buckets[ip] ?? Bucket { count: 0, window_start: now }
var count = b.count
var start = b.window_start
if now - start > window_ms { # window elapsed -> reset
start = now
count = 0
}
count = count + 1
limiter.buckets[ip] = Bucket { count: count, window_start: start }
if count > max {
fail 429 "rate limit exceeded for {ip}"
}
}
@rate_limit(60, 60000) # 60 requests / minute, per client IP
route GET "/api/data" {
respond "ok — tracking {len(limiter.buckets)} clients"
}$ for i in $(seq 1 61); do curl -s -o /dev/null -w '%{http_code} ' localhost:8080/api/data; done
# ...200 200 ... 429 (the 61st request in the window is rejected)Testing & modules
tested.weld — in-language unit tests
test "name" { … assert cond … } blocks run by weld --emit test (they compile away in the server binary), so pure logic is testable without a running server.
type Shape = Circle(float) | Rect(float, float) | Empty
fn area(s: Shape) -> float {
return match s {
Circle(r) => 3.0 * r * r, # (3.0, not pi, to keep the test exact)
Rect(w, h) => w * h,
Empty => 0.0,
}
}
fn clamp(x: int, lo: int, hi: int) -> int {
if x < lo { return lo }
if x > hi { return hi }
return x
}
route GET "/area/:r" (r: float) -> float { respond area(Shape.Circle(r)) }
test "area of shapes" {
assert area(Shape.Rect(3.0, 4.0)) == 12.0
assert area(Shape.Empty) == 0.0
assert area(Shape.Circle(2.0)) == 12.0
}
test "clamp bounds a value" {
assert clamp(5, 0, 10) == 5
assert clamp(-3, 0, 10) == 0
assert clamp(99, 0, 10) == 10
}
test "string helpers" {
assert upper("abc") == "ABC"
assert contains("hello world", "world")
}$ ./weld examples/tested.weld --emit test # compiles a Zig test file and runs it; exit 1 on failuremodular/ — a multi-file project with import
import merges each file's declarations into one program. Files are included once even via multiple paths (models.weld is imported by both files here), and cycles are safe. Compile the entry point (app.weld).
models.weld — shared types and helpers, no routes of its own:
type User = { id: int, name: string, role: string }
enum Plan {
Free = 1 : "free",
Pro = 2 : "pro",
}
fn describe(u: User) -> string {
return "{u.name} (#{u.id}, {u.role})"
}users.weld — user-facing routes; the User type and describe come from models.weld:
import "models.weld"
route GET "/users/:id" (id: int) -> User {
respond User { id: id, name: "alice", role: "admin" }
}
route GET "/users/:id/label" (id: int) -> string {
let u = User { id: id, name: "bob", role: "member" }
respond describe(u)
}app.weld — the entry point that wires it together:
import "users.weld"
import "models.weld"
route GET "/health" { respond "ok" }
route GET "/plans/:p" (p: int) -> string {
respond str(Plan(p))
}$ ./weld examples/modular/app.weld --emit bin -O ReleaseFast && ./app &
$ curl localhost:8080/users/7/label # bob (#7, member)
$ curl localhost:8080/plans/2 # proGenerating a client
Any of these services also emits a typed frontend client from the same route declarations — no drift between server and client:
./weld examples/users.weld --emit client -o api # api.ts and api.jsSee Generated Clients for the TypeScript/JS output, including typed hub clients for hub.weld.