Middleware Compared: Express, Django, Rails & FastAPI

Web Framework Internals

Chapter 9 ยท Middleware Compared: Express, Django, Rails & FastAPI

Chapter 8 built one middleware chain by hand and verified the real onion order, short-circuiting, and a real, measured timing header. This chapter checks how four real frameworks actually register that chain — and finds a real, documented bug report hiding in Express's own tutorial, a genuinely different interface underneath Rails, and one framework that supports two real, distinct middleware styles at once.

The Same Onion, Four Real Registration Syntaxes

Express (app.js)
const myLogger = function (req, res, next) { console.log('LOGGED'); next(); }; app.use(myLogger);
Django (settings.py)
MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ]
Rails / Rack (config/application.rb)
config.middleware.use MyCustomMiddleware config.middleware.insert_before Rack::Runtime, MyCustomMiddleware config.middleware.insert_after Rack::Runtime, AnotherMiddleware
FastAPI (main.py)
@app.middleware("http") async def add_process_time_header(request, call_next): ... app.add_middleware(CORSMiddleware, allow_origins=origins)

All four represent Chapter 8's own real, single idea — a chain of layers wrapping a final handler — but land on three genuinely different real registration philosophies: Express and Django both register by simple list order (a function call, or a plain Python list), Rails offers relative positioning against an already-registered middleware by name, and FastAPI, uniquely among the four, ships two real, different registration mechanisms in the same framework — both covered in full below.

Order Matters: A Real, Documented Bug in Express's Own Tutorial

Express's own official documentation doesn't just assert that registration order matters — it demonstrates the real, concrete consequence of getting it wrong, using the exact myLogger example shown above:

Express's own documentation, quoted directly
"The order of middleware loading is important: middleware functions that are loaded first are also executed first. If myLogger is loaded after the route to the root path, the request never reaches it and the app doesn't print 'LOGGED', because the route handler of the root path terminates the request-response cycle."
// correct: myLogger registered BEFORE the route -- every request prints LOGGED app.use(myLogger); app.get('/', (req, res) => { res.send('Hello World!'); }); // broken: the exact same middleware, registered AFTER the route -- // per Express's own docs, "the request never reaches it," LOGGED is never printed app.get('/', (req, res) => { res.send('Hello World!'); }); app.use(myLogger);
This is Chapter 8's own short-circuiting finding, named by Express itself
res.send() inside the route handler is Express's own real equivalent of Chapter 8's own auth_middleware returning "401 Unauthorized" without calling next() — the request-response cycle ends right there, and nothing registered afterward in the chain, middleware or otherwise, ever runs. Express's own documentation states this as a real, plain fact of registration order, not a hypothetical edge case: a middleware genuinely, silently does nothing at all if it's placed after whatever already ends the cycle.

Django's Real Default List, Read Top-Down

Django's own documentation states the identical order-dependence directly, and its own default MIDDLEWARE list (shown above) is a real, working demonstration of why the order it ships in isn't arbitrary:

Django's own documentation, quoted directly
"During the request phase, before calling the view, Django applies middleware in the order it's defined in MIDDLEWARE, top-down."

Reading Django's own real default list through that rule explains a genuine dependency baked into its own ordering: SecurityMiddleware runs first, checking and redirecting insecure requests before anything downstream ever sees them — the identical real reason a security-relevant middleware belongs outermost in any onion, not something specific to Django. CsrfViewMiddleware sits before AuthenticationMiddleware in Django's own real list, meaning a request's CSRF protection is checked before Django even knows which user, if any, is making the request — a genuine, deliberate ordering decision baked directly into the framework's own shipped defaults, not left for every new project to rediscover independently.

Rack's Genuinely Different Interface: A Middleware Is an App

Express, Django, and FastAPI all give middleware and "the real handler" recognizably different shapes — a middleware takes an extra next/call_next parameter the final handler doesn't. Rack, the interface underneath Rails, makes no such distinction at all, per its own official specification:

Rack's own official specification, quoted directly
"A Rack application is a Ruby object that responds to call. It takes exactly one argument, the environment… and returns a non-frozen Array of exactly three elements: the status, the headers, and the body."

A real Rack middleware follows the identical interface, with nothing marking it structurally as "a middleware" rather than "an application":

class MyCustomMiddleware def initialize(app) @app = app end def call(env) # code here runs before the next layer status, headers, body = @app.call(env) # code here runs after the next layer [status, headers, body] end end
A real, structural consequence, not just a stylistic difference
A middleware's own call(env) method and a genuine Rails application's own real top-level call(env) method are the exact same shape — both take one env argument, both return a real [status, headers, body] triplet. There is no separate "middleware interface" in Rack at all: MyCustomMiddleware.new(some_other_app) is itself a completely valid Rack app, and could be handed directly to a Rack server with no middleware chain wrapped around it at all. Chapter 8's own Python chain, and Express's/Django's/FastAPI's real middleware, all give "the innermost handler" a genuinely simpler signature than a middleware layer gets. Rack alone erases that distinction entirely, at the interface level, by design.

FastAPI's Two Real Middleware Styles

Chapter 8 already verified FastAPI's own @app.middleware("http") decorator, matching this chapter's own function-based Express example. FastAPI's own real CORS documentation shows a second, genuinely different style, for reusable, class-based middleware:

FastAPI's own documentation, quoted directly
A real, worked example straight from FastAPI's own CORS documentation:
from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:8080"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )
Two real, genuinely different registration shapes for the same underlying chain
@app.middleware("http") decorates a plain async function — ideal for the one-off, request-specific logic Chapter 8 built by hand (a timing header, a request-scoped log line). add_middleware(CORSMiddleware, ...) instead registers a real, reusable class, configured with keyword arguments rather than written from scratch — genuinely closer in spirit to Django's own string-referenced classes in MIDDLEWARE, or Rails' own config.middleware.use SomeClass, than to Express's or FastAPI's own function-based style. FastAPI didn't have to pick one shape over the other — it supports both, matched to two real, different real-world needs: quick, inline request/response logic vs. configurable, shareable middleware packages.

One Feature, Four Real Answers

SystemRegistration styleMiddleware shapeOrder controlled by
Expressapp.use(fn)Function (req, res, next)Call order — real, documented bug if reversed
DjangoMIDDLEWARE listClass with __call__/get_responseList position, top-down for the request
Rails / Rackconfig.middleware.use/insert_before/insert_afterAny object with call(env) — identical to a real appRelative position against an already-registered class
FastAPI@app.middleware("http") AND add_middleware()Async function, or a configurable classCall/registration order, same as Express/Django

Where This Course Is Headed

Chapter 10, this course's own capstone, assembles routing (Chapters 2–3), templating (4–5), data access (6–7), and middleware (8–9) into one real, working small application — every real bug and every real fix this course found along the way, wired together in one place.

Hands-On Exercises

Exercise 1

Using this chapter's own real Express myLogger example, explain โ€” in terms of Chapter 8's own onion model, not just "Express says so" โ€” exactly why res.send() inside the root route handler prevents a later-registered app.use(myLogger) from ever running, tracing the explanation back to what res.send() actually does to the request-response cycle.

๐Ÿ“„ View solution
Exercise 2

Using this chapter's own real Django MIDDLEWARE list, explain what would genuinely break (not just "feel wrong") if AuthenticationMiddleware were moved to run before CsrfViewMiddleware instead of after it, reasoning from what each middleware's own name says it's responsible for.

๐Ÿ“„ View solution
Exercise 3

Using this chapter's own real Rack MyCustomMiddleware class and Rack's own quoted call(env) specification, explain concretely why MyCustomMiddleware.new(some_app) is itself a fully valid, standalone Rack application โ€” and why the identical claim ("this middleware could be used on its own as a real handler") is NOT true for Chapter 8's own Python middleware functions, which take a different signature (request, next) than a handler does (request alone).

๐Ÿ“„ View solution

Chapter 9 Quick Reference

  • Four real registration styles โ€” Express's app.use() and Django's MIDDLEWARE list order-based; Rails' config.middleware positions relative to existing classes; FastAPI supports both a decorator and a class-based add_middleware()
  • Express's own documented order bug โ€” a logger middleware registered after a route that already calls res.send() genuinely never runs, quoted directly from Express's own tutorial
  • Django's real default list โ€” CsrfViewMiddleware genuinely runs before AuthenticationMiddleware in Django's own shipped defaults, a deliberate ordering choice, not an arbitrary one
  • Rack erases the middleware/app distinction entirely โ€” per Rack's own official spec, both share the identical call(env) -> [status, headers, body] interface; a middleware genuinely is a valid standalone app
  • FastAPI's two real styles โ€” @app.middleware("http") for inline function-based logic, add_middleware() for configurable, reusable classes like CORSMiddleware, verified directly from FastAPI's own CORS docs
  • Next chapter: Capstone โ€” assembling routing, templating, data access, and middleware into one working application