Skip to content

FastAPI endpoints, validation, dependencies, security, testing, and deployment reference.

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.

Create a virtual environment and install FastAPI with its standard development server dependencies:

Terminal window
python -m venv .venv
# macOS/Linux
source .venv/bin/activate
# PowerShell
.\.venv\Scripts\Activate.ps1
pip install "fastapi[standard]"

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:

Terminal window
fastapi dev main.py

Useful 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.json
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 None

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.

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 = True
from fastapi import FastAPI, status
app = FastAPI()
@app.post("/items", status_code=status.HTTP_201_CREATED)
async def create_item(item: ItemCreate):
return item

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.

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 item
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:

Terminal window
pip install python-multipart
app/
__init__.py
main.py
dependencies.py
models.py
schemas.py
routers/
__init__.py
items.py
tests/
test_items.py
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)
Terminal window
fastapi dev app/main.py

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 page

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}
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()
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 item

Keep database table models separate from request and response schema models. Apply schema changes through migrations rather than recreating tables in production.

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 user

This demonstrates dependency wiring only. Real applications should hash passwords, validate signed or stored tokens, set expiration rules, and load users from a trusted store.

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}

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.

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.

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)

Install testing dependencies:

Terminal window
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"}
Terminal window
pytest
pytest -q

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()

Keep secrets and environment-specific values out of committed source:

import os
DATABASE_URL = os.environ["DATABASE_URL"]
SECRET_KEY = os.environ["SECRET_KEY"]
Terminal window
fastapi run app/main.py

Development 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.

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"]
ProblemBetter approach
Returning passwords or private fieldsUse a dedicated response_model
Blocking network or disk work inside async defUse async libraries or run synchronous work appropriately
Putting every route in main.pySplit endpoints into APIRouter modules
Creating a database session without cleanupProvide it through a yield dependency
Trusting tokens without verificationValidate credentials and authorization server-side
Using in-process background tasks for critical workUse a durable task queue
Terminal window
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 tests
from 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