Validation
defineHandler wraps a route handler to add schema validation and type inference. It accepts any library that implements the Standard Schema spec — Zod, Valibot, and ArkType all work out of the box.
Basic usage
Section titled “Basic usage”import { defineHandler } from 'lacis'import { z } from 'zod'
export const GET = defineHandler({ params: z.object({ id: z.string().uuid() }), query: z.object({ verbose: z.coerce.boolean().optional() }), meta: { summary: 'Get user by ID', tags: ['users'] }, handler: async (req, res) => { req.params.id // string — validated and typed req.query.verbose // boolean | undefined — validated and typed res.json({ id: req.params.id }) },})The return value is a plain async function — export it like any other route handler.
Configuration fields
Section titled “Configuration fields”params, query, body
Section titled “params, query, body”Each field accepts a Standard Schema. Schemas are validated in order: params first, then query, then body. If any fails, Lacis returns 400 immediately.
params— validated againstreq.params(URL path parameters)query— validated againstreq.query(parsed query string)body— Lacis callsreq.json()internally and validates the result
Attached to the OpenAPI operation when the spec is generated. No effect at request time.
meta: { summary: 'Create user', description: 'Creates a new user account.', tags: ['users'], deprecated: false,}handler
Section titled “handler”The actual request handler with fully typed req and res.
Validation errors
Section titled “Validation errors”When validation fails, Lacis sends a 400 automatically:
{ "error": "Validation failed", "issues": [ { "message": "Invalid uuid", "path": ["id"] } ]}The handler is never called when validation fails.
Query coercion
Section titled “Query coercion”Query strings are always strings. Use your validator’s coercion utilities:
query: z.object({ page: z.coerce.number().int().positive(), active: z.coerce.boolean().optional(),})import * as v from 'valibot'
query: v.object({ page: v.pipe(v.string(), v.transform(Number), v.integer()), active: v.optional(v.pipe(v.string(), v.transform((s) => s === 'true'))),})import { type } from 'arktype'
query: type({ page: 'number.integer > 0', 'active?': 'boolean' })Body validation
Section titled “Body validation”import { defineHandler } from 'lacis'import { z } from 'zod'
export const POST = defineHandler({ body: z.object({ name: z.string().min(1), email: z.string().email(), }), meta: { summary: 'Create user', tags: ['users'] }, handler: async (req, res) => { const { name, email } = req.body // fully typed res.status(201).json({ name, email }) },})import { defineHandler } from 'lacis'import * as v from 'valibot'
export const POST = defineHandler({ body: v.object({ name: v.pipe(v.string(), v.minLength(1)), email: v.pipe(v.string(), v.email()), }), meta: { summary: 'Create user', tags: ['users'] }, handler: async (req, res) => { const { name, email } = req.body res.status(201).json({ name, email }) },})import { defineHandler } from 'lacis'import { type } from 'arktype'
export const POST = defineHandler({ body: type({ name: 'string > 0', email: 'string.email' }), meta: { summary: 'Create user', tags: ['users'] }, handler: async (req, res) => { const { name, email } = req.body res.status(201).json({ name, email }) },})Combined schemas
Section titled “Combined schemas”export const PUT = defineHandler({ params: z.object({ id: z.string().uuid() }), query: z.object({ dryRun: z.coerce.boolean().default(false) }), body: z.object({ name: z.string(), email: z.string().email() }), handler: async (req, res) => { if (req.query.dryRun) return res.json({ ok: true, dry: true }) res.json({ id: req.params.id, ...req.body }) },})Type inference
Section titled “Type inference”TypeScript infers the output types of your schemas. Change a schema, and the types in handler update automatically — no manual annotations needed.