30.3 Typed Middleware: RequestHandler and ErrorRequestHandler
Alright, let’s talk middleware. This is where the rubber meets the road in your server setup. Middleware functions are the bouncers, the paperwork processors, and the emergency crews of your application. They have access to the request object (req), the response object (res), and that all-important next function in the application’s request-response cycle.
In vanilla JavaScript, you’d just slap a function in there and hope for the best. But we’re better than that. We have types. TypeScript’s power here is that it lets you define exactly what each middleware expects and what it’s allowed to do, turning runtime guesswork into compile-time certainty.
The Core: RequestHandler
The fundamental type you’ll be living in is RequestHandler, imported directly from express. Let’s break it down.
import { RequestHandler } from 'express';
const myMiddleware: RequestHandler = (req, res, next) => {
// Do something brilliant
next();
};
The generic signature looks like this: RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals>. Whoa, take a breath. It’s not as scary as it looks. These are just optional generics that allow you to lock down the types within the req object.
P- for theParamsdictionary (e.g.,req.params.userId).ResBody- for the type of the response body (res.send(...)).ReqBody- This is the big one. It types what you expectreq.bodyto be.ReqQuery- for the query string parameters (req.query.searchTerm).Locals- for theres.localsobject, which is your own personal playground for passing data between middleware.
Here’s the beautiful part: you almost never have to specify all these. TypeScript is brilliant at inference. You define the types for your specific request, and it flows through.
// Let's get specific. Imagine we have a route to update a user's profile.
// We expect a body with an email, and a userId param.
// First, define interfaces for the structures you expect
interface UpdateUserBody {
email: string;
avatarUrl?: string; // optional, because of course it is
}
interface UpdateUserParams {
userId: string;
}
// Now, create a middleware that uses them
const validateUserUpdate: RequestHandler<UpdateUserParams, any, UpdateUserBody> = (req, res, next) => {
// Look at this glory! req.body.email is typed as 'string'
// req.params.userId is also typed as 'string'
if (!req.body.email) {
return res.status(400).send({ error: 'Email is required, my dude.' });
}
// Maybe do some sanitization here...
next(); // Everything checks out, move along.
};
app.put('/user/:userId', validateUserUpdate, (req, res) => {
// By the time the request gets here, TypeScript *and* you
// are confident about what's in req.body and req.params.
const { email } = req.body;
const { userId } = req.params;
// ... update the user
});
The Aftermath: ErrorRequestHandler
This is where a lot of people’s setups fall apart. Express has a very specific signature for error-handling middleware: it must have four arguments. Let me say that again: (err, req, res, next). Forget the err argument? Congrats, your error handler is now a regular middleware and will never be called.
TypeScript saves us from this classic foot-gun with the ErrorRequestHandler type.
import { ErrorRequestHandler } from 'express';
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
// Finally, a use-case for the 'any' type! The error can be... anything.
console.error('Well, this is awkward:', err);
// Always define a default. The server shouldn't crash while telling the user it crashed.
const statusCode = err.status || err.statusCode || 500;
const message = statusCode === 500 ? 'Something went horribly wrong on our end.' : err.message;
res.status(statusCode).send({ error: message });
};
// The crucial part: you MUST add this to your app *after* all other middleware and routes.
app.use(errorHandler);
Why is it after everything else? Because Express’s error handling is a waterfall. If an error gets thrown anywhere upstream, Express skips everything until it finds a middleware with that specific four-argument signature. Putting it last ensures it catches errors from all your routes and other middleware.
The res.locals Playground
The Locals generic is your best friend for stitching middleware together. It’s an object attached to the response that’s designed for passing data from one middleware to the next. Let’s type it properly.
// Define what you want to put on res.locals across your entire app
declare global {
namespace Express {
interface Locals {
userId?: string; // Added by an auth middleware
startTime: number; // For timing requests
}
}
}
// Now, your middleware can strongly type the assignment
const timingMiddleware: RequestHandler = (req, res, next) => {
res.locals.startTime = Date.now(); // Perfectly typed
next();
};
const authMiddleware: RequestHandler = async (req, res, next) => {
try {
const token = req.headers.authorization?.split(' ')[1];
const payload = await verifyToken(token); // your fake function
res.locals.userId = payload.userId; // Also perfectly typed
next();
} catch (error) {
next(error); // Pass the error to our fancy ErrorRequestHandler
}
};
// Later, in a route
app.get('/profile', authMiddleware, (req, res) => {
// We can access it, fully typed!
const userId = res.locals.userId;
const duration = Date.now() - res.locals.startTime;
res.send({ userId, requestDurationMs: duration });
});
This is the kind of type safety that makes you feel like you’re building with LEGOs instead of wet spaghetti. You define the contract once, and every part of your application knows exactly what to expect. No more // @ts-ignore because you can’t remember if you called it userID or userId. It’s all right there, and the compiler has your back.