Typing Express/Fastify Handlers
Type-safe HTTP handlers catch bad body, params, and query shapes at compile time - pair Express 5 generics and Fastify 5 schema inference with runtime validation at boundaries.
Search across all documentation pages
Type-safe HTTP handlers catch bad body, params, and query shapes at compile time - pair Express 5 generics and Fastify 5 schema inference with runtime validation at boundaries.
// Express 5 - typed body via generic (after validation middleware)
import type { Request, Response } from 'express';
interface CreateUserBody { email: string; name: string }
export async function createUser(
req: Request<unknown, unknown, CreateUserBody>,
res: Response,
): Promise<void> {
res.status(201).json({ id: '1', ...req.body });
}// Fastify 5 - schema-driven types
import Fastify from 'fastify';
const app = Fastify();
app.post<{ Body: { email: string } }>('/users', async (req, reply) => {
return reply.code(201).send({ id: '1', email: req.body.email });
});When to reach for this:
id, pagination)userId to requestsreq.body.foo typos before runtime// express-app.d.ts - augmentation
import 'express-serve-static-core';
declare module 'express-serve-static-core' {
interface Request {
userId?: string;
}
}// auth-middleware.ts
import type { Request, Response, NextFunction } from 'express';
export function requireAuth(req: Request, res: Response, next: NextFunction): void {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
res.status(401).json({ error: 'unauthorized' });
return;
}
req.userId = 'user-123';
next();
}// fastify-route.ts
import Fastify from 'fastify';
import { z } from 'zod';
const app = Fastify();
const CreateUser = z.object({ email: z.string().email(), name: z.string().min(1) });
app.post('/users', {
schema: {
body: {
type: 'object',
required: ['email', 'name'],
properties: { email: { type: 'string' }, name: { type: 'string' } },
},
},
}, async (req, reply) => {
const body = CreateUser.parse(req.body);
return reply.code(201).send({ id: '1', ...body });
});What this demonstrates:
userId with proper typing across handlersnext() or end response - typed NextFunction for errorsRequest<P, ResBody, ReqBody, ReqQuery, Locals> generics - rarely all filled manually; augmentation for cross-cutting fields.schema when using as const or TypeBox - Zod bridge via fastify-type-provider-zod.res.json<T>() is not enforced at runtime - still validate outbound DTOs for public APIs.(err, req, res, next); Fastify setErrorHandler with typed errors.| Framework | Request typing | Validation |
|---|---|---|
| Express 5 | Generics + augmentation | Zod middleware manual |
| Fastify 5 | Schema inference | JSON Schema + Zod parse |
| NestJS 11 | DTO classes + pipes | class-validator / Zod pipe |
import type { FastifyRequest, FastifyReply } from 'fastify';
type AuthedRequest = FastifyRequest & { userId: string };
function getUserId(req: FastifyRequest): string {
return (req as AuthedRequest).userId;
}Prefer plugins/decorators (fastify.decorateRequest) over casts when possible.
req.body without validation - types lie at runtime. Fix: Zod parse at boundary per Zod at Boundaries.any leaks via defaults. Fix: per-route handler types or shared AppRequest alias.@types/express must match Express 5. Fix: align versions in package.json.next(err) in Express. Fix: wrap async middleware utilities.| Alternative | Use When | Don't Use When |
|---|---|---|
| Zod + manual Express types | Full control of validation | You want schema perf of Fastify |
@fastify/type-provider-zod | Single Zod source in Fastify | Express-only stack |
| NestJS DTOs | Large teams, decorators OK | Minimal HTTP microservice |
| OpenAPI codegen | Contract-first public APIs | Internal-only CRUD |
Better async error propagation - still validate bodies explicitly and type augmentations for locals.
Request<{ id: string }> for route /users/:id - validate UUID format with Zod.
res.locals typing in Express - Request<..., Locals> for template middleware data.
Return type of handler influences serialization typing when schema response is defined.
Separate from HTTP handlers - use ws types or Fastify @fastify/websocket plugin typings.
Share DTO interfaces in @acme/types - framework wiring stays separate per app.
Use DTO classes with ValidationPipe - different pattern, same boundary validation principle.
res.json<UserDto>(dto) documents intent - does not validate serialization at runtime.
Express: Error or custom AppError with statusCode. Fastify: setErrorHandler infers reply shape.
Yes - install both; Node types do not include framework request fields.
declare module 'express-serve-static-core' { interface Request { ... } } extends Express types project-wide.
Request<..., ..., ..., { page?: string }> - coerce with Zod z.coerce.number() for page.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 16, 2026