Skip to content
TypeScript ts node 3 min read

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 kindTypeParameters
Normal middlewareRequestHandler(req, res, next)
Route handlerRequestHandler(req, res, next?)
Error middlewareErrorRequestHandler(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.ts file is covered by your tsconfig.json include (e.g. src/**/*). The empty import "express" keeps the file a module so declare global augments 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 RequestHandler and error handlers as ErrorRequestHandler.
  • 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: any parameter with instanceof before reading its fields.
  • Extend Express.Request through declare global declaration merging, not casts.
  • Mark merged properties optional and ensure the .d.ts is in your tsconfig include.
Last updated June 29, 2026
Was this helpful?