Middleware Patterns in Node.js and Python — Express and Django
In this tutorial, you'll learn about Middleware Patterns in Node.js and Python. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Middleware is software that sits between the client and the application logic, processing requests and responses in a pipeline where each component can modify the request, execute logic, or short-circuit the chain before it reaches the handler.
What You'll Learn
By the end of this tutorial, you will implement middleware in Express (Node.js) and Django (Python), understand the request/response pipeline, build authentication, logging, and error-handling middleware, and design modular middleware chains for large applications.
Why It Matters
Middleware eliminates code duplication by extracting cross-cutting concerns — authentication, logging, Rate Limiting, CORS, compression — into reusable pipeline components. Doda Browser uses Express middleware to authenticate API requests, log request metrics, rate-limit abusive clients, and compress responses before they leave the server.
Real-World Use
A web application needs every request to be logged, authenticated, rate-limited, and have CORS headers attached. Instead of adding this logic to every route handler, middleware applies these behaviors globally. Adding a new middleware is a single line: app.use(middleware).
Middleware Pipeline Architecture
flowchart LR
REQ[Request] --> M1[Logger Middleware]
M1 --> M2[CORS Middleware]
M2 --> M3[Auth Middleware]
M3 --> M4[Rate Limiter]
M4 --> H[Route Handler]
H --> M5[Error Handler]
M5 --> RES[Response]
style REQ fill:#22c55e,color:#fff
style H fill:#f90,color:#fff
Express Middleware Patterns
// middleware-express.js
// Express middleware patterns: application-level, router-level, error-handling
const express = require('express');
const app = express();
// ── 1. Application-level middleware (runs on every request) ──
// Logger middleware
app.use((req, res, next) => {
const start = Date.now();
console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`);
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`[${req.method}] ${req.originalUrl} -> ${res.statusCode} (${duration}ms)`);
});
next();
});
// ── 2. Built-in middleware ──
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// ── 3. Router-level middleware ──
const apiRouter = express.Router();
// Auth middleware applied only to /api routes
const requireAuth = (req, res, next) => {
const token = req.headers['authorization'];
if (!token || !token.startsWith('Bearer ')) {
console.log(`[Auth] Rejected ${req.ip} — no token`);
return res.status(401).json({ error: 'Authentication required' });
}
req.user = { id: 1, role: 'user' }; // Simulated token verification
console.log(`[Auth] Authenticated user ${req.user.id}`);
next();
};
apiRouter.use(requireAuth);
apiRouter.get('/profile', (req, res) => {
res.json({ user: req.user, message: 'Protected profile data' });
});
apiRouter.get('/data', (req, res) => {
res.json({ items: ['item1', 'item2'], user: req.user.id });
});
app.use('/api', apiRouter);
// ── 4. Public routes (no auth) ──
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// ── 5. Error-handling middleware (4 parameters) ──
app.use((err, req, res, next) => {
console.error(`[Error] ${err.message}`, err.stack);
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message,
});
});
app.listen(3000, () => console.log('Express app with middleware on :3000'));
Expected behavior:
[2026-06-22T10:00:00.000Z] GET /health
[GET] /health -> 200 (2ms)
[2026-06-22T10:00:05.000Z] GET /api/profile
[Auth] Rejected 127.0.0.1 — no token
[GET] /api/profile -> 401 (1ms)
[2026-06-22T10:00:10.000Z] GET /api/data
[Auth] Authenticated user 1
[GET] /api/data -> 200 (5ms)
The logger runs on every request. The auth middleware runs only on /API/* routes. Public routes like /health bypass authentication. Errors propagate to the 4-parameter error handler.
Django Middleware Patterns
# middleware_Django.py
# Django custom middleware patterns
import time
import logging
from Django.HTTP import JsonResponse
from Django.utils.deprecation import MiddlewareMixin
logger = logging.getLogger(__name__)
# ── 1. Request/Response Logger Middleware ──
class RequestLogMiddleware(MiddlewareMixin):
"""Log every request with duration."""
def Process_request(self, request):
"""Called before Django processes the request."""
request.start_time = time.time()
logger.info(f"Request: {request.method} {request.path}")
def Process_response(self, request, response):
"""Called after Django returns the response."""
duration = time.time() - request.start_time
logger.info(
f"Response: {request.method} {request.path} "
f"-> {response.status_code} ({duration:.2f}s)"
)
response['X-Response-Time'] = str(duration)
return response
# ── 2. Authentication Middleware ──
class APIKeyAuthMiddleware(MiddlewareMixin):
"""Validate API key on protected routes."""
EXEMPT_PATHS = ['/health', '/API/public', '/admin/login']
def Process_request(self, request):
path = request.path_info
# Skip auth for exempt paths
if any(path.startswith(p) for p in self.EXEMPT_PATHS):
return None
# Check for API key
API_key = request.headers.get('X-API-Key')
if not API_key:
logger.warning(f"Auth failed: No API key for {path}")
return JsonResponse(
{'error': 'API key required'},
status=401
)
# Validate API key (simulated — use a real validation in production)
if not API_key.startswith('sk_'):
logger.warning(f"Auth failed: Invalid API key format for {path}")
return JsonResponse(
{'error': 'Invalid API key'},
status=403
)
# Attach user info to request
request.API_user = {'id': 1, 'key_prefix': API_key[:8]}
logger.info(f"Auth: Authenticated user {request.API_user['id']}")
return None # Continue to next middleware/view
# ── 3. Rate Limiting Middleware ──
class RateLimitMiddleware(MiddlewareMixin):
"""Simple in-memory rate limiter (use Redis in production)."""
def __init__(self, get_response):
super().__init__(get_response)
self.request_counts = {} # IP -> [timestamps]
def Process_request(self, request):
client_ip = request.META.get('REMOTE_ADDR', 'unknown')
now = time.time()
window = 60 # 1 minute window
max_requests = 100
# Clean old entries
if client_ip in self.request_counts:
self.request_counts[client_ip] = [
t for t in self.request_counts[client_ip]
if now - t < window
]
else:
self.request_counts[client_ip] = []
# Check limit
if len(self.request_counts[client_ip]) >= max_requests:
logger.warning(f"Rate limit exceeded for {client_ip}")
response = JsonResponse(
{'error': 'Rate limit exceeded'},
status=429
)
response['Retry-After'] = str(window)
return response
self.request_counts[client_ip].append(now)
return None
# settings.py MIDDLEWARE configuration:
# MIDDLEWARE = [
# 'middleware_Django.RequestLogMiddleware', "# 'middleware_Django.APIKeyAuthMiddleware'", "# 'middleware_Django.RateLimitMiddleware'", "# 'Django.contrib.auth.middleware.AuthenticationMiddleware'",
# ...
# ]
Expected behavior: Every Django request flows through the middleware chain in order. The logger records timing, the auth middleware blocks requests without valid API keys on protected routes, and the rate limiter rejects excessive requests from a single IP with HTTP 429.
Middleware for Input Validation
// validation-middleware.js
// Express middleware for request validation using Zod
const Express = require('Express');
const { z } = require('zod');
const app = Express();
app.use(Express.JSON());
// ── Validation schemas ──
const createUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().min(18).max(120).optional(),
});
// ── Validation middleware Factory ──
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
const errors = result.error.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
}));
console.log(`[Validation] Rejected ${req.path}:`, errors);
return res.status(400).JSON({ error: 'Validation failed', details: errors });
}
req.validatedBody = result.data;
console.log(`[Validation] Passed for ${req.path}`);
next();
};
}
// ── Routes with validation middleware ──
app.post('/users', validate(createUserSchema), (req, res) => {
console.log(`[Users] Creating user: ${req.validatedBody.email}`);
res.status(201).JSON({
id: Date.now(),
...req.validatedBody,
createdAt: new Date().toISOString(),
});
});
// ── Test valid request ──
// POST /users {"name": "Alice", "email": "alice@example.com", "age": 30}
// Response: 201 { "id": ..., "name": "Alice", "email": "alice@example.com", ... }
// ── Test invalid request ──
// POST /users {"name": "A", "email": "not-an-email"}
// Response: 400 { "error": "Validation failed", "details": [...] }
app.listen(3001, () => console.log('Validation middleware on :3001'));
Expected behavior: The validation middleware intercepts the request before the route handler. Invalid requests receive a 400 response with detailed field-level errors. Valid requests proceed with req.validatedBody containing sanitized data. This pattern keeps route handlers clean and validation logic reusable.
Common Errors
1. Middleware Ordering Mistakes
Middleware executes in the order it is registered. Putting <a href="/backend/nodejs/">Express</a>.json() after route handlers means JSON parsing never runs for those routes. Error-handling middleware must be registered last — placing it before routes causes errors to be caught before they can propagate correctly.
2. Not Calling next() in Express
Forgetting to call next() hangs the request indefinitely. The client receives no response until a timeout occurs. Always call next() in Express middleware unless you are sending a response (error, redirect) and terminating the chain.
3. Making Blocking Calls in Middleware
Synchronous file I/O, CPU-heavy computations, or network calls in middleware block the event loop and degrade performance for all requests. Use async middleware with next() properly or offload heavy work to worker processes.
4. Django Middleware Returning None Incorrectly
In Django, Process_request must return None to continue to the next middleware or view. Returning an HttpResponse short-circuits the chain. Accidentally returning a response in a condition Branch (when you meant to continue) causes confusing behavior.
5. Ignoring Error-Handling Middleware
Without a catch-all error handler, uncaught exceptions crash the Node.js Process or return a default HTML error page from Django. Always implement an error-handling middleware that logs the error with full Stack trace and returns a consistent JSON error response.
6. Leaking Sensitive Data in Error Responses
Error-handling middleware that returns Stack traces, query parameters, or request bodies in production reveals internal system details to attackers. In production, return generic error messages and log detailed errors server-side only.
Practice Questions
1. What is the difference between application-level and router-level middleware in Express?
Application-level middleware (app.use()) runs on every request to any route. Router-level middleware (router.use()) runs only on requests matching that specific router's path prefix. This allows scoping middleware to specific route groups.
2. How does Django's middleware chain Process requests and responses?
Django middleware processes requests top-to-bottom through process_request methods. If any middleware returns an HttpResponse, the chain stops. Responses flow bottom-to-top through process_response methods, so inner middleware wraps outer middleware.
3. Why should validation be implemented as middleware rather than in route handlers?
Validation as middleware ensures every route with the same schema is validated consistently. It keeps route handlers focused on business logic, makes validation reusable across routes, and centralizes error response formatting for validation failures.
4. What happens if an Express middleware throws an error without calling next(err)?
The error propagates to Express's default error handler, which returns a 500 status with the Stack trace (in development). The app may crash if the error is unhandled. Always catch errors and pass them to next(err) for the error-handling middleware.
Challenge
Build a middleware pipeline for a REST API that: (1) logs every request with duration and status code, (2) validates API keys from the X-API-Key header on all routes except /health and /docs, (3) rate-limits each API key to 100 requests per minute using an in-memory store, (4) compresses responses with gzip for clients that accept it, (5) adds security headers (CSP, X-Frame-Options, X-Content-Type-Options), and (6) catches all unhandled errors and returns a consistent JSON error response with appropriate status codes.
Mini Project: Modular Middleware Library
# middleware_library.py
# Reusable middleware components for Django/Flask
import time
import JSON
import logging
from functools import wraps
logger = logging.getLogger(__name__)
class MiddlewareLibrary:
"""Collection of reusable middleware components."""
@staticmethod
def response_time(get_response):
"""Middleware: Add X-Response-Time header."""
@wraps(get_response)
def middleware(request):
start = time.time()
response = get_response(request)
duration = time.time() - start
response['X-Response-Time'] = f'{duration:.3f}s'
logger.info(f"Response time for {request.method} {request.path}: {duration:.3f}s")
return response
return middleware
@staticmethod
def security_headers(get_response):
"""Middleware: Add security headers to every response."""
@wraps(get_response)
def middleware(request):
response = get_response(request)
response['X-Content-Type-Options'] = 'nosniff'
response['X-Frame-Options'] = 'DENY'
response['X-XSS-Protection'] = '1; mode=block'
response['Referrer-Policy'] = 'strict-origin-when-cross-origin'
return response
return middleware
@staticmethod
def CORS(allowed_origins=None):
"""Middleware Factory: Add CORS headers."""
if allowed_origins is None:
allowed_origins = ['*']
def decorator(get_response):
@wraps(get_response)
def middleware(request):
origin = request.META.get('HTTP_ORIGIN', '')
if origin in allowed_origins or '*' in allowed_origins:
response = get_response(request)
response['Access-Control-Allow-Origin'] = origin
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
else:
response = get_response(request)
return response
return middleware
return decorator
# Usage:
# MIDDLEWARE = [
# MiddlewareLibrary.response_time, "# MiddlewareLibrary.security_headers",
# MiddlewareLibrary.CORS(allowed_origins=['HTTPS://app.example.com']),
# ]
Expected behavior: The library exposes reusable middleware decorators that can be mixed and matched in any framework. Each middleware adds specific functionality without coupling to a particular framework's middleware API.
Congratulations on completing this middleware patterns tutorial! Next, explore API Gateway patterns for gateway-level middleware, then learn about backend security best practices for authentication and authorization middleware.
- Practice daily — Add logging and request timing middleware to an existing app
- Build a project — Build a middleware library with at least 5 reusable components (auth, logging, validation, Rate Limiting, error handling)
- Explore related topics — Check out Express middleware ecosystem (helmet, CORS, morgan, compression) and Django's built-in middleware
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro