FastAPI
FastAPI endpoints, validation, dependencies, security, testing, and deployment reference.
FastAPI Developer Guide
FastAPI is a Python framework for building APIs with standard type hints. Typed route parameters and Pydantic models provide request validation, serialization, and generated OpenAPI documentation.
Getting Started
Installation
Create a virtual environment and install FastAPI with its standard development server dependencies:
python -m venv .venv
# macOS/Linux
source .venv/bin/activate
# PowerShell
.\.venv\Scripts\Activate.ps1
pip install "fastapi[standard]"First Application
Create main.py:
from fastapi import FastAPI
app = FastAPI(title="Store API")
@app.get("/")
async def root() -> dict[str, str]:
return {"message": "Hello, FastAPI"}Run the local development server:
fastapi dev main.pyUseful local URLs:
http://127.0.0.1:8000/ # API route
http://127.0.0.1:8000/docs # Swagger UI
http://127.0.0.1:8000/redoc # ReDoc documentation
http://127.0.0.1:8000/openapi.jsonPath Operations
Common HTTP Methods
from fastapi import FastAPI, status
app = FastAPI()
@app.get("/items")
async def list_items():
return []
@app.post("/items", status_code=status.HTTP_201_CREATED)
async def create_item():
return {"created": True}
@app.put("/items/{item_id}")
async def replace_item(item_id: int):
return {"id": item_id}
@app.patch("/items/{item_id}")
async def update_item(item_id: int):
return {"id": item_id}
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int):
return NonePath and Query Parameters
Parameters named in the route path become path parameters. Other simple typed parameters become query parameters:
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/{item_id}")
async def get_item(
item_id: int,
search: str | None = None,
limit: int = Query(default=20, ge=1, le=100),
offset: int = Query(default=0, ge=0),
):
return {
"id": item_id,
"search": search,
"limit": limit,
"offset": offset,
}FastAPI rejects values that do not satisfy the declared types or constraints and documents the accepted inputs in OpenAPI.
Request Bodies and Responses
Pydantic Models
Declare JSON request bodies with BaseModel:
from pydantic import BaseModel, Field
class ItemCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0)
description: str | None = None
in_stock: bool = Truefrom fastapi import FastAPI, status
app = FastAPI()
@app.post("/items", status_code=status.HTTP_201_CREATED)
async def create_item(item: ItemCreate):
return itemResponse Models
Keep input and public output models separate so internal fields are not exposed:
from pydantic import BaseModel
class UserCreate(BaseModel):
email: str
password: str
class UserPublic(BaseModel):
id: int
email: str
@app.post("/users", response_model=UserPublic, status_code=201)
async def create_user(user: UserCreate):
return {"id": 1, "email": user.email, "password": "not-returned"}The response_model filters and validates data returned to the client.
Status Codes and Errors
from fastapi import HTTPException, status
items = {1: {"id": 1, "name": "Keyboard"}}
@app.get("/items/{item_id}")
async def get_item(item_id: int):
item = items.get(item_id)
if item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Item not found",
)
return itemHeaders, Cookies, and Files
from typing import Annotated
from fastapi import Cookie, File, Header, UploadFile
@app.get("/request-info")
async def request_info(
user_agent: Annotated[str | None, Header()] = None,
session_id: Annotated[str | None, Cookie()] = None,
):
return {"user_agent": user_agent, "session_id": session_id}
@app.post("/uploads")
async def upload(file: UploadFile = File(...)):
content = await file.read()
return {"filename": file.filename, "size": len(content)}Install multipart support when accepting forms or uploaded files:
pip install python-multipartOrganizing an Application
Suggested Layout
app/
__init__.py
main.py
dependencies.py
models.py
schemas.py
routers/
__init__.py
items.py
tests/
test_items.pyUse an API Router
# app/routers/items.py
from fastapi import APIRouter, HTTPException
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/{item_id}")
async def get_item(item_id: int):
if item_id < 1:
raise HTTPException(status_code=404, detail="Item not found")
return {"id": item_id}# app/main.py
from fastapi import FastAPI
from .routers import items
app = FastAPI(title="Store API", version="1.0.0")
app.include_router(items.router)fastapi dev app/main.pyDependencies
Shared Request Logic
Dependencies are reusable callables for authorization, pagination, service objects, and database sessions:
from typing import Annotated
from fastapi import Depends, Query
def pagination(
limit: int = Query(default=20, ge=1, le=100),
offset: int = Query(default=0, ge=0),
) -> dict[str, int]:
return {"limit": limit, "offset": offset}
@app.get("/items")
async def list_items(page: Annotated[dict[str, int], Depends(pagination)]):
return pageDependencies With Cleanup
Use yield when a resource must be closed after a request:
from collections.abc import Generator
def get_db() -> Generator[str, None, None]:
connection = "database-connection"
try:
yield connection
finally:
pass # Close a real database session here.from typing import Annotated
from fastapi import Depends
@app.get("/reports")
async def reports(db: Annotated[str, Depends(get_db)]):
return {"connection": db}Database Pattern With SQLAlchemy
Session Dependency
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
DATABASE_URL = "sqlite:///./app.db"
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False},
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()CRUD Endpoint Pattern
from typing import Annotated
from fastapi import Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
class ItemPublic(BaseModel):
id: int
name: str
@app.get("/items/{item_id}", response_model=ItemPublic)
def read_item(item_id: int, db: Annotated[Session, Depends(get_db)]):
item = db.get(Item, item_id)
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
return itemKeep database table models separate from request and response schema models. Apply schema changes through migrations rather than recreating tables in production.
Authentication and Authorization
Bearer Token Dependency
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if token != "example-token":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return {"username": "demo"}
@app.get("/users/me")
async def read_me(user: Annotated[dict, Depends(current_user)]):
return userThis demonstrates dependency wiring only. Real applications should hash passwords, validate signed or stored tokens, set expiration rules, and load users from a trusted store.
Router-Level Protection
from fastapi import APIRouter, Depends
admin_router = APIRouter(
prefix="/admin",
dependencies=[Depends(current_user)],
)
@admin_router.get("/stats")
async def stats():
return {"active_users": 10}Middleware and CORS
Allow browser clients only from intended origins:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)Avoid allow_origins=["*"] when credentials are allowed or when the API should not be publicly callable from arbitrary browser origins.
Background Tasks and Lifespan
Small After-Response Tasks
from fastapi import BackgroundTasks
def write_notification(email: str) -> None:
print(f"Notify {email}")
@app.post("/notifications")
async def notify(email: str, tasks: BackgroundTasks):
tasks.add_task(write_notification, email)
return {"message": "Notification scheduled"}Use BackgroundTasks for short work in the same process. For durable or CPU-heavy jobs, use a job queue so work can retry and continue if the API process restarts.
Application Startup and Shutdown
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.ready = True
yield
app.state.ready = False
app = FastAPI(lifespan=lifespan)Testing
Test an Endpoint
Install testing dependencies:
pip install pytest httpx# tests/test_main.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, FastAPI"}pytest
pytest -qOverride Dependencies
Replace authentication or databases during tests:
from fastapi.testclient import TestClient
from app.main import app, current_user
async def fake_user():
return {"username": "tester"}
app.dependency_overrides[current_user] = fake_user
client = TestClient(app)
def test_profile():
response = client.get("/users/me")
assert response.status_code == 200
assert response.json()["username"] == "tester"
def teardown_module():
app.dependency_overrides.clear()Production and Deployment
Configuration From Environment
Keep secrets and environment-specific values out of committed source:
import os
DATABASE_URL = os.environ["DATABASE_URL"]
SECRET_KEY = os.environ["SECRET_KEY"]Run for Production
fastapi run app/main.pyDevelopment reload mode is useful locally, but it should not be used as the production process strategy. Deploy behind HTTPS, configure allowed hosts and CORS intentionally, and ensure database migrations run as part of release operations.
Container Example
FROM python:3.12-slim
WORKDIR /code
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
CMD ["fastapi", "run", "app/main.py", "--port", "8000"]Common Mistakes
| Problem | Better approach |
|---|---|
| Returning passwords or private fields | Use a dedicated response_model |
Blocking network or disk work inside async def | Use async libraries or run synchronous work appropriately |
Putting every route in main.py | Split endpoints into APIRouter modules |
| Creating a database session without cleanup | Provide it through a yield dependency |
| Trusting tokens without verification | Validate credentials and authorization server-side |
| Using in-process background tasks for critical work | Use a durable task queue |
Quick Reference
pip install "fastapi[standard]" # Install framework and common server tooling
fastapi dev main.py # Run development server with reload
fastapi run app/main.py # Run production-mode server
pytest # Run testsfrom fastapi import Depends, FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
@app.get("/resource/{resource_id}") # Read
@app.post("/resource", status_code=201) # Create
@app.put("/resource/{resource_id}") # Replace
@app.patch("/resource/{resource_id}") # Partially update
@app.delete("/resource/{resource_id}") # Delete