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.