Getting Started
First API
FastAPI is built on Starlette and Pydantic. uvicorn is the ASGI server. --reload enables auto-reload during development.
# install FastAPI and uvicorn
pip install fastapi uvicorn
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
# run: uvicorn main:app --reloadRun with Uvicorn
The app instance 'main:app' means module 'main', variable 'app'. Use --reload only in development; in production run without it and use a process manager.
# development with auto-reload
uvicorn main:app --reload
# specify host and port
uvicorn main:app --host 0.0.0.0 --port 8000
# run programmatically
import uvicorn
if __name__ == "__main__":
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)Async Path Operations
You can declare path operations with either 'def' (run in threadpool) or 'async def'. Use async def when the function performs I/O via async libraries; mixing blocking calls in async def will block the event loop.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"message": "hello"}
@app.get("/sync")
def read_sync():
return {"message": "sync also works"}
# use async when calling async libs (httpx, databases)
@app.get("/data")
async def fetch_data():
import httpx
async with httpx.AsyncClient() as client:
r = await client.get("https://api.example.com")
return r.json()Path Operation Decorators
FastAPI supports all HTTP methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. Each maps to a decorator. Use status_code= on the decorator to override the default success status.
from fastapi import FastAPI
app = FastAPI()
@app.get("/items")
def list_items(): ...
@app.post("/items")
def create_item(): ...
@app.put("/items/{item_id}")
def replace_item(item_id: int): ...
@app.patch("/items/{item_id}")
def update_item(item_id: int): ...
@app.delete("/items/{item_id}")
def delete_item(item_id: int): ...
@app.options("/items")
def options_items(): ...
@app.head("/items")
def head_items(): ...JSON Response & Status
Returning a dict/list is auto-converted to JSON with 200 (or your status_code). For full control over status, headers, or content, return a JSONResponse or Response directly.
from fastapi import FastAPI, status
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/plain")
def plain():
return {"key": "value"} # auto JSON, 200 OK
@app.post("/created", status_code=status.HTTP_201_CREATED)
def created():
return {"id": 1}
@app.get("/custom")
def custom():
return JSONResponse(
status_code=418,
content={"error": "I'm a teapot"},
headers={"X-Custom": "yes"},
)Project Structure
Split a growing app into modules with APIRouter, then register them via app.include_router(). Keep Pydantic schemas, DB setup, and dependencies in separate files for maintainability.
myapp/
main.py # creates the FastAPI app
routers/
users.py # APIRouter for /users
items.py # APIRouter for /items
models/
schemas.py # Pydantic models
database.py # DB engine & session
dependencies.py # shared Depends functions
tests/
test_main.py
# main.py
from fastapi import FastAPI
from routers import users, items
app = FastAPI()
app.include_router(users.router)
app.include_router(items.router, prefix="/items")Path Parameters
Basic Path Parameters
Values captured from the path are strings unless you add a type annotation. The parameter name in the path must match the function argument name exactly.
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id):
return {"item_id": item_id}
# GET /items/42 -> {"item_id": "42"} (string by default)Type Conversion
Adding a type annotation (int, float, bool, str, Enum) makes FastAPI validate and convert the path value. Invalid values return a 422 response with a clear error message.
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
# GET /items/3 -> {"item_id": 3}
# GET /items/abc -> 422 Validation Error (not an integer)Path Validation
Path() adds metadata and constraints to a path parameter. Numeric constraints: ge (>=), gt (>), le (<=), lt (<). Path parameters are always required, so Path() cannot set a default.
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(
item_id: int = Path(
title="The ID of the item",
description="Must be a positive integer",
ge=1,
le=1000,
),
):
return {"item_id": item_id}Numeric Constraints
Use ge/gt/le/lt on int or float path parameters to enforce numeric ranges. Combined constraints give you precise bounds validation with automatic 422 errors.
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/ratio/{value}")
def ratio(
value: float = Path(gt=0, lt=1), # 0 < value < 1
):
return {"value": value}
@app.get("/page/{page}")
def page(
page: int = Path(ge=1), # >= 1
size: int = Path(le=100), # <= 100
):
return {"page": page, "size": size}Route Order Matters
Routes are matched in declaration order. Declare specific paths like /users/me before dynamic patterns like /users/{user_id}, or the dynamic route will shadow the specific one.
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/me")
def read_current_user():
return {"user": "me"}
@app.get("/users/{user_id}")
def read_user(user_id: str):
return {"user_id": user_id}
# /users/me MUST be declared before /users/{user_id}
# otherwise 'me' would match the {user_id} patternEnum Path Parameters
Using an Enum type for a path parameter restricts it to predefined values and generates an enum schema in the OpenAPI docs. Subclass (str, Enum) so values serialize as strings.
from enum import Enum
from fastapi import FastAPI
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
app = FastAPI()
@app.get("/models/{model_name}")
def get_model(model_name: ModelName):
if model_name is ModelName.alexnet:
return {"model": model_name, "layers": 5}
return {"model": model_name}
# GET /models/resnet -> {"model": "resnet"}Query Parameters
Basic Query Parameters
Function parameters that are NOT in the path become query parameters. Providing a default makes them optional; the default is used when the query key is absent.
from fastapi import FastAPI
app = FastAPI()
fake_items = [{"item": "a"}, {"item": "b"}, {"item": "c"}]
@app.get("/items")
def list_items(skip: int = 0, limit: int = 10):
return fake_items[skip : skip + limit]
# GET /items?skip=0&limit=2
# GET /items -> skip=0, limit=10Optional Query Parameters
Use Optional[str] = None (or str | None = None on Python 3.10+) for a query parameter that may be omitted. Required parameters simply have no default value.
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: str, q: Optional[str] = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
# GET /items/1?q=hello
# GET /items/1 (q is None)Query Validation
Query() adds validation: min_length/max_length for strings, ge/gt/le/lt for numbers, pattern (regex) for string format. Use default= to set a value while still applying constraints.
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/search")
def search(q: str = Query(min_length=3, max_length=50, pattern="^[a-zA-Z]+$")):
return {"q": q}
@app.get("/page")
def page(limit: int = Query(default=10, ge=1, le=100)):
return {"limit": limit}Boolean Conversion
Bool query parameters accept many truthy values: true, 1, yes, on (case-insensitive). Anything else is interpreted as false. Invalid types like ?active=maybe return 422.
from fastapi import FastAPI
app = FastAPI()
@app.get("/flags")
def flags(active: bool = False):
return {"active": active}
# GET /flags?active=true -> {"active": true}
# GET /flags?active=1 -> {"active": true}
# GET /flags?active=yes -> {"active": true}
# GET /flags?active=off -> {"active": false}List Query Parameters
A List[str] parameter accepts the same key repeated in the query string. Use Query(default=[]) for an empty list by default, or Query(default=None) to allow missing values.
from typing import List
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items")
def list_items(q: List[str] = Query(default=[])):
return {"q": q}
# GET /items?q=a&q=b&q=c -> {"q": ["a", "b", "c"]}
# declare default=None to accept a list or nullRequired Query Parameters
Use Query(...) (the Ellipsis) to declare a required query parameter that still has validation metadata. Without a default and without Query(...), a plain annotated param is also required.
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items")
def read_item(q: str = Query(...)):
return {"q": q}
# GET /items?q=hello -> ok
# GET /items -> 422 (q is required)
# Query(...) (Ellipsis) marks a parameter as requiredRequest Body (Pydantic)
BaseModel Basics
A Pydantic BaseModel declared as a function parameter becomes the JSON request body. FastAPI validates the payload, converts types, and returns 422 on invalid data. Fields with defaults are optional.
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
in_stock: bool = True
app = FastAPI()
@app.post("/items")
def create_item(item: Item):
return item
# POST /items body: {"name": "Apple", "price": 0.5}
# -> {"name": "Apple", "price": 0.5, "in_stock": true}Field Types & Constraints
Field() adds constraints and metadata to model attributes: min_length/max_length for strings, gt/ge/lt/le for numbers, default_factory for mutable defaults. model_config with json_schema_extra adds examples to the docs.
from pydantic import BaseModel, Field
from typing import Optional
class Item(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, description="must be positive")
tax: Optional[float] = None
tags: list[str] = Field(default_factory=list)
model_config = {
"json_schema_extra": {
"examples": [{"name": "Apple", "price": 0.5, "tags": ["fruit"]}]
}
}Nested Models
Models can be nested: declare another BaseModel as a field type. FastAPI validates the full nested structure recursively, and the generated OpenAPI schema reflects the nested objects.
from pydantic import BaseModel
from typing import Optional
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
description: Optional[str] = None
image: Optional[Image] = None
images: list[Image] = []
# valid body:
# {"name": "Phone", "image": {"url": "http://x/a.png", "name": "a"}}Field Validation
Use @field_validator (Pydantic v2) to add custom validation logic per field. Raise ValueError to reject input (FastAPI surfaces it as 422). Return the (possibly transformed) value to keep it.
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
email: str
@field_validator("email")
@classmethod
def email_must_contain_at(cls, v: str) -> str:
if "@" not in v:
raise ValueError("must contain @")
return v.lower()
@field_validator("name")
@classmethod
def name_stripped(cls, v: str) -> str:
return v.strip()Optional & Default Fields
A field is required only if it has no default and is not Optional. Optional[str] = None is the standard pattern for an optional nullable field; a plain default like 0.0 makes a field optional with that value.
from pydantic import BaseModel
from typing import Optional
class Item(BaseModel):
name: str # required
description: Optional[str] = None # optional, defaults to None
price: float = 0.0 # optional, defaults to 0.0
tags: list[str] = [] # optional, defaults to empty list
# All of these are valid:
# {"name": "X"}
# {"name": "X", "price": 9.99, "tags": ["a"]}Body with Path & Query
Path, query, and body parameters can coexist in one operation. FastAPI decides by: path param if in the path, Pydantic model (or Body()) for the body, otherwise query. Optional body needs a default of None.
from fastapi import FastAPI, Path, Query
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
app = FastAPI()
@app.put("/items/{item_id}")
def update_item(
item_id: int = Path(ge=1),
q: Optional[str] = Query(default=None),
item: Item = None,
):
result = {"item_id": item_id, **item.model_dump()}
if q:
result["q"] = q
return resultResponse Model
response_model Basics
response_model filters the returned data to only the fields defined in the model, even if you return an object with extra fields. This is the standard way to hide sensitive fields like passwords.
from fastapi import FastAPI
from pydantic import BaseModel
class User(BaseModel):
username: str
email: str
hashed_password: str
class UserOut(BaseModel):
username: str
email: str
app = FastAPI()
@app.get("/users/me", response_model=UserOut)
def read_user():
return User(username="bob", email="[email protected]", hashed_password="secret")
# response: {"username": "bob", "email": "[email protected]"} (no password)Exclude Fields
response_model_include and response_model_exclude let you whitelist or blacklist fields per route without defining new models. Pass a set of field names. These are applied in addition to the response_model.
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
secret: str
app = FastAPI()
@app.get("/items/{id}", response_model=Item, response_model_exclude={"secret"})
def read_item(id: int):
return {"name": "Apple", "price": 0.5, "secret": "hidden"}
@app.get("/items", response_model=list[Item], response_model_include={"name", "price"})
def list_items():
return [{"name": "Apple", "price": 0.5, "secret": "x"}]List Response
Use response_model=list[Model] (or List[Model]) to declare that an endpoint returns an array of objects. FastAPI validates and serializes each item in the list.
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
app = FastAPI()
@app.get("/items", response_model=list[Item])
def list_items():
return [
{"name": "Apple", "price": 0.5},
{"name": "Banana", "price": 0.3},
]
# response: [{"name": "Apple", "price": 0.5}, {"name": "Banana", "price": 0.3}]Exclude Unset/Defaults
exclude_unset omits fields the client never sent (only those explicitly set appear). exclude_defaults omits fields equal to their defaults; exclude_none omits fields that are None. Useful for sparse responses.
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
price: float = 0.0
tax: float = 0.0
app = FastAPI()
@app.get("/items/{id}", response_model=Item, response_model_exclude_unset=True)
def read_item(id: int):
return {"name": "Apple", "price": 0.5}
# -> {"name": "Apple", "price": 0.5} (description, tax omitted)
# also: response_model_exclude_defaults=True
# response_model_exclude_none=TrueResponse by Alias
Field(alias=...) lets incoming JSON use a different key name than the Python attribute. response_model_by_alias=True outputs using the alias too. populate_by_name=True also allows the original attribute name on input.
from fastapi import FastAPI
from pydantic import BaseModel, ConfigDict, Field
class Item(BaseModel):
model_config = ConfigDict(populate_by_name=True)
name: str = Field(alias="itemName")
price: float
app = FastAPI()
@app.post("/items", response_model=Item, response_model_by_alias=True)
def create(item: Item):
return item
# input: {"itemName": "Apple", "price": 0.5}
# output: {"itemName": "Apple", "price": 0.5}Plain Dict Response
Returning a dict/list auto-serializes to JSON. Use response_class to change the default response type (HTMLResponse, PlainTextResponse), or return a Response subclass directly for full control.
from fastapi import FastAPI
from fastapi.responses import JSONResponse, PlainTextResponse
app = FastAPI()
@app.get("/dict")
def dict_resp():
return {"a": 1, "b": 2} # auto JSON
@app.get("/text", response_class=PlainTextResponse)
def text_resp():
return "plain text response"
@app.get("/raw")
def raw_resp():
return JSONResponse(content={"a": 1}, status_code=200)Form & UploadFile
Form Data
Form() declares a parameter parsed from application/x-www-form-urlencoded or multipart form data. You cannot mix Form() with a JSON body (BaseModel) in the same operation — pick one content type.
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login")
def login(username: str = Form(), password: str = Form()):
return {"username": username}
# Content-Type: application/x-www-form-urlencoded
# body: username=bob&password=secretUploadFile Basic
UploadFile is a streaming file representation with .filename, .content_type, .read(), .write(), .close(). Use 'await file.read()' inside async def. For large files prefer file.file (SpooledTemporaryFile) to avoid loading all into memory.
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
contents = await file.read()
return {
"filename": file.filename,
"size": len(contents),
"content_type": file.content_type,
}Multiple Files
Declare list[UploadFile] to accept multiple files in one request. Each file is sent with the same field name. The order of files in the list matches the order they were sent.
from fastapi import FastAPI, UploadFile
app = FastAPI()
@app.post("/uploads")
async def upload_many(files: list[UploadFile]):
return [
{"filename": f.filename, "size": len(await f.read())}
for f in files
]
# curl: curl -F "[email protected]" -F "[email protected]" http://localhost:8000/uploadsFile and Form Together
Form() and UploadFile (File()) can be combined in the same endpoint because both use multipart/form-data. You still cannot combine them with a JSON BaseModel body.
from fastapi import FastAPI, UploadFile, File, Form
app = FastAPI()
@app.post("/items")
async def create_item(
name: str = Form(),
description: str = Form(),
file: UploadFile = File(),
):
return {
"name": name,
"description": description,
"file": file.filename,
}Save Uploaded File
For small files, shutil.copyfileobj streams the SpooledTemporaryFile to disk efficiently. For very large files, read in chunks (e.g. 1MB) to keep memory low. Always sanitize file.filename to avoid path traversal.
import shutil
from pathlib import Path
from fastapi import FastAPI, UploadFile
app = FastAPI()
@app.post("/save")
async def save_file(file: UploadFile):
dest = Path("uploads") / file.filename
dest.parent.mkdir(exist_ok=True)
with dest.open("wb") as buf:
shutil.copyfileobj(file.file, buf)
return {"saved_to": str(dest)}
# async alternative for large files:
# async with dest.open("wb") as buf:
# while chunk := await file.read(1024 * 1024):
# buf.write(chunk)File Metadata & Size
Validate file.content_type and size before processing. After read(), the cursor is at the end; call 'await file.seek(0)' to rewind if you need to read again. Real type checking should inspect magic bytes, not just the header.
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
@app.post("/validate")
async def validate(file: UploadFile = File(...)):
# allowed types
allowed = {"image/png", "image/jpeg"}
if file.content_type not in allowed:
return {"error": "unsupported type"}
contents = await file.read()
max_size = 5 * 1024 * 1024 # 5 MB
if len(contents) > max_size:
return {"error": "file too large"}
await file.seek(0) # rewind so later reads work
return {"filename": file.filename, "size": len(contents)}Dependency Injection
Basic Depends
Depends() injects the return value of a function into your path operation. Shared logic (pagination, parsing, DB sessions) lives in one place and is reused across endpoints. The dependency's parameters are also documented in OpenAPI.
from fastapi import FastAPI, Depends
app = FastAPI()
def common_params(q: str | None = None, skip: int = 0, limit: int = 10):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items")
def list_items(commons: dict = Depends(common_params)):
return commons
@app.get("/users")
def list_users(commons: dict = Depends(common_params)):
return commonsClass Dependencies
A class with an __init__ can be a dependency; FastAPI calls it like a function. Use Depends(CommonQueryParams) or the shorthand Depends() when the parameter is annotated with the class. The instance is injected.
from fastapi import FastAPI, Depends
class CommonQueryParams:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 10):
self.q = q
self.skip = skip
self.limit = limit
app = FastAPI()
@app.get("/items")
def list_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
return {"q": commons.q, "skip": commons.skip, "limit": commons.limit}
# shorthand: commons = Depends() infers the typeNested Dependencies
Dependencies can themselves depend on other dependencies, forming a graph. FastAPI resolves the whole tree, caches each result per request, and propagates exceptions. This is ideal for layered auth (token -> user -> permissions).
from fastapi import FastAPI, Depends, Header, HTTPException
def get_token(authorization: str = Header()):
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="invalid header")
return authorization.removeprefix("Bearer ")
def get_current_user(token: str = Depends(get_token)):
if token != "admin-token":
raise HTTPException(status_code=403, detail="forbidden")
return {"username": "admin"}
app = FastAPI()
@app.get("/me")
def me(user: dict = Depends(get_current_user)):
return userYield Dependencies (Cleanup)
A dependency generator (yield) is perfect for resources needing cleanup: DB sessions, file handles, transactions. Code after yield runs as a finalizer, even if the route raises an exception. Only one yield per dependency.
from fastapi import FastAPI, Depends
def get_db():
db = open_db_session() # acquire resource
try:
yield db # injected into the route
finally:
db.close() # always runs after the request
app = FastAPI()
@app.get("/items")
def list_items(db = Depends(get_db)):
return db.query("SELECT * FROM items")Global Dependencies
Pass dependencies=[...] to FastAPI() (or an APIRouter) to enforce them on every route without injecting their return value. Useful for global auth, rate limiting, or request logging.
from fastapi import FastAPI, Depends, Header, HTTPException
def verify_token(x_token: str = Header()):
if x_token != "supersecret":
raise HTTPException(status_code=400, detail="X-Token missing")
def verify_key(x_key: str = Header()):
if x_key != "realkey":
raise HTTPException(status_code=400, detail="X-Key missing")
# applied to every route in the app
app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])
@app.get("/items")
def list_items():
return [{"item": "a"}] # token & key already checkedPer-Route Dependencies
Use dependencies=[Depends(...)] on a decorator (or APIRouter) to require a dependency for specific routes without using its return value. This keeps route signatures clean while still enforcing checks.
from fastapi import FastAPI, Depends, Header, HTTPException
def verify_token(x_token: str = Header()):
if x_token != "secret":
raise HTTPException(status_code=401, detail="bad token")
app = FastAPI()
@app.get("/public")
def public():
return {"open": True}
@app.get("/private", dependencies=[Depends(verify_token)])
def private():
return {"secret": "data"}Security (HTTPBasic / OAuth2 / JWT)
HTTP Basic Auth
HTTPBasic parses the Authorization: Basic header into username/password. Use secrets.compare_digest (not ==) to prevent timing attacks. Always return 401 with WWW-Authenticate: Basic so browsers prompt for credentials.
import secrets
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
security = HTTPBasic()
app = FastAPI()
def get_current_user(credentials: HTTPBasicCredentials = Depends(security)):
correct_user = secrets.compare_digest(credentials.username, "admin")
correct_pass = secrets.compare_digest(credentials.password, "secret")
if not (correct_user and correct_pass):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/profile")
def profile(user: str = Depends(get_current_user)):
return {"user": user}OAuth2 Password Bearer
OAuth2PasswordBearer declares that routes expect a Bearer token; the dependency extracts it from the Authorization header. tokenUrl tells the docs UI where to send credentials. The token itself is just a string until you validate it.
from fastapi import FastAPI, Depends
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
@app.get("/me")
def me(token: str = Depends(oauth2_scheme)):
return {"token": token}
# client POSTs username+password to /token, receives an access token,
# then sends it as: Authorization: Bearer <token>Create JWT Token
Use python-jose (or PyJWT) to sign tokens. The 'sub' (subject) claim conventionally holds the username or user id, and 'exp' sets expiry. Keep SECRET_KEY long, random, and loaded from environment — never commit it.
from datetime import datetime, timedelta, timezone
from jose import jwt
SECRET_KEY = "keep-this-secret"
ALGORITHM = "HS256"
def create_access_token(data: dict, expires_minutes: int = 30) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=expires_minutes)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
# token = create_access_token({"sub": "alice"})
# -> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Decode JWT Token
jwt.decode verifies the signature and the 'exp' claim automatically, raising JWTError on failure. Map the dependency to a 401 with WWW-Authenticate: Bearer so clients know to provide a valid token.
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
SECRET_KEY = "keep-this-secret"
ALGORITHM = "HS256"
def get_current_user(token: str = Depends(oauth2_scheme)) -> str:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str | None = payload.get("sub")
if username is None:
raise credentials_exception
return username
except JWTError:
raise credentials_exception
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)Password Hashing
Never store plaintext passwords. passlib with bcrypt (or argon2) handles salting and hashing. verify() compares a plaintext input against a stored hash. pip install passlib[bcrypt] to get the bcrypt backend.
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(plain: str) -> str:
return pwd_context.hash(plain)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
# usage when issuing tokens:
def authenticate_user(username: str, password: str):
user = fake_db.get(username)
if not user or not verify_password(password, user.hashed_password):
return False
return userOAuth2 Login Flow
OAuth2PasswordRequestForm reads username/password from a form-encoded POST (the standard OAuth2 password flow). Return access_token and token_type: 'bearer'. This endpoint is what tokenUrl in OAuth2PasswordBearer points to.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
app = FastAPI()
@app.post("/token")
def login(form: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form.username, form.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
)
token = create_access_token({"sub": user.username})
return {"access_token": token, "token_type": "bearer"}
# OAuth2PasswordRequestForm parses username+password from form data
# and powers the 'Authorize' button in the Swagger UI.Middleware
Add Middleware
@app.middleware('http') registers a function that wraps every request. It must await call_next(request) to get the response, then can inspect or modify it before returning. Middleware runs for all routes.
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Process-Time"] = f"{duration:.4f}"
return responseCustom Header Middleware
Middleware is the right place to attach headers that should be on every response (security headers, server banner, tracing IDs). Mutate the response object after awaiting call_next.
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_banner(request: Request, call_next):
response = await call_next(request)
response.headers["X-Powered-By"] = "FastAPI"
response.headers["X-Frame-Options"] = "DENY"
return response
# every response now carries these headersBaseHTTPMiddleware
For reusable, configurable middleware, subclass BaseHTTPMiddleware and register it with app.add_middleware(MyMiddleware, **options). This is also how the built-in CORS, GZip, and TrustedHost middlewares are added.
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import FastAPI, Request
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
response.headers["X-Duration"] = str(time.perf_counter() - start)
return response
app = FastAPI()
app.add_middleware(TimingMiddleware)
# class-based middleware is reusable and configurableHTTPS Redirect
HTTPSRedirectMiddleware returns a 308 redirect to the HTTPS version of any HTTP request. Use it behind a load balancer that terminates TLS, to guarantee clients upgrade to a secure connection.
from fastapi import FastAPI
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
app = FastAPI()
app.add_middleware(HTTPSRedirectMiddleware)
# all HTTP requests are redirected to HTTPS (308)Trusted Host
TrustedHostMiddleware rejects requests whose Host header is not in the allow-list, preventing Host-header injection attacks. Wildcards like '*.example.com' are supported. Always include 'localhost' for local development.
from fastapi import FastAPI
from fastapi.middleware.trustedhost import TrustedHostMiddleware
app = FastAPI()
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["example.com", "*.example.com", "localhost"],
)
# requests with a disallowed Host header get a 400 responseMiddleware Order
Starlette executes middleware in LIFO order for incoming requests: the last added runs first. Add CORS early (so preflight works) and GZip after. Remember this when debugging why a header set by one middleware isn't seen by another.
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Middleware runs in REVERSE order of registration for requests,
# and in registration order for responses.
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(CORSMiddleware, allow_origins=["*"])
# request flow: CORS -> GZip -> route
# response flow: route -> GZip -> CORSCORS
CORSMiddleware Setup
CORSMiddleware handles the CORS preflight (OPTIONS) and adds Access-Control-* headers. allow_origins should list the exact origins permitted; use a list of specific URLs in production rather than ['*'].
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com", "https://www.example.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/data")
def data():
return {"data": 1}Allow All Origins (Dev Only)
allow_origins=['*'] permits any origin but cannot be combined with allow_credentials=True (browsers reject it). Use this only for fully public APIs in development; in production enumerate the real origins.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
# allow_credentials=True CANNOT be combined with allow_origins=["*"]
)Allow Credentials
allow_credentials=True lets the browser send cookies and Authorization headers cross-origin. It requires specific origins (never '*'). The browser sets Access-Control-Allow-Credentials: true on responses.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_credentials=True, # send cookies / Authorization
allow_methods=["GET", "POST"],
allow_headers=["Authorization", "Content-Type"],
)
# needed when the frontend sends cookies or credentials with requestsAllowed Methods & Headers
allow_methods/allow_headers restrict what the browser may send. expose_headers lists response headers the frontend JavaScript is allowed to read (browsers otherwise hide custom headers). Preflight responses are cached by the browser.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
expose_headers=["X-Total-Count", "X-Process-Time"],
)
# expose_headers lets the frontend JS read these response headersPreflight Requests
For 'non-simple' requests (e.g. custom headers, PUT/DELETE, JSON content type), the browser sends an OPTIONS preflight. CORSMiddleware intercepts OPTIONS and responds with the allowed methods/headers. max_age controls how long the browser caches that answer.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_methods=["*"],
allow_headers=["*"],
max_age=600, # browser caches preflight result for 600s
)
# Browser sends OPTIONS preflight for non-simple requests;
# CORSMiddleware answers automatically with the right headers.Dynamic Origin Validation
For dynamic allow-lists use allow_origin_regex (matches against the request Origin) instead of a static list. This is handy when valid origins follow a pattern (e.g. any subdomain of example.com).
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
VALID_DOMAINS = {"example.com", "staging.example.com"}
def origin_check(origin: str) -> str | None:
if origin and origin.replace("https://", "").replace("http://", "") in VALID_DOMAINS:
return origin
return None
app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"https://.*\.example\.com",
allow_methods=["*"],
allow_headers=["*"],
)Exception Handling
Raise HTTPException
raise HTTPException anywhere in a route or dependency to short-circuit and return an error response. detail can be a string, dict, or list — it becomes the JSON 'detail' field. headers optionally adds response headers.
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
items = {1: "Apple", 2: "Banana"}
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id not in items:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Item not found",
headers={"X-Error": "missing"},
)
return {"item": items[item_id]}Custom Exception
Register a handler with @app.exception_handler(MyException) to convert any custom exception into an HTTP response. The handler receives the request and the exception. This centralizes error formatting for your app's domain errors.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
app = FastAPI()
@app.exception_handler(UnicornException)
async def unicorn_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"Oops, {exc.name} did it again"},
)
@app.get("/unicorns/{name}")
def read_unicorn(name: str):
if name == "yolo":
raise UnicornException(name=name)
return {"name": name}Override Validation Error
RequestValidationError is raised when input fails Pydantic validation. Override its handler with @app.exception_handler(RequestValidationError) to customize the 422 response shape, e.g. to match a company-wide error envelope.
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"success": False,
"errors": exc.errors(),
"body": exc.body,
},
)Override HTTPException Handler
Override the default HTTPException handler to enforce a consistent error JSON shape across all raised HTTPExceptions. Import the class as fastapi.exceptions.HTTPException (the same one used by raise) when re-binding its handler.
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import HTTPException as FastAPIHTTPException
app = FastAPI()
@app.exception_handler(FastAPIHTTPException)
async def custom_http_handler(request, exc: FastAPIHTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"success": False,
"status": exc.status_code,
"message": exc.detail,
},
headers=exc.headers,
)Common Status Codes
The status module gives named constants for all HTTP status codes, avoiding magic numbers. 200 (default GET), 201 (created), 204 (no content), 422 (validation) are the most common in FastAPI apps.
from fastapi import FastAPI, status
app = FastAPI()
@app.get("/ok", status_code=status.HTTP_200_OK)
def ok(): return {"ok": True}
@app.post("/create", status_code=status.HTTP_201_CREATED)
def create(): return {"id": 1}
@app.get("/no-content", status_code=status.HTTP_204_NO_CONTENT)
def no_content(): return None
@app.get("/not-found")
def missing():
# raise HTTPException(status.HTTP_404_NOT_FOUND, "nope")
...
# Other common: 400 BAD_REQUEST, 401 UNAUTHORIZED,
# 403 FORBIDDEN, 409 CONFLICT, 422 UNPROCESSABLE_ENTITY, 500 INTERNAL_SERVER_ERRORGeneric Exception Handler
Registering a handler for the base Exception catches anything not already handled, preventing stack traces from leaking to clients. Always log the full traceback server-side; return a generic 500 to the client.
import logging
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
logger = logging.getLogger("uvicorn.error")
app = FastAPI()
@app.exception_handler(Exception)
async def unhandled(request: Request, exc: Exception):
logger.exception("Unhandled error on %s", request.url.path)
return JSONResponse(
status_code=500,
content={"detail": "Internal Server Error"},
)
@app.get("/boom")
def boom():
raise RuntimeError("something exploded")Background Tasks
Basic BackgroundTasks
BackgroundTasks runs functions after the response is sent — perfect for fire-and-forget work like logging, emails, or cache invalidation. add_task schedules a function (and args) without blocking the response.
from fastapi import FastAPI, BackgroundTasks
app = FastAPI()
def write_log(message: str):
with open("log.txt", "a") as f:
f.write(message + "\n")
@app.post("/send")
def send_notification(background_tasks: BackgroundTasks):
background_tasks.add_task(write_log, "notification sent")
return {"message": "notification queued"}Multiple Tasks
Add as many tasks as you want; they run sequentially in the order added, after the response is returned. Because they run in the same process, keep them quick and non-blocking, or move to a real queue (Celery, RQ, Dramatiq).
from fastapi import FastAPI, BackgroundTasks
app = FastAPI()
def send_email(to: str, subject: str): ...
def update_cache(key: str): ...
def notify_slack(msg: str): ...
@app.post("/signup")
def signup(background_tasks: BackgroundTasks):
background_tasks.add_task(send_email, "[email protected]", "Welcome")
background_tasks.add_task(update_cache, "user_count")
background_tasks.add_task(notify_slack, "new signup")
return {"status": "ok"}Task with Dependencies
BackgroundTasks is injected just like any dependency. The task function can receive values (like a DB session) captured at request time. Note that yielded/closed resources from dependencies close after tasks finish, since tasks run within the request lifecycle's scope.
from fastapi import FastAPI, BackgroundTasks, Depends
app = FastAPI()
def get_db_session():
db = "session"
yield db
def write_log(db, message: str):
print(db, message)
@app.get("/items")
def list_items(background_tasks: BackgroundTasks, db = Depends(get_db_session)):
background_tasks.add_task(write_log, db, "list called")
return {"items": []}Async Task Function
Background task functions can be either sync (run in a threadpool) or async (awaited on the event loop). Use async tasks for I/O-bound work that itself uses async libraries, and sync tasks for CPU/blocking work.
from fastapi import FastAPI, BackgroundTasks
import httpx
app = FastAPI()
async def fetch_url(url: str):
async with httpx.AsyncClient() as client:
r = await client.get(url)
return r.status_code
@app.post("/webhook")
def webhook(url: str, background_tasks: BackgroundTasks):
background_tasks.add_task(fetch_url, url)
return {"queued": url}Background vs Celery
Use BackgroundTasks for lightweight, non-critical work that is fine to lose on restart. For jobs that must survive crashes, retry, or scale across machines, use a real task queue (Celery, RQ, Dramatiq, Arq) with a broker like Redis or RabbitMQ.
# BackgroundTasks: in-process, no broker, lost if process crashes
@app.post("/quick")
def quick(tasks: BackgroundTasks):
tasks.add_task(small_job)
return {"ok": True}
# Celery/RQ/Dramatiq: separate workers, persistent, retryable
# from celery import Celery
# @celery.task
# def big_job(payload): ...
#
# @app.post("/heavy")
# def heavy(payload: dict):
# big_job.delay(payload)
# return {"queued": True}Email Sending Example
Email sending is the classic BackgroundTasks use case: it's slow and the user shouldn't wait. The response returns immediately while the email is dispatched after. For high volume, prefer a queue and an email-service SDK with retries.
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel, EmailStr
class Message(BaseModel):
to: EmailStr
subject: str
body: str
app = FastAPI()
def send_email(to: str, subject: str, body: str):
# smtplib or your provider's SDK
import smtplib
with smtplib.SMTP("localhost") as s:
s.sendmail("[email protected]", to, f"Subject: {subject}\n\n{body}")
@app.post("/contact")
def contact(msg: Message, tasks: BackgroundTasks):
tasks.add_task(send_email, msg.to, msg.subject, msg.body)
return {"status": "queued", "to": msg.to}WebSocket
WebSocket Basic
Use @app.websocket() to define a WebSocket endpoint. You must accept() the connection before sending/receiving. The function typically loops reading messages; receive_text/receive_bytes/receive_json all await data from the client.
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
while True:
data = await ws.receive_text()
await ws.send_text(f"echo: {data}")
# client (browser):
# const ws = new WebSocket("ws://localhost:8000/ws");
# ws.onmessage = (e) => console.log(e.data);Send JSON
send_json() and receive_json() handle serialization for you, exchanging JSON objects over the socket. For binary data use send_bytes/receive_bytes. Each send/receive is a separate frame on the WebSocket.
import json
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws")
async def ws_endpoint(ws: WebSocket):
await ws.accept()
while True:
msg = await ws.receive_json()
await ws.send_json({"reply": msg.get("text", "").upper()})
# send_text / send_bytes / send_json helpers are availableWebSocket Close
WebSocketDisconnect is raised when the client closes the connection — catch it to clean up. Call ws.close(code=1000) for a normal close; common codes are 1000 (normal), 1008 (policy violation), 1011 (server error).
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket("/ws")
async def ws_endpoint(ws: WebSocket):
await ws.accept()
try:
while True:
data = await ws.receive_text()
if data == "quit":
await ws.close(code=1000)
break
await ws.send_text(data)
except WebSocketDisconnect:
print("client disconnected")Connection Manager
A ConnectionManager tracks active sockets so you can broadcast to all connected clients (e.g. a chat room). Always remove the socket on disconnect to avoid sending to closed connections and to keep memory bounded.
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
class ConnectionManager:
def __init__(self):
self.active: list[WebSocket] = []
async def connect(self, ws: WebSocket):
await ws.accept()
self.active.append(ws)
def disconnect(self, ws: WebSocket):
self.active.remove(ws)
async def broadcast(self, message: str):
for ws in self.active:
await ws.send_text(message)
manager = ConnectionManager()
app = FastAPI()
@app.websocket("/chat")
async def chat(ws: WebSocket):
await manager.connect(ws)
try:
while True:
msg = await ws.receive_text()
await manager.broadcast(msg)
except WebSocketDisconnect:
manager.disconnect(ws)WebSocket Auth via Query Param
Browsers cannot set custom headers on WebSocket connections, so auth tokens are commonly passed as query parameters (or cookies). Validate before calling accept(), and close with code 1008 (policy violation) if unauthorized.
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
app = FastAPI()
async def authenticate(token: str) -> str:
if token != "secret":
raise HTTPException(status_code=401)
return "alice"
@app.websocket("/ws")
async def ws_endpoint(ws: WebSocket):
token = ws.query_params.get("token")
if not token:
await ws.close(code=1008)
return
user = await authenticate(token)
await ws.accept()
try:
while True:
data = await ws.receive_text()
await ws.send_text(f"{user}: {data}")
except WebSocketDisconnect:
passWebSocket with Dependencies
Dependencies can be used in WebSocket endpoints just like in HTTP routes, but only dependencies that read from the initial handshake (headers, query params, cookies) make sense — there is no body. Inject them via Depends() as usual.
from fastapi import FastAPI, WebSocket, Depends, Header
app = FastAPI()
def token_validator(token: str = Header(default="")):
# for WS, headers come from the initial HTTP handshake
return token == "valid"
@app.websocket("/ws")
async def ws_endpoint(ws: WebSocket, valid: bool = Depends(token_validator)):
if not valid:
await ws.close(code=1008)
return
await ws.accept()
while True:
msg = await ws.receive_text()
await ws.send_text(msg)Database (SQLAlchemy)
SQLAlchemy Setup
create_engine sets up the DB connection pool; SessionLocal is a factory for sessions; Base is the declarative base for models. For SQLite with FastAPI, set check_same_thread=False because requests may run in different threads.
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
SQLALCHEMY_DATABASE_URL = "sqlite:///./app.db"
# postgresql: "postgresql://user:pass@localhost:5432/dbname"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}, # sqlite only
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
Base = declarative_base()Session Dependency
A yield dependency is the canonical way to provide a DB session: each request gets a fresh session that is closed in the finally block. This guarantees cleanup even if the route raises an exception.
from fastapi import Depends
from sqlalchemy.orm import Session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/items")
def list_items(db: Session = Depends(get_db)):
return db.query(Item).all()
# each request gets its own session, closed automatically afterwardModel Definition
Define tables as SQLAlchemy ORM models subclassing Base. Use Alembic for schema migrations in production rather than create_all(). Columns can be indexed, nullable, and have defaults applied at the database or Python level.
from sqlalchemy import Column, Integer, String, Boolean
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True, nullable=False)
price = Column(Integer, default=0)
in_stock = Column(Boolean, default=True)
# create tables (for dev; use Alembic migrations in prod)
Base.metadata.create_all(bind=engine)Create Record
add() stages a new row, commit() writes it to the DB, and refresh() reloads the instance with server-generated values (like the auto-increment id). Wrap multiple writes in a transaction and commit once at the end.
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
app = FastAPI()
@app.post("/items", status_code=201)
def create_item(payload: ItemCreate, db: Session = Depends(get_db)):
item = Item(name=payload.name, price=payload.price)
db.add(item)
db.commit()
db.refresh(item) # load generated id
return itemQuery Records
db.query(Model) starts a query; .filter() applies WHERE conditions, .offset()/.limit() paginate, .first() returns one row or None, .all() returns a list. Use .filter_by(name=x) for simple equality conditions.
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
app = FastAPI()
@app.get("/items")
def list_items(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
return db.query(Item).offset(skip).limit(limit).all()
@app.get("/items/{item_id}")
def get_item(item_id: int, db: Session = Depends(get_db)):
item = db.query(Item).filter(Item.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="not found")
return itemUpdate & Delete
For partial updates, iterate model_dump(exclude_unset=True) so only provided fields change. delete() stages removal; commit() finalizes it. Return 204 No Content for DELETE on success.
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
app = FastAPI()
@app.put("/items/{item_id}")
def update_item(item_id: int, payload: ItemUpdate, db: Session = Depends(get_db)):
item = db.query(Item).filter(Item.id == item_id).first()
if not item:
raise HTTPException(status_code=404, detail="not found")
for k, v in payload.model_dump(exclude_unset=True).items():
setattr(item, k, v)
db.commit()
db.refresh(item)
return item
@app.delete("/items/{item_id}", status_code=204)
def delete_item(item_id: int, db: Session = Depends(get_db)):
item = db.query(Item).filter(Item.id == item_id).first()
if item:
db.delete(item)
db.commit()Database (Tortoise ORM)
Tortoise Setup
Tortoise ORM is async-native, pairing well with FastAPI. Init it at startup with db_url and the module(s) containing your models. generate_schemas() is fine for dev; use Aerich for migrations in production. Use the lifespan handler instead of on_event in new code.
from fastapi import FastAPI
from tortoise import Tortoise
app = FastAPI()
async def init():
await Tortoise.init(
db_url="sqlite://db.sqlite3",
modules={"models": ["app.models"]},
)
await Tortoise.generate_schemas()
@app.on_event("startup")
async def startup():
await init()
@app.on_event("shutdown")
async def shutdown():
await Tortoise.close_connections()Model Definition
Tortoise models subclass Model and declare fields via tortoise.fields. Field types map to DB column types. Set the table name in the inner Meta class. Relationships use ForeignKeyField, ManyToManyField, etc.
from tortoise import fields
from tortoise.models import Model
class Item(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
price = fields.FloatField(default=0.0)
created_at = fields.DatetimeField(auto_now_add=True)
class Meta:
table = "items"
def __str__(self):
return self.nameCreate Record
Item.create() is a coroutine that inserts and returns the new row. To return a Pydantic response, build a BaseModel with from_tortoise_orm() (via tortoise.contrib.pydantic) so Tortoise serializes async-loaded relations correctly.
from fastapi import FastAPI
from pydantic import BaseModel
from .models import Item
app = FastAPI()
class ItemIn(BaseModel):
name: str
price: float
@app.post("/items", response_model=ItemOut, status_code=201)
async def create_item(payload: ItemIn):
item = await Item.create(name=payload.name, price=payload.price)
return await ItemOut.from_tortoise_orm(item)Async Query
Tortoise queries are awaited: .all() returns a queryset, .filter() adds conditions, .get_or_none() fetches one row or None, .first() returns the first match. Avoid .get() unless you expect exactly one row (it raises if not found).
from fastapi import FastAPI, HTTPException
from .models import Item
app = FastAPI()
@app.get("/items")
async def list_items(skip: int = 0, limit: int = 10):
return await Item.all().offset(skip).limit(limit)
@app.get("/items/{item_id}")
async def get_item(item_id: int):
item = await Item.get_or_none(id=item_id)
if not item:
raise HTTPException(status_code=404, detail="not found")
return itemUpdate & Delete
Modify model attributes then await save() to update a row, or await Item.filter(id=...).update(name=x) for bulk updates. await item.delete() removes the row. Each method is a coroutine and must be awaited.
from fastapi import FastAPI, HTTPException
from .models import Item
app = FastAPI()
@app.put("/items/{item_id}")
async def update_item(item_id: int, name: str):
item = await Item.get_or_none(id=item_id)
if not item:
raise HTTPException(status_code=404, detail="not found")
item.name = name
await item.save()
return item
@app.delete("/items/{item_id}", status_code=204)
async def delete_item(item_id: int):
item = await Item.get_or_none(id=item_id)
if item:
await item.delete()Tortoise Pydantic Schemas
tortoise.contrib.pydantic generates Pydantic models directly from Tortoise models, so you don't maintain two schemas. Use pydantic_model_creator for a single object and pydantic_queryset_creator for a list response.
from tortoise.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator
from .models import Item
ItemOut = pydantic_model_creator(Item, name="ItemOut")
ItemList = pydantic_queryset_creator(Item)
@app.get("/items", response_model=ItemList)
async def list_items():
return await Item.all()
# auto-generates a Pydantic model from the Tortoise model,
# including types and docstrings.Testing (TestClient)
TestClient Basic
TestClient wraps your app and lets you make requests in-process (no real socket). It's based on httpx and works with pytest out of the box. Import the same app instance you run in production.
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"Hello": "World"}
# TestClient is based on httpx; no network is involved —
# it calls the ASGI app directly in-process.Test GET & POST
Pass json= for JSON bodies, params= for query params, headers= for headers, and files= for multipart uploads. Validation failures return 422 — assert on status_code and the structure of the error 'detail'.
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_create_item():
response = client.post(
"/items",
json={"name": "Apple", "price": 0.5},
)
assert response.status_code == 201
assert response.json()["name"] == "Apple"
def test_validation_error():
response = client.post("/items", json={"name": "Apple"}) # missing price
assert response.status_code == 422Pytest Fixture
A pytest fixture returning a TestClient keeps tests DRY and lets each test get a fresh client. For DB-backed tests, add a fixture that creates/drops tables or uses a transaction rollback per test to keep them isolated.
import pytest
from fastapi.testclient import TestClient
from main import app
@pytest.fixture
def client():
return TestClient(app)
def test_read(client):
r = client.get("/items")
assert r.status_code == 200
def test_create(client):
r = client.post("/items", json={"name": "X"})
assert r.status_code == 201
# run: pytest -vOverride Dependencies
app.dependency_overrides swaps a dependency (like the real DB session or auth) for a mock/fake during tests, without changing the routes. This is the canonical way to test endpoints in isolation from external services. Clear it after tests.
from fastapi.testclient import TestClient
from main import app, get_db
def get_test_db():
# return a test session or mock
yield FakeSession()
app.dependency_overrides[get_db] = get_test_db
client = TestClient(app)
def test_items():
r = client.get("/items")
assert r.status_code == 200
# restore afterwards:
app.dependency_overrides.clear()Test Authentication
OAuth2PasswordRequestForm expects form-encoded data (data=, not json=). Use the returned token in the Authorization: Bearer header for subsequent protected requests. To bypass auth entirely, override the get_current_user dependency instead.
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def get_token():
r = client.post(
"/token",
data={"username": "admin", "password": "secret"},
)
return r.json()["access_token"]
def test_protected():
token = get_token()
r = client.get(
"/me",
headers={"Authorization": f"Bearer {token}"},
)
assert r.status_code == 200
assert r.json()["username"] == "admin"Test WebSocket
TestClient.websocket_connect() opens a WebSocket within a context manager. Use send_text/receive_text (and the json/bytes variants) to exchange messages. Receiving on a closed connection raises WebSocketDisconnect.
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_websocket():
with client.websocket_connect("/ws") as ws:
ws.send_text("hello")
data = ws.receive_text()
assert data == "echo: hello"
def test_websocket_close():
with client.websocket_connect("/ws") as ws:
ws.send_text("quit")
# the server closes the connectionOpenAPI Docs
Swagger UI
FastAPI auto-generates an interactive Swagger UI at /docs from your routes, types, and docstrings. Set title/description/version on the app, and add a docstring or summary/description on each route to enrich the UI.
from fastapi import FastAPI
app = FastAPI(
title="My API",
description="Sample API with auto docs",
version="1.0.0",
)
@app.get("/items", summary="List items", description="Returns all items")
def list_items():
"""Get every item in the catalog."""
return [{"id": 1}]
# interactive docs at: http://localhost:8000/docsReDoc
ReDoc at /redoc is a read-only, documentation-focused alternative to Swagger. The raw OpenAPI schema lives at /openapi.json and can be fed to other tools. Disable docs with docs_url=None, redoc_url=None, openapi_url=None (e.g. in production).
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
return {"hello": "world"}
# ReDoc (alternative docs UI) at: http://localhost:8000/redoc
# raw OpenAPI JSON at: http://localhost:8000/openapi.json
# disable docs entirely:
# app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)Tags & Grouping
Tags group routes in the docs UI. Pre-declare tag metadata (name, description, externalDocs) in openapi_tags so the group appears even before any route uses it. Set deprecated=True to mark an endpoint as deprecated in the docs.
from fastapi import FastAPI
app = FastAPI(openapi_tags=[
{"name": "users", "description": "User management"},
{"name": "items", "description": "Item operations"},
])
@app.get("/users", tags=["users"])
def list_users(): ...
@app.post("/items", tags=["items"])
def create_item(): ...
@app.get("/items/{id}", tags=["items"], deprecated=True)
def get_item(id: int): ...Route Metadata
Decorators accept metadata that flows into OpenAPI: summary (short title), description (long text), response_description, tags, deprecated, and status_code. A docstring is used as the description if you don't pass one explicitly.
from fastapi import FastAPI, status
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
app = FastAPI()
@app.post(
"/items",
response_model=Item,
status_code=status.HTTP_201_CREATED,
summary="Create an item",
description="Creates a new item and returns it.",
tags=["items"],
response_description="The created item",
)
def create_item(item: Item):
"""Create endpoint docstring — also shown in docs."""
return itemField Examples
Field(examples=[...]) and Body(examples=[...]) add example payloads to the Swagger UI's 'Try it out' and example dropdown. Body examples can include multiple cases with summaries to show valid and invalid inputs.
from pydantic import BaseModel, Field
from fastapi import FastAPI, Body
class Item(BaseModel):
name: str = Field(examples=["Apple"])
price: float = Field(examples=[0.5], gt=0)
app = FastAPI()
@app.post("/items")
def create_item(item: Item):
return item
@app.post("/manual")
def manual(
item: Item = Body(
examples=[
{"summary": "valid", "value": {"name": "Apple", "price": 0.5}},
{"summary": "invalid", "value": {"name": "Apple", "price": -1}},
],
),
):
return itemCustom OpenAPI Schema
Override app.openapi to customize the generated schema — add a logo, extra metadata, or remove/transform operations. Cache the result in app.openapi_schema so it's only generated once. The schema is what /openapi.json serves.
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
app = FastAPI()
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
schema = get_openapi(
title="Custom API",
version="2.0.0",
routes=app.routes,
)
schema["info"]["x-logo"] = {"url": "https://example.com/logo.png"}
app.openapi_schema = schema
return schema
app.openapi = custom_openapiDeployment (Uvicorn / Gunicorn)
Uvicorn Production
For production, run Uvicorn behind a reverse proxy (Nginx, Caddy, a cloud load balancer). Use --proxy-headers so the app sees the client's real IP/host from X-Forwarded-For/X-Forwarded-Proto. Avoid --reload in production.
# Run directly with uvicorn (single worker)
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1
# Recommended: behind a reverse proxy (nginx) on port 8000
# Use --proxy-headers so X-Forwarded-* is trusted
uvicorn main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips='*'Gunicorn with Uvicorn Workers
Gunicorn manages multiple Uvicorn worker processes, giving you graceful reloads, restarts on crash, and pre-fork scaling. Use the UvicornWorker class. Set workers to (2 * CPU cores + 1) as a starting point, then benchmark.
# install the uvicorn worker class
pip install "uvicorn[standard]" gunicorn
# run 4 worker processes managed by gunicorn
gunicorn main:app \
-w 4 \
-k uvicorn.workers.UvicornWorker \
-b 0.0.0.0:8000 \
--timeout 120
# -w workers (commonly 2*CPU+1), -k worker classDocker Deployment
A slim Python base image keeps the image small. Install dependencies before copying code to leverage Docker's layer cache. Run with gunicorn+uvicorn workers inside the container, and expose port 8000 to the host or orchestrator.
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "main:app", "-w", "4", \
"-k", "uvicorn.workers.UvicornWorker", \
"-b", "0.0.0.0:8000"]
# build & run:
# docker build -t myapi .
# docker run -p 8000:8000 myapiEnvironment Configuration
pydantic-settings (formerly pydantic.BaseSettings) loads config from environment variables and .env files with type validation. This lets the same image run in different environments (dev/staging/prod) just by changing env vars.
import os
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str = "sqlite:///./app.db"
secret_key: str = "change-me"
debug: bool = False
allowed_origins: list[str] = ["http://localhost:3000"]
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = Settings()
# .env file:
# DATABASE_URL=postgresql://user:pass@db:5432/app
# SECRET_KEY=long-random-string
# DEBUG=falseNginx Reverse Proxy
Nginx terminates HTTP(S) and forwards to Uvicorn on localhost:8000. Set the X-Forwarded-* headers (and use --proxy-headers on Uvicorn) so the app sees the real client IP and scheme. The Upgrade/Connection lines enable WebSocket support.
# /etc/nginx/conf.d/api.conf
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
}
}Scaling & Concurrency
Scale two ways: more Uvicorn worker processes per machine (CPU-bound), and more machines behind a load balancer (horizontal). For async apps, the event loop handles many concurrent I/O requests per worker. Add a /health endpoint so orchestrators (k8s, ECS) can probe liveness.
# Workers = process count (vertical scaling); pick ~2*CPU
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker
# Inside async app: concurrency is cooperative via the event loop.
# CPU-bound work blocks it — offload to a threadpool or task queue.
# Horizontal scaling: run several containers behind a load balancer.
# Use a shared store (Redis) for session state and rate limits.
# Health check endpoint for the balancer:
@app.get("/health")
def health():
return {"status": "ok"}Advanced Topics
Lifespan Events
The lifespan context manager replaces the deprecated on_event('startup'/'shutdown') handlers. Code before yield runs at startup; code after yield runs at shutdown, even if startup fails. Use it to init DB connections, ML models, and caches.
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# runs at startup
print("starting up")
app.state.cache = {}
yield
# runs at shutdown
print("shutting down")
await app.state.cache.clear()
app = FastAPI(lifespan=lifespan)
# the old @app.on_event("startup") / "shutdown" are deprecated
# in favor of the lifespan handler above.Sub Applications (Mount)
app.mount() attaches a sub-application under a path prefix, useful for versioned APIs or combining multiple apps. Each sub-app has its own routes and OpenAPI schema; the parent's docs don't include the child's routes by default.
from fastapi import FastAPI
app = FastAPI()
api_v1 = FastAPI()
api_v2 = FastAPI()
@api_v1.get("/items")
def v1_items():
return {"version": 1}
@api_v2.get("/items")
def v2_items():
return {"version": 2}
app.mount("/v1", api_v1)
app.mount("/v2", api_v2)
# GET /v1/items -> {"version": 1}
# GET /v2/items -> {"version": 2}APIRouter
APIRouter groups related routes into a module with shared prefix, tags, dependencies, and responses. Include it via app.include_router(router) (optionally with its own prefix or tags). This is the standard pattern for structuring non-trivial apps.
from fastapi import APIRouter, Depends
router = APIRouter(
prefix="/items",
tags=["items"],
dependencies=[Depends(verify_token)],
responses={404: {"description": "Not found"}},
)
@router.get("/")
def list_items():
return []
@router.get("/{item_id}")
def get_item(item_id: int):
return {"id": item_id}
# in main.py: app.include_router(router)Static Files
StaticFiles serves a directory of static assets (images, CSS, JS) at a URL prefix. Mount it once per directory. For SPA frontends, mount a final catch-all route pointing at index.html. Static files bypass the routing system for speed.
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
# serve files from ./static at /static
app.mount("/static", StaticFiles(directory="static"), name="static")
# /static/logo.png -> ./static/logo.png
# /static/css/app.css -> ./static/css/app.cssStreaming Response
StreamingResponse sends a response generated in chunks (a generator/iterator) instead of building the whole body in memory — ideal for large CSV exports, file streaming, or SSE. Set media_type appropriately so clients handle the stream correctly.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import csv, io
app = FastAPI()
def iter_csv():
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["id", "name"])
for i in range(1000):
writer.writerow([i, f"name-{i}"])
yield output.getvalue()
output.seek(0)
output.truncate(0)
@app.get("/export.csv")
def export():
return StreamingResponse(
iter_csv(),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=export.csv"},
)Custom Route Class
Subclassing APIRoute lets you wrap every route handler with custom logic (logging, tracing, timing) without touching each endpoint. Set app.router.route_class or pass route_class to APIRouter. The handler runs for each matching request.
from fastapi import FastAPI, Request, Response
from fastapi.routing import APIRoute
from typing import Callable
class LoggingRoute(APIRoute):
def get_route_handler(self) -> Callable:
original = super().get_route_handler()
async def handler(request: Request) -> Response:
print(f"-> {request.method} {request.url.path}")
response = await original(request)
print(f"<- {response.status_code}")
return response
return handler
app = FastAPI()
app.router.route_class = LoggingRoute
@app.get("/items")
def items():
return {"items": []}Snippets de FastAPI relacionados
Copy-paste ready code for common tasks.
Path Parameters
Capture typed path segments with validation.
Query Parameters
Parse query strings with defaults and validation.
Pydantic Request Body
Validate JSON bodies with Pydantic models.
Dependency Injection
Share logic via Depends and yield-based dependencies.
Response Model
Shape and filter responses with response_model.
JWT Auth
Issue and verify JSON Web Tokens with OAuth2.
Async and Await
Define async handlers and run blocking work in a thread.
Middleware
Add CORS, timing, and custom middleware.
Was this helpful?