Let a handler see the user that an auth middleware attached to the request
An auth middleware verifies a token and attaches req.user. Downstream handlers read req.user and today it is typed any — or, worse, the project declaration-merged user onto every Express.Request, so unauthenticated handlers also believe it is there.
Requirements: implement withAuth so the wrapped handler sees req.user as User — required, not optional — while a plain handler that was never wrapped must not see user at all. No global augmentation, no any, no assertion inside the wrapped handler.
import type { Request, RequestHandler, Response } from "express";
interface User { id: string; role: "admin" | "user" }
type AuthedRequest = Request & { user: User };
function withAuth(
handler: (req: AuthedRequest, res: Response) => void,
): RequestHandler {
// your code here
}
Write the implementation.
Never augment the global Request. The injected property is an intersection, Request & { user: User }. withAuth returns a RequestHandler that attaches user and calls the handler with the narrowed request, so only it sees user, as required.
- ✗Global-augmenting
Requestso every handler believesuserexists - ✗Making
useroptional and sprinkling non-null assertions in each handler - ✗Registering a handler that demands
AuthedRequestdirectly on the router
- →How would you compose two such wrappers so a handler sees both
userandtenant? - →Why does global augmentation weaken the guarantee even when the property is optional?
The solution
import type { Request, RequestHandler, Response } from "express";
interface User { id: string; role: "admin" | "user" }
type AuthedRequest = Request & { user: User };
function withAuth(
handler: (req: AuthedRequest, res: Response) => void,
): RequestHandler {
return (req, res, next) => {
const user = verifyToken(req.header("authorization"));
if (!user) {
res.status(401).json({ error: "unauthorized" });
return;
}
const authed = Object.assign(req, { user }) as AuthedRequest; // ← the one assertion
try {
handler(authed, res);
} catch (err) {
next(err);
}
};
}
Usage:
app.get("/me", withAuth((req, res) => {
res.json({ id: req.user.id }); // user: User — required, no ?. and no !
}));
app.get("/health", (req, res) => {
req.user;
// ~~~~ Property 'user' does not exist on type 'Request' — exactly right
});
Why not global augmentation
declare global { namespace Express { interface Request { user?: User } } } adds user to every request in the project. An unauthenticated handler starts "seeing" it too, and the optionality is then papered over with req.user! all over the codebase — the same assertion, only smeared everywhere.
The wrapper inverts that: the context lives in the type of the handler that actually received it, and there is exactly one unsound spot — the Object.assign line, where the value really is put on the object.