Python FastAPI: Modern Python Web Framework

FastAPI has emerged as the modern solution for building production-grade APIs in Python. Created by Sebastián Ramírez in 2018, it leverages Python 3.6+ type hints to provide automatic request...

Key Insights

  • FastAPI combines Flask’s simplicity with automatic API documentation and type safety through Python type hints, making it the fastest-growing Python web framework for building APIs
  • The framework’s native async/await support and Pydantic validation deliver performance comparable to Node.js and Go while maintaining Python’s developer-friendly syntax
  • FastAPI’s automatic OpenAPI documentation generation and built-in dependency injection system eliminate boilerplate code that plagues Flask and Django REST Framework projects

Why FastAPI Matters

FastAPI has emerged as the modern solution for building production-grade APIs in Python. Created by Sebastián Ramírez in 2018, it leverages Python 3.6+ type hints to provide automatic request validation, serialization, and API documentation. Unlike Flask, which requires extensive third-party libraries for validation and documentation, or Django REST Framework, which carries Django’s heavyweight ORM and admin interface, FastAPI delivers a focused, high-performance toolkit for API development.

The framework’s adoption has exploded because it solves real pain points: automatic data validation prevents bad data from entering your system, type hints catch bugs at development time, and built-in async support handles thousands of concurrent connections without the callback hell of older frameworks.

from fastapi import FastAPI

app = FastAPI()

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

@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}

Run this with uvicorn main:app --reload and navigate to /docs to see automatically generated interactive API documentation. That’s FastAPI’s value proposition in under 15 lines.

Type Hints and Pydantic Models

FastAPI’s killer feature is Pydantic integration. Define your data structures once, and FastAPI handles validation, serialization, and documentation automatically. This eliminates the manual validation code that clutters Flask applications.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime

app = FastAPI()

class UserCreate(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: EmailStr
    full_name: Optional[str] = None

class UserResponse(BaseModel):
    id: int
    username: str
    email: str
    full_name: Optional[str]
    created_at: datetime
    
    class Config:
        orm_mode = True

@app.post("/users/", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
    # Validation already happened automatically
    # In production, save to database here
    return {
        "id": 1,
        "username": user.username,
        "email": user.email,
        "full_name": user.full_name,
        "created_at": datetime.now()
    }

The response_model parameter ensures FastAPI only returns specified fields, preventing accidental data leaks. The orm_mode configuration allows Pydantic to work with ORM objects, not just dictionaries.

Building Production-Ready CRUD APIs

Real applications need full CRUD operations with proper error handling and database integration. Here’s a complete example using an in-memory store (replace with your database):

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
from typing import Dict, Optional

app = FastAPI()

class Product(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

class ProductUpdate(BaseModel):
    name: Optional[str] = None
    description: Optional[str] = None
    price: Optional[float] = None
    tax: Optional[float] = None

# In-memory database
products_db: Dict[int, Product] = {}
product_id_counter = 1

@app.post("/products/", response_model=Product, status_code=status.HTTP_201_CREATED)
async def create_product(product: Product):
    global product_id_counter
    product_id = product_id_counter
    products_db[product_id] = product
    product_id_counter += 1
    return product

@app.get("/products/{product_id}", response_model=Product)
async def read_product(product_id: int):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    return products_db[product_id]

@app.put("/products/{product_id}", response_model=Product)
async def update_product(product_id: int, product: Product):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    products_db[product_id] = product
    return product

@app.patch("/products/{product_id}", response_model=Product)
async def partial_update_product(product_id: int, product_update: ProductUpdate):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    
    stored_product = products_db[product_id]
    update_data = product_update.dict(exclude_unset=True)
    updated_product = stored_product.copy(update=update_data)
    products_db[product_id] = updated_product
    return updated_product

@app.delete("/products/{product_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_product(product_id: int):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    del products_db[product_id]

Notice the distinction between PUT (full replacement) and PATCH (partial update). The exclude_unset=True parameter ensures only provided fields are updated.

Async Operations for Performance

FastAPI’s async support isn’t just for show—it enables handling thousands of concurrent requests efficiently. Use async endpoints when performing I/O operations like database queries or external API calls.

from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import select

DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname"

engine = create_async_engine(DATABASE_URL, echo=True)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

app = FastAPI()

async def get_db():
    async with async_session() as session:
        yield session

@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User).filter(User.id == user_id))
    user = result.scalar_one_or_none()
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

Use regular def functions for CPU-bound operations or when using synchronous libraries. FastAPI runs sync endpoints in a thread pool automatically.

Authentication with JWT

Most production APIs need authentication. Here’s a practical JWT implementation:

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from pydantic import BaseModel

SECRET_KEY = "your-secret-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

app = FastAPI()

class Token(BaseModel):
    access_token: str
    token_type: str

def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    return username

@app.post("/token", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    # Verify user credentials (check database in production)
    if form_data.username != "test" or form_data.password != "test":
        raise HTTPException(status_code=400, detail="Incorrect credentials")
    
    access_token = create_access_token(data={"sub": form_data.username})
    return {"access_token": access_token, "token_type": "bearer"}

@app.get("/users/me")
async def read_users_me(current_user: str = Depends(get_current_user)):
    return {"username": current_user}

The Depends system is FastAPI’s dependency injection. It’s reusable, testable, and keeps your code clean.

Testing FastAPI Applications

FastAPI includes a test client that makes testing straightforward:

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_create_product():
    response = client.post(
        "/products/",
        json={"name": "Test Product", "price": 29.99}
    )
    assert response.status_code == 201
    assert response.json()["name"] == "Test Product"

def test_read_product_not_found():
    response = client.get("/products/999")
    assert response.status_code == 404

def test_authentication():
    response = client.post(
        "/token",
        data={"username": "test", "password": "test"}
    )
    assert response.status_code == 200
    token = response.json()["access_token"]
    
    response = client.get(
        "/users/me",
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 200

Production Deployment

Deploy FastAPI with Uvicorn behind Gunicorn for production. Here’s a production-ready Docker setup:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["gunicorn", "main:app", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]

And a docker-compose.yml:

version: '3.8'

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/appdb
      - SECRET_KEY=${SECRET_KEY}
    depends_on:
      - db
    
  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=appdb
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

FastAPI has earned its popularity through practical design decisions: type safety without verbosity, performance without complexity, and automatic documentation without configuration. For new API projects in Python, it’s the obvious choice.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.