Express.js Framework Patterns
Purpose
Essential Express.js patterns for building scalable backend APIs, emphasizing clean routing, middleware composition, and proper request/response handling.
When to Use This Skill
-
Creating or modifying Express routes
-
Building middleware (auth, validation, error handling)
-
Working with Express Request/Response objects
-
Implementing BaseController pattern
-
Error handling in Express
Clean Route Pattern
Routes Only Route
Routes should ONLY:
-
✅ Define route paths
-
✅ Register middleware
-
✅ Delegate to controllers
Routes should NEVER:
-
❌ Contain business logic
-
❌ Access database directly
-
❌ Implement validation logic
-
❌ Format complex responses
import { Router } from 'express'; import { UserController } from '../controllers/UserController'; import { SSOMiddlewareClient } from '../middleware/SSOMiddleware';
const router = Router(); const controller = new UserController();
// Clean delegation - no business logic router.get('/:id', SSOMiddlewareClient.verifyLoginStatus, async (req, res) => controller.getUser(req, res) );
router.post('/', SSOMiddlewareClient.verifyLoginStatus, async (req, res) => controller.createUser(req, res) );
export default router;
BaseController Pattern
Implementation
import * as Sentry from '@sentry/node'; import { Response } from 'express';
export abstract class BaseController { protected handleError( error: unknown, res: Response, context: string, statusCode = 500 ): void { Sentry.withScope((scope) => { scope.setTag('controller', this.constructor.name); scope.setTag('operation', context); Sentry.captureException(error); });
res.status(statusCode).json({
success: false,
error: {
message: error instanceof Error ? error.message : 'An error occurred',
code: statusCode,
},
});
}
protected handleSuccess<T>(
res: Response,
data: T,
message?: string,
statusCode = 200
): void {
res.status(statusCode).json({
success: true,
message,
data,
});
}
protected async withTransaction<T>(
name: string,
operation: string,
callback: () => Promise<T>
): Promise<T> {
return await Sentry.startSpan({ name, op: operation }, callback);
}
protected addBreadcrumb(
message: string,
category: string,
data?: Record<string, any>
): void {
Sentry.addBreadcrumb({ message, category, level: 'info', data });
}
}
Using BaseController
import { Request, Response } from 'express'; import { BaseController } from './BaseController'; import { UserService } from '../services/userService'; import { createUserSchema } from '../validators/userSchemas';
export class UserController extends BaseController { private userService: UserService;
constructor() {
super();
this.userService = new UserService();
}
async getUser(req: Request, res: Response): Promise<void> {
try {
this.addBreadcrumb('Fetching user', 'user_controller', {
userId: req.params.id
});
const user = await this.userService.findById(req.params.id);
if (!user) {
return this.handleError(
new Error('User not found'),
res,
'getUser',
404
);
}
this.handleSuccess(res, user);
} catch (error) {
this.handleError(error, res, 'getUser');
}
}
async createUser(req: Request, res: Response): Promise<void> {
try {
const validated = createUserSchema.parse(req.body);
const user = await this.withTransaction(
'user.create',
'db.query',
() => this.userService.create(validated)
);
this.handleSuccess(res, user, 'User created successfully', 201);
} catch (error) {
this.handleError(error, res, 'createUser');
}
}
}
Middleware Patterns
Authentication
import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; import { config } from '../config/unifiedConfig';
export class SSOMiddlewareClient { static verifyLoginStatus(req: Request, res: Response, next: NextFunction): void { const token = req.cookies.refresh_token;
if (!token) {
return res.status(401).json({ error: 'Not authenticated' });
}
try {
const decoded = jwt.verify(token, config.tokens.jwt);
res.locals.claims = decoded;
res.locals.effectiveUserId = decoded.sub;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
}
Audit with AsyncLocalStorage
import { Request, Response, NextFunction } from 'express'; import { AsyncLocalStorage } from 'async_hooks'; import { v4 as uuidv4 } from 'uuid';
export interface AuditContext { userId: string; userName?: string; requestId: string; timestamp: Date; }
export const auditContextStorage = new AsyncLocalStorage<AuditContext>();
export function auditMiddleware(req: Request, res: Response, next: NextFunction): void { const context: AuditContext = { userId: res.locals.effectiveUserId || 'anonymous', userName: res.locals.claims?.preferred_username, timestamp: new Date(), requestId: req.id || uuidv4(), };
auditContextStorage.run(context, () => next());
}
export function getAuditContext(): AuditContext | null { return auditContextStorage.getStore() || null; }
Error Boundary
import { Request, Response, NextFunction } from 'express'; import * as Sentry from '@sentry/node';
export function errorBoundary( error: Error, req: Request, res: Response, next: NextFunction ): void { const statusCode = error.statusCode || 500;
Sentry.captureException(error);
res.status(statusCode).json({
success: false,
error: {
message: error.message,
code: error.name,
},
});
}
// Async wrapper export function asyncErrorWrapper( handler: (req: Request, res: Response, next: NextFunction) => Promise<any> ) { return async (req: Request, res: Response, next: NextFunction) => { try { await handler(req, res, next); } catch (error) { next(error); } }; }
Middleware Ordering
Critical Order
import express from 'express'; import * as Sentry from '@sentry/node';
const app = express();
// 1. Sentry request handler (FIRST) app.use(Sentry.Handlers.requestHandler());
// 2. Body/cookie parsing app.use(express.json()); app.use(cookieParser());
// 3. Routes app.use('/api/users', userRoutes);
// 4. Error handler (AFTER routes) app.use(errorBoundary);
// 5. Sentry error handler (LAST) app.use(Sentry.Handlers.errorHandler());
Rules:
-
Sentry request handler FIRST
-
Body/cookie parsers before routes
-
Error handlers AFTER all routes
-
Sentry error handler LAST
Request/Response Handling
Typed Requests
interface CreateUserRequest { email: string; name: string; password: string; }
async function createUser( req: Request<{}, {}, CreateUserRequest>, res: Response ): Promise<void> { const { email, name, password } = req.body; // Typed }
Response Patterns
// Success (200) res.json({ success: true, data: user });
// Created (201) res.status(201).json({ success: true, data: user });
// Error (400/500) res.status(400).json({ success: false, error: { message: 'Invalid input' } });
HTTP Status Codes
Code Use Case
200 Success (GET, PUT)
201 Created (POST)
204 No Content (DELETE)
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
500 Server Error
Common Mistakes
- Business Logic in Routes
// ❌ Never do this router.post('/submit', async (req, res) => { // 100+ lines of logic const user = await db.user.create(req.body); const workflow = await processWorkflow(user); res.json(workflow); });
// ✅ Do this router.post('/submit', (req, res) => controller.submit(req, res));
- Wrong Middleware Order
// ❌ Error handler before routes app.use(errorBoundary); app.use('/api', routes); // Won't catch errors
// ✅ Error handler after routes app.use('/api', routes); app.use(errorBoundary);
- No Error Handling
// ❌ Unhandled errors crash server router.get('/user/:id', async (req, res) => { const user = await userService.get(req.params.id); // May throw res.json(user); });
// ✅ Proper error handling async getUser(req: Request, res: Response): Promise<void> { try { const user = await this.userService.get(req.params.id); this.handleSuccess(res, user); } catch (error) { this.handleError(error, res, 'getUser'); } }
Common Imports
// Express core import express, { Request, Response, NextFunction, Router } from 'express';
// Middleware import cookieParser from 'cookie-parser'; import cors from 'cors';
// Sentry import * as Sentry from '@sentry/node';
// Utilities import { AsyncLocalStorage } from 'async_hooks';
Best Practices
-
Keep Routes Clean - Routes only route, delegate to controllers
-
Use BaseController - Consistent error handling and response formatting
-
Proper Middleware Order - Sentry → Parsers → Routes → Error handlers
-
Type Everything - Use TypeScript for Request/Response types
-
Handle All Errors - Use try-catch in controllers, error boundaries globally
Related Skills:
-
nodejs - Core Node.js patterns and async handling
-
backend-dev-guidelines - Complete backend architecture guide
-
prisma - Database patterns with Prisma ORM