If you're building APIs in Python, FastAPI isn't just an option—it's the standard. This comprehensive guide will take you from your first endpoint to production-ready applications.
What Makes FastAPI Special?
FastAPI combines the best of Python's ecosystem into one cohesive framework:
- Performance: Rivals NodeJS and Go thanks to Starlette and Uvicorn
- Type Safety: Pydantic validation catches bugs before they hit production
- Auto-Documentation: Swagger UI and ReDoc out of the box
- Async Native: Built for the async/await era
- Developer Experience: Fewer bugs, faster development, better code
Getting Started
pip install fastapi uvicorn[standard]Create your first application:
from fastapi import FastAPI
app = FastAPI(
title="My API",
description="A production-ready API",
version="1.0.0"
)
@app.get("/")
async def root():
return {"message": "Hello, FastAPI!"}Run it:
uvicorn main:app --reloadVisit http://localhost:8000/docs to see your interactive API documentation.
Path Parameters and Query Strings
FastAPI makes parameter handling elegant:
from typing import Optional
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
include_posts: bool = False,
limit: Optional[int] = None
):
"""
Fetch a user by ID.
- **user_id**: The unique user identifier
- **include_posts**: Whether to include user's posts
- **limit**: Maximum number of posts to return
"""
return {
"user_id": user_id,
"include_posts": include_posts,
"limit": limit
}Request Bodies with Pydantic
Define your data models once, get validation everywhere:
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
from typing import List, Optional
class UserCreate(BaseModel):
email: EmailStr
username: str = Field(..., min_length=3, max_length=50)
full_name: Optional[str] = None
class Config:
json_schema_extra = {
"example": {
"email": "user@example.com",
"username": "johndoe",
"full_name": "John Doe"
}
}
class UserResponse(BaseModel):
id: int
email: EmailStr
username: str
created_at: datetime
@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
# Your user creation logic here
return {
"id": 1,
"email": user.email,
"username": user.username,
"created_at": datetime.utcnow()
}Dependency Injection
FastAPI's dependency injection system is powerful and intuitive:
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
"""Extract and validate the current user from the JWT token."""
user = decode_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user
async def get_current_active_user(
current_user = Depends(get_current_user)
):
"""Ensure the user is active."""
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return current_user
@app.get("/users/me")
async def read_users_me(
current_user = Depends(get_current_active_user)
):
return current_userDatabase Integration
Connect to your database using async patterns:
from sqlalchemy.ext.asyncio import (
create_async_engine,
AsyncSession,
async_sessionmaker
)
from sqlalchemy.orm import declarative_base
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/db"
engine = create_async_engine(DATABASE_URL, echo=True)
async_session = async_sessionmaker(engine, class_=AsyncSession)
Base = declarative_base()
async def get_db():
async with async_session() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
@app.get("/items")
async def get_items(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Item))
return result.scalars().all()Background Tasks
Run non-blocking operations without making users wait:
from fastapi import BackgroundTasks
def send_email(email: str, message: str):
"""Send an email in the background."""
# Your email sending logic
print(f"Sending email to {email}: {message}")
@app.post("/subscribe")
async def subscribe(
email: str,
background_tasks: BackgroundTasks
):
background_tasks.add_task(
send_email,
email,
"Welcome to our newsletter!"
)
return {"message": "Subscription successful"}Middleware and CORS
Configure your application for the real world:
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
import time
# CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Custom timing middleware
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
app.add_middleware(TimingMiddleware)Error Handling
Create consistent, informative error responses:
from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse
class AppException(Exception):
def __init__(self, code: str, message: str, status_code: int = 400):
self.code = code
self.message = message
self.status_code = status_code
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message
}
}
)
# Usage
@app.get("/items/{item_id}")
async def get_item(item_id: int):
item = await find_item(item_id)
if not item:
raise AppException(
code="ITEM_NOT_FOUND",
message=f"Item {item_id} does not exist",
status_code=404
)
return itemTesting Your API
FastAPI makes testing a breeze:
from fastapi.testclient import TestClient
import pytest
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, FastAPI!"}
def test_create_user():
response = client.post(
"/users",
json={
"email": "test@example.com",
"username": "testuser"
}
)
assert response.status_code == 201
assert response.json()["email"] == "test@example.com"
@pytest.mark.asyncio
async def test_async_endpoint():
async with AsyncClient(app=app, base_url="http://test") as ac:
response = await ac.get("/async-endpoint")
assert response.status_code == 200API Versioning
Structure your API for long-term evolution:
from fastapi import APIRouter
# Version 1
v1_router = APIRouter(prefix="/api/v1", tags=["v1"])
@v1_router.get("/users")
async def get_users_v1():
return {"version": "1.0", "users": []}
# Version 2 with new features
v2_router = APIRouter(prefix="/api/v2", tags=["v2"])
@v2_router.get("/users")
async def get_users_v2():
return {
"version": "2.0",
"users": [],
"pagination": {"page": 1, "total": 0}
}
app.include_router(v1_router)
app.include_router(v2_router)Production Checklist
Before deploying, ensure you have:
- HTTPS Only: Use a reverse proxy like Nginx or Traefik
- Rate Limiting: Protect against abuse
- Logging: Structured JSON logs for observability
- Health Checks:
/healthand/readyendpoints - Graceful Shutdown: Handle SIGTERM properly
- Environment Variables: Never hardcode secrets
- Database Connection Pooling: Optimize connections
- Monitoring: Prometheus metrics integration
Conclusion
FastAPI is more than a framework—it's a productivity multiplier. By embracing Python's type system and modern async patterns, it lets you build APIs that are fast to write, fast to run, and a joy to maintain.
Want the complete production setup? ForgeAPI gives you everything covered in this guide, pre-configured and battle-tested.