Skip to main content

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

  1. Order matters - Place middleware in the correct order (auth before authorization)
  2. Fail fast - Return early for invalid requests
  3. Use next() - Always call next() or send a response
  4. Handle errors - Wrap async middleware in try/catch
  5. Log appropriately - Log errors, not tokens or sensitive data
  6. Rate limit - Protect expensive operations
  7. Validate input - Use Zod or similar for validation
  8. Set headers - Add informative headers (rate limit, etc.)
  9. Fail open for non-critical - Rate limiting should fail open on Redis errors
  10. Test middleware - Write unit tests for middleware functions