Building REST APIs with FastAPI
FastAPI is a modern Python web framework designed for building high-performance REST APIs. It combines automatic OpenAPI documentation, type-safe request handling via Pydantic, and asynchronous support — all while matching the performance of Node.js and Go (thanks to Starlette and Pydantic’s Rust core). This article walks through building a complete CRUD API with FastAPI, covering path operations, dependency injection, validation, and more.
Getting Started
FastAPI is built on Starlette (the ASGI framework) and Pydantic (the validation library). Install it with pip install fastapi uvicorn. A minimal application defines a FastAPI instance and a few path operations using Python type annotations. The type annotations serve double duty: they enable editor autocompletion and type checking, and FastAPI uses them to generate OpenAPI documentation and validate request data automatically. Start the server with uvicorn main:app --reload and visit /docs for interactive Swagger UI or /redoc for ReDoc documentation.
from fastapi import FastAPI
app = FastAPI(title="My API", version="1.0.0")
@app.get("/")
def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
The path parameter item_id is declared as int, so FastAPI automatically validates that it is a valid integer and returns a 422 error if it is not. The query parameter q is optional (defaults to None) and is a string. The OpenAPI schema reflects this — documenting the path parameter type, the query parameter, and the response format — without writing any additional configuration.
Request Validation with Pydantic
For POST, PUT, and PATCH requests, define a Pydantic model for the request body. FastAPI automatically validates the incoming JSON against the model, returning detailed field-level error messages if validation fails. Pydantic models also serve as the response model, ensuring that outgoing data matches the schema. Use response_model in the decorator to control what fields are included in the response and to enable automatic response filtering and documentation.
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
class ItemCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0)
description: Optional[str] = None
tax: Optional[float] = None
class ItemResponse(BaseModel):
id: int
name: str
price: float
description: Optional[str] = None
created_at: datetime
items_db = {}
counter = 0
@app.post("/items", response_model=ItemResponse, status_code=201)
def create_item(item: ItemCreate):
global counter
counter += 1
db_item = {
"id": counter,
"name": item.name,
"price": item.price,
"description": item.description,
"created_at": datetime.now()
}
items_db[counter] = db_item
return db_item
@app.get("/items/{item_id}", response_model=ItemResponse)
def get_item(item_id: int):
if item_id not in items_db:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Item not found")
return items_db[item_id]
The Field() function adds validation constraints directly in the model: min_length=1, gt=0 (greater than 0). If validation fails, FastAPI returns a 422 response with a JSON body listing every field that failed and why — the client does not need to guess what went wrong. The response_model ensures that only the fields defined in ItemResponse are returned, even if the database object has extra fields.
Dependency Injection
FastAPI’s dependency injection system lets you extract common logic (database sessions, authentication, configuration) into reusable dependencies. A dependency is any callable that returns a value, declared with Depends(). FastAPI manages the dependency lifecycle, resolving dependencies in order and caching them per request. This makes your path operations thin — they just call the relevant service and return a response, while dependencies handle cross-cutting concerns like authentication, database connections, and rate limiting.
from fastapi import FastAPI, Depends, HTTPException, status
# Authentication dependency
async def get_current_user(token: str):
if token != "secret-token":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return {"username": "alice", "role": "admin"}
# Database dependency
def get_db():
db = Database.connect()
try:
yield db
finally:
db.close()
# Path operation using dependencies
@app.get("/users/me")
def read_current_user(
current_user: dict = Depends(get_current_user),
db: Database = Depends(get_db)
):
return {
"user": current_user,
"items": db.query("SELECT * FROM items")
}
Error Handling and Configuration
# Custom exception handler
from fastapi import 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}}
)
# Application settings with Pydantic
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
debug: bool = False
api_key: str
class Config:
env_file = ".env"
settings = Settings()
@app.get("/config")
def get_config():
return {"debug": settings.debug}
FastAPI’s combination of type safety, automatic documentation, and dependency injection makes it the most productive Python framework for building APIs. The automatic OpenAPI documentation ensures your API is always documented correctly (because it is generated from the actual code), and the Pydantic integration catches data issues at the earliest possible moment — when the request arrives at your server.
