Typed Middleware & Handlers
Express handlers and middleware are just functions, but typing them well is what unlocks autocompletion on req, res, and next and stops subtle mistakes like forgetting to call next() or returning the wrong value. The community @types/express package ships dedicated function types — RequestHandler and ErrorRequestHandler — that describe these signatures precisely. This page shows how to use them, how to keep async handlers type-safe, and how to sidestep the next() pitfalls that trip up most TypeScript Express projects.
Typing a request handler
A standard middleware or route handler receives req, res, and next. Rather than annotating each parameter by hand, annotate the whole function with RequestHandler. This single type wires up all three parameters correctly and gives you a return type that matches what Express expects.
import { RequestHandler } from "express";
const requestLogger: RequestHandler = (req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
};
const getUser: RequestHandler = (req, res) => {
res.json({ id: req.params.id, name: "Ada" });
};
RequestHandler is generic. Its parameters let you pin down route params, the response body, the request body, and the query string — in that order: RequestHandler<Params, ResBody, ReqBody, ReqQuery>. Supplying them turns req and res into fully typed objects.
import { RequestHandler } from "express";
interface UserParams { id: string }
interface UserBody { name: string; email: string }
interface UserResponse { id: string; name: string }
const updateUser: RequestHandler<UserParams, UserResponse, UserBody> = (
req,
res
) => {
const { name } = req.body; // typed as string
res.json({ id: req.params.id, name }); // body checked against UserResponse
};
| Generic slot | Maps to | Example |
|---|---|---|
Params | req.params | { id: string } |
ResBody | res.json(...) argument | { id: string; name: string } |
ReqBody | req.body | { name: string } |
ReqQuery | req.query | { page?: string } |
Tip: Annotate the variable with
RequestHandlerrather than annotatingreq: Request,res: Responseinline. The single annotation lets TypeScript infer the parameter types for you and keeps the generics in one readable place.
Typing error-handling middleware
Express recognises error handlers by their arity: a middleware function with four parameters (err, req, res, next) is treated as an error handler. The matching type is ErrorRequestHandler. Using it ensures you keep all four parameters — drop one and Express silently treats the function as ordinary middleware.
import { ErrorRequestHandler } from "express";
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
const status = err instanceof RangeError ? 400 : 500;
res.status(status).json({ error: err.message });
};
// Registered last, after all routes
app.use(errorHandler);
The err parameter is typed as any (or unknown under stricter configs) because anything can be thrown. Narrow it before use — check instanceof Error or a custom error class — instead of trusting that err.message exists.
Typing async wrappers
Async handlers are where typing and runtime behaviour most often diverge. In Express 4, a rejected promise inside an async handler is not caught automatically — the rejection escapes and crashes the process unless you forward it to next. The usual fix is a small wrapper, and typing it well preserves the handler’s own generics.
import { RequestHandler, Request, Response, NextFunction } from "express";
const asyncHandler =
(fn: RequestHandler): RequestHandler =>
(req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
Use it to wrap any async route, and rejected promises are routed straight to your error-handling middleware:
import express from "express";
const app = express();
app.use(express.json());
app.get(
"/users/:id",
asyncHandler(async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) throw new Error("User not found");
res.json(user);
})
);
A request for a missing user now produces a clean error response through your handler rather than an unhandled rejection:
Output:
GET /users/999 -> 500 Internal Server Error
{ "error": "User not found" }
Express 5 changes this: it automatically forwards rejected promises from async handlers to
next, so the wrapper becomes optional. It remains useful for consistent logging or when supporting both major versions.
Avoiding next() pitfalls
The most common typing mistake is returning the result of res or next. In a RequestHandler, the expected return type is void (or a promise of it), so writing return res.json(...) to end a function early can clash with strict lint rules and obscure control flow.
// Avoid: returning the response object
const bad: RequestHandler = (req, res, next) => {
if (!req.headers.authorization) return res.status(401).end();
next();
};
// Prefer: send, then return void
const good: RequestHandler = (req, res, next) => {
if (!req.headers.authorization) {
res.status(401).end();
return;
}
next();
};
Two more rules keep middleware predictable: call next() exactly once per path through the function, and call next(err) (passing an argument) to jump to error handling rather than continuing the normal chain. Calling next() after already sending a response triggers an “ERR_HTTP_HEADERS_SENT” error at runtime — a separate return after every res.send/res.json prevents it.
Best Practices
- Annotate handlers with
RequestHandlerand error handlers withErrorRequestHandlerinstead of typingreq/resparameters individually. - Use the four generic slots (
Params,ResBody,ReqBody,ReqQuery) to makereq.body,req.params, andres.jsonfully type-checked. - Wrap async handlers (or rely on Express 5’s built-in forwarding) so rejected promises reach your error middleware instead of crashing the process.
- Keep
nextcallbacks returningvoid— send the response, thenreturn, rather thanreturn res.json(...). - Call
next()once per code path andnext(err)to delegate to error handling; never call it after the response is already sent. - Narrow the
errparameter in error handlers withinstanceofchecks before reading its properties.