Gerson

Gerson

Passionate developer specializing in web development, cloud architecture, and system design.

TypeScriptReactNext.jsPythonFastAPISQLNode.jsAWS

Building Type-Safe REST APIs with FastAPI and Pydantic v2

FastAPI combines Python's simplicity with automatic validation, OpenAPI documentation, and async support. This guide covers building production-grade APIs with Pydantic v2 models, dependency injection, and proper error handling.

Gersonhttps://fastapi.tiangolo.com/
Python code representing API development

FastAPI has become my go-to framework for building Python APIs. It combines the simplicity of Flask with automatic request validation, OpenAPI documentation generation, and first-class async support — all powered by Python's type hints and Pydantic for data validation.

With Pydantic v2 (a complete rewrite in Rust), validation performance improved by 5-50x depending on the workload. If you're building APIs in Python, this combination is hard to beat.

Why FastAPI?

FastAPI's core idea is that your Python type annotations should be the single source of truth. Define your types once, and FastAPI uses them for request validation, response serialization, API documentation, and editor autocompletion. There's no separate schema file, no validation middleware, and no documentation that drifts out of sync.

main.py — A complete API in 30 lines

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
from datetime import datetime

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

class UserCreate(BaseModel):
    name: str
    email: EmailStr
    age: int | None = None

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

users_db: dict[int, UserResponse] = {}
next_id = 1

@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
    global next_id
    db_user = UserResponse(
        id=next_id, name=user.name, email=user.email,
        created_at=datetime.now()
    )
    users_db[next_id] = db_user
    next_id += 1
    return db_user

@app.get("/users/{user_id}", response_model=UserResponse)
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]

Run this with uvicorn main:app --reload and you immediately get a fully documented API at /docs (Swagger UI) and /redoc (ReDoc), with request validation that returns clear error messages for invalid inputs.

Pydantic v2: The Validation Engine

Pydantic v2 is a complete rewrite with a Rust core (pydantic-core). The performance improvements are dramatic, but the API changes also make it more capable:

Advanced Pydantic v2 models

from pydantic import BaseModel, Field, field_validator, model_validator
from typing import Literal

class ProductCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    price: float = Field(gt=0, description="Price in USD")
    category: Literal["electronics", "books", "clothing"]
    tags: list[str] = Field(default_factory=list, max_length=10)

    @field_validator("name")
    @classmethod
    def name_must_be_titlecase(cls, v: str) -> str:
        return v.strip().title()

    @model_validator(mode="after")
    def validate_price_for_category(self):
        if self.category == "books" and self.price > 500:
            raise ValueError("Book price seems unreasonably high")
        return self

Dependency Injection

FastAPI's dependency injection system is one of its strongest features. It lets you declare what each endpoint needs, and FastAPI resolves and provides those dependencies automatically:

Dependencies for database sessions and auth

from fastapi import Depends, Header
from sqlalchemy.ext.asyncio import AsyncSession

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session() as session:
        yield session

async def get_current_user(
    authorization: str = Header(...),
    db: AsyncSession = Depends(get_db),
) -> User:
    token = authorization.removeprefix("Bearer ")
    user = await verify_token(token, db)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user

@app.get("/me", response_model=UserResponse)
async def get_profile(user: User = Depends(get_current_user)):
    return user

Dependencies can depend on other dependencies, forming a graph that FastAPI resolves automatically. This keeps your endpoint functions clean — they declare what they need, and the framework handles the wiring.

Async All the Way

FastAPI is built on Starlette and supports async natively. For I/O-bound workloads (database queries, HTTP calls, file operations), async endpoints handle significantly more concurrent requests than synchronous equivalents:

Async endpoint with concurrent operations

import asyncio
from httpx import AsyncClient

@app.get("/dashboard")
async def get_dashboard(user: User = Depends(get_current_user)):
    async with AsyncClient() as client:
        # Run multiple API calls concurrently
        profile_task = client.get(f"/api/profile/{user.id}")
        stats_task = client.get(f"/api/stats/{user.id}")
        notifications_task = client.get(f"/api/notifications/{user.id}")

        profile, stats, notifications = await asyncio.gather(
            profile_task, stats_task, notifications_task
        )

    return {
        "profile": profile.json(),
        "stats": stats.json(),
        "notifications": notifications.json(),
    }

Pro Tip: Use async def for endpoints that do I/O (database, HTTP calls). Use plain def for CPU-bound endpoints — FastAPI runs them in a thread pool automatically so they don't block the event loop.

Resources