What Django and FastAPI Are Actually Doing
We’ve established that HTTP is text and that WSGI is a callable interface. Now let’s look at what Django and FastAPI actually do with that interface — because once you see it, the framework becomes a much less mysterious box.
We’ll trace a request through each one, following the actual code path (simplified to keep it readable). The goal is not to understand every detail of Django’s internals — the Django team has written excellent documentation for that. The goal is to see the skeleton: the WSGI entry point, the routing, and the response serialization.
Django’s Request Path
A Django project has a WSGI entrypoint file, generated by startproject:
# myproject/wsgi.py
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
application = get_wsgi_application()
application here is what Gunicorn will call. Let’s follow get_wsgi_application():
# django/core/wsgi.py
def get_wsgi_application():
django.setup()
return WSGIHandler()
It runs Django setup (loads settings, connects signals, initializes apps) and returns a WSGIHandler. Let’s look at WSGIHandler:
# django/core/handlers/wsgi.py (simplified)
class WSGIHandler(base.BaseHandler):
request_class = WSGIRequest
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.load_middleware() # Build the middleware stack
def __call__(self, environ, start_response):
# Convert environ to a Django request object
request = self.request_class(environ)
# Run the full middleware/view pipeline
response = self.get_response(request)
# Django response -> HTTP status string and headers list
status = '%d %s' % (response.status_code, response.reason_phrase)
response_headers = list(response.items())
for c in response.cookies.values():
response_headers.append(('Set-Cookie', c.output(header='')))
# Tell the WSGI server what status and headers to use
start_response(status, response_headers)
# Return the response body as an iterable
if request.method == 'HEAD':
return [b'']
return response
The __call__ method is the WSGI application. It takes environ and start_response, does Django things, and returns a response iterable.
The Middleware Stack
self.load_middleware() builds a chain of callables. If your MIDDLEWARE setting looks like:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'myapp.middleware.CustomMiddleware',
]
Then load_middleware() constructs something conceptually like:
def _get_response_none(request):
# The actual view dispatcher
return view_function(request)
handler = _get_response_none
for middleware_path in reversed(MIDDLEWARE):
middleware_class = import_string(middleware_path)
handler = middleware_class(handler)
self._middleware_chain = handler
Each middleware wraps the next one. When you call self.get_response(request), you’re calling self._middleware_chain(request), which unwinds through each middleware layer until it hits the view. This is exactly the turtles-all-the-way-down middleware pattern we’ll implement ourselves in the WSGI section.
The URL Dispatcher
Inside get_response, Django eventually calls:
# django/core/handlers/base.py (simplified)
def _get_response(self, request):
callback, callback_args, callback_kwargs = self.resolve_request(request)
response = callback(request, *callback_args, **callback_kwargs)
return response
resolve_request does URL routing: it takes request.path_info and walks through the urlpatterns list, matching regex patterns or path converters until it finds a match. The match returns the view function and any captured URL parameters.
That view function is what you write. Django calls it. You return an HttpResponse. Django serializes it. Done.
FastAPI’s Request Path
FastAPI is an ASGI framework (we’ll cover ASGI in Part III), but the same “it’s just a callable” principle applies.
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id}
app here is a FastAPI instance. FastAPI inherits from Starlette, which implements the ASGI interface. When Uvicorn calls your application:
# uvicorn calls this
await app(scope, receive, send)
Starlette’s __call__ (simplified):
class Starlette:
async def __call__(self, scope, receive, send):
scope["app"] = self
if self.middleware_stack is None:
self.middleware_stack = self.build_middleware_stack()
await self.middleware_stack(scope, receive, send)
Same pattern: a middleware stack, built once, called on every request.
At the bottom of the stack is the router. FastAPI’s router matches the path and HTTP method against registered routes, extracts path parameters, and calls your endpoint function.
The Clever Part: Dependency Injection and Type Hints
The thing FastAPI adds is automatic parsing of function parameters using type hints. When you write:
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: Optional[str] = None):
return {"item_id": item_id, "q": q}
FastAPI uses inspect.signature to introspect the function, reads the type annotations, and automatically:
- Extracts
item_idfrom the path (because it’s in the{item_id}path template) - Extracts
qfrom the query string (because it’s not in the path) - Converts
item_idtointand validates it - Returns a 422 if conversion fails
This is done at startup (when the route is registered) using Pydantic and Python’s inspect module. There’s no magic — it’s reflection and type coercion applied systematically.
# What FastAPI is doing under the hood (very simplified)
import inspect
from typing import get_type_hints
def build_endpoint_handler(func):
sig = inspect.signature(func)
hints = get_type_hints(func)
async def handler(scope, receive, send):
# Extract path params, query params from scope
path_params = scope.get("path_params", {})
query_string = scope.get("query_string", b"").decode()
# Build kwargs for the function
kwargs = {}
for name, param in sig.parameters.items():
if name in path_params:
kwargs[name] = hints[name](path_params[name]) # type coercion
# ... query param extraction, body parsing, etc.
result = await func(**kwargs)
# Serialize result to JSON response
# ...
return handler
That’s the core of FastAPI’s “magic”. It’s Python’s inspect module and type coercion, applied at startup to build efficient request handlers.
Flask’s Request Path
Flask is simpler than Django but uses the same WSGI interface. The Flask class has a __call__ method:
class Flask:
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
ctx.push()
try:
response = self.full_dispatch_request()
return response(environ, start_response)
finally:
ctx.pop()
Flask’s “request context” and “application context” are thread-local (or greenlet-local) storage — that’s how flask.request works without being passed as a parameter. When you access request.method in a Flask view, Flask looks up the current request from a thread-local stack that was pushed when ctx.push() was called.
This is convenient, but it’s not magic. It’s an implicit parameter passing mechanism. WSGI is synchronous and single-threaded-per-request, so thread-local storage works. This is also why Flask’s approach breaks down with async — thread-locals don’t survive across await points, which is one reason Flask’s async support required careful workarounds.
What They All Have in Common
Every Python web framework, at its core, does this:
environ/scope
│
▼
┌─────────────────────────────────────────────────────┐
│ Middleware stack │
│ ├── Security / CORS / compression / auth │
│ ├── Session management │
│ └── (your middleware here) │
│ │ │
│ ▼ │
│ URL Router │
│ └── match path → find handler function │
│ │ │
│ ▼ │
│ Handler / View │
│ └── your code runs here │
│ │ │
│ ▼ │
│ Response serialization │
│ └── status + headers + body → bytes │
└─────────────────────────────────────────────────────┘
│
▼
start_response(status, headers) + return [body_bytes]
The framework is providing:
- A way to compose middleware (the stack builder)
- URL routing (pattern matching on
PATH_INFO) - Request parsing (wrapping
environin a convenient object) - Response serialization (turning your return value into WSGI-compatible bytes)
None of these are hard to understand. Some are hard to implement well — Django’s URL dispatcher handles edge cases you’d never think of, and FastAPI’s type coercion is quite sophisticated. But conceptually, they’re all doing the same four things.
Building It Yourself
The rest of Part II is devoted to building each of these pieces from scratch. By the time we’re done, you’ll have:
- A working WSGI server
- A middleware stack
- A URL router
- Request and Response classes
None of it will be production-ready in the sense that Django is production-ready. But all of it will be correct, and building it will give you a ground-level understanding that reading the Django source code alone doesn’t provide.
The question isn’t “how does Django do routing?” The question is “what problem does routing solve, and what’s the simplest possible correct implementation?” Once you’ve answered the second question yourself, the first becomes easy to read.
Let’s start with the spec.