Web Development with FastAPI

Build modern async APIs with FastAPI — routes, Pydantic models, dependency injection, middleware, and testing.

Intermediate 45 min read 🐍 Python

What is FastAPI?

FastAPI is a modern, high-performance Python web framework for building APIs. It's built on top of Starlette (for async web handling) and Pydantic (for data validation). FastAPI is as fast as Node.js and Go, while being the most pleasant Python web framework to use.

Why FastAPI?

Automatic API docs (Swagger UI), built-in data validation, async support, type hints everywhere, and incredible performance. It's the fastest-growing Python web framework and the default choice for new API projects.

# Install
pip install fastapi uvicorn

# Run your app
uvicorn main:app --reload

Basic Routes

A FastAPI app is a collection of route handlers — functions that respond to HTTP requests. Each route has a path, an HTTP method, and a handler function:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello, World!"}

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    """Path parameter with automatic type validation."""
    return {"user_id": user_id, "name": f"User {user_id}"}

@app.get("/items")
async def list_items(skip: int = 0, limit: int = 10):
    """Query parameters with defaults."""
    return {"skip": skip, "limit": limit, "items": ["a", "b", "c"][skip:skip+limit]}

Notice the type hints: user_id: int tells FastAPI to automatically validate that the path parameter is an integer. If someone requests /users/abc, they get a clear error message without you writing any validation code.

Key Takeaway: FastAPI uses Python type hints for automatic request validation, serialization, and API documentation. Write the types, get validation for free.

Pydantic Models — Request & Response

For complex request/response bodies, define Pydantic models. They validate data automatically and generate clear error messages:

from pydantic import BaseModel, EmailStr, Field
from typing import Optional

class UserCreate(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    email: str
    age: Optional[int] = Field(None, ge=0, le=150)
    tags: list[str] = []

class UserResponse(BaseModel):
    id: int
    name: str
    email: str

# In-memory "database"
users_db = {}
next_id = 1

@app.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate):
    global next_id
    new_user = {"id": next_id, **user.model_dump()}
    users_db[next_id] = new_user
    next_id += 1
    return new_user

Send a POST with invalid data and FastAPI returns a detailed 422 error explaining exactly what's wrong — no manual validation needed.

Full CRUD Example

from fastapi import FastAPI, HTTPException

app = FastAPI(title="User API", version="1.0")

@app.get("/users")
async def list_users():
    return list(users_db.values())

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")
    return users_db[user_id]

@app.put("/users/{user_id}")
async def update_user(user_id: int, user: UserCreate):
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")
    users_db[user_id] = {"id": user_id, **user.model_dump()}
    return users_db[user_id]

@app.delete("/users/{user_id}")
async def delete_user(user_id: int):
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")
    deleted = users_db.pop(user_id)
    return {"message": "Deleted", "user": deleted}

Automatic API Docs

Visit /docs for interactive Swagger UI documentation. Visit /redoc for ReDoc documentation. Both are auto-generated from your route definitions and Pydantic models — always up to date.

Dependency Injection

FastAPI's dependency injection system is one of its most powerful features. It lets you declare what a route handler needs, and FastAPI provides it automatically:

from fastapi import Depends, Header, HTTPException

# Dependency: verify auth token
async def get_current_user(authorization: str = Header()):
    if not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Invalid token")
    token = authorization.split(" ")[1]
    # In reality, decode and verify the JWT here
    return {"user_id": 1, "name": "Alice", "token": token}

# Dependency: database session
async def get_db():
    db = SessionLocal()
    try:
        yield db  # Provide to route handler
    finally:
        db.close()  # Cleanup after request

@app.get("/profile")
async def get_profile(user=Depends(get_current_user)):
    """This route requires authentication."""
    return {"message": f"Hello, {user['name']}!", "user": user}

@app.get("/items")
async def list_items(db=Depends(get_db), user=Depends(get_current_user)):
    """Multiple dependencies — both DB and auth."""
    items = db.query(Item).filter(Item.owner_id == user["user_id"]).all()
    return items

Middleware & CORS

from fastapi.middleware.cors import CORSMiddleware
import time

# CORS — allow frontend to call this API
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # Frontend URL
    allow_methods=["*"],
    allow_headers=["*"],
)

# Custom middleware — log request timing
@app.middleware("http")
async def add_timing(request, call_next):
    start = time.time()
    response = await call_next(request)
    elapsed = time.time() - start
    response.headers["X-Process-Time"] = f"{elapsed:.4f}"
    return response

Testing FastAPI Apps

from fastapi.testclient import TestClient

client = TestClient(app)

def test_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello, World!"}

def test_create_user():
    response = client.post("/users", json={
        "name": "Alice",
        "email": "[email protected]"
    })
    assert response.status_code == 200
    data = response.json()
    assert data["name"] == "Alice"
    assert "id" in data

def test_get_nonexistent_user():
    response = client.get("/users/9999")
    assert response.status_code == 404
Key Takeaway: FastAPI's TestClient lets you test your API without running a server. Combined with pytest, you get fast, reliable API tests.
🔍 Deep Dive: Async in FastAPI

FastAPI supports both async def and regular def routes. Use async def when your route does I/O (database queries, API calls, file reads) — it lets the server handle other requests while waiting. Use regular def for CPU-bound work. FastAPI runs sync functions in a thread pool automatically, so both work correctly.

⚠️ Common Mistake: Blocking in Async Routes

Wrong:

@app.get("/data")
async def get_data():
    import time
    time.sleep(5)  # BLOCKS the entire event loop!
    return {"data": "result"}

Why: time.sleep() is a blocking call. In an async route, it blocks the entire server, not just this request.

Instead:

import asyncio

@app.get("/data")
async def get_data():
    await asyncio.sleep(5)  # Non-blocking!
    return {"data": "result"}