Validate and transform request data using Zod and NextRush's built-in ValidationError system.
TypeScript types disappear at runtime. Without validation, malformed input propagates through your system, causes silent failures, and opens security holes.
This guide shows how to validate request data using Zod with NextRush — in both functional and class-based styles.
@nextrush/validation provides a validate() middleware built specifically for this task — it
validates the body/query/params against any Standard Schema
library in one call and reuses NextRush's ValidationError. It is not re-exported by nextrush
or nextrush/class — install it directly. Its own test suite integration-tests against Zod
only; Valibot and ArkType are structurally compatible (same ~standard contract) but not
exercised by that package's tests. This guide first shows @nextrush/validation's validate(),
then a hand-rolled pattern for cases needing more control than one middleware call gives you.
For most routes, validate() is less code than hand-rolling safeParse + ValidationError
yourself. It validates and replaces ctx.body with the coerced value on success:
src/routes/users.ts
import { createApp, listen } from 'nextrush';import { json } from '@nextrush/body-parser';import { validate } from '@nextrush/validation';import { z } from 'zod';const CreateUserSchema = z.object({ name: z.string().min(1, 'Name is required').max(100), email: z.string().email('Invalid email format'), age: z.number().int().min(18, 'Must be at least 18').optional(),});const app = createApp();app.post('/users', json(), validate(CreateUserSchema), (ctx) => { // ctx.body is now the validated, coerced value — no cast, no second variable ctx.status = 201; ctx.json({ data: ctx.body });});listen(app, 8080);
Validate query and route params together by passing a spec map instead of a bare schema:
import { validate } from '@nextrush/validation';const PaginationSchema = z.object({ page: z.coerce.number().int().min(1).default(1), limit: z.coerce.number().int().min(1).max(100).default(10),});app.get('/users', validate({ query: PaginationSchema }), (ctx) => { // ctx.query is validated but left as strings — @nextrush/validation never // coerces query/params in place, only ctx.body ctx.json({ page: ctx.query.page, limit: ctx.query.limit });});
Invalid input never reaches the handler — validate() throws the same ValidationError this
guide covers throughout, so it flows into errorHandler() exactly like a manually-thrown one. See
the @nextrush/validation README for the full options reference.
The rest of this guide covers validating without @nextrush/validation — useful when you need
per-field control validate()'s single middleware call doesn't give you, or when composing
validation into a helper function rather than route-level middleware.
Consider `z.int()` instead. This API is considered *legacy*; it will never be removed but a better alternative exists.
int()._ZodNumber<$ZodNumberInternals<number>>.min(value: number, params?: string | z.core.$ZodCheckGreaterThanParams): z.ZodNumbermin(18, 'Must be at least 18').ZodType<any, any, $ZodNumberInternals<number>>.optional(): z.ZodOptional<z.ZodNumber>optional(),});export type
type CreateUserInput = { name: string; email: string; age?: number | undefined;}
CreateUserInput =import zz.
type infer<T> = T extends { _zod: { output: any; };} ? T["_zod"]["output"] : unknownexport infer
Use safeParse and throw a ValidationError on failure:
src/routes/users.ts
import { createRouter, ValidationError } from 'nextrush';import type { ValidationIssue } from 'nextrush';import { CreateUserSchema } from '../schemas/user.js';const router = createRouter();router.post('/users', (ctx) => { const result = CreateUserSchema.safeParse(ctx.body); if (!result.success) { const issues: ValidationIssue[] = result.error.issues.map((issue) => ({ path: issue.path.join('.'), message: issue.message, })); throw new ValidationError(issues); } // result.data is typed as { name: string; email: string; age?: number } ctx.status = 201; ctx.json({ data: result.data });});export { router };
ValidationError takes an array of ValidationIssue objects as the first argument and an optional message as the second. Each issue uses path (not field) to identify the failing property.
The @Body, @Param, and @Query decorators accept a transform option. The transform runs before your method executes. If it throws, the request fails with an error.
The builder extracts the raw body from the context
Your transform function runs (awaited for async transforms)
If the transform throws, it is wrapped in a ParameterInjectionError
If it succeeds, the validated result is passed to your method
Use parseAsync instead of parse for the transform. The handler builder awaits all transforms,
and parseAsync handles schemas with async refinements correctly.
Each issue has a path string and a message. The rule and expected fields appear when set. The received field is stripped from JSON output to prevent leaking sensitive input values.
Built-in Convenience Errors
@nextrush/errors also exports RequiredFieldError, TypeMismatchError, RangeValidationError,
LengthError, PatternError, InvalidEmailError, and InvalidUrlError. These are
ValidationError subclasses with pre-built issue formats for common cases.
Passes a string and
{@linkcode
replaceValue
}
to the `[Symbol.replace]` method on
{@linkcode
searchValue
}
. This method is expected to implement its own replacement algorithm.
@paramsearchValue An object that supports searching for and replacing matches within a string.@paramreplaceValue The replacement text.
Validate at the boundary — validate input the moment it enters your system, not deep inside business logic.
Use z.coerce for query parameters — query values are always strings. z.object({ page: z.number() }) fails; z.coerce.number() works.
Use parseAsync in decorator transforms — the handler builder awaits transforms, so async refinements work correctly.
Write descriptive error messages — z.string().min(1, 'Name is required') is better than z.string().min(1, 'Invalid').
Separate input and output schemas — never return passwords or internal IDs in API responses.
Zod's .parse() throws a ZodError, not a NextRush ValidationError. In functional routes, use
the validate helper to convert Zod errors into ValidationError with proper issues format. In
decorator transforms, thrown errors are wrapped in a ParameterInjectionError automatically.