Skip to content

Types & Records

Weld is statically typed. The primitive types are int, float, bool, and string, plus T? (optional) and [T] (list).

bytes

bytes is raw binary data — distinct from string (UTF-8 text). Use it for file contents and binary payloads:

  • File uploads: upload("field") returns bytes?.
  • Binary downloads: a route can respond a bytes value directly, sending it as the raw response body (set the content type with type "application/octet-stream" and, e.g., a Content-Disposition header).
  • Crypto: random_bytes(n) and the decoders (base64_decode / hex_decode) return bytes; the encoders (base64_encode / hex_encode) and sha256 / hmac_sha256 accept bytes (or a string).

len(b) is the byte count and b[i] is the i-th byte as an int. Convert to/from text with to_bytes(string) and to_string(bytes). bytes is intentionally not interchangeable with string, so binary never gets treated as text by accident.

weld
route GET "/token" -> string {
    respond hex_encode(random_bytes(16))     # 16 random bytes -> 32 hex chars
}

route POST "/upload" -> string {
    let file = upload("file") else { fail 400 "no file" }
    respond "got {len(file)} bytes, sha256={sha256(file)}"
}

route GET "/blob" -> bytes {
    type "application/octet-stream"
    respond random_bytes(256)                # raw binary body
}

Not yet: a bytes field inside a record (JSON base64) and binary database columns — store base64/hex text in a string field for now.

Records

Declare record types with [T] lists and T? optional fields. You never write JSON: return a record or list and Weld encodes it (Content-Type application/json); return a string and it is sent as text.

weld
type User = { id: int, name: string, email: string? }

route GET "/users/:id" (id: int) -> User {
    respond User { id: id, name: "Ada Lovelace", email: "ada@example.com" }
}
route GET "/users" -> [User] {
    respond [ User { id: 1, name: "Ada", email: null } ]     # → JSON array
}
sh
$ curl localhost:8080/users/42
{"id":42,"name":"Ada Lovelace","email":"ada@example.com"}

Record types become Zig structs for the server and TypeScript interfaces for the client, and a route's -> T return type flows through to the client method's return type.

Optionals

T? is an optional value. null is a valid value for it, and the ?? coalescing operator supplies a default:

weld
route GET "/greet/:name" (name: string, query loud: bool = false) {
    respond "hello {name} (loud={loud})\n"
}

An optional field encodes as JSON null when absent. To read an optional safely, use x ?? fallback, or unwrap it with let y = x else { … } (see Error Handling).

Lists

[T] is a mutable slice. You can index it (xs[i], bounds-checked), index-assign (xs[i] = v), preallocate with filled(n, v), and grow with push:

weld
route GET "/sum/:n" (n: int) {
    var total = 0
    var xs = push([1, 2, 3], n)     # build a list
    for x in xs { total = total + x }
    respond "total={total} first={xs[0]}\n"
}

Maps

[K: V] maps take string or int keys. Literals are ["a": 1] or [:] (empty); lookup m[k] yields V? (null if absent); insert with m[k] = v. Builtins: has(m, k), remove(m, k), len(m), and for k, v in m iteration.

weld
state counts { var hits: [string: int] = [:] }

route GET "/hit/:page" (page: string) {
    let n = (counts.hits[page] ?? 0) + 1
    counts.hits[page] = n
    respond "{n}"
}

A map (or any value) stored in state persists across requests — see State.

Request bodies

Declare body x: T to parse a JSON request body into a typed record (invalid JSON → 400):

weld
type NewUser = { name: string, email: string? }

route POST "/users" (body input: NewUser) -> User {
    respond User { id: 1, name: input.name, email: input.email }
}

The generated client method then takes a typed argument: postUsers(input: NewUser).