Validation
Validate request data with type safety
Installing Zod
bun add zodBody Validation
Validate request bodies:
import { route } from "@axi-js/core";
import { z } from "zod";
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(18).optional(),
});
export const createUser = route
.post()
.body(createUserSchema)
.handle(async (ctx) => {
// ctx.body is typed and validated
const { name, email, age } = ctx.body;
return { user: { name, email, age } };
});Query Validation
Validate query parameters:
const searchSchema = z.object({
q: z.string(),
limit: z.coerce.number().default(10),
offset: z.coerce.number().default(0),
});
export const searchItems = route
.get()
.query(searchSchema)
.handle(async (ctx) => {
const { q, limit, offset } = ctx.query;
const results = await search(q, { limit, offset });
return { results };
});Params Validation
Validate route parameters:
const paramsSchema = z.object({
id: z.string().uuid(),
});
export const getUser = route
.get()
.params(paramsSchema)
.handle(async (ctx) => {
// ctx.params.id is a valid UUID
const user = await getUser(ctx.params.id);
return { user };
});Multiple Validators
Chain multiple validators:
const paramsSchema = z.object({
id: z.string().uuid(),
});
const bodySchema = z.object({
name: z.string(),
email: z.string().email(),
});
export const updateUser = route
.put()
.params(paramsSchema)
.body(bodySchema)
.handle(async (ctx) => {
const user = await updateUser(ctx.params.id, ctx.body);
return { user };
});Custom Error Messages
Provide custom error messages:
const userSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
age: z
.number()
.min(18, "Must be at least 18 years old")
.max(120, "Invalid age"),
});
export const createUser = route
.post()
.body(userSchema)
.handle(async (ctx) => {
return { user: ctx.body };
});Complex Validation
Use Zod's advanced features:
const addressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}$/),
});
const userSchema = z.object({
name: z.string(),
email: z.string().email(),
role: z.enum(["user", "admin"]),
address: addressSchema,
tags: z.array(z.string()).optional(),
metadata: z.record(z.string()).optional(),
});
export const createUser = route
.post()
.body(userSchema)
.handle(async (ctx) => {
return { user: ctx.body };
});Error Handling
Validation errors are automatically handled. A failed validation returns 400 with a structured Problem Details response (RFC 7807):
{
"type": "https://axi.vnce.eu/errors/validation",
"title": "Validation Error",
"status": 400,
"detail": "Request validation failed",
"errors": [
{
"field": "body.email",
"code": "invalid_email",
"message": "Invalid email address",
"expected": "email",
"received": "not-an-email"
}
]
}Each entry in errors includes the field path (prefixed with body., query., or params.), a machine-readable code, and a human-readable message. Zod schemas are supported out of the box; any object with a parse method works too.
Custom Validators
Use any validator with a parse method:
class CustomValidator {
parse(data: unknown) {
if (typeof data !== "object") {
throw new Error("Expected object");
}
return data;
}
}
export const processData = route
.post()
.body(new CustomValidator())
.handle(async (ctx) => {
return { data: ctx.body };
});Transformations
Transform data during validation:
const userSchema = z.object({
email: z
.string()
.email()
.transform((s) => s.toLowerCase()),
name: z.string().transform((s) => s.trim()),
createdAt: z.string().transform((s) => new Date(s)),
});
export const createUser = route
.post()
.body(userSchema)
.handle(async (ctx) => {
// email is lowercase, name is trimmed, createdAt is a Date
return { user: ctx.body };
});