Typed Middleware and Error Handling

Middleware and Augmented Request

import type { NextFunction, Request, Response, ErrorRequestHandler, RequestHandler } from "express";

// types/express.d.ts adds: interface Request { user?: { id: string; role: "admin" | "customer" } }

export const requireAuth: RequestHandler = (req, res, next) => {
  const token = req.header("authorization")?.replace("Bearer ", "");
  if (!token) return res.status(401).json({ message: "Login required" });
  req.user = { id: "u_1", role: token === "admin-token" ? "admin" : "customer" };
  next();
};

export const requireRole = (role: "admin"): RequestHandler => (req, res, next) =>
  req.user?.role === role ? next() : res.status(403).json({ message: "Forbidden" });

export class HttpError extends Error {
  constructor(public status: number, message: string) { super(message); }
}

// async errors: wrap handlers so rejected promises reach the error handler (Express 5 does this automatically)
export const asyncHandler =
  <P, ResB, ReqB>(fn: (req: Request<P, ResB, ReqB>, res: Response<ResB>, next: NextFunction) => Promise<unknown>) =>
  (req: Request<P, ResB, ReqB>, res: Response<ResB>, next: NextFunction) => { fn(req, res, next).catch(next); };

export const errorHandler: ErrorRequestHandler = (err: unknown, _req, res, _next) => {
  const status = err instanceof HttpError ? err.status : 500;
  const message = err instanceof HttpError ? err.message : "Internal server error";
  res.status(status).json({ message });
};

The Middleware Idea, Runnable

Example

Express middleware is a chain of functions that each call next(). This typed simulation shows how auth, role check and the handler run in order, and how a thrown HttpError becomes a response.

type Req = { path: string; headers: Record<string, string>; user?: { id: string; role: "admin" | "customer" } };
type Res = { status: number; body: unknown };
type Middleware = (req: Req, res: Res, next: () => void) => void;
class HttpError extends Error { constructor(public status: number, message: string) { super(message); } }

const auth: Middleware = (req, _res, next) => {
  const token = req.headers.authorization;
  if (!token) throw new HttpError(401, "Login required");
  req.user = { id: "u_1", role: token === "admin-token" ? "admin" : "customer" };
  next();
};
const adminOnly: Middleware = (req, _res, next) => {
  if (req.user?.role !== "admin") throw new HttpError(403, "Forbidden");
  next();
};
const handler: Middleware = (req, res) => { res.status = 200; res.body = { report: "sales", by: req.user?.id }; };

function run(chain: Middleware[], req: Req): Res {
  const res: Res = { status: 404, body: null };
  const step = (i: number): void => { chain[i]?.(req, res, () => step(i + 1)); };
  try { step(0); } catch (e) {
    res.status = e instanceof HttpError ? e.status : 500;
    res.body = { message: e instanceof Error ? e.message : "error" };
  }
  return res;
}

const chain = [auth, adminOnly, handler];
for (const token of [undefined, "user-token", "admin-token"]) {
  const r = run(chain, { path: "/admin/reports", headers: token ? { authorization: token } : {} });
  console.log(String(token).padEnd(11), r.status, JSON.stringify(r.body));
}

Output

undefined   401 {"message":"Login required"}
user-token  403 {"message":"Forbidden"}
admin-token 200 {"report":"sales","by":"u_1"}