Validation Overview
Reference: Typebox GitHub Repository
Deserve validates request input through Typebox contracts, a zero-dependency contract library that ships with the framework. A contract describes one source of a request, the validator middleware runs it before the handler, and the handler reads typed data that already passed every rule.
This sits beside the other middleware and watches the request before it reaches a route, the same place CORS and Session hook in.
The Three Pieces
Validation comes together from three exports, each with one job:
Validator.definebuilds a contract from a transform and optional guards. See Define Schema.Validator.checkturns a schema into middleware that validates request sources. See Validator Middleware.ctx.get.validated()reads validated data inside a handler. See Reading Validated Data.

A Schema Maps Sources To Contracts
A schema is a plain object that pairs a request source with a contract:
import { Validator } from '@neabyte/deserve'
// One contract per request source
const schema = {
body: Validator.define((body: { name: string }) => body)
}Four sources exist, and each one reads from a matching part of the Context:
| Source | Reads from | Shape |
|---|---|---|
body | ctx.get.body() | raw parsed body |
cookies | ctx.get.cookie() | Record<string, string> |
headers | ctx.get.header() | Record<string, string> |
query | ctx.get.query() | Record<string, string> |
Route params are not a validation source because they resolve after middleware runs. Validate them inside the handler with a direct contract call, covered in Reading Validated Data.
The Request Flow
A validated request moves through four steps:
- The validator middleware reads each source named in the schema
- It runs the matching contract on that source value
- A passing contract stores its output on the context
- The handler reads that output with full types through
ctx.get.validated()

import { Router, Validator } from '@neabyte/deserve'
const router = new Router({
routes: { directory: './routes' }
})
const schema = {
body: Validator.define((body: { name: string }) => ({ name: body.name.trim() }))
}
// Validate the body before the handler
router.use('/users', Validator.check(schema))
await router.serve(8000)export function POST(ctx: Context): Response {
// Read typed data that already passed
const { body } = ctx.get.validated<typeof schema>()
return ctx.send.json({ created: body.name })
}Failures Become 422
A contract that rejects its input throws, and the framework maps that throw to a 422 Unprocessable Content response. The failure reasons ride on error.cause as a string array, so a custom handler reads them and surfaces exactly which field went wrong. Error shaping stays in one place through Object Details, the same router.catch that handles every other error.
A throw from client input never becomes a 500. That mapping rule lives in Reading Validated Data.
Where To Go Next
- Define Schema - write a contract with a transform and guards
- Validator Middleware - register validation per source and per route
- Reading Validated Data - read typed output and check params in a handler
- Advanced Patterns - pick a schema per method on a shared prefix