Middleware
Overview
Middleware functions are executed sequentially for each request. They can:
- Modify the request/response objects
- End the request-response cycle
- Call the next middleware in the stack
- Handle errors
Middleware Chain
// src/app.ts
import express from 'express';
import helmet from 'helmet';
import { corsMiddleware } from './middleware/cors.middleware';
import { loggingMiddleware } from './middleware/logging.middleware';
import { errorHandler } from './middleware/error.middleware';
import routes from './routes/index';
const app = express();
// 1. Security headers
app.use(helmet());
// 2. CORS
app.use(corsMiddleware);
// 3. Request logging
app.use(loggingMiddleware);
// 4. Raw body for Stripe webhooks (before JSON parser)
app.use('/webhooks/stripe', express.raw({ type: 'application/json' }));
// 5. JSON body parser
app.use(express.json());
// 6. Routes (with their own middleware)
app.use(routes);
// 7. Error handler (must be last)
app.use(errorHandler);
Core Middleware
1. CORS Middleware
File: src/middleware/cors.middleware.ts
Handles Cross-Origin Resource Sharing.
Configuration:
const corsOptions: CorsOptions = {
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl)
if (!origin) {
callback(null, true);
return;
}
if (env.NODE_ENV === 'production') {
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',').map(o => o.trim()) || [];
if (isOriginAllowed(origin, allowedOrigins)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
} else {
// Development: Allow localhost
if (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:')) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
};
Supported origin patterns:
- Exact match:
https://bookwish.io - Wildcard subdomains:
https://*.bookwish.store - Multiple origins:
https://app.bookwish.io,https://admin.bookwish.io
Environment variable:
ALLOWED_ORIGINS=https://bookwish.io,https://*.bookwish.store
2. Authentication Middleware
File: src/middleware/auth.middleware.ts
Verifies JWT tokens and attaches user info to request.
Required Authentication
export async function authenticate(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({
error: 'Unauthorized',
message: 'Missing or invalid authorization header'
});
return;
}
const token = authHeader.substring(7);
try {
const payload = verifyAccessToken(token);
// Fetch current user tier from database
const user = await prisma.user.findUnique({
where: { id: payload.userId },
select: { id: true, isGuest: true, tier: true },
});
if (!user) {
res.status(401).json({
error: 'Unauthorized',
message: 'User not found'
});
return;
}
req.user = {
id: user.id,
isGuest: user.isGuest,
tier: user.tier,
};
next();
} catch (error) {
res.status(401).json({
error: 'Unauthorized',
message: 'Invalid or expired token'
});
}
}
Usage:
router.post('/protected', authenticate, controller.handler);
Optional Authentication
export async function optionalAuth(
req: Request,
_res: Response,
next: NextFunction
): Promise<void> {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
req.user = undefined;
next();
return;
}
const token = authHeader.substring(7);
try {
const payload = verifyAccessToken(token);
const user = await prisma.user.findUnique({
where: { id: payload.userId },
select: { id: true, isGuest: true, tier: true },
});
req.user = user ? {
id: user.id,
isGuest: user.isGuest,
tier: user.tier,
} : undefined;
} catch (error) {
req.user = undefined;
}
next();
}
Usage:
router.get('/public', optionalAuth, controller.handler);
3. Tier Gating Middleware
File: src/middleware/tier.middleware.ts
Restricts access based on user tier.
const tierHierarchy: Record<UserTier, number> = {
guest: 0,
free: 1,
premium: 2,
bookstore: 3,
admin: 4,
};
export function requireTier(minTier: UserTier) {
return (req: Request, res: Response, next: NextFunction): void => {
if (!req.user) {
res.status(401).json({
error: 'Unauthorized',
message: 'Authentication required'
});
return;
}
const userTierLevel = tierHierarchy[req.user.tier];
const minTierLevel = tierHierarchy[minTier];
if (userTierLevel < minTierLevel) {
res.status(403).json({
error: 'upgrade_required',
minTier,
message: `This feature requires ${minTier} tier or higher`,
});
return;
}
next();
};
}
Usage:
import { UserTier } from '@prisma/client';
// Require premium tier
router.post('/premium-feature', authenticate, requireTier(UserTier.premium), controller.handler);
// Require bookstore tier
router.post('/store-management', authenticate, requireTier(UserTier.bookstore), controller.handler);
4. Store Access Middleware
File: src/middleware/store-owner.middleware.ts
Verifies store ownership or staff access.
Store Access (Owner or Staff)
export async function requireStoreAccess(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
if (!req.user) {
res.status(401).json({
error: 'Unauthorized',
message: 'Authentication required',
});
return;
}
if (req.user.isGuest) {
res.status(403).json({
error: 'Forbidden',
message: 'Guest users cannot manage stores',
});
return;
}
const storeId = req.params.storeId || req.body.store_id || req.query.store_id;
if (!storeId) {
res.status(400).json({
error: 'BadRequest',
message: 'Store ID is required',
});
return;
}
// Check if user owns the store
const store = await prisma.store.findUnique({
where: { id: storeId },
select: { id: true, ownerUserId: true },
});
if (!store) {
res.status(404).json({
error: 'NotFound',
message: 'Store not found',
});
return;
}
if (store.ownerUserId === req.user.id) {
req.store = { id: store.id, role: 'owner' };
next();
return;
}
// Check if user is staff
const staffMember = await prisma.storeStaff.findUnique({
where: {
storeId_userId: {
storeId: store.id,
userId: req.user.id,
},
},
select: {
role: true,
permissions: true,
},
});
if (staffMember) {
req.store = {
id: store.id,
role: staffMember.role,
permissions: staffMember.permissions as Record<string, any>,
};
next();
return;
}
res.status(403).json({
error: 'Forbidden',
message: 'You do not have access to this store',
});
}
Usage:
router.patch('/stores/:storeId/settings', authenticate, requireStoreAccess, controller.handler);
Store Ownership (Owner Only)
export async function requireStoreOwnership(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
// Similar to requireStoreAccess but only allows owners
// ...
}
Usage:
router.delete('/stores/:storeId', authenticate, requireStoreOwnership, controller.handler);
5. Rate Limiting Middleware
File: src/middleware/rate-limit.middleware.ts
Prevents abuse with configurable rate limits.
Configuration:
export const rateLimits: Record<string, RateLimitConfig> = {
create_line: { limit: 10, windowSeconds: 60 }, // 10 per minute
create_line_hourly: { limit: 50, windowSeconds: 3600 }, // 50 per hour
create_review: { limit: 5, windowSeconds: 3600 }, // 5 per hour
follow_user: { limit: 30, windowSeconds: 3600 }, // 30 per hour
like: { limit: 100, windowSeconds: 3600 }, // 100 per hour
report: { limit: 10, windowSeconds: 3600 }, // 10 per hour
search: { limit: 60, windowSeconds: 60 }, // 60 per minute
feed: { limit: 120, windowSeconds: 60 }, // 120 per minute
};
Usage:
import { rateLimit } from '../middleware/rate-limit.middleware';
router.post('/lines', authenticate, rateLimit('create_line'), controller.createLine);
router.post('/reviews', authenticate, rateLimit('create_review'), controller.createReview);
router.post('/follow', authenticate, rateLimit('follow_user'), controller.follow);
Response headers:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1640000000000
Rate limit exceeded response:
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Please try again in 45 seconds.",
"retryAfter": 45000,
"limit": 10,
"remaining": 0
}
Monthly Post Throttle
Free users are limited to 30 posts (lines, reviews, notes) per month.
router.post('/lines', authenticate, monthlyPostThrottle(), controller.createLine);
Response:
{
"error": "monthly_limit_reached",
"message": "Free accounts are limited to 30 posts per month. Upgrade to Premium for unlimited posts.",
"limit": 30,
"current": 30,
"resetsAt": "2024-02-01T00:00:00.000Z",
"upgradeUrl": "/upgrade"
}
6. Admin Middleware
File: src/middleware/admin.middleware.ts
Restricts access to admin-only endpoints.
export function requireAdmin(
req: Request,
res: Response,
next: NextFunction
): void {
if (!req.user) {
res.status(401).json({
error: 'Unauthorized',
message: 'Authentication required'
});
return;
}
if (req.user.tier !== 'admin') {
res.status(403).json({
error: 'Forbidden',
message: 'Admin access required'
});
return;
}
next();
}
Usage:
router.get('/admin/reports', authenticate, requireAdmin, controller.getReports);
7. Logging Middleware
File: src/middleware/logging.middleware.ts
Logs all HTTP requests.
import morgan from 'morgan';
export const loggingMiddleware = morgan(
process.env.NODE_ENV === 'production'
? 'combined' // Apache combined log format
: 'dev' // Colored output for development
);
Example output (dev):
GET /api/books?q=gatsby 200 45.123 ms - 1234
POST /api/wishlists 201 12.456 ms - 567
8. Error Handler Middleware
File: src/middleware/error.middleware.ts
Catches and formats all errors.
export function errorHandler(
error: Error,
_req: Request,
res: Response,
_next: NextFunction
): void {
const response: ErrorResponse = {
error: error.name || 'InternalServerError',
message: error.message || 'An unexpected error occurred',
};
let statusCode = 500;
if (error instanceof z.ZodError) {
statusCode = 400;
response.error = 'ValidationError';
response.message = 'Request validation failed';
response.details = error.errors.map(err => ({
path: err.path.join('.'),
message: err.message,
}));
} else if (error.name === 'UnauthorizedError') {
statusCode = 401;
} else if (error.name === 'ForbiddenError') {
statusCode = 403;
} else if (error.name === 'NotFoundError') {
statusCode = 404;
}
// Include stack trace in development
if (process.env.NODE_ENV !== 'production') {
response.details = response.details || error.stack;
}
res.status(statusCode).json(response);
}
Middleware Execution Order
For a typical authenticated request:
1. helmet() → Security headers
2. corsMiddleware → CORS validation
3. loggingMiddleware → Request logging
4. express.json() → Parse JSON body
5. authenticate → Verify JWT token
6. requireTier() → Check user tier
7. requireStoreAccess() → Check store access
8. rateLimit() → Check rate limits
9. Controller → Execute handler
10. errorHandler → Catch errors (if any)
Request Context
Middleware can attach data to the request object:
// After authentication middleware
req.user: {
id: string;
isGuest: boolean;
tier: UserTier;
} | undefined
// After store access middleware
req.store: {
id: string;
role: string;
permissions?: Record<string, any>;
} | undefined
Custom Middleware Example
// src/middleware/custom.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { logger } from '../lib/logger';
export function logUserId(
req: Request,
_res: Response,
next: NextFunction
): void {
if (req.user) {
logger.info('request.user', {
userId: req.user.id,
tier: req.user.tier,
path: req.path,
});
}
next();
}
Usage:
router.use(logUserId);
Best Practices
- Order matters - Place middleware in the correct order (auth before authorization)
- Fail fast - Return early for invalid requests
- Use next() - Always call
next()or send a response - Handle errors - Wrap async middleware in try/catch
- Log appropriately - Log errors, not tokens or sensitive data
- Rate limit - Protect expensive operations
- Validate input - Use Zod or similar for validation
- Set headers - Add informative headers (rate limit, etc.)
- Fail open for non-critical - Rate limiting should fail open on Redis errors
- Test middleware - Write unit tests for middleware functions