Typing Middleware
Middleware is the heart of an Express app: functions that run between the incoming request and the final handler. TypeScript models them with two types — RequestHandler for ordinary middleware and ErrorRequestHandler for error handlers — and both come from @types/express. Beyond typing the function signatures, you often need to attach data to the request object (an authenticated req.user, a request id) in a way the rest of the app can see. This page covers both handler types and the declaration-merging pattern for safely extending Request.
The RequestHandler type
Ordinary middleware has the same (req, res, next) signature as a route handler. The RequestHandler type captures it, and like Request, it is generic over params, response body, request body, and query.
import type { RequestHandler } from "express";
const logger: RequestHandler = (req, _res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // pass control to the next middleware
};
app.use(logger);
Typing the constant as RequestHandler infers req, res, and next automatically, so you never annotate them by hand. Always call next() (or send a response) — forgetting it leaves the request hanging, and the type system cannot catch that.
Error-handling middleware
Express recognises error middleware by its arity: a function with four parameters (err, req, res, next). The ErrorRequestHandler type encodes this so the compiler enforces the correct shape. Register it last, after all routes.
import type { ErrorRequestHandler } from "express";
const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
const status = err instanceof HttpError ? err.status : 500;
res.status(status).json({ message: err.message ?? "Internal Error" });
};
app.use(errorHandler);
The err parameter is typed as any by default because anything can be thrown in JavaScript. Narrow it with instanceof checks before reading properties. Keep all four parameters even if unused — dropping next turns it back into ordinary middleware and Express stops treating it as an error handler.
| Handler kind | Type | Parameters |
|---|---|---|
| Normal middleware | RequestHandler | (req, res, next) |
| Route handler | RequestHandler | (req, res, next?) |
| Error middleware | ErrorRequestHandler | (err, req, res, next) |
Extending req.user via declaration merging
Authentication middleware typically attaches the current user to the request. Assigning req.user directly fails to compile because Request has no user property. The correct fix is declaration merging: augment Express’s own Request interface so the property exists everywhere.
// src/types/express.d.ts
import "express";
declare global {
namespace Express {
interface Request {
user?: { id: string; roles: string[] };
}
}
}
Express defines a global Express namespace specifically so applications can extend it. Because interface Request is open, your declaration merges into the existing one, adding user to every Request in the project. Mark it optional (?) since it is only populated after the auth middleware runs.
Make sure the
.d.tsfile is covered by yourtsconfig.jsoninclude(e.g.src/**/*). The emptyimport "express"keeps the file a module sodeclare globalaugments rather than replaces the global scope.
Using the augmented request
Once merged, the property is fully typed across the whole app — in the auth middleware that sets it and in every downstream handler that reads it.
import type { RequestHandler } from "express";
const requireAuth: RequestHandler = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
res.status(401).json({ message: "Unauthorized" });
return;
}
req.user = { id: "u_1", roles: ["admin"] }; // ✅ typed, no error
next();
};
app.get("/me", requireAuth, (req, res) => {
res.json({ id: req.user?.id }); // req.user is typed everywhere
});
Notice the explicit return after sending the 401 response: it stops execution without calling next(). Because RequestHandler expects a void return, you cannot return res.status(401)... directly — call it on its own line, then return.
Best Practices
- Type middleware as
RequestHandlerand error handlers asErrorRequestHandler. - Always call
next()or send a response; the compiler cannot detect a hung request. - Keep all four parameters on error middleware so Express recognises its arity.
- Narrow the
err: anyparameter withinstanceofbefore reading its fields. - Extend
Express.Requestthroughdeclare globaldeclaration merging, not casts. - Mark merged properties optional and ensure the
.d.tsis in your tsconfiginclude.