Building REST APIs with FastAPI — A Complete Guide
A complete, production-focused walkthrough of building REST APIs with FastAPI — Pydantic models, dependency injection, async endpoints, SQLAlchemy and Docker.
Introduction
FastAPI has quietly become the default choice for new Python backends. It's fast (on par with Node and Go for I/O-bound work), it generates OpenAPI docs for free, and its dependency injection system makes testing genuinely pleasant. This guide builds a production-shaped REST API from an empty folder to a Dockerized service backed by Postgres.
If you're coming from Spring Boot, the mental model maps cleanly — see the Java equivalent at Building REST APIs with Spring Boot for a side-by-side comparison.
Key takeaways
- FastAPI uses Python type hints as the contract for validation, serialization and OpenAPI docs.
- Pydantic v2 is the equivalent of Spring's DTO + Jakarta Validation, but ~10× faster.
- Dependency injection is built in — no third-party container required.
- Async endpoints are a real performance win for I/O-bound workloads; CPU-bound work belongs in a worker.
- A clean project structure (routers → services → repositories) scales from one developer to a team.
Project structure
app/
main.py # FastAPI() + router registration
api/
v1/
users.py # router
items.py
core/
config.py # pydantic-settings
security.py
db/
base.py # SQLAlchemy Base + engine
session.py
models/ # SQLAlchemy ORM models
schemas/ # Pydantic request/response models
services/ # business logic
repositories/ # data access
Package-by-feature works just as well — pick one and enforce it.
Step 1 — Bootstrap the app
pip install "fastapi[standard]" "sqlalchemy[asyncio]" asyncpg pydantic-settings
```python
# app/main.py
from fastapi import FastAPI
from app.api.v1 import users, itemsapp = FastAPI(title="MasterLab API", version="1.0.0") app.include_router(users.router, prefix="/api/v1/users", tags=["users"]) app.include_router(items.router, prefix="/api/v1/items", tags=["items"]) ```
fastapi dev app/main.py
Open http://localhost:8000/docs — Swagger UI is generated from your type hints, with no annotations or YAML.
Step 2 — Pydantic models (your DTOs)
```python
# app/schemas/user.py
from pydantic import BaseModel, EmailStr, Field
from datetime import datetimeclass UserCreate(BaseModel): email: EmailStr full_name: str = Field(min_length=1, max_length=120) password: str = Field(min_length=8, max_length=128)
class UserOut(BaseModel): id: int email: EmailStr full_name: str created_at: datetime
model_config = {"from_attributes": True} ```
Validation is automatic — a malformed request returns a structured 422 with field-level errors.
Step 3 — Async SQLAlchemy
```python
# app/db/session.py
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engineengine = create_async_engine(settings.DATABASE_URL, pool_size=10, pool_pre_ping=True) SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_session(): async with SessionLocal() as session: yield session ```
```python
# app/models/user.py
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase
from datetime import datetimeclass Base(DeclarativeBase): ...
class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) email: Mapped[str] = mapped_column(unique=True, index=True) full_name: Mapped[str] password_hash: Mapped[str] created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) ```
Step 4 — Dependency injection
```python
# app/api/v1/users.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_session
from app.schemas.user import UserCreate, UserOut
from app.services.user_service import UserServicerouter = APIRouter()
def get_user_service(db: AsyncSession = Depends(get_session)) -> UserService: return UserService(db)
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED) async def create_user( payload: UserCreate, svc: UserService = Depends(get_user_service), ): user = await svc.create(payload) if user is None: raise HTTPException(409, "Email already registered") return user ```
The Depends() graph is constructed per-request, so testing means overriding one function.
Step 5 — Error handling
```python
from fastapi import Request
from fastapi.responses import JSONResponseclass DomainError(Exception): def __init__(self, code: str, message: str, status: int = 400): self.code, self.message, self.status = code, message, status
@app.exception_handler(DomainError) async def domain_error_handler(_: Request, exc: DomainError): return JSONResponse(status_code=exc.status, content={"code": exc.code, "message": exc.message}) ```
Step 6 — Dockerize
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", \
"-w", "4", "-b", "0.0.0.0:8000"]
Production best practices
- Use Gunicorn + UvicornWorker in production, not bare
uvicorn --reload. - Pin Pydantic, FastAPI and SQLAlchemy in
requirements.txt— minor versions matter. - Put all settings in pydantic-settings and load from env vars, never from disk.
- Return explicit response_model on every route — no leaked password hashes.
- Add a
/healthzand/readyzendpoint for Kubernetes probes.
Common mistakes
- Mixing
defandasync defendpoints carelessly — sync endpoints run on a threadpool and silently throttle concurrency under load. - Calling
requests(sync) from an async endpoint — usehttpx.AsyncClient. - Forgetting
expire_on_commit=False— accessing ORM attributes after commit triggers a lazy load and crashes in async code.
Tools used in this tutorial
The setup below uses the following tools. Versions matter less than the role each one plays, so swap freely as long as you keep the responsibilities the same.
- Python 3.12+
- FastAPI
- Uvicorn / Gunicorn
- Pydantic v2
- SQLAlchemy 2.x
- Docker
- Kubernetes
Real-world production context
In production, a Python backend like this rarely runs as a single uvicorn process on a laptop. Teams build the app into a container image, push it to a registry, and run it behind a managed load balancer with autoscaling, health checks and centralized logging. Async FastAPI services typically use multiple Uvicorn workers managed by Gunicorn, a managed Postgres for state, Redis for cache and rate-limit counters, and an object store for uploads. The reason cloud hosting becomes a hard requirement is operational: zero-downtime rollouts, log aggregation, metrics, and TLS at the edge are not optional once real users depend on the API.
Recommended tools & deployment options
Once the tutorial works on your machine, the next question is *where do I run this for real?* These are the platforms most Python backend teams reach for:
- DigitalOcean — the simplest path from a working FastAPI container to a public URL. App Platform deploys directly from a Dockerfile, managed Postgres and Redis are one click away, and pricing is predictable. A common way to deploy the setup in this tutorial is using a cloud provider like DigitalOcean when you want to ship quickly without learning a full cloud SDK.
- AWS — the default for enterprise workloads. ECS Fargate or EKS run containers without you managing servers, RDS handles Postgres, and CloudWatch covers logs and metrics.
- Docker — the packaging format every modern deploy target understands. Build once, run the same image locally, in CI and in production.
- Kubernetes (managed: EKS, DOKS, GKE) — the right choice once you have more than a handful of services, need rolling updates, autoscaling and policy-driven networking.
A VPS or managed cloud service is required to run this architecture end-to-end — uvicorn --reload is for development, not for serving traffic.
FAQ
Is FastAPI production-ready? Yes — Netflix, Microsoft and Uber publicly run it at scale.
FastAPI vs Flask? Flask is sync-first and has no built-in validation. FastAPI gives you async, validation and docs for free. New projects should default to FastAPI.
Do I need async everywhere? No. Use async for I/O-bound endpoints (DB, HTTP, Redis). Pure CPU work belongs in a Celery/RQ worker.
Next steps & related tutorials
Keep the momentum going with the next tutorial in this learning path:
Architecture
REST API — Layered Backend
TL;DR
Key takeaways
- Understand the core concepts behind Building REST APIs with FastAPI — A Complete Guide in a production context.
- Apply the patterns to real Python & FastAPI systems, not just toy examples.
- Recognize the trade-offs, failure modes, and operational concerns before adopting them.
- Get a clear path to the next step — related tutorials, tools, and reference architectures.
Avoid these
Common mistakes
1. Copy-pasting code without understanding the trade-offs
It's tempting to ship a snippet from a blog post into production, but Python & FastAPI patterns only work when the failure modes are understood. Always reason about timeouts, retries, and consistency.
2. Skipping observability from day one
Structured logs, metrics, and traces are not optional. Wire them in before you ship — debugging Python & FastAPI systems without them is painful and expensive.
3. Optimizing too early
Premature caching, sharding, or microservice extraction adds operational cost. Validate the bottleneck with real measurements first.
4. Ignoring security defaults
Secrets in env files, open management ports, missing RBAC — these are the most common production incidents. Treat security as part of the definition of done.
Ship it safely
Production best practices
Apply these before promoting Building REST APIs with FastAPI — A Complete Guide to a real production environment.
Scalability
Design Python & FastAPI services to scale horizontally. Keep request handlers stateless, push session and cache state to external stores (Redis, the database), and benchmark p95/p99 latency under realistic load before tuning.
Monitoring & Observability
Emit metrics (RED/USE), structured JSON logs, and distributed traces from day one. Wire dashboards and alerts to SLOs you actually care about — error rate, latency, saturation — not vanity metrics.
Logging
Log with correlation IDs, never log secrets or PII, and centralize logs (ELK, Loki, CloudWatch). Use levels deliberately: INFO for state changes, WARN for recoverable issues, ERROR for incidents.
Security
Apply least-privilege IAM, rotate secrets through a vault, validate every input, and patch dependencies on a schedule. For HTTP services, enable TLS everywhere and set sensible security headers.
Testing
Layer unit, integration, and contract tests. Run them in CI on every PR, and add smoke tests post-deploy. For Python & FastAPI systems, also run chaos and load tests before a major release.
Reliability & Rollouts
Ship with health checks, readiness probes, graceful shutdown, and a rollback strategy. Prefer canary or blue/green deploys over big-bang releases.
Questions
Frequently asked questions
Is this tutorial up to date?
Yes. This tutorial was last reviewed and updated on May 26, 2026. We revisit popular Python & FastAPI tutorials regularly to keep them aligned with current best practices.
What level is this tutorial aimed at?
It is written for working developers with some backend experience. Beginners can still follow along, and senior engineers will find production-grade patterns and trade-off discussions.
Do I need to follow every step in order?
The walkthrough is sequential because each step depends on the previous one. If you only need a specific concept, the table of contents at the top of the article lets you jump straight to that section.
Where can I find the source code?
Code samples are inlined in the tutorial. When a companion repository is published it will be linked at the top of this page.
Go deeper
Further reading
More From the Channel
Follow the full tutorial series on YouTube
The MasterLabSystems channel publishes in-depth, project-based tutorials on Java, Spring Boot, microservices, Docker, Kubernetes, AWS and DevOps — the same topics covered on this site, with full code walkthroughs.
Stay in the Loop
Get the next tutorial in your inbox
next tutorial →
FastAPI Microservices Architecture Explained Step by Step
Related tutorials
FastAPI Microservices Architecture Explained Step by Step
How to design and build a Python microservices architecture with FastAPI — services, API gateway, async messaging, Redis, Postgres and Docker Compose.
Dockerizing a FastAPI Application the Right Way
Build small, fast, secure Docker images for FastAPI — multi-stage builds, Gunicorn + Uvicorn workers, non-root users, and production-ready Dockerfiles.
CI/CD Pipeline for FastAPI with GitHub Actions and Docker
Build a complete CI/CD pipeline for a FastAPI app — pytest, linting, Docker image builds, container registry push and deployment from GitHub Actions.
