Skip to content

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.

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.

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 against req.params (URL path parameters)
  • query — validated against req.query (parsed query string)
  • body — Lacis calls req.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,
}

The actual request handler with fully typed req and res.

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 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 { 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 })
},
})
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 })
},
})

TypeScript infers the output types of your schemas. Change a schema, and the types in handler update automatically — no manual annotations needed.