Intermediate

Building a REST API is one of the most common tasks in modern software development, and FastAPI has quickly become the go-to Python framework for doing it. If you have been using Flask or Django REST Framework and wondered whether there is a faster, more modern alternative with built-in data validation and automatic documentation, FastAPI is your answer.

You will need Python 3.8 or later, plus two packages: fastapi and uvicorn. Install them with pip install fastapi uvicorn. FastAPI uses standard Python type hints for request validation and automatic OpenAPI documentation, so everything you already know about type annotations transfers directly.

This guide walks you through building a complete REST API from scratch: defining routes, handling path and query parameters, validating request bodies with Pydantic models, returning proper HTTP status codes, and running your API with Uvicorn. By the end you will have a fully functional CRUD API for managing a collection of books.

FastAPI in 30 Seconds: Quick Example

Here is the simplest possible FastAPI application. Save it and run it to see your first API endpoint in action.

# quick_example.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI!"}

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

Run it with: uvicorn quick_example:app --reload

Output (visiting http://127.0.0.1:8000/):

{"message": "Hello, FastAPI!"}

Output (visiting http://127.0.0.1:8000/items/42?q=search):

{"item_id": 42, "query": "search"}

Notice how item_id: int automatically validates that the path parameter is an integer. If you visit /items/hello, FastAPI returns a 422 validation error without you writing any validation code. The --reload flag makes Uvicorn restart when you change code, which is perfect for development.

What Is FastAPI and Why Use It?

FastAPI is a modern Python web framework for building APIs. It was created by Sebastian Ramirez and released in 2018. The framework is built on top of Starlette (for the web parts) and Pydantic (for data validation), combining the best of both into a developer-friendly experience.

FeatureFastAPIFlaskDjango REST
Async supportNative (async/await)Limited (via extensions)Limited
Data validationBuilt-in (Pydantic)Manual or extensionsSerializers
Auto documentationSwagger + ReDocManual or extensionsBrowsable API
Type hintsRequired (powers validation)OptionalOptional
PerformanceVery fast (ASGI)Moderate (WSGI)Moderate (WSGI)
Learning curveLowLowMedium-High

The biggest practical advantage is that FastAPI uses your type hints to generate request validation, response serialization, and API documentation automatically. You write the type annotations you would write anyway, and the framework does the rest.

FastAPI REST API architecture design
Good API design is like good plumbing — nobody notices until it breaks.

Setting Up Your FastAPI Project

Let us set up a proper project structure. Create a new directory and install the dependencies.

# setup_commands.sh
mkdir fastapi-books && cd fastapi-books
pip install fastapi uvicorn

Create the following file structure. We will build this up step by step.

# project_structure.txt
fastapi-books/
    main.py          # Application entry point
    models.py        # Pydantic models for request/response
    database.py      # In-memory database (for simplicity)

Defining Data Models with Pydantic

Start by defining what a “book” looks like using Pydantic models. These models handle both validation and serialization.

# models.py
from pydantic import BaseModel, Field
from typing import Optional

class BookCreate(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    author: str = Field(..., min_length=1, max_length=100)
    year: int = Field(..., ge=1000, le=2030)
    isbn: Optional[str] = Field(None, pattern=r"^\d{10}(\d{3})?$")

class BookResponse(BaseModel):
    id: int
    title: str
    author: str
    year: int
    isbn: Optional[str] = None

class BookUpdate(BaseModel):
    title: Optional[str] = Field(None, min_length=1, max_length=200)
    author: Optional[str] = Field(None, min_length=1, max_length=100)
    year: Optional[int] = Field(None, ge=1000, le=2030)
    isbn: Optional[str] = Field(None, pattern=r"^\d{10}(\d{3})?$")

The Field function adds validation constraints: min_length, max_length, ge (greater than or equal), and pattern (regex). The ... means the field is required. Optional[str] = None means the field is optional with a default of None.

In-Memory Database

# database.py
from models import BookResponse

books_db: dict[int, dict] = {}
next_id: int = 1

def get_next_id() -> int:
    global next_id
    current = next_id
    next_id += 1
    return current

Building CRUD Endpoints

Now let us build the full API with Create, Read, Update, and Delete operations.

# main.py
from fastapi import FastAPI, HTTPException, Query
from models import BookCreate, BookResponse, BookUpdate
from database import books_db, get_next_id

app = FastAPI(
    title="Books API",
    description="A simple REST API for managing books",
    version="1.0.0",
)

@app.post("/books", response_model=BookResponse, status_code=201)
def create_book(book: BookCreate):
    book_id = get_next_id()
    book_data = {"id": book_id, **book.model_dump()}
    books_db[book_id] = book_data
    return book_data

@app.get("/books", response_model=list[BookResponse])
def list_books(
    skip: int = Query(0, ge=0),
    limit: int = Query(10, ge=1, le=100),
    author: str = Query(None),
):
    results = list(books_db.values())
    if author:
        results = [b for b in results if author.lower() in b["author"].lower()]
    return results[skip : skip + limit]

@app.get("/books/{book_id}", response_model=BookResponse)
def get_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    return books_db[book_id]

@app.put("/books/{book_id}", response_model=BookResponse)
def update_book(book_id: int, book: BookUpdate):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    update_data = book.model_dump(exclude_unset=True)
    books_db[book_id].update(update_data)
    return books_db[book_id]

@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    del books_db[book_id]

Each endpoint uses type hints to define what it accepts and returns. The response_model parameter tells FastAPI to validate and serialize the response using that Pydantic model. status_code=201 sets the HTTP status for successful creation. HTTPException returns proper error responses with the right status codes.

Building FastAPI endpoints
Four HTTP verbs, infinite ways to get the status codes wrong.

Path Parameters, Query Parameters, and Request Bodies

FastAPI distinguishes between three types of input automatically based on where they appear in your function signature.

# parameters_demo.py
from fastapi import FastAPI, Query, Path

app = FastAPI()

@app.get("/users/{user_id}/posts")
def get_user_posts(
    user_id: int = Path(..., ge=1, description="The ID of the user"),
    page: int = Query(1, ge=1, description="Page number"),
    per_page: int = Query(10, ge=1, le=50, description="Items per page"),
    sort_by: str = Query("date", pattern="^(date|title|likes)$"),
):
    return {
        "user_id": user_id,
        "page": page,
        "per_page": per_page,
        "sort_by": sort_by,
        "posts": [f"Post {i}" for i in range(1, per_page + 1)],
    }

Output (GET /users/5/posts?page=2&sort_by=likes):

{
  "user_id": 5,
  "page": 2,
  "per_page": 10,
  "sort_by": "likes",
  "posts": ["Post 1", "Post 2", "Post 3", ...]
}

The rules are simple: if a parameter name matches a path variable in the URL template (like {user_id}), it is a path parameter. If the parameter has a default value or is annotated with Query(), it is a query parameter. If the parameter is a Pydantic model, it is parsed from the request body.

Error Handling

FastAPI provides clean error handling through exceptions and custom exception handlers.

# error_handling.py
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

app = FastAPI()

class BookNotFoundError(Exception):
    def __init__(self, book_id: int):
        self.book_id = book_id

@app.exception_handler(BookNotFoundError)
async def book_not_found_handler(request: Request, exc: BookNotFoundError):
    return JSONResponse(
        status_code=404,
        content={"error": "not_found", "detail": f"Book {exc.book_id} does not exist"},
    )

@app.get("/books/{book_id}")
def get_book(book_id: int):
    books = {1: "Python Crash Course", 2: "Fluent Python"}
    if book_id not in books:
        raise BookNotFoundError(book_id)
    return {"id": book_id, "title": books[book_id]}

Output (GET /books/99):

{"error": "not_found", "detail": "Book 99 does not exist"}

Async Endpoints

FastAPI supports both synchronous and asynchronous endpoint functions. Use async def when your endpoint does I/O operations like database queries or HTTP requests.

# async_demo.py
import asyncio
from fastapi import FastAPI

app = FastAPI()

@app.get("/sync")
def sync_endpoint():
    return {"type": "synchronous"}

@app.get("/async")
async def async_endpoint():
    await asyncio.sleep(0.1)
    return {"type": "asynchronous"}

Regular def functions run in a thread pool, so they do not block other requests. async def functions run on the event loop and should use await for any I/O operations. If your function does not use await, use regular def — there is no benefit to making it async.

Request validation in FastAPI
Pydantic validates your data so your database doesn’t have to file a complaint.

Automatic API Documentation

One of FastAPI’s best features is automatic API documentation. When you run your application, visit these URLs to see interactive documentation:

# documentation_urls.txt
Swagger UI:  http://127.0.0.1:8000/docs
ReDoc:       http://127.0.0.1:8000/redoc
OpenAPI JSON: http://127.0.0.1:8000/openapi.json

The Swagger UI lets you test every endpoint directly in the browser. It reads your type hints, Pydantic models, and docstrings to generate accurate request/response schemas. This means your documentation is always in sync with your code — no manual updates needed.

Real-Life Example: Complete Books API

Deploying FastAPI application
It works on localhost. Now make it work everywhere else.

Let us put everything together into a single, runnable file that demonstrates the complete CRUD workflow.

# books_api.py
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
from typing import Optional

app = FastAPI(title="Books API", version="1.0.0")

# In-memory database
books_db: dict[int, dict] = {}
next_id = 1

class BookCreate(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    author: str = Field(..., min_length=1, max_length=100)
    year: int = Field(..., ge=1000, le=2030)

class BookResponse(BaseModel):
    id: int
    title: str
    author: str
    year: int

class BookUpdate(BaseModel):
    title: Optional[str] = None
    author: Optional[str] = None
    year: Optional[int] = None

@app.post("/books", response_model=BookResponse, status_code=201)
def create_book(book: BookCreate):
    global next_id
    book_data = {"id": next_id, **book.model_dump()}
    books_db[next_id] = book_data
    next_id += 1
    return book_data

@app.get("/books", response_model=list[BookResponse])
def list_books(skip: int = Query(0, ge=0), limit: int = Query(10, ge=1, le=100)):
    return list(books_db.values())[skip : skip + limit]

@app.get("/books/{book_id}", response_model=BookResponse)
def get_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(404, "Book not found")
    return books_db[book_id]

@app.put("/books/{book_id}", response_model=BookResponse)
def update_book(book_id: int, book: BookUpdate):
    if book_id not in books_db:
        raise HTTPException(404, "Book not found")
    books_db[book_id].update(book.model_dump(exclude_unset=True))
    return books_db[book_id]

@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
    if book_id not in books_db:
        raise HTTPException(404, "Book not found")
    del books_db[book_id]

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Testing with curl:

# Create a book
curl -X POST http://127.0.0.1:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Python Crash Course", "author": "Eric Matthes", "year": 2023}'

# Response: {"id": 1, "title": "Python Crash Course", "author": "Eric Matthes", "year": 2023}

# List all books
curl http://127.0.0.1:8000/books
# Response: [{"id": 1, "title": "Python Crash Course", ...}]

# Update a book
curl -X PUT http://127.0.0.1:8000/books/1 \
  -H "Content-Type: application/json" \
  -d '{"year": 2024}'

# Delete a book
curl -X DELETE http://127.0.0.1:8000/books/1

Run this with python books_api.py and test the endpoints using curl, httpie, or the built-in Swagger UI at /docs. You can extend this example by adding a real database (SQLAlchemy or SQLModel), authentication, pagination headers, and response caching.

Frequently Asked Questions

Can I migrate my Flask app to FastAPI?

Yes, and the migration is usually straightforward. Flask routes map 1:1 to FastAPI routes. The main changes are adding type hints to your parameters and converting request parsing from request.json to Pydantic models. FastAPI has a migration guide in its documentation.

Is FastAPI production-ready?

Yes. FastAPI is used in production by Microsoft, Netflix, Uber, and many other companies. Deploy it with Uvicorn behind a reverse proxy like Nginx, or use Gunicorn with Uvicorn workers for multi-process setups.

How do I connect FastAPI to a database?

Use SQLAlchemy 2.0 with async sessions, or use SQLModel (created by FastAPI’s author) which combines SQLAlchemy and Pydantic. For simple projects, you can also use databases with raw SQL queries. FastAPI’s documentation has complete examples for each approach.

How do I add authentication?

FastAPI has built-in support for OAuth2 with JWT tokens. Use fastapi.security.OAuth2PasswordBearer for token-based auth, or implement API key authentication with custom dependencies. See our companion article on FastAPI authentication for a complete walkthrough.

How do I test FastAPI endpoints?

Use TestClient from fastapi.testclient (which wraps httpx). It lets you make requests to your API without running a server, making unit tests fast and reliable.

Conclusion

FastAPI makes building REST APIs in Python fast and enjoyable. The combination of automatic validation through type hints, built-in async support, and interactive Swagger documentation means you spend less time on boilerplate and more time on your application logic. Start with the books API example, then extend it with a real database and authentication.

For the complete documentation, visit fastapi.tiangolo.com.

Minimal FastAPI App

FastAPI is built on Starlette + Pydantic. The decorator-based router turns a Python function into an HTTP endpoint:

# pip install fastapi uvicorn

# main.py
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="My API", version="1.0.0")

class User(BaseModel):
    id: int
    name: str
    email: str

users = []

@app.get("/")
def root():
    return {"message": "Hello"}

@app.get("/users", response_model=list[User])
def list_users():
    return users

@app.post("/users", response_model=User, status_code=201)
def create_user(user: User):
    users.append(user)
    return user

# Run: uvicorn main:app --reload --port 8000

Visit http://localhost:8000/docs and you get interactive API docs generated from the Pydantic models — no separate documentation step.

Path Parameters and Query Parameters

FastAPI infers parameter type from function signatures. Path params become path components; query params become URL queries:

from fastapi import HTTPException

@app.get("/users/{user_id}")
def get_user(user_id: int):    # path param — validated as int
    for u in users:
        if u.id == user_id:
            return u
    raise HTTPException(status_code=404, detail="User not found")

@app.get("/search")
def search(q: str, limit: int = 10, sort: str = "id"):
    # q is required (no default), limit and sort optional with defaults
    return {"query": q, "limit": limit, "sort": sort}

Type annotations drive validation. A request to /users/abc automatically returns 422 with “value is not a valid integer”.

Dependency Injection

FastAPI’s Depends() wires up shared dependencies — DB sessions, auth checks, common query params — without global state:

from fastapi import Depends, HTTPException, Header
from typing import Annotated

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

async def verify_token(authorization: Annotated[str, Header()] = ""):
    token = authorization.removeprefix("Bearer ")
    if not is_valid(token):
        raise HTTPException(status_code=401, detail="Invalid token")
    return get_user_from_token(token)

@app.get("/profile")
def profile(
    user = Depends(verify_token),
    db = Depends(get_db),
):
    return db.get_user_full(user.id)

Dependencies can be nested, cached per-request, and made async. The yield pattern handles setup/teardown like a context manager.

Async Endpoints

Use async def for I/O-bound endpoints (database, external APIs). Use plain def for CPU-bound work — FastAPI runs those in a thread pool to avoid blocking the loop:

import httpx

@app.get("/proxy")
async def proxy(url: str):
    async with httpx.AsyncClient() as client:
        resp = await client.get(url)
    return {"status": resp.status_code, "data": resp.text}

@app.post("/render")
def render_pdf(html: str):
    # Synchronous — runs in thread pool
    pdf_bytes = wkhtmltopdf_convert(html)
    return Response(content=pdf_bytes, media_type="application/pdf")

Background Tasks

For fire-and-forget work after a response (sending emails, logging), use BackgroundTasks:

from fastapi import BackgroundTasks

def send_welcome_email(email: str):
    # Slow SMTP call — don't make the user wait
    smtp_send(email, "Welcome!", "...")

@app.post("/signup")
def signup(user: User, background_tasks: BackgroundTasks):
    save_user(user)
    background_tasks.add_task(send_welcome_email, user.email)
    return {"status": "created"}

For heavier work or work that must survive crashes, use Celery instead — BackgroundTasks are in-process only.

Common Pitfalls

  • Sync code in async endpoint. time.sleep() in async def blocks the event loop. Use await asyncio.sleep() or move to def.
  • Forgetting response_model. Without it, FastAPI returns whatever you return — including extra fields. Always declare it for stable API contracts.
  • Mutable global state. users = [] works in dev, breaks under multi-worker uvicorn. Use a database from day one.
  • Async DB session in sync endpoint. If you use SQLAlchemy async, your endpoint must also be async. Mixing types throws confusing errors.
  • Missing dependencies in tests. Use app.dependency_overrides[get_db] = lambda: test_db to swap dependencies without touching production code.

FAQ

Q: FastAPI or Flask?
A: FastAPI for new APIs — async by default, auto-docs, Pydantic validation. Flask for synchronous, template-rendering apps or legacy ecosystems.

Q: How do I deploy FastAPI?
A: uvicorn main:app behind a reverse proxy (nginx, Caddy). For containerized: Docker + uvicorn. For serverless: Mangum adapter for AWS Lambda. Avoid uvicorn --reload in production.

Q: WebSockets in FastAPI?
A: Built in — @app.websocket("/ws"). See FastAPI’s WebSocket docs.

Q: How do I do auth?
A: For tokens, write a Depends(verify_token) dependency. For OAuth/OIDC, use authlib or fastapi-users. Avoid rolling your own crypto.

Q: How do I run sync ORMs (Django, peewee) inside async FastAPI?
A: Either use sync endpoints (FastAPI runs them in a thread pool) or wrap calls in await asyncio.to_thread(sync_func, ...). Don’t fight the framework — pick async or sync per endpoint.

Wrapping Up

FastAPI’s appeal is the combination of speed (Starlette / Uvicorn), type safety (Pydantic), and zero-effort docs (OpenAPI from your Python). For new Python web APIs in 2026, it’s the default choice. Start with the routers and Pydantic models, add dependency injection when you have shared concerns, and reach for BackgroundTasks (or Celery for heavier work) when you need async-after-response.