Routers & App Structure

RoadmapsPython Backend

Scenario

Your FastAPI app started as a sleek 50-line `main.py`. Six months later, it's a 4,000-line monster. Every time someone adds a route, you get Git merge conflicts. Finding the logic for updating a user profile involves scrolling past 50 unrelated endpoints.

How do you split a FastAPI application across multiple files without breaking the routing?

Mental model

A router is a specialized department in a store. `main.py` is just the store's front door.

Think of `main.py` as the store directory at the entrance. It doesn't stock the shelves; it just says 'Groceries are down Aisle 1, Electronics are down Aisle 2'. In FastAPI, an `APIRouter` acts as a 'mini-FastAPI' app for a specific department (like users or items). You define routes on the router, and then the main app simply 'includes' the router, pointing traffic to it.

Deep dive

When building an API, grouping related endpoints (like all `/users` operations or all `/orders` operations) is crucial for maintainability. If you put everything in one file, it becomes unreadable and causes merge conflicts on a team.

Single-file APIs do not scale to team development.

FastAPI provides `APIRouter`, which works exactly like the main `FastAPI` app instance but is designed to be modular. You create an `APIRouter` in a separate file, attach routes to it using `@router.get()` or `@router.post()`, and export it.

An APIRouter is a 'mini-app' that holds a group of related endpoints.

In your `main.py`, you wire these modules together using `app.include_router()`. When you include a router, you can assign a common `prefix` (like `/users`) and `tags` (for the OpenAPI docs). This saves you from typing `/users` on every single route in the router file.

The main app imports and includes routers, often applying a common prefix.

Because you can apply prefixes at the `include_router` level, the paths inside the router file should be relative to that prefix. If your prefix is `/users`, a route for `/users/{id}` should just be defined as `/{id}` in the router.

Router paths combine with the include prefix to form the final URL.

Code examples

1. The Router File (routers/users.py)

from fastapi import APIRouter

# Create a router specifically for user-related endpoints
router = APIRouter()

# Note the path is just "/", because we will prefix it with "/users" in main.py
@router.get("/")
def get_users():
    return [{"username": "alice"}, {"username": "bob"}]

@router.get("/{user_id}")
def get_user(user_id: int):
    return {"user_id": user_id, "username": "alice"}

We use `APIRouter` instead of `FastAPI`. The paths here (`/` and `/{user_id}`) are intentionally missing the `/users` prefix, making the code more DRY (Don't Repeat Yourself).

2. The Main App (main.py)

from fastapi import FastAPI
from routers import users  # Import the module containing your router

app = FastAPI()

# Wire the router into the main app, applying the prefix here
app.include_router(
    users.router,
    prefix="/users",
    tags=["Users"]
)

@app.get("/")
def root():
    return {"message": "Welcome to the API"}

The main app acts as a registry. By passing `prefix="/users"`, FastAPI automatically prepends it to every route inside `users.router`. The final URLs become `/users/` and `/users/{user_id}`.

Key points

Common mistakes

Recall questions

Questions & answers

How do you structure a large FastAPI application to prevent `main.py` from becoming an unmaintainable monolith?

Split endpoints by resource into separate files using `APIRouter`. Each file acts as a mini-app (e.g., `users.py`, `orders.py`). Then, import these routers into `main.py` and register them using `app.include_router()`, often applying a common prefix like `/users` to the entire group.

A developer adds a router to a FastAPI app with `app.include_router(users_router, prefix="/api/users")`. Inside the router, they define a route as `@router.get("/api/users/profile")`. What bug does this introduce?

Double prefixing. The final URL generated by FastAPI will concatenate them, resulting in `/api/users/api/users/profile`. The route inside the router should just be `@router.get("/profile")`.

Continue learning

Previous: Request Body & response_model

Next: async vs def Endpoints

Return to Python Backend Roadmap