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_overrides for testing
  • ✅ CRUD endpoints with Pydantic v2 and HTTPException
  • ✅ CORS configured with CORSMiddleware (Fundamentals)
  • response_model and 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__.py as the place to organize your exports
  • 🆕 Custom middleware with @app.middleware("http")
  • 🆕 Lifespan events with asynccontextmanager
  • 🆕 Logging and timing middleware

From monolith to modular

AspectYour app today (one file)Your app after this module
Structureapp/main.py (200+ lines)app/main.py + app/routers/ + app/dependencies/
EndpointsAll in main.pySplit by domain into routers
DependenciesAll in main.pyIn app/dependencies/ as modules
ModelsAll in main.pyIn app/models/ as modules
Cross-cuttingNothing, or by handLogging and timing middleware
Startup/shutdownNothingLifespan events
NavigationScrolling through one long fileSmall, 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:

  1. Hard to navigate. Where's the TaskUpdate model? Scroll. Where's the get_task_or_404 dependency? More scrolling. Where's the DELETE endpoint? Even more scrolling.

  2. Team conflicts. If two developers work on different endpoints, they both edit main.py. Constant merge conflicts.

  3. It doesn't scale. Add users, categories, tags, notifications — each domain adds 50-100 lines. Soon you have a 1000+ line file.

  4. No cross-cutting concerns. There's no centralized logging. There's no request timing. Every endpoint would handle its own logging.

  5. 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/tasks is 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 APIRouter to group endpoints by domain
  • ✅ Use prefix, tags, and dependencies on 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 asynccontextmanager for 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 prerequisiteWhat to do
You didn't finish Module 1Do at least capsules 02 and 05 (basic Depends and the project)
You don't have DI implementedYou can still follow this module, but the project will land less hard
Your app has no dependenciesCreate at least pagination_params and get_task_or_404

Module roadmap

CapsuleTopicWhat you'll learn
01Introduction (this capsule)Context, the monolith problem, an APIRouter/Middleware overview
02APIRouter and modularizationCreating routers, prefixes, tags, include_router(), shared dependencies
03A professional folder structureapp/routers/, app/dependencies/, app/models/, init.py
04Custom middleware and events@app.middleware, logging, timing, lifespan events
05Project: a modular appTurning 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?

CriterionMiddlewareDependency
ScopeEVERY requestSpecific endpoints
Access to the responseYes (before and after)No (only before the endpoint)
Can modify headersYesNot directly
Can block the requestYesYes (HTTPException)
Receives the endpoint's parametersNoYes (query, path, body)
Use casesLogging, timing, CORS, headersAuth, 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:

  1. Create the router — Move endpoints from main.py into a router
  2. Connect it with include_router — Check that the endpoints still work the same
  3. Move the dependencies — Extract them into separate files
  4. Add middleware — Implement logging and timing
  5. 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.py is 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-Time to 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.py goes from 250+ lines to ~30: create app + include routers

Additional resources

  1. FastAPI - Bigger Applications — APIRouter, include_router, modular structure
  2. FastAPI - Middleware — Creating custom middleware
  3. FastAPI - Lifespan Events — Startup/shutdown with asynccontextmanager
  4. FastAPI - APIRouter — The complete APIRouter reference
  5. Python - Packagesinit.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.