Request Validator
Validate incoming HTTP requests using Zod with a framework-agnostic core and lightweight adapters for Express, Fastify, and Hono.
The Request Validator block validates incoming HTTP requests before they reach your route handlers.
It provides a framework-agnostic validation core powered by Zod and lightweight adapters for supported frameworks. The core is responsible only for validation, while adapters integrate it with your web framework.
Features
- Framework-agnostic validation core
- Powered by Zod with full type inference
- Validate request body, query parameters, and route parameters
- Automatically parses, coerces, and normalizes request data
- Access fully typed validated values via
validator.validated(req) - Express, Fastify, and Hono adapters
- Optional error propagation to global error handler
- TypeScript-first API
File Structure
request-validator
├── adapters
│ ├── express.ts
│ ├── fastify.ts
│ └── hono.ts
├── core.ts
└── contract.ts- adapters/ — Framework-specific middleware that integrates the core validator with Express, Fastify, and Hono.
- core.ts —
coreValidatorfunction wrapping Zod'sschema.parse(). - contract.ts —
RequestValidationSchematype for defining body, query, and params schemas.
Installation
pnpm dlx blockend-cli add request-validatorDetect Project
Blockend detects your configured backend framework and installs the appropriate adapter.
Install Dependencies
zod is installed automatically if not already present.
Generate Files
The validator block is generated inside your configured blocks directory.
Copy the files below into your project's blocks directory.
Peer Dependencies
| Package | Required for |
|---|---|
zod | Core validation |
express | Express adapter |
fastify | Fastify adapter |
hono | Hono adapter |
blocks/request-validator/core.ts
Core validation function wrapping Zod's schema.parse().
import { z } from "zod";
/**
* Validates arbitrary data against a Zod schema.
*
* Returns the parsed data when validation succeeds.
* Throws a ValidationError when validation fails.
*/
export function coreValidator<TSchema extends z.ZodTypeAny>(
schema: TSchema,
data: unknown
): z.infer<TSchema> {
return schema.parse(data);
}
blocks/request-validator/contract.ts
Type contract for defining validation schemas across body, query, and params.
import { z } from "zod";
/**
* Schemas for the different parts of an incoming HTTP request.
*
* The generic parameters preserve the concrete Zod schema types so
* adapters can infer the validated data instead of falling back to
* `unknown`.
*/
export interface RequestValidationSchema<
TBody extends z.ZodTypeAny = z.ZodTypeAny,
TQuery extends z.ZodTypeAny = z.ZodTypeAny,
TParams extends z.ZodTypeAny = z.ZodTypeAny
> {
body?: TBody;
query?: TQuery;
params?: TParams;
}
blocks/request-validator/adapters/express.ts
Express middleware that validates requests and exposes validator.validated(req).
import type { Request, Response, NextFunction, RequestHandler } from "express";
import { z, ZodError } from "zod";
import { coreValidator } from "../core.js";
import type { RequestValidationSchema } from "../contract.js";
/**
* Controls how validation failures are handled.
*/
type ExpressValidatorOptions = {
/**
* When `true`, validation errors are forwarded to Express via `next(error)`
* so they can be handled by a global error handler.
*
* When `false` (default), this middleware immediately returns a
* `400 Bad Request` response using its built-in JSON format.
*/
propagateErrors?: boolean;
};
/**
* Internal symbol used to store the validated request payload on the request.
*
* Using a Symbol avoids collisions with application-defined request properties.
*/
export const VALIDATED_KEY = Symbol("blockend.validated");
/**
* The validated and parsed request data.
*
* Values are already transformed by Zod (defaults applied, coercion performed,
* transforms executed, etc.).
*/
type ValidatedData<
TBody extends z.ZodTypeAny,
TQuery extends z.ZodTypeAny,
TParams extends z.ZodTypeAny
> = {
body: z.infer<TBody>;
query: z.infer<TQuery>;
params: z.infer<TParams>;
};
/**
* Custom request type that carries the validated payload.
*/
interface ValidatedRequest<
TBody extends z.ZodTypeAny = z.ZodTypeAny,
TQuery extends z.ZodTypeAny = z.ZodTypeAny,
TParams extends z.ZodTypeAny = z.ZodTypeAny
> extends Request {
[VALIDATED_KEY]?: ValidatedData<TBody, TQuery, TParams>;
}
/**
* Express middleware augmented with a helper for retrieving the fully typed,
* validated request data.
*/
type ValidatorMiddleware<
TBody extends z.ZodTypeAny,
TQuery extends z.ZodTypeAny,
TParams extends z.ZodTypeAny
> = RequestHandler & {
/**
* Returns the validated request payload.
*
* Throws if the middleware has not yet executed for the current request.
*/
validated: (req: Request) => ValidatedData<TBody, TQuery, TParams>;
};
/**
* Creates Express middleware that validates the request body, query string,
* and route parameters using Zod schemas.
*
* On success:
* - Request values are parsed using Zod.
* - Default values, coercion, and transforms are applied.
* - `req.body`, `req.query`, and `req.params` are replaced with the parsed values.
* - The typed values are also accessible via `middleware.validated(req)`.
*
* On validation failure:
* - Returns a `400 Bad Request` response by default.
* - Or forwards the `ZodError` to the next error handler when
* `propagateErrors` is enabled.
*/
export function expressValidator<
TBody extends z.ZodTypeAny = z.ZodTypeAny,
TQuery extends z.ZodTypeAny = z.ZodTypeAny,
TParams extends z.ZodTypeAny = z.ZodTypeAny
>(
schemas: RequestValidationSchema<TBody, TQuery, TParams>,
options: ExpressValidatorOptions = { propagateErrors: false }
): ValidatorMiddleware<TBody, TQuery, TParams> {
const middleware: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
const validatedReq = req as ValidatedRequest<TBody, TQuery, TParams>;
try {
const bodySchema = schemas.body ?? z.any();
const querySchema = schemas.query ?? z.any();
const paramsSchema = schemas.params ?? z.any();
// Validate each part independently so TypeScript can track the
// inferred type of each piece without losing it inside z.object().
const body = coreValidator(bodySchema, req.body);
const query = coreValidator(querySchema, req.query);
const params = coreValidator(paramsSchema, req.params);
req.body = body;
Object.defineProperty(req, "query", {
value: query,
writable: true,
enumerable: true,
configurable: true
});
Object.defineProperty(req, "params", {
value: params,
writable: true,
enumerable: true,
configurable: true
});
validatedReq[VALIDATED_KEY] = {
body,
query,
params
};
next();
} catch (error) {
if (error instanceof ZodError) {
const errors = z.flattenError(error).fieldErrors;
if (options.propagateErrors) {
next(error);
return;
}
res.status(400).json({
success: false,
data: null,
message: "Validation failed",
errors
});
return;
}
next(error);
}
};
(middleware as ValidatorMiddleware<TBody, TQuery, TParams>).validated = (
req: Request
): ValidatedData<TBody, TQuery, TParams> => {
const validatedReq = req as ValidatedRequest<TBody, TQuery, TParams>;
const data = validatedReq[VALIDATED_KEY];
if (!data) {
throw new Error(
"[blockend] expressValidator middleware has not run for this request. " +
"Ensure it is registered before your route handler."
);
}
return data;
};
return middleware as ValidatorMiddleware<TBody, TQuery, TParams>;
}
blocks/request-validator/adapters/fastify.ts
Fastify preHandler hook that validates requests and exposes validator.validated(request).
import { z, ZodError } from "zod";
import type { FastifyReply, FastifyRequest } from "fastify";
import { coreValidator } from "../core";
import type { RequestValidationSchema } from "../contract";
export interface FastifyValidatorOptions {
propagateErrors?: boolean;
}
export const VALIDATED_KEY = Symbol("blockend.validated");
type ValidatedData<
TBody extends z.ZodTypeAny,
TQuery extends z.ZodTypeAny,
TParams extends z.ZodTypeAny
> = {
body: z.infer<TBody>;
query: z.infer<TQuery>;
params: z.infer<TParams>;
};
interface ValidatedRequest<
TBody extends z.ZodTypeAny,
TQuery extends z.ZodTypeAny,
TParams extends z.ZodTypeAny
> extends FastifyRequest {
[VALIDATED_KEY]?: ValidatedData<TBody, TQuery, TParams>;
}
type ValidatorHook<
TBody extends z.ZodTypeAny,
TQuery extends z.ZodTypeAny,
TParams extends z.ZodTypeAny
> = {
(request: FastifyRequest, reply: FastifyReply): Promise<void>;
validated(request: FastifyRequest): ValidatedData<TBody, TQuery, TParams>;
};
export function fastifyValidator<
TBody extends z.ZodTypeAny = z.ZodTypeAny,
TQuery extends z.ZodTypeAny = z.ZodTypeAny,
TParams extends z.ZodTypeAny = z.ZodTypeAny
>(
schemas: RequestValidationSchema<TBody, TQuery, TParams>,
options: FastifyValidatorOptions = {}
): ValidatorHook<TBody, TQuery, TParams> {
const hook = async (request: FastifyRequest, reply: FastifyReply) => {
const validatedReq = request as ValidatedRequest<TBody, TQuery, TParams>;
try {
const body = coreValidator(schemas.body ?? z.any(), request.body);
const query = coreValidator(schemas.query ?? z.any(), request.query);
const params = coreValidator(schemas.params ?? z.any(), request.params);
request.body = body;
request.query = query;
request.params = params;
validatedReq[VALIDATED_KEY] = {
body,
query,
params
};
} catch (err) {
if (err instanceof ZodError) {
if (options.propagateErrors) {
throw err;
}
return reply.status(400).send({
success: false,
data: null,
message: "Validation failed",
errors: z.flattenError(err).fieldErrors
});
}
throw err;
}
};
hook.validated = (request: FastifyRequest): ValidatedData<TBody, TQuery, TParams> => {
const validatedReq = request as ValidatedRequest<TBody, TQuery, TParams>;
const data = validatedReq[VALIDATED_KEY];
if (!data) {
throw new Error("[blockend] fastifyValidator has not run for this request.");
}
return data;
};
return hook as ValidatorHook<TBody, TQuery, TParams>;
}
blocks/request-validator/adapters/hono.ts
Hono middleware that validates requests and exposes validated(c).
import { z, ZodError } from "zod";
import type { Context, MiddlewareHandler } from "hono";
import { coreValidator } from "../core";
import type { RequestValidationSchema } from "../contract";
type HonoValidatorOptions = {
/**
* When true, throws the ZodError so Hono's error handler can catch it.
* Otherwise returns a 400 response.
*/
propagateErrors?: boolean;
};
export type ValidatedData<
TBody extends z.ZodTypeAny,
TQuery extends z.ZodTypeAny,
TParams extends z.ZodTypeAny
> = {
body: z.infer<TBody>;
query: z.infer<TQuery>;
params: z.infer<TParams>;
};
const VALIDATED_KEY = "blockend.validated";
export function honoValidator<
TBody extends z.ZodTypeAny = z.ZodTypeAny,
TQuery extends z.ZodTypeAny = z.ZodTypeAny,
TParams extends z.ZodTypeAny = z.ZodTypeAny
>(
schemas: RequestValidationSchema<TBody, TQuery, TParams>,
options: HonoValidatorOptions = {}
): MiddlewareHandler {
return async (c, next) => {
try {
const bodySchema = schemas.body ?? z.any();
const querySchema = schemas.query ?? z.any();
const paramsSchema = schemas.params ?? z.any();
const body = coreValidator(bodySchema, await c.req.json().catch(() => undefined));
const query = coreValidator(
querySchema,
Object.fromEntries(new URL(c.req.url).searchParams.entries())
);
const params = coreValidator(paramsSchema, c.req.param());
c.set(VALIDATED_KEY, {
body,
query,
params
});
await next();
} catch (error) {
if (error instanceof ZodError) {
if (options.propagateErrors) {
throw error;
}
return c.json(
{
success: false,
data: null,
message: "Validation failed",
errors: z.flattenError(error).fieldErrors
},
400
);
}
throw error;
}
};
}
export function validated<
TBody extends z.ZodTypeAny,
TQuery extends z.ZodTypeAny,
TParams extends z.ZodTypeAny
>(c: Context): ValidatedData<TBody, TQuery, TParams> {
const data = c.get(VALIDATED_KEY);
if (!data) {
throw new Error(
"[blockend] honoValidator middleware has not run for this request. " +
"Ensure it is registered before your route handler."
);
}
return data as ValidatedData<TBody, TQuery, TParams>;
}
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
propagateErrors | boolean | false | Forward ZodError to next error handler instead of returning 400 |
Exported APIs
| Export | Description |
|---|---|
coreValidator | Validates arbitrary data against a Zod schema |
expressValidator | Creates Express middleware with validator.validated(req) |
fastifyValidator | Creates Fastify preHandler hook with validator.validated(request) |
honoValidator | Creates Hono middleware |
validated (Hono) | Retrieves validated data from Hono context |
RequestValidationSchema | Type for body, query, and params schema definitions |
VALIDATED_KEY | Symbol used to store validated data on the request object |
Architecture
Request
│
▼
Adapter (Express / Fastify / Hono)
│ (validates body, query, params against Zod schemas)
▼
Validation
├── Success → Replace req.body/query/params + store typed data
│ │
│ ▼
│ Route Handler
│ │
│ ▼
│ validator.validated(req) — fully typed
│
└── Failure → Return 400 response OR propagate ZodErrorThe adapter validates each part of the request (body, query, params) independently using the core validator. On success, it replaces the original request properties with the parsed values and stores the typed data. On failure, it either returns a 400 response or forwards the ZodError to your global error handler.
When to Use
- You want type-safe request validation with full TypeScript inference.
- You need consistent validation across multiple frameworks.
- You want to separate validation logic from route handlers.
When Not to Use
- You prefer a different validation library (Joi, Yup, etc.).
- You don't need framework adapters and want to use Zod directly.
Usage
Express
import { expressValidator } from "@/blocks/request-validator/adapters/express";
import { z } from "zod";
const validator = expressValidator({
body: z.object({
name: z.string(),
email: z.string().email()
}),
query: z.object({
page: z.coerce.number().default(1)
}),
params: z.object({
id: z.string().uuid()
})
});
app.post("/users/:id", validator, (req, res) => {
const { body, query, params } = validator.validated(req);
res.json({ body, query, params });
});Fastify
import { fastifyValidator } from "@/blocks/request-validator/adapters/fastify";
import { z } from "zod";
const validator = fastifyValidator({
body: z.object({ name: z.string(), age: z.coerce.number() })
});
fastify.post("/", { preHandler: validator }, async (request) => {
const { body } = validator.validated(request);
return body;
});Hono
import { honoValidator, validated } from "@/blocks/request-validator/adapters/hono";
import { z } from "zod";
app.post(
"/users/:id",
honoValidator({
body: z.object({ name: z.string() }),
params: z.object({ id: z.string() })
}),
(c) => {
const data = validated(c);
return c.json(data);
}
);Error Propagation
Forward validation errors to your global error handler instead of returning 400:
const validator = expressValidator(schema, { propagateErrors: true });
app.post("/users", validator, controller);
// Your global error handler receives ZodError
app.use((err, req, res, next) => {
if (err instanceof ZodError) {
return res.status(422).json({ error: "Validation failed", details: err.issues });
}
next(err);
});API Reference
coreValidator
function coreValidator<TSchema extends z.ZodTypeAny>(
schema: TSchema,
data: unknown
): z.infer<TSchema>;Validates arbitrary data against a Zod schema. Throws ZodError on failure.
expressValidator
function expressValidator<TBody, TQuery, TParams>(
schemas: RequestValidationSchema<TBody, TQuery, TParams>,
options?: { propagateErrors?: boolean }
): ValidatorMiddleware<TBody, TQuery, TParams>;Creates Express middleware. Returns a middleware function with a .validated(req) helper.
fastifyValidator
function fastifyValidator<TBody, TQuery, TParams>(
schemas: RequestValidationSchema<TBody, TQuery, TParams>,
options?: FastifyValidatorOptions
): ValidatorHook<TBody, TQuery, TParams>;Creates a Fastify preHandler hook. Returns a hook function with a .validated(request) helper.
honoValidator
function honoValidator<TBody, TQuery, TParams>(
schemas: RequestValidationSchema<TBody, TQuery, TParams>,
options?: HonoValidatorOptions
): MiddlewareHandler;Creates Hono middleware. Use the standalone validated(c) function to retrieve typed data.
validated (Hono)
function validated<TBody, TQuery, TParams>(c: Context): ValidatedData<TBody, TQuery, TParams>;Retrieves the validated request data from Hono's context. Throws if the middleware has not run.
RequestValidationSchema
interface RequestValidationSchema<TBody, TQuery, TParams> {
body?: TBody;
query?: TQuery;
params?: TParams;
}Examples
Composition with Multiple Validators
Chain validators for multi-step validation (e.g., auth + data):
const authValidator = expressValidator({
body: z.object({ token: z.string() })
});
const dataValidator = expressValidator({
body: z.object({ title: z.string() })
});
app.post("/posts", authValidator, dataValidator, (req, res) => {
const auth = authValidator.validated(req);
const data = dataValidator.validated(req);
// both are fully typed
});Related Blocks
- Error Handler — Enable
propagateErrors: trueto delegate validation errors to your global error handler.
FAQ
What happens when validation succeeds?
The adapter replaces req.body, req.query, and req.params with the parsed values. Defaults, coercions, and transforms are applied. The typed data is also accessible via validator.validated(req).
What happens when validation fails?
By default, the adapter returns a 400 Bad Request response with field-level error details. When propagateErrors: true is set, the ZodError is forwarded to the next error handler.
Can I validate only certain parts of the request?
Yes. Only the schemas you provide are validated. For example, if you only specify body, the validator ignores query and params.
Does validator.validated(req) work before the middleware runs?
No. It throws an error reminding you to register the middleware before your route handler.