Steve Kinney

Full-Stack TypeScript

Type-Safe Middleware with Express

Type-Safe Middleware Chains

Express middleware can be challenging to type correctly, especially when middleware adds properties to the request:

// Define middleware that adds user to request
interface RequestWithUser extends Request {
	user: {
		id: UserId;
		roles: string[];
	};
}

function attachUser(req: Request, res: Response, next: NextFunction): void {
	// Authenticate and attach user
	(req as RequestWithUser).user = {
		id: createUserId('123'),
		roles: ['user'],
	};
	next();
}

// Type guard middleware
function isAuthenticated(req: Request, res: Response, next: NextFunction): void {
	if (!('user' in req)) {
		return res.status(401).send('Unauthorized');
	}
	next();
}

// Create a type-safe middleware chain builder
function createProtectedRoute<
	P = ParamsDictionary,
	ResBody = any,
	ReqBody = any,
	ReqQuery = ParsedQs,
>(
	handler: (
		req: RequestWithUser & Request<P, ResBody, ReqBody, ReqQuery>,
		res: Response<ResBody>,
		next: NextFunction,
	) => void,
) {
	return [attachUser, isAuthenticated, handler as RequestHandler];
}

// Use it to define protected routes
app.get(
	'/admin',
	...createProtectedRoute<{}, { message: string }>((req, res) => {
		// req.user is properly typed and guaranteed to exist
		if (!req.user.roles.includes('admin')) {
			return res.status(403).json({ message: 'Forbidden' });
		}

		res.json({ message: 'Welcome to admin area' });
	}),
);

This pattern guarantees that req.user exists and is properly typed in your route handler, with both compile-time and runtime checks.