6 min read
Web Backend Fundamentals Index
Tier 1 -- Core Web & API Basics
Authentication & Authorization
Tier 2 -- Communication Models
Tier 3 -- Production & API Hardening
Web Backend Fundamentals Index
Tier 1 -- Core Web & API Basics
Authentication & Authorization
Tier 2 -- Communication Models
Tier 3 -- Production & API Hardening
Middleware and Interceptors
1. What It Is
is code that runs between the moment a request arrives at the server and the moment a response is sent. It sits in the request/response pipeline, can inspect or modify either, and decides whether to pass the request along to the next handler or short-circuit.
Interceptors are the same concept under a different name — common in frameworks like NestJS (TypeScript), Spring (Java), and . The underlying idea is identical: wrap request handling with cross-cutting logic.
Why it matters: is how you implement authentication, logging, , input validation, error handling, and — all without cluttering every individual route handler. It is one of the most widely used patterns in backend development.
A backend team notices that every route handler in their API duplicates the same authentication check at the start of the function body. Which approach best addresses this problem while keeping individual route handlers clean?
2. How It Works in Practice
The Pipeline Model
A request passes through a chain of functions before reaching the route handler. Each can:
- Run code before passing to the next (e.g., log start time)
- Modify the request object (e.g., attach
req.userafter auth) - Modify the response (e.g., add headers)
- Short-circuit and return a response early (e.g., 401 on auth failure)
- Run code after the handler (e.g., log end time, error handling)
Request → [Logger] → [Auth] → [Rate Limiter] → Route Handler → [Error Handler] → Response
Express.js Middleware
// Middleware signature: (req, res, next) => void function loggerMiddleware(req, res, next) { const start = Date.now(); console.log(`--> ${req.method} ${req.path}`); res.on('finish', () => { console.log(`<-- ${req.method} ${req.path} ${res.statusCode} ${Date.now() - start}ms`); }); next(); // Pass control to the next middleware } // Auth middleware — short-circuits on failure function authMiddleware(req, res, next) { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) return res.status(401).json({ error: 'Unauthorized' }); try { req.user = verifyToken(token); next(); } catch { res.status(401).json({ error: 'Invalid token' }); } } // Apply globally app.use(loggerMiddleware); // Apply to specific routes app.get('/api/profile', authMiddleware, profileHandler);
Error-Handling Middleware
In Express, a four-argument middleware function is an error handler:
app.use((err, req, res, next) => { console.error(err.stack); const status = err.status || 500; res.status(status).json({ error: err.message || 'Internal Server Error' }); });
Django Middleware
Django middleware is a class with hooks for request and response:
class TimingMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): import time start = time.time() response = self.get_response(request) # calls next middleware or view elapsed = time.time() - start response['X-Response-Time'] = f'{elapsed:.3f}s' return response # settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'myapp.middleware.TimingMiddleware', # ... ]
NestJS Interceptors (TypeScript)
Interceptors in NestJS wrap method execution with before and after logic:
@Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const start = Date.now(); return next.handle().pipe( tap(() => console.log(`Request took ${Date.now() - start}ms`)), ); } }
An Express.js auth middleware receives a request with no Authorization header. Which behavior correctly implements short-circuit logic for this case?
3. Common Real-World Usage
Authentication/Authorization: verify tokens, attach user context to the request — runs before all protected route handlers.
Request logging: log method, path, response time, status code — essential for observability.
: count requests per IP or API key, return 429 Too Many Requests when the limit is exceeded (e.g., express-rate-limit, Django Ratelimit).
: set Access-Control-Allow-Origin and related headers — often a built-in ( in Express, CorsMiddleware in Django).
Body parsing / validation: parse JSON bodies, validate schemas (Joi, Zod, Pydantic) before the handler sees the data.
Request ID injection: generate a unique request ID (UUID) per request and attach it to the request object + response headers for tracing across logs.
Compression: gzip/brotli response bodies (compression in Express).
Your API is experiencing abuse from a single client sending thousands of requests per minute, causing degraded performance for other users. Which middleware pattern is most appropriate to address this, and what HTTP status code should it return when the limit is exceeded?
4. Trade-offs and Common Mistakes
Ordering matters: runs in the order it is registered. Auth must run before route handlers that need authorization. Error-handling middleware must be last.
Forgetting to call next(): in Express, not calling next() silently hangs the request — the client never gets a response and eventually times out. Easy bug to introduce, hard to notice in testing.
Catching errors from async middleware: Express 4 does not automatically handle rejected promises from async middleware. Wrap async handlers or use Express 5 (which does handle them natively):
// Express 4 — must catch manually app.use(async (req, res, next) => { try { await doSomething(); next(); } catch (err) { next(err); // forward to error-handling middleware } });
Doing heavy work in middleware on every request: middleware that makes a database call on every request (e.g., looking up permissions from DB per-request without caching) can become a performance bottleneck. Cache aggressively or batch where possible.
Global vs. scoped middleware: applying auth globally and then trying to exclude public routes with exceptions is fragile. Prefer applying auth explicitly to protected route groups.
5. Interview Angle
How to explain it
" is a function that sits in the request pipeline and runs before or after the route handler. It's used for cross-cutting concerns — auth, logging, , error handling — so those don't leak into every individual handler. The key properties are: it can modify the request and response, pass control to the next function, or short-circuit and return early."
Common interview questions
Q: How would you implement authentication across all routes without repeating code? A: Write an auth that verifies the token and attaches the user to the request object, then apply it globally or to a router group that covers all protected routes.
Q: How does error handling work in Express middleware?
A: You define a four-argument middleware (err, req, res, next). When any earlier middleware or handler calls next(err) or throws (in Express 5), Express skips to the nearest error-handling middleware.
Q: What is the difference between middleware and interceptors? A: They are the same concept under different names. Middleware is the term used in Express/Django; interceptors are used in NestJS, Spring, and . Both wrap request handling with before/after logic.
Q: How would you add request tracing across your API?
A: Generate a UUID in the first middleware, attach it to the request and set it as a response header (X-Request-ID). Pass it into all log statements for that request so you can correlate logs across services.
Your team wants to add distributed tracing so that every incoming API request gets a unique ID that appears in all related log entries and is returned in the response headers. Where is the best place to implement this logic, and why?
6. Practical Examples
Composable middleware stack (Express)
const express = require('express'); const rateLimit = require('express-rate-limit'); const helmet = require('helmet'); const { authenticate } = require('./auth'); const app = express(); // Global middleware — all requests app.use(helmet()); app.use(express.json()); app.use(requestIdMiddleware); app.use(loggerMiddleware); // Public routes (no auth) app.post('/auth/login', loginHandler); // Protected API — auth + rate limiting const apiRouter = express.Router(); apiRouter.use(authenticate); apiRouter.use(rateLimit({ windowMs: 60_000, max: 100 })); apiRouter.get('/me', getMeHandler); apiRouter.get('/orders', getOrdersHandler); app.use('/api', apiRouter); // Error handler — must be last app.use(globalErrorHandler);
Request ID middleware
const { randomUUID } = require('crypto'); function requestIdMiddleware(req, res, next) { req.id = req.headers['x-request-id'] || randomUUID(); res.setHeader('X-Request-ID', req.id); next(); }
Async error wrapper (Express 4)
const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; app.get('/users/:id', asyncHandler(async (req, res) => { const user = await db.findUser(req.params.id); if (!user) throw Object.assign(new Error('Not found'), { status: 404 }); res.json(user); }));
In an Express application, a developer wants to apply rate limiting and authentication only to routes under /api, while leaving /auth/login publicly accessible. Which approach correctly achieves this?
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.