FastAPI's pitch is real: type hints become validation, a Pydantic model becomes a request schema, and an OpenAPI doc appears for free. The part nobody warns you about is that this same speed makes it just as easy to end up with forty routes crammed into one main.py file.
Routers per resource, not one giant app instance
Splitting endpoints into an APIRouter per resource — users, orders, auth — and mounting each with app.include_router() keeps route files short enough to actually hold in your head. It also means two people can add endpoints to different resources without touching the same file.
Pydantic schemas are not your database models
Reusing a single Pydantic model for the database layer, the request body, and the response body works right up until you need a field that's writable on create but read-only on response — like an id or created_at. Separate Create, Update, and Read schemas per resource cost a bit of duplication but remove an entire category of accidental data leaks.
Dependency injection in FastAPI isn't just for auth checks — it's the cleanest place to put anything a route needs but shouldn't construct itself: a db session, a current user, a feature flag lookup.
Async all the way down, or not at all
Mixing a synchronous database driver into async def route handlers blocks the entire event loop on every query, which quietly erases the concurrency FastAPI is supposed to give you. Either commit to an async driver (asyncpg, motor) end to end, or use regular def routes and let FastAPI run them in a thread pool — but don't mix the two inside the same handler.
Background tasks aren't a job queue
FastAPI's BackgroundTasks is great for fire-and-forget work that can tolerate being lost on a server restart, like sending a confirmation email. Anything that needs retries, scheduling, or guaranteed delivery — payment webhooks, report generation — belongs in a real task queue like Celery or arq, not bolted onto the request/response cycle.



