Schema validation
Schemas validate at runtime and narrow request types. Passed as the fourth argument of a route method.
Body, query, and params
import { HttpServer, z } from "jsr:@webtools/expressapi";
const server = new HttpServer()
.post("/users", (req, res) => {
const { name, email, age } = req.body; // typed and validated
return res.status(201).json({ name, email, age });
}, [], {
body: z.object({
name: z.string().min(3).max(50),
email: z.string().email(),
age: z.optional(z.number().int().positive()),
}),
})
.get("/search", (req, res) => {
return res.json({ q: req.query.q, page: req.query.page ?? 1 });
}, [], {
query: z.object({
q: z.string().min(1),
page: z.optional(z.number().int().positive()),
}),
})
.get("/users/:id", (req, res) => res.json({ id: req.params.id }), [], {
params: z.object({ id: z.string().uuid() }),
});Validating params is only needed when you want more than string (for example a UUID). See the full builder list in the z API reference.
Optional keys
z.optional() makes the key optional in the inferred type so omitted keys stay absent instead of becoming undefined:
const schema = z.object({ q: z.string(), page: z.optional(z.string()) });
// inferred as { q: string; page?: string }z.nullable(z.string()) is string | null - the key is still required unless you also wrap with optional.
Validation errors
Invalid data returns 400 before the handler runs:
{
"success": false,
"error": "400 Bad Request.",
"details": [
{
"path": ["email"],
"message": "Invalid email format",
"code": "invalid_string"
}
]
}Files and multipart
.post("/upload", (req, res) => {
const file = req.body.file;
return res.json({ name: file.name, size: file.size });
}, [], {
body: z.object({
file: z.file().maxSize(2_000_000).type(["image/", "application/pdf"]),
caption: z.optional(z.string()),
}),
})More on uploads and downloads: Files.
JSON strings in query or forms
z.object and z.array also accept a JSON string, which they parse before validating. Useful when structured data arrives as a query parameter or a form field.
.get("/filter", (req, res) => res.json(req.query.filter), [], {
query: z.object({
filter: z.object({
tags: z.array(z.string()),
active: z.boolean(),
}),
}),
})
// GET /filter?filter={"tags":["a"],"active":true}Available builders
- Strings -
min,max,length,email,uuid,url,regex,startsWith,endsWith - Numbers -
int,positive,negative,min,max(coerced from strings) - Booleans - accepts
true/false/1/0/on/off - Files -
z.file()withminSize,maxSize,type - Composites -
object,array,union,enum,optional,nullable,any
Every builder takes an optional custom message as its last argument, for example z.string("Name is required").