FastAPI Complete Guide: Build High-Performance Python APIs in 2026
FastAPI is the fastest-growing Python web framework. It combines the simplicity of Flask with the performance of Node.js, adds automatic data validation through Pydantic, and generates interactive API documentation out of the box. Built on top of Starlette for the web layer and Pydantic for the data layer, FastAPI lets you write production-grade APIs with Python type hints — and it catches bugs before they reach production.
This guide takes you from your first endpoint through production deployment, with working code at every step. You will learn path operations, Pydantic models, dependency injection, JWT authentication, database integration, background tasks, testing, and performance tuning.
Table of Contents
1. What Is FastAPI?
FastAPI is a modern, high-performance web framework for building APIs with Python 3.7+ based on standard Python type hints. Created by Sebastian Ramirez and first released in 2018, it has become one of the most starred Python web frameworks on GitHub.
Key features that set FastAPI apart:
- Speed — On par with Node.js and Go, thanks to Starlette and async support
- Automatic validation — Request data is validated using Pydantic models and Python type hints
- Auto-generated docs — Interactive Swagger UI at
/docsand ReDoc at/redoc - Dependency injection — Built-in DI system for clean, testable code
- Async-first — Native
async/awaitsupport without third-party extensions - Standards-based — Built on OpenAPI 3.1 and JSON Schema
- Editor support — Excellent autocomplete and type checking in VS Code, PyCharm, and other IDEs
| Feature | FastAPI | Flask | Django REST |
|---|---|---|---|
| Async support | Native | Limited | Partial |
| Auto docs | Built-in | Extensions | Built-in |
| Validation | Pydantic (auto) | Manual | Serializers |
| Type hints | Core feature | Optional | Optional |
| Performance | Very high | Moderate | Moderate |
| Dependency injection | Built-in | No | No |
2. Installation & Setup
Install FastAPI and Uvicorn (the ASGI server) in a virtual environment:
# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
# venv\Scripts\activate # Windows
# Install FastAPI with all optional dependencies
pip install "fastapi[standard]"
# Or install just the essentials
pip install fastapi uvicorn[standard]
A clean project structure for a FastAPI application:
my-api/
app/
__init__.py
main.py # FastAPI app instance and startup
config.py # Settings via pydantic-settings
models.py # SQLAlchemy models
schemas.py # Pydantic request/response schemas
database.py # DB engine and session
dependencies.py # Shared dependencies (auth, DB session)
routers/
__init__.py
users.py # /users endpoints
items.py # /items endpoints
tests/
test_users.py
test_items.py
requirements.txt
Dockerfile
Create your first app in app/main.py:
from fastapi import FastAPI
# Create the FastAPI application instance
app = FastAPI(
title="My API",
description="A production-ready FastAPI application",
version="1.0.0",
)
@app.get("/")
async def root():
return {"message": "Hello, FastAPI!"}
Run the development server:
# Start with auto-reload for development
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Visit http://localhost:8000/docs for interactive Swagger UI
# Visit http://localhost:8000/redoc for ReDoc documentation
3. Path Operations
FastAPI uses decorators for each HTTP method. Path parameters are extracted from the URL, and query parameters are taken from function arguments that are not part of the path:
from fastapi import FastAPI, Query, Path
app = FastAPI()
# GET with path parameters
@app.get("/users/{user_id}")
async def get_user(
user_id: int = Path(..., title="User ID", ge=1)
):
return {"user_id": user_id}
# GET with query parameters
@app.get("/items/")
async def list_items(
skip: int = Query(0, ge=0, description="Items to skip"),
limit: int = Query(20, ge=1, le=100, description="Max items"),
search: str | None = Query(None, min_length=1, max_length=100),
):
# skip, limit, and search come from ?skip=0&limit=20&search=foo
return {"skip": skip, "limit": limit, "search": search}
# POST to create a resource
@app.post("/users/", status_code=201)
async def create_user(name: str, email: str):
return {"name": name, "email": email}
# PUT to update a resource
@app.put("/users/{user_id}")
async def update_user(user_id: int, name: str):
return {"user_id": user_id, "name": name}
# DELETE a resource
@app.delete("/users/{user_id}", status_code=204)
async def delete_user(user_id: int):
return None
The Path() and Query() functions add validation, metadata, and documentation. The first argument (...) means the parameter is required. Use a default value to make it optional.
4. Request Body & Pydantic Models
Pydantic models are the backbone of FastAPI's data validation. Define a model with type hints and FastAPI automatically validates incoming JSON, returns clear error messages, and generates schema documentation:
from pydantic import BaseModel, Field, EmailStr
from datetime import datetime
# Request model for creating a user
class UserCreate(BaseModel):
name: str = Field(
..., min_length=1, max_length=100,
examples=["Alice Johnson"]
)
email: EmailStr # Validates email format automatically
age: int = Field(..., ge=13, le=150, description="User age")
bio: str | None = Field(None, max_length=500)
# Nested models for complex data
class Address(BaseModel):
street: str
city: str
country: str = "US"
zip_code: str = Field(..., pattern=r"^\d{5}(-\d{4})?$")
class UserWithAddress(BaseModel):
name: str
email: EmailStr
addresses: list[Address] = [] # List of nested objects
# Use the model in an endpoint
@app.post("/users/", status_code=201)
async def create_user(user: UserCreate):
# user is already validated - if invalid, FastAPI returns 422
return {"id": 1, **user.model_dump()}
When a client sends invalid data, FastAPI automatically returns a 422 response with details about every validation error:
{
"detail": [
{
"type": "value_error",
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"input": "not-an-email"
}
]
}
Custom validators let you add business logic to your models:
from pydantic import BaseModel, field_validator, model_validator
class OrderCreate(BaseModel):
product_id: int
quantity: int = Field(..., ge=1, le=1000)
discount_code: str | None = None
@field_validator("discount_code")
@classmethod
def validate_discount(cls, v):
if v and not v.startswith("PROMO-"):
raise ValueError("Discount codes must start with PROMO-")
return v.upper() if v else v
@model_validator(mode="after")
def check_order(self):
if self.quantity > 100 and not self.discount_code:
raise ValueError("Bulk orders over 100 need a discount code")
return self
5. Response Models
Use response_model to control what data is sent back to the client. This filters out internal fields like passwords and adds documentation to the OpenAPI schema:
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
name: str
email: EmailStr
password: str # Sent by client, but never returned
class UserResponse(BaseModel):
id: int
name: str
email: EmailStr
is_active: bool = True
model_config = {"from_attributes": True} # Read from ORM objects
# response_model filters out the password field
@app.post("/users/", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
# Even if the db_user object has a password field,
# response_model ensures it is never sent to the client
db_user = save_to_db(user)
return db_user
# Return a list of items
@app.get("/users/", response_model=list[UserResponse])
async def list_users():
return get_all_users()
# Custom responses for different status codes
from fastapi.responses import JSONResponse
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
user = find_user(user_id)
if not user:
return JSONResponse(
status_code=404,
content={"detail": "User not found"}
)
return user
6. Dependency Injection
FastAPI's dependency injection system is one of its most powerful features. Use Depends() to inject shared logic — database sessions, authentication, pagination, rate limiting — into any endpoint:
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
app = FastAPI()
# Database session dependency
def get_db():
db = SessionLocal()
try:
yield db # yield makes this a generator dependency
finally:
db.close() # Always close, even on errors
# Pagination dependency (reusable across endpoints)
class Pagination:
def __init__(
self,
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
):
self.skip = skip
self.limit = limit
# Use dependencies in endpoints
@app.get("/users/")
async def list_users(
db: Session = Depends(get_db),
pagination: Pagination = Depends(),
):
users = db.query(User).offset(pagination.skip).limit(pagination.limit).all()
return users
# Sub-dependencies: dependencies can depend on other dependencies
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db),
):
user = decode_token_and_find_user(token, db)
if not user:
raise HTTPException(status_code=401, detail="Invalid token")
return user
def get_admin_user(
current_user: User = Depends(get_current_user),
):
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Admin required")
return current_user
@app.delete("/users/{user_id}")
async def delete_user(
user_id: int,
admin: User = Depends(get_admin_user), # Chain of deps
db: Session = Depends(get_db),
):
# Only admins reach this code
db.query(User).filter(User.id == user_id).delete()
db.commit()
7. Authentication with JWT
FastAPI provides built-in OAuth2 support. Here is a complete JWT authentication flow with password hashing:
from datetime import datetime, timedelta
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# Configuration
SECRET_KEY = "your-secret-key-from-env" # Use os.environ in production
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Password hashing with bcrypt
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# OAuth2 token URL (also creates the login button in Swagger UI)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None
# Hash and verify passwords
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
# Create JWT tokens
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
# Dependency to get current user from token
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_user_from_db(username)
if user is None:
raise credentials_exception
return user
# Login endpoint
@app.post("/token", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=401,
detail="Incorrect username or password",
)
access_token = create_access_token(
data={"sub": user.username},
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
)
return {"access_token": access_token, "token_type": "bearer"}
# Protected endpoint
@app.get("/users/me")
async def read_users_me(current_user = Depends(get_current_user)):
return current_user
8. Database Integration
FastAPI works with any database. The most common setup is SQLAlchemy with a session dependency. Here is the full pattern:
# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
SQLALCHEMY_DATABASE_URL = "postgresql://user:pass@localhost/mydb"
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
pass
# app/models.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
name = Column(String(100), nullable=False)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
# Relationship: one user has many items
items = relationship("Item", back_populates="owner")
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
description = Column(String)
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", back_populates="items")
# app/routers/users.py - CRUD endpoints
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from .. import models, schemas
from ..dependencies import get_db
router = APIRouter(prefix="/users", tags=["users"])
@router.post("/", response_model=schemas.UserResponse, status_code=201)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
# Check for existing email
existing = db.query(models.User).filter(
models.User.email == user.email
).first()
if existing:
raise HTTPException(status_code=400, detail="Email already registered")
db_user = models.User(
email=user.email,
name=user.name,
hashed_password=get_password_hash(user.password),
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
Async database access with SQLAlchemy 2.0:
# Async engine with asyncpg
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
engine = create_async_engine(
"postgresql+asyncpg://user:pass@localhost/mydb",
pool_size=20,
max_overflow=10,
)
AsyncSession = async_sessionmaker(engine, expire_on_commit=False)
async def get_db():
async with AsyncSession() as session:
yield session
@router.get("/{user_id}", response_model=schemas.UserResponse)
async def get_user(user_id: int, db = Depends(get_db)):
result = await db.execute(
select(models.User).where(models.User.id == user_id)
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
9. Middleware & CORS
Middleware runs before and after every request. Use it for logging, timing, authentication checks, and CORS configuration:
import time
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# CORS middleware - allow your frontend to call the API
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000", # React dev server
"https://myapp.example.com", # Production frontend
],
allow_credentials=True,
allow_methods=["*"], # Allow all HTTP methods
allow_headers=["*"], # Allow all headers
)
# Custom middleware to log request duration
@app.middleware("http")
async def add_timing_header(request: Request, call_next):
start_time = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start_time
response.headers["X-Response-Time"] = f"{duration:.4f}s"
return response
# Trusted host middleware (prevent host header attacks)
from fastapi.middleware.trustedhost import TrustedHostMiddleware
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["myapp.example.com", "localhost"],
)
10. Background Tasks
FastAPI's BackgroundTasks lets you run code after the response is sent — perfect for sending emails, processing uploads, or updating caches without making the client wait:
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
# Background task function
def send_welcome_email(email: str, name: str):
# This runs after the response is sent
print(f"Sending welcome email to {email}")
# smtp.send(to=email, subject="Welcome!", body=f"Hi {name}...")
def write_audit_log(user_id: int, action: str):
# Log the action to a file or database
with open("audit.log", "a") as f:
f.write(f"{user_id}: {action}\n")
@app.post("/users/", status_code=201)
async def create_user(
name: str,
email: str,
background_tasks: BackgroundTasks,
):
user = save_user_to_db(name, email)
# Queue multiple background tasks
background_tasks.add_task(send_welcome_email, email, name)
background_tasks.add_task(write_audit_log, user.id, "user_created")
# Response is sent immediately, tasks run after
return {"id": user.id, "name": name}
For heavy or long-running tasks, use a proper task queue like Celery or arq instead of BackgroundTasks.
11. Testing
FastAPI provides a TestClient built on httpx that lets you test endpoints without starting a server:
# tests/test_users.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.database import Base, engine
from app.dependencies import get_db
# Override the database dependency with a test database
SQLALCHEMY_TEST_URL = "sqlite:///./test.db"
test_engine = create_engine(SQLALCHEMY_TEST_URL)
TestSession = sessionmaker(bind=test_engine)
def override_get_db():
db = TestSession()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
# Create test client
client = TestClient(app)
@pytest.fixture(autouse=True)
def setup_db():
Base.metadata.create_all(bind=test_engine)
yield
Base.metadata.drop_all(bind=test_engine)
def test_create_user():
response = client.post("/users/", json={
"name": "Alice",
"email": "alice@example.com",
"password": "securepass123",
})
assert response.status_code == 201
data = response.json()
assert data["name"] == "Alice"
assert data["email"] == "alice@example.com"
assert "password" not in data # response_model filters it
def test_create_user_duplicate_email():
client.post("/users/", json={
"name": "Alice",
"email": "alice@example.com",
"password": "pass1",
})
response = client.post("/users/", json={
"name": "Bob",
"email": "alice@example.com",
"password": "pass2",
})
assert response.status_code == 400
def test_get_user_not_found():
response = client.get("/users/999")
assert response.status_code == 404
Async testing with pytest-asyncio:
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
@pytest.mark.anyio
async def test_async_root():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
response = await ac.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, FastAPI!"}
12. Deployment
For production, run Uvicorn behind Gunicorn for process management, and place a reverse proxy in front for SSL and static files:
# Production command: Gunicorn with Uvicorn workers
gunicorn app.main:app \
-w 4 \
-k uvicorn.workers.UvicornWorker \
-b 0.0.0.0:8000 \
--access-logfile - \
--error-logfile -
Dockerfile for containerized deployment:
FROM python:3.12-slim
WORKDIR /app
# Install dependencies first (cached layer)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY ./app ./app
# Create non-root user
RUN adduser --disabled-password --no-create-home appuser
USER appuser
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
# Start with Gunicorn + Uvicorn workers
CMD ["gunicorn", "app.main:app", \
"-w", "4", \
"-k", "uvicorn.workers.UvicornWorker", \
"-b", "0.0.0.0:8000"]
Production settings checklist:
- Set
debug=Falseand remove--reload - Use environment variables for secrets (never hardcode
SECRET_KEY) - Set workers to
2 * CPU_CORES + 1(or2 * CPU_CORESfor I/O-bound apps) - Enable structured JSON logging for log aggregation
- Add a
/healthendpoint for load balancer health checks - Configure connection pooling for your database
- Place behind Nginx or Traefik for SSL termination
13. Performance Tips
FastAPI is already fast, but these practices will squeeze out the best performance:
Use async for I/O-bound operations. If your endpoint calls a database, external API, or file system, make it async:
# Good: async for I/O-bound work
@app.get("/data")
async def get_data():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/data")
return response.json()
# Good: sync for CPU-bound work (FastAPI runs it in a thread pool)
@app.get("/compute")
def compute_heavy():
result = expensive_calculation() # Blocking but in thread pool
return {"result": result}
Connection pooling is critical for database performance:
# Configure SQLAlchemy pool for production
engine = create_engine(
DATABASE_URL,
pool_size=20, # Max persistent connections
max_overflow=10, # Extra connections when pool is full
pool_pre_ping=True, # Test connections before use
pool_recycle=3600, # Recycle connections after 1 hour
)
Response caching with a simple in-memory cache:
from functools import lru_cache
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
# Use orjson for faster JSON serialization
app = FastAPI(default_response_class=ORJSONResponse)
# Cache configuration object (loaded once)
@lru_cache
def get_settings():
return Settings() # Read from env vars once, cache forever
# For HTTP caching, set Cache-Control headers
@app.get("/public-data")
async def public_data():
data = await fetch_public_data()
return ORJSONResponse(
content=data,
headers={"Cache-Control": "public, max-age=300"},
)
Additional tips:
- Use
orjsonfor 2–5x faster JSON serialization (pip install orjson) - Use
response_model_exclude_unset=Trueto skip null fields in responses - Profile with
py-spyto find actual bottlenecks before optimizing - Use Redis for caching frequent queries and rate limiting
- Avoid global state — use dependency injection for shared resources
Frequently Asked Questions
Is FastAPI faster than Flask and Django?
Yes, FastAPI is significantly faster than both Flask and Django for API workloads. In benchmarks, FastAPI handles 2–3x more requests per second than Flask because it runs on ASGI (Uvicorn/Starlette) with native async support, while Flask and Django use synchronous WSGI. FastAPI's performance is comparable to Node.js and Go frameworks. The speed difference is most pronounced for I/O-bound workloads like database queries and external API calls, where async concurrency allows FastAPI to handle many requests simultaneously instead of blocking on each one.
How does FastAPI compare to Flask?
FastAPI and Flask serve similar purposes but differ in philosophy and features. FastAPI provides automatic request validation via Pydantic, auto-generated OpenAPI documentation, native async/await support, and dependency injection out of the box. Flask is a minimal micro-framework that requires you to add validation, documentation, and async support through extensions. FastAPI has better performance due to its ASGI foundation. Flask has a larger ecosystem of extensions and more community resources due to its longer history. Choose FastAPI for new API projects, especially if you need automatic docs and type safety. Choose Flask if you need server-side HTML rendering with Jinja2 or have an existing Flask codebase.
Can I use FastAPI with a synchronous database like SQLAlchemy?
Yes, FastAPI works with synchronous SQLAlchemy. Define your database functions as regular (non-async) functions and FastAPI will automatically run them in a thread pool so they do not block the event loop. Alternatively, use SQLAlchemy 2.0's async engine with asyncpg or aiosqlite for true async database access. For most applications, synchronous SQLAlchemy with FastAPI's thread pool is simpler and performs well. Switch to async SQLAlchemy only if you need maximum concurrency for database-heavy workloads.
How do I deploy FastAPI to production?
Deploy FastAPI with Uvicorn as the ASGI server, managed by Gunicorn for process management: run gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker with 2–4 workers per CPU core. Place a reverse proxy like Nginx or Traefik in front to handle SSL, static files, and load balancing. For containerized deployments, use the official Python slim image in Docker, install dependencies, and expose the Uvicorn port. Set environment variables for secrets and database URLs, enable structured JSON logging, and add health check endpoints.
How do I handle authentication in FastAPI?
FastAPI has built-in support for OAuth2 with the OAuth2PasswordBearer security scheme. The standard approach is JWT authentication: create a /token endpoint that validates credentials and returns a signed JWT, then use a dependency function to decode and verify the token on protected routes. Hash passwords with bcrypt via the passlib library. FastAPI's dependency injection makes it easy to create reusable auth dependencies like get_current_user that extract the user from the token. For OAuth2 with third-party providers, use authlib or httpx-oauth. FastAPI's automatic OpenAPI docs will show a login button for testing authenticated endpoints.
Conclusion
FastAPI gives you the speed of Go and Node.js with the readability and ecosystem of Python. Its combination of Pydantic validation, automatic OpenAPI docs, dependency injection, and native async support makes it the strongest choice for building modern Python APIs. Start with a single file to learn the patterns, then scale to a full project structure with routers, models, and dependencies as your application grows.
The patterns covered in this guide — type-safe models, injectable dependencies, token-based auth, and async database access — will carry you from prototype to production. FastAPI's documentation is excellent, and its growing community means answers to most questions are a search away.