Building Scalable REST APIs with FastAPI and PostgreSQL
FastAPI and PostgreSQL make a fast, reliable foundation for production APIs — if you structure sessions, pooling, and pagination correctly from the start.
Most APIs don't fail because of the framework. They fail because of connection leaks, unbounded queries, and list endpoints that return thousands of rows. FastAPI is quick to prototype but rewards a little discipline when you take it to production.
Start with a clear project structure
Separate concerns early so the codebase stays navigable as it grows. A structure that scales well looks like this:
- app/api/routers — request/response handling only
- app/schemas — Pydantic models for validation
- app/models — SQLAlchemy ORM models
- app/services — business logic, reusable across routers
- app/db — engine, session factory, and dependencies
Use async sessions and close them properly
One of the most common production leaks is a session that never closes. Use a dependency that guarantees cleanup:
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
engine = create_async_engine(DATABASE_URL, pool_size=10, max_overflow=20)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_db():
async with SessionLocal() as session:
yield session
Injecting get_db with Depends means the session is always released, even when a request raises.
Paginate every list endpoint
Never return an unbounded collection. Offset pagination is fine for small tables; for large ones, keyset pagination stays fast because it uses the index instead of scanning skipped rows.
@router.get("/orders")
async def list_orders(limit: int = 20, offset: int = 0, db=Depends(get_db)):
result = await db.execute(
select(Order).order_by(Order.created_at.desc())
.limit(min(limit, 100)).offset(offset)
)
return result.scalars().all()
Don't let the database be the bottleneck
- Add indexes on columns you filter and sort by, not on everything.
- Select only the columns you need instead of
SELECT *. - Size the connection pool to your worker count — an oversized pool can overwhelm Postgres.
Ship with observability
Structured logs, a /health endpoint, and migrations run through CI turn "it works on my machine" into something you can operate. Add request timing middleware early; it costs nothing and saves hours later.
None of this is exotic. It's the difference between an API that survives its first real traffic spike and one that falls over the moment a list endpoint gets popular.
Back to all articles