Structuring a Large FastAPI Project with APIRouter

A
Author
25 Apr, 2026 1 min read 257 views

A single main.py is perfect for a prototype. As the API grows, split it by feature so each area of the product lives together.

A feature-based layout

app/
  main.py               # creates the app, includes routers
  config.py             # settings from environment
  db.py                 # engine, session dependency
  deps.py               # shared dependencies (current user, pagination)
  users/
    router.py           # HTTP layer
    schemas.py          # Pydantic models
    models.py           # SQLAlchemy models
    service.py          # business logic
  orders/
    router.py
    schemas.py
    models.py
    service.py
tests/

Routers

# app/orders/router.py
from fastapi import APIRouter
from app.deps import CurrentUser, DbSession
from . import service
from .schemas import OrderIn, OrderOut

router = APIRouter(prefix="/orders", tags=["orders"])

@router.get("", response_model=list[OrderOut])
async def my_orders(user: CurrentUser, db: DbSession):
    return await service.list_for_user(db, user.id)

@router.post("", response_model=OrderOut, status_code=201)
async def place_order(data: OrderIn, user: CurrentUser, db: DbSession):
    return await service.place(db, user, data)
# app/main.py
from fastapi import FastAPI
from app.orders.router import router as orders_router
from app.users.router import router as users_router

app = FastAPI(title="Shop API")
app.include_router(users_router)
app.include_router(orders_router)

Keep routers thin

Routers translate HTTP into function calls. Business rules (stock checks, pricing, emails) belong in service.py, which you can test without HTTP and reuse from background jobs or CLIs.

Settings in one place

# app/config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str
    jwt_secret: str
    debug: bool = False

settings = Settings()   # reads environment variables (and .env if configured)

Versioning

To version the API, mount routers under a prefix: app.include_router(orders_router, prefix="/v1"). Add a /v2 router only for endpoints that actually change.

Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.