API Routers and Project Structure

Keeping every endpoint in main.py does not scale. APIRouter lets you split path operations into modules and mount them on the app with prefixes and tags.

Creating a Router

# routers/users.py
from fastapi import APIRouter

router = APIRouter(
    prefix="/users",
    tags=["users"],
    dependencies=[],  # router-wide dependencies (e.g. auth)
)

@router.get("/")
def list_users():
    return [{"username": "alice"}]

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

Mounting Routers on the App

# main.py
from fastapi import FastAPI
from routers import users, items

app = FastAPI()
app.include_router(users.router)
app.include_router(items.router, prefix="/api", tags=["items"])

A Scalable Layout (Bigger Apps)

app/
├── main.py              # app factory, middleware, include routers
├── core/
│   ├── config.py        # pydantic-settings
│   └── security.py      # hashing, JWT
├── db/
│   └── session.py       # engine, SessionLocal
├── models/              # SQLAlchemy / DB models
├── schemas/             # Pydantic models (in/out)
├── crud/                # DB operations per entity
├── routers/             # APIRouter per entity
│   ├── users.py
│   └── items.py
├── dependencies.py      # shared Depends (get_db, current_user)
└── tests/

Rule of thumb: schemas never import from models logic-wise they are independent; routers depend on crud and schemas; crud depends on models. This keeps layers swappable and testable.

Versioning an API

app_v1 = FastAPI()
app_v1.include_router(users_v1.router, prefix="/v1")

app = FastAPI()
app.mount("/api", app_v1)  # sub-application mounting

Alternatively use prefixes only (prefix="/api/v1") — simpler and usually enough until you need truly different apps.

Quick Questions and Answers

Q: Can two routers define the same path?

A: Yes, and the first matching route registered wins. This is a common source of "my endpoint returns the wrong thing" bugs — check include order, especially for a catch-all like /{item_id} declared before a fixed path.

Q: Should I create the FastAPI() app inside main.py or in a factory function?

A: For anything beyond a demo, use a factory: def create_app() -> FastAPI: that wires routers, middleware and events. It makes testing (fresh app per suite) and different configs per environment trivial.

Q: Where do shared dependencies like get_db live?

A: In a central dependencies.py (or per-domain module). Routers import them and declare db: Session = Depends(get_db) — never create sessions directly inside endpoints.

Conclusion

APIRouter is how FastAPI apps grow: one router per resource, mounted with prefixes and tags, backed by a layered folder structure (routers → crud → models, with pydantic schemas at the boundary). Start simple, but structure early.