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")returnsbytes?. - Binary downloads: a route can
respondabytesvalue directly, sending it as the raw response body (set the content type withtype "application/octet-stream"and, e.g., aContent-Dispositionheader). - Crypto:
random_bytes(n)and the decoders (base64_decode/hex_decode) returnbytes; the encoders (base64_encode/hex_encode) andsha256/hmac_sha256acceptbytes(or astring).
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.
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
bytesfield inside a record (JSON base64) and binary database columns — store base64/hex text in astringfield 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.
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
}$ 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:
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:
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.
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):
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).