Middleware and Interceptors

6 min read

Reading Progress0%
Web Backend Fundamentals Index
Tier 1 -- Core Web & API Basics
Tier 2 -- Communication Models
Tier 3 -- Production & API Hardening
Web Backend Fundamentals Index
Tier 1 -- Core Web & API Basics
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.


QUICK CHECK

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?

Choose one answer

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.user after 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`)),
    );
  }
}

QUICK CHECK

An Express.js auth middleware receives a request with no Authorization header. Which behavior correctly implements short-circuit logic for this case?

Choose one answer

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).


QUICK CHECK

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?

Choose one answer

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.


QUICK CHECK

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?

Choose one answer

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);
}));
QUICK CHECK

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?

Choose one answer