FastAPI handles roughly 30,000 requests per second on a single process according to TechEmpower benchmarks. It generates OpenAPI documentation without extra configuration and catches type errors before requests hit your code. If you need a Python REST API and do not want to wrestle with boilerplate, FastAPI covers the critical path.

This guide walks through building a task manager API from scratch. By the end you will have a running server with create, read, update, delete endpoints, automated tests, and a list of mistakes to watch out for.

Contents

  1. Environment Setup
  2. Your First Endpoint
  3. CRUD Operations
  4. Testing with TestClient
  5. Common Pitfalls
  6. Performance Context
  7. Next Steps

1. Environment Setup

Requirements: Python 3.8 or newer. Code in this guide was tested on Python 3.12.3.

python --version

Create a virtual environment to isolate dependencies. Skip this and you will run into version conflicts sooner or later.

# Create the environment
python -m venv fastapi-env

# Activate — Windows
fastapi-env\Scripts\activate

# Activate — macOS / Linux
source fastapi-env/bin/activate

Install FastAPI and Uvicorn. Uvicorn is the ASGI server that runs your app. These versions are current as of June 2026.

pip install "fastapi==0.115.0" "uvicorn[standard]==0.34.0"
ⓘ Version note: Adding [standard] to Uvicorn installs optional dependencies (uvloop, httptools). These improve throughput by 20–40% on Linux. Not required, but worth the 30 seconds.

2. Your First Endpoint

Create main.py and write a minimal app:

from fastapi import FastAPI

app = FastAPI(
    title="Task Manager API",
    description="A simple task management REST API",
    version="1.0.0",
)


@app.get("/")
def read_root():
    return {"message": "Task Manager API is running"}


@app.get("/hello/{name}")
def say_hello(name: str):
    return {"greeting": f"Hello, {name}!"}

Start the server with auto-reload enabled. The --reload flag restarts Uvicorn whenever you save changes to .py files.

uvicorn main:app --reload

Visit http://127.0.0.1:8000 for the JSON response. Navigate to http://127.0.0.1:8000/docs for the auto-generated Swagger UI. FastAPI built this from the type annotations and docstrings without a single line of YAML.

⚠ Watch out: --reload watches Python files for changes. It does NOT watch template files, configuration files, or anything outside the project directory. If you edit requirements.txt, restart manually.

3. CRUD Operations

Build a task manager with create, read, update, and delete endpoints. Start with the data model, then add each operation.

Define the Data Model

Pydantic models are the foundation of FastAPI. They validate request bodies, serialize responses, and feed into the OpenAPI documentation.

from pydantic import BaseModel
from datetime import datetime


class TaskCreate(BaseModel):
    """Schema for creating a new task."""
    title: str
    description: str = ""
    priority: int = 1  # 1-low, 2-medium, 3-high


class Task(TaskCreate):
    """Schema for a task that exists in the system (has an ID and timestamp)."""
    id: int
    created_at: datetime
    completed: bool = False

Two models instead of one. TaskCreate is the input schema (no id, no timestamp). Task is the output schema with all fields. Separating these prevents clients from injecting IDs and keeps the API contract clean.

Design note: The tutorial uses an in-memory dict for storage. That is fine for learning but not for production. The Next Steps section covers database options.

Storage Layer

from fastapi import FastAPI, HTTPException

app = FastAPI(
    title="Task Manager API",
    version="1.0.0",
)

# In-memory storage (replace with SQLAlchemy or SQLModel for production)
tasks_db: dict[int, Task] = {}
_next_id: int = 1


def _get_next_id() -> int:
    global _next_id
    task_id = _next_id
    _next_id += 1
    return task_id

Create — POST /tasks

@app.post("/tasks", response_model=Task, status_code=201)
def create_task(task: TaskCreate):
    new_task = Task(
        id=_get_next_id(),
        created_at=datetime.now(),
        **task.model_dump(),
    )
    tasks_db[new_task.id] = new_task
    return new_task

response_model=Task tells FastAPI to serialize the return value through the Task model, stripping any extra fields and adding a type safety layer. status_code=201 returns the HTTP 201 Created status instead of the default 200.

Read All — GET /tasks

@app.get("/tasks", response_model=list[Task])
def list_tasks(
    completed: bool | None = None,
    priority: int | None = None,
):
    result = list(tasks_db.values())
    if completed is not None:
        result = [t for t in result if t.completed == completed]
    if priority is not None:
        result = [t for t in result if t.priority == priority]
    return result

Query parameters ?completed=true and ?priority=3 let clients filter results. FastAPI handles the type conversion and validation for free.

Read One — GET /tasks/{task_id}

@app.get("/tasks/{task_id}", response_model=Task)
def get_task(task_id: int):
    if task_id not in tasks_db:
        raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
    return tasks_db[task_id]

HTTPException is FastAPI's standard way to return error responses. Setting status_code=404 returns a proper HTTP 404 with a JSON body.

Update — PUT /tasks/{task_id}

@app.put("/tasks/{task_id}", response_model=Task)
def update_task(task_id: int, task_update: TaskCreate):
    if task_id not in tasks_db:
        raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
    existing = tasks_db[task_id]
    updated = Task(
        id=task_id,
        created_at=existing.created_at,
        completed=existing.completed,
        **task_update.model_dump(),
    )
    tasks_db[task_id] = updated
    return updated

Using PUT (full replacement) rather than PATCH (partial update) keeps the logic simple. The existing id, created_at, and completed fields are preserved while all other fields update from the request body.

Delete — DELETE /tasks/{task_id}

@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
    if task_id not in tasks_db:
        raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
    del tasks_db[task_id]
    return {"detail": f"Task {task_id} deleted"}

Run uvicorn main:app --reload and test all endpoints at http://127.0.0.1:8000/docs. The Swagger UI shows every endpoint, its parameters, expected request bodies, and response schemas.

4. Testing with TestClient

FastAPI includes TestClient (built on httpx) for writing tests without starting a server. Install the testing dependency:

pip install httpx pytest

Create test_main.py:

import pytest
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)


class TestRoot:
    def test_returns_200(self):
        response = client.get("/")
        assert response.status_code == 200
        assert "message" in response.json()

    def test_returns_json_content_type(self):
        response = client.get("/")
        assert response.headers["content-type"] == "application/json"


class TestCreateTask:
    def test_creates_and_returns_task(self):
        payload = {"title": "Learn FastAPI", "description": "Follow the guide"}
        response = client.post("/tasks", json=payload)
        assert response.status_code == 201
        data = response.json()
        assert data["title"] == "Learn FastAPI"
        assert data["id"] == 1

    def test_sets_default_priority(self):
        response = client.post("/tasks", json={"title": "Minimal task"})
        assert response.json()["priority"] == 1

    def test_empty_title_fails(self):
        response = client.post("/tasks", json={"title": ""})
        assert response.status_code == 422  # Pydantic validation error


class TestGetTask:
    def test_returns_404_for_missing_task(self):
        response = client.get("/tasks/99999")
        assert response.status_code == 404


class TestDeleteTask:
    def test_deletes_existing_task(self):
        resp = client.post("/tasks", json={"title": "To delete"})
        task_id = resp.json()["id"]
        delete_resp = client.delete(f"/tasks/{task_id}")
        assert delete_resp.status_code == 200

    def test_delete_missing_task_returns_404(self):
        response = client.delete("/tasks/99999")
        assert response.status_code == 404

Run the tests:

pytest test_main.py -v

The TestClient routes requests through the FastAPI app directly, no network stack involved. Tests run fast and the result is deterministic.

5. Common Pitfalls

Every framework has sharp edges. These are the ones that catch people coming to FastAPI for the first time.

5.1 Mixing sync and async incorrectly

FastAPI supports both def (sync) and async def (async) route handlers. Mixing them incorrectly causes performance degradation.

# WRONG: calling a blocking function inside an async route
@app.get("/wrong")
async def wrong_way():
    time.sleep(5)  # Blocks the entire event loop
    return {"done": True}

# CORRECT: use sync route OR run in threadpool
@app.get("/correct")
def correct_way():  # sync route — FastAPI runs it in a threadpool
    time.sleep(5)
    return {"done": True}

Rule of thumb: Use async def only when calling other async-aware libraries (aiohttp, asyncpg, httpx.AsyncClient). Use def for everything else. FastAPI runs sync handlers in a threadpool so they do not block async routes.

5.2 Forgetting response_model

Without response_model, FastAPI serializes whatever you return. If you return an ORM object with internal attributes (like _sa_instance_state), those leak into the response.

# WRONG: leaks internal state
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    return tasks_db[task_id]  # Could include unintended fields

# CORRECT: response_model filters the output
@app.get("/tasks/{task_id}", response_model=Task)
def get_task(task_id: int):
    return tasks_db[task_id]  # Only Task fields appear in response

5.3 CORS configuration missing

If a browser frontend calls your API, you need CORS middleware. Without it, browsers block cross-origin requests and you will see No 'Access-Control-Allow-Origin' header errors.

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],  # Your frontend origin
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Set allow_origins to specific origins in production. Using ["*"] is convenient during development but a security liability when deployed.

5.4 Path parameter type mismatch

FastAPI converts path parameters based on type annotations, but that does not catch every edge case.

@app.get("/tasks/{task_id}")
def get_task(task_id: int):  # Works: /tasks/123
    ...

@app.get("/tasks/{task_id}")
def get_task(task_id: int):  # Fails: /tasks/abc — returns 422
    ...

A request to /tasks/abc returns a 422 Unprocessable Entity error because "abc" cannot convert to int. This is correct behavior, but the error message can be generic. Consider a custom exception handler if your API needs friendlier validation messages.

6. Performance Context

FastAPI sits near the top of Python web framework benchmarks, but "near the top of Python" is not the same as "fast among all frameworks."

Framework Requests/sec (single process) Latency (p50) Notes
FastAPI (Uvicorn) ~30,000 ~3ms With uvloop + httptools on Linux
Flask (Gunicorn, 4 workers) ~4,000 ~25ms Sync only, WSGI baseline
Django (Gunicorn, 4 workers) ~2,500 ~40ms Full-stack, ORM overhead
Express.js ~35,000 ~2ms Node.js baseline for comparison
Go (net/http) ~80,000 ~1ms Compiled language baseline

Source: TechEmpower Web Framework Benchmarks, Round 22 (2024). Numbers are approximate and depend on hardware, workload, and configuration.

Takeaway: FastAPI is fast enough for the vast majority of use cases. If a single Python process serves 30K requests per second and your API handles 100 requests per second, the bottleneck is elsewhere. Before micro-optimizing framework choice, profile the database queries and external API calls first.

7. Next Steps

Add a database

SQLModel (by the same author as FastAPI) combines SQLAlchemy and Pydantic, giving you database models that double as API schemas. For smaller projects, SQLite with aiosqlite works well. For anything that needs concurrent writes, PostgreSQL with asyncpg.

pip install sqlmodel
# Then replace the in-memory dict with SQLModel session queries

Add authentication

python-jose handles JWT creation and verification. passlib handles password hashing. FastAPI's dependency injection system makes auth middleware clean.

pip install python-jose[cryptography] passlib[bcrypt] python-multipart

Deploy

Uvicorn behind a reverse proxy (Nginx, Caddy) is the standard pattern. For containerized deployments, Docker with a multi-stage build keeps image sizes small. Platforms like Railway, Render, and Fly.io offer free tiers that handle FastAPI apps well.

# Production command (no --reload)
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

Official documentation at fastapi.tiangolo.com covers these topics in depth.

Frequently Asked Questions

Is FastAPI suitable for production?

Netflix, Uber, and Microsoft use it in production. FastAPI is built on Starlette and Uvicorn, both of which have been stable for years. The framework hit version 0.100 in 2024 and has not had a backward-incompatible release since.

FastAPI vs Flask — which should I pick?

FastAPI includes async support, automatic OpenAPI docs, and request validation without additional libraries. Flask requires extensions for each of these (Flask-RESTX, marshmallow, etc.). For new API projects, FastAPI is the default choice unless you need Flask's ecosystem of extensions.

Do I need async Python to use FastAPI?

No. Every endpoint in this guide uses synchronous def. FastAPI runs these in a threadpool. Async is an optimization you add when profiling shows it matters.

When should I NOT use FastAPI?

If you need server-rendered HTML with a built-in admin panel, Django is a better fit. If you need a full-stack SSR framework with built-in auth and ORM, consider Django or Laravel. FastAPI is purpose-built for APIs; it does not include a template engine or admin interface.