Building Scalable APIs with FastAPI and Python
A deep dive into building production-ready REST APIs using FastAPI, async Python, and best practices for scalability and security.
Building Scalable APIs with FastAPI and Python
FastAPI has become my go-to framework for building high-performance Python APIs. After shipping several production systems with it, here's what I've learned about building APIs that actually scale.
Why FastAPI?
FastAPI combines the best of both worlds — the developer experience of Django and the raw performance of async Python. It generates OpenAPI docs automatically, enforces types via Pydantic, and handles async I/O natively.
Key advantages:
- ▸Speed: On par with NodeJS and Go for I/O-bound tasks
- ▸Type safety: Pydantic models catch bugs at startup, not at 3 AM
- ▸Auto-docs: Swagger UI and ReDoc out of the box
- ▸Dependency injection: Clean, testable architecture
Project Structure
Here is a production-ready folder layout:
my-api/
├── app/
│ ├── api/
│ │ ├── v1/
│ │ │ ├── routes/
│ │ │ │ ├── users.py
│ │ │ │ └── items.py
│ │ │ └── router.py
│ ├── core/
│ │ ├── config.py
│ │ └── security.py
│ ├── db/
│ │ ├── models.py
│ │ └── session.py
│ ├── schemas/
│ │ └── user.py
│ └── main.py
├── tests/
└── pyproject.toml
Async Database Access
The biggest performance win comes from async database drivers. Here's how I set up SQLAlchemy with async support:
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname"
engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_db():
async with AsyncSessionLocal() as session:
yield session
Dependency Injection for Auth
FastAPI's dependency system makes JWT auth clean and reusable:
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer
security = HTTPBearer()
async def get_current_user(
token: str = Depends(security),
db: AsyncSession = Depends(get_db),
) -> User:
payload = decode_jwt(token.credentials)
user = await db.get(User, payload["sub"])
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return user
Rate Limiting
Always rate-limit your public endpoints. I use slowapi:
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@app.get("/items")
@limiter.limit("100/minute")
async def get_items(request: Request, db: AsyncSession = Depends(get_db)):
return await item_service.get_all(db)
Watching a Quick Demo
Here's a 60-second overview of FastAPI's interactive docs:
Background Tasks
For fire-and-forget work (emails, webhooks), use FastAPI's BackgroundTasks:
from fastapi import BackgroundTasks
def send_welcome_email(email: str):
# runs after response is sent
mailer.send(email, subject="Welcome!")
@app.post("/users")
async def create_user(user: UserCreate, background_tasks: BackgroundTasks):
new_user = await user_service.create(user)
background_tasks.add_task(send_welcome_email, new_user.email)
return new_user
Key Takeaways
- ▸Use async everywhere — mixing sync DB calls with async routes kills the performance benefit
- ▸Validate at the boundary — Pydantic models on every request/response, no exceptions
- ▸Version your API —
/api/v1/from day one saves painful migrations later - ▸Cache aggressively — Redis +
fastapi-cachefor read-heavy endpoints - ▸Instrument everything — OpenTelemetry traces make production debugging survivable
FastAPI gives you a strong foundation. The rest is about discipline in how you structure the application and enforce consistency across your team.
Have questions or a specific use case? Reach out via the contact form.
Enjoyed this article?
Get in Touch