Module 2: APIRouter and Middleware
Module 2 Introduction: APIRouter and Middleware
Overview
Your FastAPI app works. It has DI, it has validation, it has a full CRUD. But open your main.py and count the lines. Models, dependencies, helpers, endpoints — all in a single file. With 7 endpoints and 5 dependencies you're already past 200 lines. Add authentication, more endpoints, more models, and you're on your way to a 1000+ line file nobody wants to maintain.
APIRouter solves the first half of the problem: modularization. Instead of having every endpoint in main.py, you split them into separate routers by domain. tasks_router handles everything task-related. users_router handles everything user-related. main.py just wires the pieces together with app.include_router().
Middleware solves the second half: cross-cutting concerns. These are the things that apply to every request no matter the endpoint: logging, timing, security headers, CORS. Instead of adding that logic to every endpoint, a middleware intercepts every request before and after the handler.
Together, APIRouter and Middleware turn your app from a single-file monolith into a modular application with a professional structure. It's what separates a prototype from a project a team can maintain.
Where are we in the guide?
You're in Module 2 of 6 of the FastAPI Advanced Features guide:
Module 1: Dependency Injection ✅ (completed)
→ Depends(), sub-dependencies, yield, class-based, overrides
Module 2: APIRouter and Middleware ← YOU ARE HERE
→ Modularization, routers, custom middleware, lifespan events
Module 3: Advanced Response Models
Module 4: Background Tasks
Module 5: WebSockets and File Uploads
Module 6: Project — Advanced Task Manager API
Cumulative progression
FastAPI Fundamentals: CRUD + Pydantic + Error Handling + CORS
↓
Module 1: + Dependency Injection (DRY, testable code) ✅
↓
Module 2: + APIRouter and Middleware (a modular app) ← HERE
↓
Module 3: + Advanced Response Models
↓
Modules 4-6: + Advanced features + Project
Module 1 gave you DRY code: reusable dependencies, sub-dependencies, yield for lifecycle. But it all still lives in one file. Module 2 takes those pieces and organizes them into a structure that scales: endpoints in routers, dependencies in modules, middleware for cross-cutting concerns.
What you already know vs. what's new
What you already know
From the previous modules you bring:
- ✅ Dependency Injection with
Depends(), sub-dependencies, yield - ✅ Class-based dependencies with
__call__ - ✅
dependency_overridesfor testing - ✅ CRUD endpoints with Pydantic v2 and HTTPException
- ✅ CORS configured with
CORSMiddleware(Fundamentals) - ✅
response_modeland separate Create/Update/Response models
What's new in this module
- 🆕
APIRouter— grouping endpoints by domain with prefixes and tags - 🆕
include_router()— connecting routers to the main app - 🆕 Router-level dependencies —
APIRouter(dependencies=[...]) - 🆕 A professional folder structure —
app/routers/,app/dependencies/,app/models/ - 🆕
__init__.pyas the place to organize your exports - 🆕 Custom middleware with
@app.middleware("http") - 🆕 Lifespan events with
asynccontextmanager - 🆕 Logging and timing middleware
From monolith to modular
| Aspect | Your app today (one file) | Your app after this module |
|---|---|---|
| Structure | app/main.py (200+ lines) | app/main.py + app/routers/ + app/dependencies/ |
| Endpoints | All in main.py | Split by domain into routers |
| Dependencies | All in main.py | In app/dependencies/ as modules |
| Models | All in main.py | In app/models/ as modules |
| Cross-cutting | Nothing, or by hand | Logging and timing middleware |
| Startup/shutdown | Nothing | Lifespan events |
| Navigation | Scrolling through one long file | Small, focused files |
The problem: everything in main.py
Let's look at what a typical main.py looks like after Module 1:
# app/main.py — 250+ lines in a single file
from typing import Optional
from datetime import datetime
from fastapi import FastAPI, Depends, HTTPException, Query
from pydantic import BaseModel, Field
# --- Models (40 lines) ---
class TaskCreate(BaseModel): ...
class TaskUpdate(BaseModel): ...
class PaginationResult(BaseModel): ...
class TaskFilters(BaseModel): ...
# --- Data (30 lines) ---
initial_tasks = [...]
task_store = initial_tasks[:]
# --- Dependencies (40 lines) ---
def get_task_store(): ...
def pagination_params(...): ...
def task_filters(...): ...
def get_task_or_404(...): ...
class Paginator: ...
# --- Helpers (15 lines) ---
def apply_filters(...): ...
# --- App + Endpoints (100+ lines) ---
app = FastAPI(...)
@app.get("/")
def root(): ...
@app.get("/tasks")
def list_tasks(...): ...
@app.get("/tasks/stats")
def task_stats(...): ...
@app.get("/tasks/{task_id}")
def get_task(...): ...
@app.post("/tasks")
def create_task(...): ...
@app.patch("/tasks/{task_id}")
def update_task(...): ...
@app.delete("/tasks/{task_id}")
def delete_task(...): ...
This works, but it has real problems:
-
Hard to navigate. Where's the
TaskUpdatemodel? Scroll. Where's theget_task_or_404dependency? More scrolling. Where's the DELETE endpoint? Even more scrolling. -
Team conflicts. If two developers work on different endpoints, they both edit
main.py. Constant merge conflicts. -
It doesn't scale. Add users, categories, tags, notifications — each domain adds 50-100 lines. Soon you have a 1000+ line file.
-
No cross-cutting concerns. There's no centralized logging. There's no request timing. Every endpoint would handle its own logging.
-
No lifecycle management. There's nowhere to initialize resources when the app boots or clean them up when it stops.
The solution: APIRouter + Middleware + Structure
APIRouter for modularization
APIRouter is a "mini FastAPI" that groups endpoints. It has the same capabilities as app for defining endpoints, but then it gets connected to the main app:
# app/routers/tasks.py
from fastapi import APIRouter
router = APIRouter(prefix="/tasks", tags=["Tasks"])
@router.get("/")
def list_tasks():
return [{"id": 1}]
@router.get("/{task_id}")
def get_task(task_id: int):
return {"id": task_id}
# app/main.py
from fastapi import FastAPI
from app.routers import tasks
app = FastAPI()
app.include_router(tasks.router)
Now main.py is clean and declarative: it just creates the app and includes routers.
Middleware for cross-cutting concerns
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
start = time.time()
response = await call_next(request)
duration = time.time() - start
response.headers["X-Process-Time"] = f"{duration:.4f}"
return response
This middleware measures how long every request takes and adds it as a header. Without touching a single endpoint.
Lifespan events for lifecycle
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
print("🚀 App starting — initializing resources")
yield
print("🛑 App stopping — cleaning up resources")
app = FastAPI(lifespan=lifespan)
lifespan runs when the app starts and when it stops. Perfect for initializing connections, loading configuration, and cleaning up resources.
What this module does NOT cover
- ❌ Authentication middleware — Covered in the Auth guide. Here you'll see custom middleware for logging/timing
- ❌ Rate limiting middleware — An advanced pattern that requires storage (Redis)
- ❌ CORS in depth — You already covered it in Fundamentals. Here it's only mentioned for reference
- ❌ Multiple apps / sub-applications — Mounting apps inside apps is an advanced pattern
- ❌ APIRouter with versioning —
/v1/tasks,/v2/tasksis an API design pattern, not a FastAPI one
Why these boundaries?
The focus is structure and organization, not new functionality. You're learning to modularize your app and add cross-cutting concerns. The specific patterns (auth, rate limiting) use the same tools but with different business logic.
Module goal
By the end of this module you'll be able to:
- ✅ Create an
APIRouterto group endpoints by domain - ✅ Use
prefix,tags, anddependencieson routers - ✅ Connect routers to the app with
include_router() - ✅ Organize your project with a professional folder structure
- ✅ Create custom middleware for logging and timing
- ✅ Understand the request → middleware → handler → middleware → response flow
- ✅ Use lifespan events with
asynccontextmanagerfor startup/shutdown - ✅ Move dependencies, models, and routers into separate files
- ✅ Keep your imports clean with
__init__.py
Prerequisites
For this module you need:
- Module 1 completed — DI with
Depends(), sub-dependencies, yield - Your refactored To-Do API — With pagination, filter, and lookup dependencies
- Python 3.9+ with FastAPI installed
Quick check
cd fastapi-advanced
source venv/bin/activate
uvicorn app.main:app --reload
Check that GET /tasks, POST /tasks, and GET /tasks/{id} work correctly.
What if you don't meet a prerequisite?
| Missing prerequisite | What to do |
|---|---|
| You didn't finish Module 1 | Do at least capsules 02 and 05 (basic Depends and the project) |
| You don't have DI implemented | You can still follow this module, but the project will land less hard |
| Your app has no dependencies | Create at least pagination_params and get_task_or_404 |
Module roadmap
| Capsule | Topic | What you'll learn |
|---|---|---|
| 01 | Introduction (this capsule) | Context, the monolith problem, an APIRouter/Middleware overview |
| 02 | APIRouter and modularization | Creating routers, prefixes, tags, include_router(), shared dependencies |
| 03 | A professional folder structure | app/routers/, app/dependencies/, app/models/, init.py |
| 04 | Custom middleware and events | @app.middleware, logging, timing, lifespan events |
| 05 | Project: a modular app | Turning the To-Do API into a fully modular app |
The learning flow
First you'll learn to create an APIRouter to split endpoints by domain — prefixes, tags, and how to connect them to the main app (Capsule 02). Then you'll organize your project with a professional folder structure, moving models, dependencies, and routers into separate files (Capsule 03). Next you'll add custom middleware for logging and timing, plus lifespan events for startup/shutdown (Capsule 04). Finally, you'll bring it all together by turning your To-Do API into a fully modular app (Capsule 05).
The progression is: split endpoints → organize files → add middleware → integrate.
Capsule by capsule
Capsule 02 — APIRouter and modularization: You'll create your first APIRouter, give it the /tasks prefix and the "Tasks" tag, and move the task endpoints into it. You'll see how include_router() connects the router to the app. You'll explore router-level dependencies — like requiring authentication for every endpoint in a domain without repeating Depends() on each one.
Capsule 03 — Folder structure: The most important reorganization in this module. You'll create the app/routers/, app/dependencies/, and app/models/ folders, and move the corresponding code. You'll see how __init__.py lets you export symbols cleanly. By the end, your main.py will be 20-30 lines: create the app, include the routers, done.
Capsule 04 — Custom middleware and events: Middleware intercepts every request. You'll create a timing middleware that measures how long each request takes, a logging one that records method/path/status, and you'll see lifespan events for running code when the app starts and stops. You'll understand the difference between middleware (cross-cutting) and dependencies (per-endpoint).
Capsule 05 — Project: a modular app: The full transformation. Your To-Do API goes from a monolithic main.py to a modular structure with routers, dependencies in separate files, logging/timing middleware, and lifespan events. The result is a project any developer can navigate and maintain.
The core concept: separation of concerns
Separation of concerns is the principle that every piece of code has one responsibility:
main.py → App configuration, including routers
routers/tasks.py → Task endpoints
routers/root.py → General endpoints (health, info)
dependencies/ → Reusable dependency functions
models/ → Pydantic models
middleware/ → Custom middleware
Each file does one thing. If you need to change pagination, you go to dependencies/pagination.py. If you need to add a task endpoint, you go to routers/tasks.py. If you need to change the logging, you go to middleware/logging.py.
Middleware vs. Dependencies: when to use which?
| Criterion | Middleware | Dependency |
|---|---|---|
| Scope | EVERY request | Specific endpoints |
| Access to the response | Yes (before and after) | No (only before the endpoint) |
| Can modify headers | Yes | Not directly |
| Can block the request | Yes | Yes (HTTPException) |
| Receives the endpoint's parameters | No | Yes (query, path, body) |
| Use cases | Logging, timing, CORS, headers | Auth, pagination, validation |
The rule: If it applies to every request no matter the endpoint → middleware. If it applies to specific endpoints and needs parameters from the request → dependency.
Connecting to the project
Your To-Do API is about to go from a monolith to a modular app:
Before (Module 1) — everything in main.py
fastapi-advanced/
├── app/
│ ├── __init__.py
│ └── main.py ← 250+ lines, everything here
└── venv/
After (this module) — a professional structure
fastapi-advanced/
├── app/
│ ├── __init__.py
│ ├── main.py ← 30 lines: create app + include routers
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── tasks.py ← task endpoints
│ │ └── root.py ← general endpoints
│ ├── dependencies/
│ │ ├── __init__.py
│ │ ├── pagination.py ← pagination_params, Paginator
│ │ ├── tasks.py ← get_task_or_404, task_filters
│ │ └── data.py ← get_task_store
│ ├── models/
│ │ ├── __init__.py
│ │ └── tasks.py ← TaskCreate, TaskUpdate, etc.
│ └── middleware/
│ ├── __init__.py
│ └── logging.py ← timing, request logging
└── venv/
Each file is short (30-80 lines), focused on one thing, and easy to find.
How to use this guide in Module 2
A hands-on approach
The cycle in this module is refactor and reorganize:
- Create the router — Move endpoints from
main.pyinto a router - Connect it with include_router — Check that the endpoints still work the same
- Move the dependencies — Extract them into separate files
- Add middleware — Implement logging and timing
- Verify — The same curls/tests as before should still work
Estimated time
- Introduction capsule (this one): 15-20 minutes
- Technical capsules (02-04): 35-50 minutes each
- Project (05): 60-90 minutes
- Module 2 total: 3-4 hours
A refactoring tip
Refactor step by step: move one router, check that it works, move the next one. Don't move everything at once — if something breaks, you won't know what caused it. After each step, run uvicorn app.main:app --reload and hit an endpoint.
Evidence of success
When you finish this module (all 5 capsules), you'll know you succeeded if:
- ✅
main.pyis under 40 lines - ✅ The task endpoints live in
app/routers/tasks.py - ✅ The dependencies live in
app/dependencies/ - ✅ The Pydantic models live in
app/models/ - ✅ There's a timing middleware that adds
X-Process-Timeto every response - ✅ There's a logging middleware that prints method, path, and status
- ✅ Lifespan events print a message when the app starts and stops
- ✅ Every endpoint works exactly like it did before
- ✅ You can find any piece of code in under 5 seconds
- ✅ You understand the difference between a middleware and a dependency
Summary
- This is Module 2 of 6, focused on modularization with APIRouter and middleware
- APIRouter splits endpoints by domain — each router is like a mini-app
- Middleware intercepts every request for cross-cutting concerns
- Lifespan events run code when the app starts and stops
- The professional folder structure has
routers/,dependencies/,models/,middleware/ - Separation of concerns: each file has one responsibility
- Middleware = every request; Dependencies = specific endpoints
main.pygoes from 250+ lines to ~30: create app + include routers
Additional resources
- FastAPI - Bigger Applications — APIRouter, include_router, modular structure
- FastAPI - Middleware — Creating custom middleware
- FastAPI - Lifespan Events — Startup/shutdown with asynccontextmanager
- FastAPI - APIRouter — The complete APIRouter reference
- Python - Packages — init.py and organizing modules
What's next?
Next capsule: APIRouter and Modularization — You'll create your first router, give it a prefix and tags, move endpoints into it, and connect it to the app with include_router(). Your main.py will start slimming down.