VOOZH about

URL: https://tech-insider.org/fastapi-tutorial-rest-api-python-2026-2/

⇱ FastAPI Tutorial: Build a REST API in 13 Steps [2026]


Skip to content
April 26, 2026
24 min read

FastAPI has become the gold standard for Python web development in 2026, with version 0.136.1 released on April 23, 2026, powering production APIs at companies from Netflix to Uber. This tutorial walks you through building a complete production-ready REST API with FastAPI in 13 hands-on steps, covering Pydantic v2, async endpoints, JWT authentication, dependency injection, WebSockets, background tasks, automated testing, Docker deployment, and Kubernetes orchestration. By the end, you will have a working FastAPI project with full OpenAPI documentation, 90%+ test coverage, and a CI/CD-ready Docker image.

Whether you are migrating from Flask, Django, or Node.js, FastAPI’s automatic OpenAPI generation, type-hint-driven validation, and async-first architecture make it the fastest path to a modern Python API. This guide assumes basic Python knowledge, but every code block runs end to end on Python 3.12 or higher. Estimated time to complete the full project: 2 to 3 hours.

Why FastAPI Dominates Python Web Development in 2026

FastAPI’s rise from a 2019 side project to the most-recommended Python web framework in 2026 is a story of timing and engineering. Sebastian Ramirez built FastAPI on top of Starlette and Pydantic, two libraries that already solved hard problems: ASGI compatibility and runtime data validation. By stitching them together with Python’s type-hint system, FastAPI eliminated the boilerplate that made Flask and Django Rest Framework feel dated for greenfield API work. The framework now sits at the top of the Stack Overflow Python web framework satisfaction rankings and has been adopted by Netflix for internal tooling, Microsoft for Azure components, and Uber for ML model serving.

The performance story is a major differentiator. FastAPI runs on Uvicorn, an ASGI server built on uvloop and httptools, that consistently posts request-per-second numbers in the tens of thousands on commodity hardware. Compared to Flask 3.x synchronous request handling, FastAPI typically posts 2x to 4x higher throughput on I/O-bound workloads, and against Django Rest Framework the gap widens further on JSON-heavy endpoints because Pydantic v2’s Rust-backed core handles serialization in a fraction of the time of DRF serializers. For ML inference APIs, where every millisecond of latency translates to GPU cost, this is the deciding factor.

Beyond raw speed, the developer experience is what locks teams in. Type hints feed three systems at once: editor autocompletion, runtime validation, and OpenAPI 3.1 schema generation. The interactive Swagger UI and ReDoc pages render automatically at /docs and /redoc, so the API documentation never drifts from the implementation. Dependency injection through function parameters keeps database sessions, authentication, and configuration cleanly separated, and the framework’s first-party support for async/await means you can mix sync and async endpoints in the same app without wrapping anything in thread executors.

FastAPI vs Flask vs Django REST Framework in 2026

Choosing a Python web framework in 2026 has narrowed to three real options: FastAPI for new APIs, Django for full-stack apps with admin needs, and Flask for legacy or extremely simple services. The table below summarizes the practical differences that affect day-to-day work.

👁 FastAPI vs Flask vs Django REST Framework in 2026
CapabilityFastAPI 0.136.1Flask 3.xDjango REST Framework 3.15
Async supportNative async/awaitPartial via async viewsLimited (sync ORM core)
Auto OpenAPI docsBuilt in (Swagger and ReDoc)Requires Flask-Smorest or APISpecRequires drf-spectacular
Runtime validationPydantic v2 (Rust-backed)Marshmallow or manualDRF serializers (Python)
Dependency injectionBuilt in via Depends()None nativeNone native
WebSocketsFirst class via StarletteFlask-Sock extensionChannels (separate stack)
Type-hint-driven APIYesNoNo
Typical RPS (JSON echo)20k to 60k5k to 15k3k to 8k
Production-readinessUsed at Netflix, Uber, MicrosoftMature ecosystemMature, opinionated

The takeaway is straightforward: if you are building a new REST or GraphQL service that does not need the Django admin, FastAPI is the productivity winner. Flask is still the right choice for a tiny webhook or a one-file proof of concept, and Django remains unmatched when you need batteries-included auth, admin, and ORM in a monolith. Everywhere else, FastAPI’s combination of speed, type safety, and free documentation pages is hard to beat.

Prerequisites and Environment Setup

Before writing any code, lock down the toolchain. FastAPI 0.136.1 requires Python 3.10 or higher, but Python 3.12 is the recommended baseline in 2026 for performance, dependency compatibility, and access to recent typing features such as PEP 695 generics. Older runtimes like Python 3.8 and 3.9 will not install the latest FastAPI release. The other non-negotiables are a recent pip, a virtual environment tool, and Docker if you plan to follow the deployment steps. The full prerequisite checklist:

  • Python: 3.12.x (3.10+ minimum)
  • pip: 24.0 or higher (run python -m pip install --upgrade pip)
  • FastAPI: 0.136.1 (latest as of April 23, 2026)
  • Uvicorn: latest version with standard extras
  • Pydantic: v2.x (installed automatically with FastAPI)
  • SQLAlchemy: 2.x for the database layer
  • pytest: 8.x and httpx for the test client
  • Docker: latest stable for containerized deployment
  • Editor: VS Code, PyCharm, or Cursor with the Python extension
  • Operating system: Linux, macOS, or Windows with WSL2

Verify your Python install before proceeding. The output should report 3.10.x or higher; if it does not, install the current stable release from the official Python downloads page before continuing.

$ python --version
Python 3.12.4

$ python -m pip --version
pip 24.2 from /usr/local/lib/python3.12/site-packages/pip (python 3.12)

Step 1: Create the Project and Virtual Environment

Start with a clean directory and a per-project virtual environment so dependency versions do not collide with system Python or other projects. The venv module ships with the standard library, so no extra tooling is required. Activate the environment before running any pip command in the rest of this tutorial.

mkdir fastapi-tutorial && cd fastapi-tutorial
python -m venv .venv

# Linux or macOS
source .venv/bin/activate

# Windows PowerShell
.venvScriptsActivate.ps1

# Confirm activation
which python
# /home/you/fastapi-tutorial/.venv/bin/python

With the virtual environment active, create a minimal project layout. A flat structure works fine for a tutorial, but the convention this guide follows scales cleanly to multi-router production apps: an app/ package for source, a tests/ directory for pytest, and a top-level requirements.txt file. Create the directories with one command:

mkdir -p app/routers app/models tests
touch app/__init__.py app/main.py app/database.py app/dependencies.py
touch app/routers/__init__.py app/routers/items.py app/routers/users.py
touch tests/__init__.py tests/test_main.py
touch requirements.txt .env Dockerfile

Step 2: Install FastAPI and Pin Dependencies

FastAPI is still in the 0.x semver range, which means every minor release can introduce breaking changes. The official FastAPI versioning guide recommends pinning to a narrow range like >=0.136.1,<0.137.0 so an unattended pip install does not drag in a backwards-incompatible update overnight. Add the following to your requirements.txt:

# requirements.txt
fastapi>=0.136.1,<0.137.0
uvicorn[standard]>=0.30.0
pydantic>=2.7.0,<3.0.0
pydantic-settings>=2.3.0
sqlalchemy>=2.0.30
python-jose[cryptography]>=3.3.0
passlib[bcrypt]>=1.7.4
python-multipart>=0.0.9
httpx>=0.27.0
pytest>=8.2.0
pytest-asyncio>=0.23.0

Install everything in one go and freeze the resolved versions for reproducibility. The uvicorn[standard] extra pulls in uvloop and httptools, which is what unlocks the high-throughput numbers reported in the benchmarks earlier.

python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip freeze > requirements.lock.txt

Step 3: Write Your First FastAPI Endpoint

Open app/main.py and add a tiny app to confirm that the install works. Three things to notice in the snippet below: the type hint on q: str | None = None drives both query parameter parsing and the OpenAPI schema, the path parameter item_id: int triggers automatic 422 errors when the caller passes a non-integer, and the async def declaration lets the function run cooperatively on the event loop without changing the call site.

👁 Step 3: Write Your First FastAPI Endpoint
# app/main.py
from fastapi import FastAPI

app = FastAPI(
 title="FastAPI Tutorial 2026",
 version="1.0.0",
 description="Production-ready FastAPI starter built on Python 3.12.",
)


@app.get("/")
async def read_root():
 return {"status": "ok", "framework": "FastAPI", "version": "0.136.1"}


@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
 return {"item_id": item_id, "q": q}

Run the development server with hot reload. The --reload flag watches for file changes and restarts Uvicorn in milliseconds, which is the workflow you want during the rest of the tutorial.

uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

# Expected output
INFO: Will watch for changes in these directories: ['/home/you/fastapi-tutorial']
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process using StatReload
INFO: Application startup complete.

Visit http://127.0.0.1:8000 in your browser to see the JSON response, then open http://127.0.0.1:8000/docs to see the auto-generated Swagger UI. Try changing item_id to a non-integer in the address bar; FastAPI returns a structured validation error without a single line of validation code on your part.

Step 4: Define Pydantic v2 Models for Request and Response

Pydantic v2’s Rust-backed core is one of the main reasons FastAPI 0.136.1 outperforms older Python frameworks. Instead of writing custom serializers, you declare data shapes as classes and FastAPI uses them for parsing, validation, OpenAPI schema generation, and response serialization. The Pydantic documentation details every available field type; the example below covers the patterns used in 80% of real APIs.

# app/models/schemas.py
from datetime import datetime
from pydantic import BaseModel, EmailStr, Field, ConfigDict


class ItemBase(BaseModel):
 name: str = Field(min_length=1, max_length=120)
 description: str | None = Field(default=None, max_length=2_000)
 price: float = Field(gt=0, le=1_000_000)
 in_stock: bool = True


class ItemCreate(ItemBase):
 pass


class ItemRead(ItemBase):
 id: int
 created_at: datetime

 model_config = ConfigDict(from_attributes=True)


class UserCreate(BaseModel):
 email: EmailStr
 password: str = Field(min_length=12, max_length=128)
 full_name: str | None = None


class UserRead(BaseModel):
 id: int
 email: EmailStr
 full_name: str | None = None
 is_active: bool

 model_config = ConfigDict(from_attributes=True)

Three Pydantic v2 details worth memorizing. First, ConfigDict(from_attributes=True) replaces the v1 orm_mode setting and is what lets FastAPI accept SQLAlchemy ORM instances as response payloads. Second, EmailStr requires the email-validator package, which Pydantic pulls in automatically on first use. Third, Field() constraints like min_length and gt become OpenAPI schema properties, so the Swagger UI shows the rules to API consumers.

Step 5: Add a SQLAlchemy 2.x Database Layer

Real APIs need persistence. SQLAlchemy 2.x is the de facto Python ORM, and its 2.0-style declarative mapping pairs cleanly with FastAPI’s dependency injection. For the tutorial, use SQLite to avoid running a separate database; the same code points at PostgreSQL or MySQL by changing the connection URL.

# app/database.py
from datetime import datetime
from sqlalchemy import create_engine, String, DateTime, Float, Boolean
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker

DATABASE_URL = "sqlite:///./app.db"

engine = create_engine(
 DATABASE_URL, connect_args={"check_same_thread": False}, echo=False
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)


class Base(DeclarativeBase):
 pass


class Item(Base):
 __tablename__ = "items"

 id: Mapped[int] = mapped_column(primary_key=True)
 name: Mapped[str] = mapped_column(String(120), index=True)
 description: Mapped[str | None] = mapped_column(String(2_000), nullable=True)
 price: Mapped[float] = mapped_column(Float)
 in_stock: Mapped[bool] = mapped_column(Boolean, default=True)
 created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)


class User(Base):
 __tablename__ = "users"

 id: Mapped[int] = mapped_column(primary_key=True)
 email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
 hashed_password: Mapped[str] = mapped_column(String(255))
 full_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
 is_active: Mapped[bool] = mapped_column(Boolean, default=True)


Base.metadata.create_all(bind=engine)

The Base.metadata.create_all() call creates the SQLite file and tables on first run. In production you replace this with Alembic migrations, but for a tutorial it keeps the moving parts to a minimum. Note the check_same_thread argument: it is required for SQLite plus FastAPI because the framework can route requests across threads, and the default SQLite driver objects to that.

Step 6: Use Dependency Injection for Database Sessions

FastAPI’s dependency injection system is the cleanest way to scope a database session to a single request. The Depends() helper takes any callable, runs it before the path operation, and passes the return value as a parameter. A generator function with a try/finally guarantees the session always closes even if the handler raises.

# app/dependencies.py
from typing import Annotated
from fastapi import Depends
from sqlalchemy.orm import Session
from app.database import SessionLocal


def get_db():
 db = SessionLocal()
 try:
 yield db
 finally:
 db.close()


SessionDep = Annotated[Session, Depends(get_db)]

The Annotated[Session, Depends(get_db)] alias is the recommended FastAPI 2026 pattern. It saves you from writing db: Session = Depends(get_db) on every endpoint and keeps the type information clean for editors and OpenAPI generation. Use it consistently in every router that touches the database.

Step 7: Build a CRUD Router for Items

Routers split a FastAPI app into mountable modules so the main file does not balloon to thousands of lines. Each router lives in its own file, declares its own prefix and tags, and registers with the main app via app.include_router(). Below is a complete CRUD router for items, exercising path parameters, query parameters, request bodies, and response models.

👁 Step 7: Build a CRUD Router for Items
# app/routers/items.py
from fastapi import APIRouter, HTTPException, status
from app.database import Item
from app.dependencies import SessionDep
from app.models.schemas import ItemCreate, ItemRead

router = APIRouter(prefix="/items", tags=["items"])


@router.post("/", response_model=ItemRead, status_code=status.HTTP_201_CREATED)
async def create_item(payload: ItemCreate, db: SessionDep):
 item = Item(**payload.model_dump())
 db.add(item)
 db.commit()
 db.refresh(item)
 return item


@router.get("/", response_model=list[ItemRead])
async def list_items(db: SessionDep, skip: int = 0, limit: int = 50):
 return db.query(Item).offset(skip).limit(min(limit, 200)).all()


@router.get("/{item_id}", response_model=ItemRead)
async def read_item(item_id: int, db: SessionDep):
 item = db.get(Item, item_id)
 if item is None:
 raise HTTPException(status_code=404, detail="Item not found")
 return item


@router.put("/{item_id}", response_model=ItemRead)
async def update_item(item_id: int, payload: ItemCreate, db: SessionDep):
 item = db.get(Item, item_id)
 if item is None:
 raise HTTPException(status_code=404, detail="Item not found")
 for key, value in payload.model_dump().items():
 setattr(item, key, value)
 db.commit()
 db.refresh(item)
 return item


@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int, db: SessionDep):
 item = db.get(Item, item_id)
 if item is None:
 raise HTTPException(status_code=404, detail="Item not found")
 db.delete(item)
 db.commit()
 return None

Wire the router into the main app. Update app/main.py to import and include it; FastAPI handles prefix concatenation, tag grouping in Swagger, and OpenAPI schema merging automatically.

# app/main.py (additions)
from app.routers import items, users

app.include_router(items.router)
app.include_router(users.router)

Step 8: Add JWT Authentication with OAuth2 Password Flow

Real APIs need authentication. FastAPI ships first-class support for OAuth2 password bearer flow, which is the simplest secure pattern for username/password APIs. Combine it with a JWT and you get stateless authentication that survives load balancer rotation. The example below uses python-jose for the JWT and passlib with bcrypt for password hashing; both are pulled in by the requirements file in Step 2.

# app/security.py
from datetime import datetime, timedelta, timezone
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from passlib.context import CryptContext
from app.database import User
from app.dependencies import SessionDep

SECRET_KEY = "change-me-in-production-use-a-32-byte-random-string"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="users/token")


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)


def create_access_token(subject: str) -> str:
 expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
 payload = {"sub": subject, "exp": expire}
 return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)


def get_current_user(
 token: Annotated[str, Depends(oauth2_scheme)],
 db: SessionDep,
) -> User:
 credentials_exc = 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])
 email: str | None = payload.get("sub")
 if email is None:
 raise credentials_exc
 except JWTError:
 raise credentials_exc
 user = db.query(User).filter(User.email == email).first()
 if user is None:
 raise credentials_exc
 return user

Add the user router with a token endpoint and a protected /me route. Notice how get_current_user is reused as a dependency on any path that needs authentication; FastAPI runs it before the handler and injects the resolved User object.

# app/routers/users.py
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from app.database import User
from app.dependencies import SessionDep
from app.models.schemas import UserCreate, UserRead
from app.security import (
 hash_password,
 verify_password,
 create_access_token,
 get_current_user,
)

router = APIRouter(prefix="/users", tags=["users"])


@router.post("/", response_model=UserRead, status_code=201)
async def register(payload: UserCreate, db: SessionDep):
 if db.query(User).filter(User.email == payload.email).first():
 raise HTTPException(status_code=409, detail="Email already registered")
 user = User(
 email=payload.email,
 hashed_password=hash_password(payload.password),
 full_name=payload.full_name,
 )
 db.add(user)
 db.commit()
 db.refresh(user)
 return user


@router.post("/token")
async def login(
 form: Annotated[OAuth2PasswordRequestForm, Depends()],
 db: SessionDep,
):
 user = db.query(User).filter(User.email == form.username).first()
 if not user or not verify_password(form.password, user.hashed_password):
 raise HTTPException(status_code=401, detail="Invalid credentials")
 return {"access_token": create_access_token(user.email), "token_type": "bearer"}


@router.get("/me", response_model=UserRead)
async def read_me(current_user: Annotated[User, Depends(get_current_user)]):
 return current_user

Step 9: Stream Real-Time Updates with WebSockets

FastAPI inherits Starlette’s WebSocket support, which means real-time bidirectional communication is a few lines of code. The handler below accepts a WebSocket connection, broadcasts every received message back to the client with a server timestamp, and cleanly closes on disconnect. The pattern scales to chat apps, live dashboards, and streaming model inference results from an LLM.

# app/routers/ws.py
from datetime import datetime, timezone
from fastapi import APIRouter, WebSocket, WebSocketDisconnect

router = APIRouter()


@router.websocket("/ws/echo")
async def websocket_echo(websocket: WebSocket):
 await websocket.accept()
 try:
 while True:
 payload = await websocket.receive_text()
 await websocket.send_json({
 "echo": payload,
 "server_time": datetime.now(timezone.utc).isoformat(),
 })
 except WebSocketDisconnect:
 return

Test the endpoint quickly with the websockets CLI or a few lines of Python. Open two terminals, run the server in one, and send messages from the other to see the echo loop in action.

python -c "
import asyncio, websockets
async def main():
 async with websockets.connect('ws://127.0.0.1:8000/ws/echo') as ws:
 await ws.send('hello')
 print(await ws.recv())
asyncio.run(main())
"
# {"echo": "hello", "server_time": "2026-04-26T19:21:08.901+00:00"}

Step 10: Offload Slow Work with BackgroundTasks

Some operations should not block the HTTP response: sending a confirmation email, writing an audit log, warming a cache. FastAPI’s BackgroundTasks object queues a callable to run after the response goes out, with the same event loop and dependency context as the request. For longer or distributed work, swap it for Celery or Arq, but BackgroundTasks covers the 80% case with zero infrastructure.

# app/routers/notifications.py
import logging
from fastapi import APIRouter, BackgroundTasks
from pydantic import BaseModel, EmailStr

logger = logging.getLogger("uvicorn.error")
router = APIRouter(prefix="/notifications", tags=["notifications"])


class EmailPayload(BaseModel):
 to: EmailStr
 subject: str
 body: str


def send_email(payload: EmailPayload) -> None:
 # Replace with SES, SendGrid, or aiosmtplib in production
 logger.info("Sending email to %s: %s", payload.to, payload.subject)


@router.post("/email", status_code=202)
async def queue_email(payload: EmailPayload, background_tasks: BackgroundTasks):
 background_tasks.add_task(send_email, payload)
 return {"queued": True}

Step 11: Add Middleware, CORS, and Structured Logging

Production APIs need at least three cross-cutting concerns wired up: cross-origin resource sharing for browser clients, request-scoped logging for observability, and rate limiting for abuse protection. FastAPI inherits Starlette’s middleware system, so anything that conforms to the ASGI middleware contract drops in. The example below adds the official CORS middleware and a custom timing middleware that logs how long each request took.

👁 Step 11: Add Middleware, CORS, and Structured Logging
# app/main.py (final shape)
import logging
import time
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from app.routers import items, users, notifications, ws

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("api")

app = FastAPI(
 title="FastAPI Tutorial 2026",
 version="1.0.0",
 description="Production-ready FastAPI starter built on Python 3.12.",
)

app.add_middleware(
 CORSMiddleware,
 allow_origins=["https://app.example.com", "http://localhost:5173"],
 allow_credentials=True,
 allow_methods=["*"],
 allow_headers=["*"],
)


@app.middleware("http")
async def add_timing_header(request: Request, call_next):
 start = time.perf_counter()
 response = await call_next(request)
 elapsed_ms = (time.perf_counter() - start) * 1000
 response.headers["X-Process-Time-ms"] = f"{elapsed_ms:.2f}"
 logger.info("%s %s %s %.2fms",
 request.method, request.url.path, response.status_code, elapsed_ms)
 return response


app.include_router(items.router)
app.include_router(users.router)
app.include_router(notifications.router)
app.include_router(ws.router)


@app.get("/health", tags=["meta"])
async def health():
 return {"status": "ok"}

Two production tips. Replace allow_origins=["*"] with an explicit allowlist in real deployments because wildcard CORS combined with cookies is a security mistake. For rate limiting, the slowapi package layers a Redis-backed limiter on top of FastAPI without writing custom middleware.

Step 12: Test Everything with pytest and TestClient

FastAPI’s TestClient wraps httpx and gives you a synchronous interface to the ASGI app, which makes it trivial to drive tests without spinning up a real server. Combine it with pytest fixtures and a per-test SQLite database to get an isolated, deterministic suite. Aim for at least 90% line coverage; FastAPI apps have so little glue code that this target is realistic.

# tests/test_main.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.database import Base, engine

client = TestClient(app)


@pytest.fixture(autouse=True)
def reset_db():
 Base.metadata.drop_all(bind=engine)
 Base.metadata.create_all(bind=engine)
 yield


def test_health():
 response = client.get("/health")
 assert response.status_code == 200
 assert response.json() == {"status": "ok"}


def test_create_and_read_item():
 create = client.post("/items/", json={
 "name": "Mechanical keyboard",
 "description": "65 percent layout",
 "price": 129.99,
 "in_stock": True,
 })
 assert create.status_code == 201
 item_id = create.json()["id"]

 read = client.get(f"/items/{item_id}")
 assert read.status_code == 200
 assert read.json()["name"] == "Mechanical keyboard"


def test_register_and_login():
 register = client.post("/users/", json={
 "email": "[email protected]",
 "password": "correcthorsebatterystaple",
 "full_name": "Ada Lovelace",
 })
 assert register.status_code == 201

 login = client.post(
 "/users/token",
 data={"username": "[email protected]", "password": "correcthorsebatterystaple"},
 )
 assert login.status_code == 200
 assert "access_token" in login.json()


def test_invalid_payload_returns_422():
 response = client.post("/items/", json={"name": "", "price": -1})
 assert response.status_code == 422
 body = response.json()
 assert "detail" in body

Run the suite and capture coverage in one command. The expected output below shows the four tests passing in well under a second on Python 3.12; that is what good FastAPI test ergonomics look like.

$ pytest -v --tb=short
============================= test session starts =============================
platform linux -- Python 3.12.4, pytest-8.2.0
collected 4 items

tests/test_main.py::test_health PASSED [ 25%]
tests/test_main.py::test_create_and_read_item PASSED [ 50%]
tests/test_main.py::test_register_and_login PASSED [ 75%]
tests/test_main.py::test_invalid_payload_returns_422 PASSED [100%]

============================== 4 passed in 0.61s ==============================

Step 13: Containerize with Docker and Deploy

The standard production deployment for FastAPI in 2026 is a slim Python image running Uvicorn with multiple workers behind a reverse proxy or a managed load balancer. The Dockerfile below uses a multi-stage build to keep the final image under 200 MB and runs the app as a non-root user. The official Docker getting started guide covers the basics if Docker is new to you.

# Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.12-slim AS runtime
RUN useradd --create-home --shell /bin/bash app
WORKDIR /home/app
COPY --from=builder /install /usr/local
COPY --chown=app:app . .
USER app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

Build the image, run it locally, and exercise the health endpoint. From there, push to Docker Hub or AWS ECR and deploy to ECS, Cloud Run, Fly.io, or any Kubernetes cluster. The image runs the same in every environment.

docker build -t fastapi-tutorial:1.0.0 .
docker run --rm -p 8000:8000 fastapi-tutorial:1.0.0

# In another terminal
curl -s http://localhost:8000/health
# {"status":"ok"}

For Kubernetes, a minimal deployment plus service manifest pairs FastAPI with horizontal pod autoscaling and rolling updates. The example below assumes you already have a cluster and a built image pushed to a registry.

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: fastapi-tutorial
spec:
 replicas: 3
 selector:
 matchLabels:
 app: fastapi-tutorial
 template:
 metadata:
 labels:
 app: fastapi-tutorial
 spec:
 containers:
 - name: api
 image: your-registry/fastapi-tutorial:1.0.0
 ports:
 - containerPort: 8000
 readinessProbe:
 httpGet:
 path: /health
 port: 8000
 initialDelaySeconds: 5
 livenessProbe:
 httpGet:
 path: /health
 port: 8000
 initialDelaySeconds: 30
---
apiVersion: v1
kind: Service
metadata:
 name: fastapi-tutorial
spec:
 selector:
 app: fastapi-tutorial
 ports:
 - port: 80
 targetPort: 8000
 type: ClusterIP

Common Pitfalls and How to Avoid Them

Even with FastAPI’s excellent defaults, a handful of mistakes show up in almost every codebase migrated from Flask or Django. Avoid them and you will spend less time debugging in production.

  • Not pinning FastAPI versions: The 0.x semver range means a minor release can break your code. Pin to >=0.136.1,<0.137.0 in requirements.txt and run integration tests before bumping.
  • Mixing sync and blocking I/O inside async endpoints: A blocking requests.get() call in an async def handler stalls the event loop for every concurrent request. Use httpx.AsyncClient or asyncio.to_thread() for blocking work.
  • Returning ORM models without from_attributes=True: FastAPI cannot serialize SQLAlchemy instances unless the response model has the Pydantic v2 ConfigDict(from_attributes=True) setting.
  • Storing secrets in source: Hard-coding the JWT SECRET_KEY as in Step 8 is fine for a tutorial but disastrous in production. Load it from environment variables via pydantic-settings.
  • Disabling strict Content-Type checks: FastAPI 0.132.0 enabled strict JSON Content-Type by default. Some legacy clients send text/plain for JSON bodies; either fix the clients or override the parser explicitly.
  • Skipping the --workers flag in production: A single Uvicorn worker uses one CPU core. Set --workers to roughly the number of cores, or run Uvicorn behind Gunicorn with the Uvicorn worker class.
  • Forgetting connect_args with SQLite: Without {"check_same_thread": False}, FastAPI threaded requests crash on SQLite immediately.
  • Wildcard CORS plus credentials: Combining allow_origins=["*"] with allow_credentials=True is a security hole; browsers reject it, and modern lint tools flag it.

Advanced Tips for Production FastAPI

Once the basics are working, several advanced patterns separate hobby projects from production-grade FastAPI deployments. Use them selectively as your traffic and team size grow.

👁 Advanced Tips for Production FastAPI
  • Settings via pydantic-settings: Move every secret and feature flag into a BaseSettings subclass that reads from environment variables and a local .env file. The result is type-checked configuration with zero runtime parsing code.
  • Async database drivers: SQLAlchemy 2.x supports asyncio engines through asyncpg for PostgreSQL or aiosqlite for SQLite. Pair them with AsyncSession in your dependency for true non-blocking database access.
  • Structured exception handlers: Register a global app.exception_handler(SQLAlchemyError) handler that maps database errors to clean 500 responses with a stable error code your clients can branch on.
  • OpenAPI customization: Override app.openapi to inject security schemes, server URLs, and contact info. The result is documentation that doubles as API contract for client SDK generation.
  • Versioned routers: Mount routers under /v1 and /v2 prefixes from day one. Cheap insurance against the day a breaking change becomes unavoidable.
  • Rate limiting with slowapi: Drop in a Redis-backed rate limiter that respects per-IP and per-user budgets without rewriting your endpoints.
  • Observability with OpenTelemetry: The opentelemetry-instrumentation-fastapi package adds distributed tracing, metrics, and log correlation in three lines.
  • Hot reload off in production: Never run --reload in production; it disables several optimizations and adds disk-watch overhead.

Performance Benchmarks: FastAPI vs the Field

Benchmarks always come with caveats, but the directional gap between FastAPI and older Python frameworks is consistent across published 2025 to 2026 reports. The numbers below approximate community benchmarks (TechEmpower-style JSON serialization on a single 4-core box, identical hardware). Treat them as ballpark figures, not absolute promises, and re-run them on your own workload before making architectural calls.

FrameworkRPS (JSON serialization)p50 latencyp99 latencyMemory per worker
FastAPI 0.136.1 + Uvicorn52,0001.8 ms9 ms78 MB
Starlette 0.46 (raw)61,0001.5 ms7 ms62 MB
Flask 3.0 + Gunicorn sync12,4007.2 ms34 ms91 MB
Django 5.0 + Gunicorn sync6,90013 ms58 ms140 MB
Node.js 22 + Express 448,0002.0 ms10 ms72 MB
Go 1.22 + net/http110,0000.9 ms4 ms22 MB

Two things to take from the table. FastAPI is competitive with Node.js Express for I/O-bound JSON workloads, which is the surprising and often the decisive result for teams choosing between Python and Node. Go is still meaningfully faster, but for most product APIs the productivity gap to FastAPI dwarfs the throughput gap, and you can always pull a hot path into a Go microservice later.

Troubleshooting Common FastAPI Errors

The list below covers the eight most common errors developers hit during their first month with FastAPI, plus the fix that resolves each one in 90% of cases.

  • 422 Unprocessable Entity on every request: Almost always a Pydantic validation failure. Check the detail array in the response body for the exact field and constraint that failed.
  • “Could not validate credentials” loop: The JWT SECRET_KEY changed between server restarts and existing tokens no longer verify. Set the key from an environment variable so it survives restarts.
  • SQLite “table is locked” errors: You forgot connect_args={"check_same_thread": False} or you have multiple writer threads. Switch to PostgreSQL for concurrent writes.
  • CORS preflight failures: The browser sends OPTIONS requests that your CORS middleware does not allow. Add the missing methods or origins to CORSMiddleware.
  • “Pydantic v1 BaseModel is deprecated” warnings: A dependency is still importing from pydantic.v1. Run pip list --outdated and upgrade.
  • WebSocket connection closes immediately: Often a missing await websocket.accept() at the top of the handler.
  • OpenAPI docs do not show new endpoint: The router was created but not included via app.include_router(...), or you forgot to restart Uvicorn without --reload.
  • Background tasks never run: The handler returned before adding the task, or you raised an exception that aborted the response. Add the task before any code that can fail.

Frequently Asked Questions

What is the latest version of FastAPI in April 2026?

FastAPI 0.136.1 was released on April 23, 2026. It requires Python 3.10 or higher and is the recommended version to pin in new projects. Always pin a narrow range like >=0.136.1,<0.137.0 in requirements.txt because FastAPI is still in 0.x semver and minor releases can include breaking changes.

Is FastAPI faster than Flask and Django?

Yes, by a comfortable margin on I/O-bound JSON workloads. Community benchmarks consistently show FastAPI on Uvicorn delivering roughly 4x the requests per second of Flask 3.x with Gunicorn sync workers, and around 7x the throughput of Django 5.0 in the same conditions. The gap shrinks on CPU-bound work because all three frameworks are limited by Python’s GIL.

Do I need to learn async to use FastAPI?

No. FastAPI accepts both def and async def path operations, and the framework runs sync handlers in a thread pool automatically. Start with synchronous handlers if your team is new to asyncio, then migrate hot paths to async as you adopt async database drivers and HTTP clients like httpx.AsyncClient.

How does FastAPI handle authentication?

FastAPI provides built-in security utilities for OAuth2 password flow, OAuth2 with scopes, API key headers, HTTP Basic, and OpenID Connect. Step 8 in this tutorial implements the most common pattern, OAuth2 password flow with JWT bearer tokens. For OpenID Connect with providers like Auth0, Okta, or Keycloak, use fastapi-oauth2 or the underlying Authlib library.

Can FastAPI replace Django for full-stack apps?

For pure APIs and SPA backends, yes, and it usually does so with less code. For full-stack apps that lean heavily on Django’s admin, ORM, and templating system, the answer is no; you would end up rebuilding several Django subsystems and lose the productivity edge. A common 2026 pattern is to keep Django for the admin and CMS layer and add a FastAPI service for performance-sensitive APIs and ML model serving.

How do I deploy FastAPI to AWS, GCP, or Azure?

Containerize with the Dockerfile in Step 13 and deploy to AWS ECS or App Runner, Google Cloud Run, or Azure Container Apps; all three accept the same image. For Kubernetes, the manifests in Step 13 work on EKS, GKE, and AKS without modification. AWS Lambda is also viable through the mangum adapter, which translates API Gateway events to ASGI calls.

What is Pydantic v2 and why does FastAPI need it?

Pydantic v2 is a complete rewrite of Pydantic with a Rust-backed core (pydantic-core) that is roughly 5x to 50x faster at validation and serialization than the v1 pure-Python implementation. FastAPI 0.119.0 and later use Pydantic v2 by default, with a temporary pydantic.v1 shim for gradual migration. The performance improvement is one of the main reasons FastAPI 0.136.1 outperforms older Python web frameworks.

Where can I find the official FastAPI documentation?

The canonical reference lives at the official FastAPI documentation site, with release notes at the same domain and the source on the FastAPI GitHub repository. The Pydantic side is documented at the Pydantic documentation site, and the underlying ASGI server has its own pages on the Uvicorn project site.

Related Coverage

Final Thoughts: When to Choose FastAPI in 2026

FastAPI 0.136.1 is the right choice in 2026 for any new Python REST or GraphQL API where developer productivity, runtime performance, and type-safe contracts matter. The combination of Pydantic v2’s Rust-backed validation, Starlette’s ASGI foundation, and Uvicorn’s high-throughput event loop produces a stack that comfortably handles tens of thousands of requests per second per worker on commodity hardware while keeping the code base smaller than the equivalent Flask or Django implementation. The framework’s automatic OpenAPI 3.1 schema generation eliminates the documentation drift that plagues older Python projects, and its dependency injection system makes shared concerns like database sessions, authentication, and configuration trivial to test.

Where FastAPI is not the right answer: a one-file webhook can stay on Flask, and a content-management-heavy app with admin, auth, and ORM tightly coupled belongs on Django. For everything else (microservices, ML inference APIs, internal tooling, mobile and SPA backends, real-time WebSocket services) FastAPI’s combination of speed, ergonomics, and ecosystem maturity makes it the dominant Python web framework heading into the second half of the decade. Pin your versions, write tests, deploy in containers, and the framework rewards disciplined teams with one of the best developer experiences in modern web development.

👁 Sofia Lindström

Sofia Lindström

Editor-in-Chief

Sofia Lindström is the Editor-in-Chief at Tech Insider, where she leads editorial strategy and oversees coverage across AI, cybersecurity, and enterprise technology. With over a decade in Swedish tech journalism, she previously served as technology editor at Dagens Industri and covered the Nordic startup ecosystem for Breakit. Sofia holds an MSc in Media Technology from KTH Royal Institute of Technology and is a frequent speaker at Web Summit and Slush. She is passionate about making complex technology accessible to business leaders.

View all articles
👁 Tech Insider
Tech
Insider

Tech Insider delivers in-depth coverage of the technologies shaping the future: AI, cybersecurity, cloud computing, hardware, and the trends that matter.

Company

Explore

Categories

© 2026 Tech Insider Media AB. All rights reserved.