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() with minSize, 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").