Introduction
FastAPI is a modern, high-performance web framework for building APIs with Python 3.7+ based on standard Python type hints. Created by Sebastián Ramírez (tiangolo) and first released in December 2018, FastAPI has rapidly become one of the most popular Python web frameworks, earning over 75,000 GitHub stars and adoption by companies like Microsoft, Netflix, Uber, and Samsung.
FastAPI is built on two foundational libraries:
- Starlette — A lightweight ASGI (Asynchronous Server Gateway Interface) framework that provides the core web functionality: routing, request/response handling, WebSocket support, middleware, and background tasks. Starlette gives FastAPI its exceptional performance characteristics.
- Pydantic — A data validation and serialization library that uses Python type annotations to define data schemas. Pydantic powers FastAPI’s automatic request validation, response serialization, and documentation generation. All without writing a single line of validation code manually.
What makes FastAPI stand out from other Python web frameworks is its philosophy: leverage modern Python features — type hints, async/await, and dataclasses — to eliminate boilerplate, catch errors at development time, and automatically generate production-ready API documentation. You write less code, and the code you write is more correct.
The name “FastAPI” refers to two things: the speed of the framework itself (comparable to Node.js and Go in benchmarks) and the speed at which you can develop APIs with it. Sebastián Ramírez estimates that FastAPI increases developer productivity by 200-300% compared to traditional approaches, while simultaneously reducing bugs by about 40% through its type-driven validation system.
This tutorial series will take you from zero to building production-ready APIs with FastAPI. We will cover everything a senior developer needs to know — from basic routing to database integration, authentication, testing, and deployment. By the end, you will have the skills to architect and build real-world applications with FastAPI.
Why FastAPI?
Before diving into code, you need to understand why FastAPI has taken the Python ecosystem by storm. There are five core reasons that make it the framework of choice for modern Python API development.
1. Blazing Fast Performance
FastAPI consistently ranks among the fastest Python web frameworks in independent benchmarks. Built on Starlette and running on Uvicorn (an ASGI server powered by uvloop and httptools), FastAPI achieves throughput numbers that rival Node.js and Go frameworks.
| Framework | Language | Requests/sec (JSON serialization) | Relative Speed |
|---|---|---|---|
| Uvicorn (raw ASGI) | Python | ~70,000 | 1.0x (baseline) |
| FastAPI | Python | ~50,000-60,000 | 0.8x |
| Express.js | Node.js | ~45,000-55,000 | 0.75x |
| Flask (Gunicorn) | Python | ~8,000-12,000 | 0.15x |
| Django (Gunicorn) | Python | ~5,000-8,000 | 0.10x |
| Gin | Go | ~80,000-100,000 | 1.3x |
The performance advantage over Flask and Django comes from two architectural decisions:
- ASGI vs WSGI — FastAPI uses the Asynchronous Server Gateway Interface, which supports non-blocking I/O natively. When your API waits for a database query, file read, or external API call, the server can handle other requests simultaneously. WSGI frameworks like Flask block the entire worker process during I/O operations.
- Uvicorn’s event loop — Uvicorn uses
uvloop, a drop-in replacement for Python’sasyncioevent loop written in Cython. It is 2-4x faster than the default event loop and comparable to the event loop implementations in Node.js and Go.
2. Automatic Interactive Documentation
FastAPI automatically generates interactive API documentation from your code. You get two documentation interfaces out of the box, with zero additional configuration:
- Swagger UI (available at
/docs) — An interactive documentation page where you can read about every endpoint, see request/response schemas, and test endpoints directly from the browser. Click “Try it out,” fill in parameters, hit “Execute,” and see the response immediately. - ReDoc (available at
/redoc) — A cleaner, more readable documentation view that is ideal for sharing with frontend developers, QA teams, or external API consumers. ReDoc generates a three-panel layout with navigation, endpoint details, and request/response examples.
Both documentation interfaces are generated from the OpenAPI (formerly Swagger) schema that FastAPI creates automatically from your route definitions, type hints, and Pydantic models. Every parameter type, validation rule, response model, and example you define in code appears in the docs instantly. There is no separate documentation file to maintain, no YAML spec to write by hand, and no risk of your docs drifting out of sync with your code.
3. Type Safety and Automatic Validation
FastAPI uses Python type hints not just for documentation, but for runtime data validation. When you declare that a parameter is an int, FastAPI will automatically reject requests that send a string. When you define a Pydantic model with constraints, FastAPI validates every incoming request against that model before your handler function ever runs.
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
email: str = Field(..., pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")
age: int = Field(..., ge=13, le=120)
@app.post("/users")
async def create_user(user: UserCreate):
# If we reach this line, the data is guaranteed valid
# FastAPI already validated username length, email format, and age range
return {"message": f"User {user.username} created successfully"}If a client sends {"username": "ab", "email": "not-an-email", "age": 5}, FastAPI returns a detailed 422 Unprocessable Entity response listing every validation error — without you writing a single line of validation code.
4. Native Async Support
FastAPI was built from the ground up for Python’s async/await syntax. Unlike Flask, which bolted on async support years after its initial release, FastAPI treats asynchronous programming as a first-class citizen.
import httpx
from fastapi import FastAPI
app = FastAPI()
@app.get("/weather/{city}")
async def get_weather(city: str):
# Non-blocking HTTP call - server handles other requests while waiting
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://api.weatherapi.com/v1/current.json?q={city}"
)
return response.json()With async, a single server process can handle thousands of concurrent connections. This is critical for modern API backends that aggregate data from multiple services, interact with databases, or serve real-time data over WebSockets.
def instead of async def. FastAPI will run it in a thread pool to avoid blocking the event loop. You can mix sync and async handlers freely in the same application.
5. Developer Productivity
FastAPI dramatically reduces the amount of code you need to write. Features that require dozens of lines in other frameworks — request validation, serialization, documentation, error handling — happen automatically. Consider what you get for free just by adding type hints:
- Request body parsing and validation
- Query parameter parsing and validation
- Path parameter parsing and type conversion
- Response serialization and filtering
- OpenAPI schema generation
- Interactive documentation
- Editor autocompletion and type checking
Your IDE also becomes dramatically more helpful. Because FastAPI uses standard Python type hints, editors like VS Code and PyCharm provide accurate autocompletion, inline error detection, and refactoring support. You catch bugs before running your code, not after deploying it.
FastAPI vs Flask vs Django
Choosing the right Python web framework is one of the most important architectural decisions you will make. Each framework has distinct strengths, and the right choice depends on your project requirements, team experience, and deployment constraints. Here is a detailed comparison.
| Feature | FastAPI | Flask | Django |
|---|---|---|---|
| Released | 2018 | 2010 | 2005 |
| Type | ASGI (async-first) | WSGI (sync, async bolt-on) | WSGI (async views since 3.1) |
| Performance | Very high (~50K req/s) | Moderate (~10K req/s) | Moderate (~7K req/s) |
| Async Support | Native, first-class | Limited (since 2.0) | Partial (views only, ORM sync) |
| Data Validation | Built-in (Pydantic) | Manual or third-party (marshmallow) | Built-in (forms/serializers) |
| API Documentation | Auto-generated (Swagger + ReDoc) | Manual or third-party (flasgger) | Manual or third-party (DRF) |
| ORM | None built-in (use SQLAlchemy, Tortoise) | None built-in (use SQLAlchemy) | Built-in Django ORM |
| Admin Panel | None built-in | None built-in (Flask-Admin) | Built-in, powerful |
| Authentication | OAuth2, JWT via dependencies | Third-party (Flask-Login, JWT) | Built-in (sessions, permissions) |
| Template Engine | Jinja2 (optional) | Jinja2 (built-in) | Django Templates (built-in) |
| Type Hints | Required, core feature | Optional, decorative | Optional, decorative |
| WebSocket Support | Built-in, native | Third-party (Flask-SocketIO) | Django Channels (add-on) |
| Learning Curve | Low-Medium | Low | Medium-High |
| Ecosystem Size | Growing rapidly | Very large, mature | Largest, most mature |
| Best For | APIs, microservices, async backends | Simple APIs, prototypes, learning | Full-stack web apps, CMS, e-commerce |
| Deployment | Uvicorn / Gunicorn + Uvicorn workers | Gunicorn / uWSGI | Gunicorn / uWSGI |
When to Choose FastAPI
- You are building a REST or GraphQL API. FastAPI was purpose-built for APIs. Automatic validation, serialization, and documentation make it the most productive choice for API development.
- Performance matters. If your application handles high concurrency or real-time data (WebSockets, server-sent events, streaming), FastAPI’s async-first architecture delivers throughput that Flask and Django cannot match without significant architectural changes.
- You value type safety. If you prefer catching errors at development time rather than in production, FastAPI’s type-driven approach is ideal. Your IDE becomes a powerful validation tool.
- You need modern async features. Background tasks, WebSocket connections, streaming responses, and concurrent external API calls are natural in FastAPI.
- Your team already uses type hints. If your Python codebase already uses type annotations, FastAPI will feel like a natural extension of how you write code.
When to Choose Flask
- You need maximum flexibility. Flask’s unopinionated nature lets you structure your project however you want. There is no prescribed way to do anything.
- You are prototyping quickly. Flask’s simplicity means you can go from idea to working prototype in minutes. A “Hello World” is 5 lines of code.
- You are building a small web application with templates. If your application serves HTML pages with minimal API surface, Flask’s Jinja2 integration is excellent.
- You need a mature ecosystem. Flask has been around since 2010 and has extensions for virtually everything. Whatever you need, someone has probably built a Flask extension for it.
When to Choose Django
- You are building a full-stack web application. Django’s “batteries included” approach (ORM, admin panel, authentication, forms, migrations) is unmatched for traditional web applications.
- You need a built-in admin interface. Django’s admin panel is one of the most powerful features in any web framework. For content management, back-office tools, and data administration, nothing comes close.
- You want convention over configuration. Django makes many decisions for you. In a large team, this consistency is valuable.
- You are building an e-commerce site or CMS. Django powers sites like Instagram, Pinterest, and Mozilla. Its ecosystem for content-heavy applications is the most mature in Python.
Installation & Setup
Prerequisites
Before installing FastAPI, ensure you have the following:
- Python 3.7 or higher — FastAPI requires Python 3.7+ because it relies on type hints and
async/awaitsyntax. We recommend Python 3.10+ for the best experience (better error messages, structural pattern matching, and improved type hint syntax). - pip — Python’s package installer (included with Python 3.4+).
- A virtual environment — Always use a virtual environment for Python projects. If you are unfamiliar with virtual environments, review our Python Advanced – Virtual Environments & pip tutorial.
Check your Python version:
python3 --version
# Python 3.11.6 (or higher)Creating a Virtual Environment
Create a dedicated directory for your FastAPI project and set up a virtual environment:
# Create project directory
mkdir fastapi-tutorial
cd fastapi-tutorial
# Create virtual environment
python3 -m venv venv
# Activate virtual environment
# On macOS/Linux:
source venv/bin/activate
# On Windows:
# venv\Scripts\activate
# Verify activation (should show venv path)
which pythonInstalling FastAPI and Uvicorn
FastAPI itself is just the framework. You also need an ASGI server to run your application. Uvicorn is the recommended choice.
# Install FastAPI with all optional dependencies
pip install "fastapi[standard]"The fastapi[standard] install includes:
- fastapi — The framework itself
- uvicorn[standard] — The ASGI server with high-performance extras (
uvloop,httptools) - pydantic — Data validation (core dependency)
- starlette — The ASGI toolkit (core dependency)
- python-multipart — Required for form data parsing
- email-validator — Email validation support for Pydantic
- httpx — Async HTTP client (for testing)
- jinja2 — Template engine (optional, for HTML responses)
pip install fastapi uvicorn. The [standard] extras are recommended because they include everything you will need for a real project.
Alternatively, install components individually for more control:
# Minimal installation
pip install fastapi uvicorn
# Add extras as needed
pip install python-multipart # For form data
pip install python-jose[cryptography] # For JWT tokens
pip install passlib[bcrypt] # For password hashing
pip install sqlalchemy # For database ORM
pip install alembic # For database migrationsVerify the installation:
pip show fastapiExpected output:
Name: fastapi
Version: 0.115.x
Summary: FastAPI framework, high performance, easy to learn, fast to code, ready for production
Home-page: https://github.com/fastapi/fastapi
Author-email: Sebastián Ramírez <tiangolo@gmail.com>
License: MIT
Requires: pydantic, starlette, typing-extensionsFreezing Dependencies
Always save your installed packages to a requirements file so your project is reproducible:
# Save current dependencies
pip freeze > requirements.txt
# Later, install from requirements file
pip install -r requirements.txtYour First FastAPI Application
Let us build a “Hello World” application to see FastAPI in action. Create a file called main.py:
from fastapi import FastAPI
# Create the FastAPI application instance
app = FastAPI()
# Define a route using a path operation decorator
@app.get("/")
async def root():
"""Root endpoint that returns a welcome message."""
return {"message": "Hello, World!"}That is it. Four lines of actual code (plus the import) and you have a fully functional API with automatic documentation. Let us break down what each line does:
from fastapi import FastAPI— Import the FastAPI class from the framework.app = FastAPI()— Create an application instance. This is the central object that holds all your routes, middleware, event handlers, and configuration. By convention, this variable is namedapp.@app.get("/")— A path operation decorator. It tells FastAPI that the function below handles GET requests to the path/. The@app.getdecorator registers the function as a handler for HTTP GET requests.async def root()— The path operation function (also called a route handler or endpoint function). It is declared asasyncbecause FastAPI is an async framework. This function runs whenever a GET request hits/.return {"message": "Hello, World!"}— Return a Python dictionary. FastAPI automatically converts this to a JSON response with the correctContent-Type: application/jsonheader and a 200 status code.
Running the Application
Start the development server with Uvicorn:
uvicorn main:app --reloadLet us break down this command:
main— The Python module (file) name,main.pyapp— The FastAPI application instance inside that module--reload— Enable auto-reload. Uvicorn watches your files and automatically restarts the server when you save changes. Use this only in development, never in production.
You should see output like this:
INFO: Will watch for changes in these directories: ['/path/to/fastapi-tutorial']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720] using StatReload
INFO: Started server process [28722]
INFO: Waiting for application startup.
INFO: Application startup complete.Testing Your Endpoint
Open your browser and navigate to http://127.0.0.1:8000. You should see:
{"message": "Hello, World!"}Or test with curl from another terminal:
curl http://127.0.0.1:8000
# {"message":"Hello, World!"}Adding More Endpoints
Let us expand our application with a few more endpoints to demonstrate different features:
from fastapi import FastAPI
app = FastAPI(
title="My First FastAPI App",
description="A tutorial application to learn FastAPI basics",
version="1.0.0"
)
@app.get("/")
async def root():
"""Root endpoint - welcome message."""
return {"message": "Hello, World!", "docs": "/docs"}
@app.get("/health")
async def health_check():
"""Health check endpoint for monitoring."""
return {"status": "healthy"}
@app.get("/items/{item_id}")
async def read_item(item_id: int):
"""Read a single item by its ID.
The item_id parameter is automatically parsed as an integer.
If a non-integer is provided, FastAPI returns a 422 error.
"""
return {"item_id": item_id, "name": f"Item {item_id}"}
@app.get("/search")
async def search_items(q: str, limit: int = 10, offset: int = 0):
"""Search for items with query parameters.
- q: required search query (string)
- limit: optional, defaults to 10
- offset: optional, defaults to 0
"""
return {
"query": q,
"limit": limit,
"offset": offset,
"results": [f"Result for '{q}' #{i}" for i in range(offset, offset + limit)]
}Save the file, and Uvicorn will automatically reload. Try these URLs:
http://127.0.0.1:8000/— Welcome messagehttp://127.0.0.1:8000/health— Health checkhttp://127.0.0.1:8000/items/42— Item lookup (try/items/abcto see validation error)http://127.0.0.1:8000/search?q=python— Search with query parameterhttp://127.0.0.1:8000/search?q=python&limit=5&offset=10— Search with all parameters
--reload flag should only be used during development. In production, run Uvicorn without --reload and with multiple workers: uvicorn main:app --workers 4. We will cover production deployment in detail in the FastAPI – Deployment tutorial.
Automatic Interactive Documentation
One of FastAPI’s most impressive features is automatic API documentation. Without writing a single line of documentation code, you already have two fully interactive documentation interfaces.
Swagger UI (/docs)
Navigate to http://127.0.0.1:8000/docs to access the Swagger UI documentation. This interface provides:
- A list of all endpoints grouped by tags, showing the HTTP method, path, and summary
- Request parameter details — path parameters, query parameters, request bodies, with their types and constraints
- Response schemas — the structure and types of response data
- “Try it out” functionality — click any endpoint, fill in parameters, click “Execute,” and see the actual response from your running server
- cURL commands — after executing a request, Swagger UI shows the equivalent
curlcommand
Here is what happens when you click “Try it out” on the /items/{item_id} endpoint:
- Swagger UI shows an input field for
item_id - You enter a value (e.g.,
42) - Click “Execute”
- Swagger UI sends a real HTTP request to your server
- The response appears below, along with the response code, headers, and the
curlcommand equivalent
ReDoc (/redoc)
Navigate to http://127.0.0.1:8000/redoc for the ReDoc documentation interface. ReDoc presents the same information in a three-panel layout:
- Left panel — Navigation menu with all endpoints
- Center panel — Endpoint details, parameters, and descriptions
- Right panel — Request/response examples and code snippets
ReDoc is often preferred for external-facing API documentation because it is cleaner and more readable. Many teams use Swagger UI for internal development and testing, and ReDoc for public documentation.
The OpenAPI Schema
Both documentation interfaces are powered by the OpenAPI schema, which FastAPI generates automatically. You can access the raw schema at http://127.0.0.1:8000/openapi.json:
curl http://127.0.0.1:8000/openapi.json | python3 -m json.toolThis returns a JSON document following the OpenAPI 3.1 specification. This schema can be used for:
- Code generation — Automatically generate client SDKs in TypeScript, Java, Go, or any language using tools like openapi-generator
- API testing — Import the schema into Postman, Insomnia, or other API testing tools
- Contract testing — Validate that your API conforms to its specification
- API gateways — Configure AWS API Gateway, Kong, or Apigee from the schema
Customizing Documentation
You can enrich the auto-generated documentation by adding metadata to your FastAPI instance and route handlers:
from fastapi import FastAPI
app = FastAPI(
title="Tutorial API",
description="""
## Tutorial API for learning FastAPI
This API demonstrates:
* **CRUD operations** on items
* **User management** with authentication
* **Search functionality** with pagination
### Authentication
Most endpoints require a valid JWT token in the Authorization header.
""",
version="2.0.0",
terms_of_service="https://example.com/terms",
contact={
"name": "API Support",
"url": "https://example.com/support",
"email": "support@example.com",
},
license_info={
"name": "MIT License",
"url": "https://opensource.org/licenses/MIT",
},
)
@app.get(
"/items/{item_id}",
summary="Get a single item",
description="Retrieve an item by its unique integer ID. Returns 404 if not found.",
response_description="The requested item",
tags=["Items"],
deprecated=False,
)
async def read_item(item_id: int):
return {"item_id": item_id}The tags parameter groups endpoints in the documentation. The summary appears as the endpoint title, and description provides detailed explanation. You can also use docstrings on your handler functions — FastAPI will use them as the endpoint description if no explicit description parameter is provided.
app = FastAPI(docs_url=None, redoc_url=None). You can also change their URLs: app = FastAPI(docs_url="/api/docs", redoc_url="/api/redoc").
Python Type Hints Primer
Type hints are the foundation of FastAPI. If you are not comfortable with Python type hints, this section will give you everything you need to know. If you are already experienced with type hints, you can skip to the next section.
What Are Type Hints?
Type hints (also called type annotations) were introduced in Python 3.5 via PEP 484. They allow you to declare the expected types of variables, function parameters, and return values. Python itself does not enforce these types at runtime — it is still a dynamically typed language. But tools like mypy, Pyright, and FastAPI use them extensively.
# Without type hints
def greet(name):
return f"Hello, {name}"
# With type hints
def greet(name: str) -> str:
return f"Hello, {name}"The type-hinted version tells us (and our tools) that name should be a string and the function returns a string. The runtime behavior is identical.
Basic Types
# Primitive types
name: str = "FastAPI"
age: int = 30
price: float = 19.99
is_active: bool = True
# None type
result: None = None
# Function with type hints
def calculate_total(price: float, quantity: int, tax_rate: float = 0.08) -> float:
"""Calculate total price with tax."""
subtotal = price * quantity
return subtotal * (1 + tax_rate)Collection Types
For collections, you specify both the container type and the type of elements it contains:
# Python 3.9+ syntax (recommended)
names: list[str] = ["Alice", "Bob", "Charlie"]
scores: dict[str, int] = {"Alice": 95, "Bob": 87}
unique_ids: set[int] = {1, 2, 3}
coordinates: tuple[float, float] = (40.7128, -74.0060)
# Python 3.7-3.8 syntax (use typing module)
from typing import List, Dict, Set, Tuple
names: List[str] = ["Alice", "Bob", "Charlie"]
scores: Dict[str, int] = {"Alice": 95, "Bob": 87}
unique_ids: Set[int] = {1, 2, 3}
coordinates: Tuple[float, float] = (40.7128, -74.0060)list[str] instead of List[str]). FastAPI works with both syntaxes. We recommend the newer syntax if your project targets Python 3.9+.
Optional and Union Types
# Python 3.10+ syntax
def find_user(user_id: int) -> dict | None:
"""Returns user dict or None if not found."""
...
def process_input(value: str | int) -> str:
"""Accepts either a string or integer."""
return str(value)
# Python 3.7-3.9 syntax
from typing import Optional, Union
def find_user(user_id: int) -> Optional[dict]:
"""Returns user dict or None if not found."""
...
def process_input(value: Union[str, int]) -> str:
"""Accepts either a string or integer."""
return str(value)Optional[X] is equivalent to Union[X, None] or X | None. It means the value can be of type X or None.
Why Type Hints Matter in FastAPI
In most Python code, type hints are informational — they help your IDE and static analysis tools but have no runtime effect. FastAPI changes this equation. FastAPI uses type hints at runtime to:
- Parse request data — If you declare a path parameter as
int, FastAPI converts the string from the URL to an integer - Validate input — If conversion fails (e.g., “abc” cannot become an
int), FastAPI returns a 422 validation error - Generate documentation — The OpenAPI schema reflects your type hints, showing exact parameter types and constraints
- Serialize responses — Response models filter and format output based on type annotations
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(
user_id: int, # Path param: parsed as int, 422 if invalid
include_posts: bool = False, # Query param: parsed as bool, default False
limit: int = 10, # Query param: parsed as int, default 10
) -> dict: # Return type hint for documentation
"""
FastAPI uses these type hints to:
1. Extract user_id from the URL path and convert to int
2. Extract include_posts and limit from query string
3. Validate all types and return 422 on failure
4. Document all parameters with their types in /docs
"""
user = {"id": user_id, "name": f"User {user_id}"}
if include_posts:
user["posts"] = [{"title": f"Post {i}"} for i in range(limit)]
return userWith this single function definition, FastAPI knows that /users/abc is invalid (not an integer), /users/42?include_posts=true&limit=5 is valid, and the docs should show all three parameters with their types and defaults.
Path Operations
In FastAPI, a “path operation” is the combination of an HTTP method and a URL path. This is the fundamental building block of any API. Let us explore how FastAPI handles HTTP methods and how to use path operation decorators.
HTTP Methods Overview
HTTP defines several request methods (also called “verbs”) that indicate the desired action to perform on a resource:
| Method | Purpose | Has Request Body? | Idempotent? | Safe? | FastAPI Decorator |
|---|---|---|---|---|---|
| GET | Read / retrieve a resource | No (typically) | Yes | Yes | @app.get() |
| POST | Create a new resource | Yes | No | No | @app.post() |
| PUT | Replace a resource entirely | Yes | Yes | No | @app.put() |
| PATCH | Partially update a resource | Yes | No | No | @app.patch() |
| DELETE | Remove a resource | No (typically) | Yes | No | @app.delete() |
| OPTIONS | Describe communication options | No | Yes | Yes | @app.options() |
| HEAD | GET without response body | No | Yes | Yes | @app.head() |
Path Operation Decorators in Practice
Here is a complete CRUD (Create, Read, Update, Delete) example for a “books” resource:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
# In-memory "database" for demonstration
books_db: dict[int, dict] = {}
next_id: int = 1
class BookCreate(BaseModel):
title: str
author: str
year: int
isbn: str | None = None
class BookUpdate(BaseModel):
title: str | None = None
author: str | None = None
year: int | None = None
isbn: str | None = None
# CREATE - POST
@app.post("/books", status_code=201)
async def create_book(book: BookCreate):
"""Create a new book."""
global next_id
book_data = book.model_dump()
book_data["id"] = next_id
books_db[next_id] = book_data
next_id += 1
return book_data
# READ ALL - GET
@app.get("/books")
async def list_books(skip: int = 0, limit: int = 20):
"""List all books with pagination."""
all_books = list(books_db.values())
return all_books[skip : skip + limit]
# READ ONE - GET
@app.get("/books/{book_id}")
async def get_book(book_id: int):
"""Get a single book by ID."""
if book_id not in books_db:
raise HTTPException(status_code=404, detail="Book not found")
return books_db[book_id]
# FULL UPDATE - PUT
@app.put("/books/{book_id}")
async def replace_book(book_id: int, book: BookCreate):
"""Replace a book entirely."""
if book_id not in books_db:
raise HTTPException(status_code=404, detail="Book not found")
book_data = book.model_dump()
book_data["id"] = book_id
books_db[book_id] = book_data
return book_data
# PARTIAL UPDATE - PATCH
@app.patch("/books/{book_id}")
async def update_book(book_id: int, book: BookUpdate):
"""Partially update a book."""
if book_id not in books_db:
raise HTTPException(status_code=404, detail="Book not found")
stored_book = books_db[book_id]
update_data = book.model_dump(exclude_unset=True)
stored_book.update(update_data)
return stored_book
# DELETE - DELETE
@app.delete("/books/{book_id}", status_code=204)
async def delete_book(book_id: int):
"""Delete a book."""
if book_id not in books_db:
raise HTTPException(status_code=404, detail="Book not found")
del books_db[book_id]
return NoneKey patterns to notice:
status_code=201on the POST handler — returns 201 Created instead of the default 200 OKstatus_code=204on the DELETE handler — returns 204 No Content (standard for successful deletes)HTTPException— FastAPI’s built-in exception class for returning HTTP error responses. Thestatus_codeanddetailare sent to the client as a JSON error response- Separate models for create vs update —
BookCreaterequires all fields;BookUpdatemakes all fields optional for partial updates model_dump(exclude_unset=True)— Only includes fields that were explicitly sent in the request, so PATCH works correctly
Path Parameters
Path parameters are variables embedded in the URL path. FastAPI extracts them automatically:
from fastapi import FastAPI
from enum import Enum
app = FastAPI()
# Basic path parameter with type validation
@app.get("/users/{user_id}")
async def get_user(user_id: int):
return {"user_id": user_id}
# Multiple path parameters
@app.get("/organizations/{org_id}/teams/{team_id}")
async def get_team(org_id: int, team_id: int):
return {"org_id": org_id, "team_id": team_id}
# Enum path parameter - restricts to predefined values
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
# Only accepts "alexnet", "resnet", or "lenet"
# Returns 422 for any other value
return {"model": model_name, "message": f"Selected {model_name.value}"}
# File path parameter (captures the rest of the path)
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
# Matches /files/home/user/document.txt
return {"file_path": file_path}Query Parameters
Any function parameter that is not a path parameter is automatically treated as a query parameter:
from fastapi import FastAPI, Query
app = FastAPI()
# Basic query parameters
@app.get("/items")
async def list_items(
skip: int = 0, # Optional with default
limit: int = 10, # Optional with default
q: str | None = None # Optional, can be None
):
# URL: /items?skip=20&limit=5&q=search_term
return {"skip": skip, "limit": limit, "q": q}
# Required query parameter (no default value)
@app.get("/search")
async def search(q: str):
# URL: /search?q=python (q is required)
return {"query": q}
# Advanced query validation with Query()
@app.get("/products")
async def list_products(
q: str | None = Query(
default=None,
min_length=3,
max_length=50,
pattern=r"^[a-zA-Z0-9\s]+$",
title="Search query",
description="Search products by name. Minimum 3 characters.",
examples=["laptop", "wireless mouse"],
),
page: int = Query(default=1, ge=1, description="Page number (starting from 1)"),
size: int = Query(default=20, ge=1, le=100, description="Items per page (max 100)"),
sort_by: str = Query(default="name", pattern=r"^(name|price|rating)$"),
):
return {
"query": q,
"page": page,
"size": size,
"sort_by": sort_by,
}The Query() function provides additional validation and metadata. The ge (greater than or equal), le (less than or equal), gt (greater than), lt (less than) constraints work for numeric parameters. The min_length, max_length, and pattern constraints work for string parameters. All of these constraints appear automatically in the generated documentation.
Request & Response Cycle
Understanding how FastAPI processes a request from arrival to response is essential for debugging, performance optimization, and middleware development. Here is the complete lifecycle of an HTTP request in a FastAPI application.
The ASGI Standard
FastAPI is an ASGI application. ASGI (Asynchronous Server Gateway Interface) is the successor to WSGI, designed for async Python frameworks. The key differences:
| Feature | WSGI | ASGI |
|---|---|---|
| Concurrency Model | Synchronous (one request per thread) | Asynchronous (many requests per thread) |
| Protocol Support | HTTP only | HTTP, WebSocket, HTTP/2 |
| Long-lived Connections | Not supported | Native support |
| Background Tasks | Require external tools (Celery) | Built-in support |
| Frameworks | Flask, Django | FastAPI, Starlette, Django 3.0+ |
| Servers | Gunicorn, uWSGI | Uvicorn, Hypercorn, Daphne |
Request Lifecycle Step by Step
When a client sends an HTTP request to your FastAPI application, here is exactly what happens:
# Visualizing the request lifecycle
# Step 1: Client sends HTTP request
# GET /users/42?include_email=true HTTP/1.1
# Host: api.example.com
# Authorization: Bearer eyJhbGci...
# Step 2: Uvicorn (ASGI server) receives the raw TCP connection
# - Parses HTTP protocol
# - Creates ASGI scope dictionary
# - Passes to the ASGI application (FastAPI/Starlette)
# Step 3: Starlette middleware stack processes the request
# - CORS middleware
# - GZip middleware
# - Authentication middleware
# - Custom middleware
# (Each middleware can modify request/response or short-circuit)
# Step 4: Starlette router matches the URL path to a route
# - Finds the handler for GET /users/{user_id}
# - Extracts path parameters: user_id = "42"
# Step 5: FastAPI dependency injection
# - Resolves dependencies declared with Depends()
# - Runs authentication, database session creation, etc.
# Step 6: FastAPI request parsing and validation
# - Converts path params: user_id = int("42") = 42
# - Parses query params: include_email = bool("true") = True
# - Validates request body against Pydantic model (if applicable)
# - Returns 422 if validation fails
# Step 7: Your handler function executes
# async def get_user(user_id: int, include_email: bool = False):
# user = await db.get_user(user_id)
# return user
# Step 8: FastAPI response processing
# - Serializes return value to JSON
# - Applies response_model filtering (if defined)
# - Sets status code and headers
# - Validates response against response_model (in debug mode)
# Step 9: Middleware stack processes the response (in reverse order)
# - GZip compresses response body
# - CORS adds Access-Control headers
# Step 10: Uvicorn sends HTTP response to client
# HTTP/1.1 200 OK
# Content-Type: application/json
# {"id": 42, "name": "Alice", "email": "alice@example.com"}Middleware
Middleware is code that runs before every request and after every response. It wraps the entire application like layers of an onion. Here is how to add middleware in FastAPI:
import time
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Built-in CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "https://myapp.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Custom middleware: request timing
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
"""Measure and log request processing time."""
start_time = time.perf_counter()
response = await call_next(request)
process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = f"{process_time:.4f}"
print(f"{request.method} {request.url.path} - {process_time:.4f}s")
return response
# Custom middleware: request ID tracking
import uuid
@app.middleware("http")
async def add_request_id(request: Request, call_next):
"""Add a unique request ID to every request/response."""
request_id = str(uuid.uuid4())
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
@app.get("/")
async def root(request: Request):
return {
"message": "Hello",
"request_id": request.state.request_id
}Middleware execution order matters. Middleware is applied in the order it is added, but wraps the application like nested layers:
- Request arrives → first middleware added runs first
- Request passes through all middleware to the handler
- Response returns → last middleware added runs first (reverse order)
The Request Object
FastAPI gives you access to the raw Starlette Request object when you need low-level control:
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/request-info")
async def request_info(request: Request):
"""Inspect the raw request object."""
return {
"method": request.method,
"url": str(request.url),
"base_url": str(request.base_url),
"path": request.url.path,
"query_params": dict(request.query_params),
"path_params": request.path_params,
"headers": dict(request.headers),
"client_host": request.client.host if request.client else None,
"client_port": request.client.port if request.client else None,
"cookies": request.cookies,
}Response Types
FastAPI supports multiple response types beyond simple JSON:
from fastapi import FastAPI
from fastapi.responses import (
JSONResponse,
HTMLResponse,
PlainTextResponse,
RedirectResponse,
StreamingResponse,
FileResponse,
)
app = FastAPI()
# Default: JSON response (automatic)
@app.get("/json")
async def json_response():
return {"message": "This is JSON"} # Automatically wrapped in JSONResponse
# Explicit JSON with custom status code and headers
@app.get("/custom-json")
async def custom_json():
return JSONResponse(
content={"message": "Custom response"},
status_code=201,
headers={"X-Custom-Header": "value"},
)
# HTML response
@app.get("/html", response_class=HTMLResponse)
async def html_response():
return "<h1>Hello from FastAPI</h1><p>This is HTML.</p>"
# Plain text response
@app.get("/text", response_class=PlainTextResponse)
async def text_response():
return "Just plain text"
# Redirect response
@app.get("/old-page")
async def redirect():
return RedirectResponse(url="/new-page", status_code=301)
# File download
@app.get("/download")
async def download_file():
return FileResponse(
path="report.pdf",
filename="monthly-report.pdf",
media_type="application/pdf",
)
# Streaming response (for large data)
@app.get("/stream")
async def stream_data():
async def generate():
for i in range(100):
yield f"data: chunk {i}\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")Project Structure Best Practices
A “Hello World” in a single file is fine for learning, but real applications need proper organization. Here is how professional FastAPI projects are structured.
Small Project (Single Module)
For small APIs with fewer than 10 endpoints:
fastapi-project/
├── main.py # Application entry point, all routes
├── models.py # Pydantic models (schemas)
├── database.py # Database connection and session
├── requirements.txt # Dependencies
├── .env # Environment variables (never commit)
├── .gitignore
└── tests/
├── __init__.py
└── test_main.pyMedium Project (Package with Routers)
For APIs with 10-50 endpoints, organized by domain:
fastapi-project/
├── app/
│ ├── __init__.py
│ ├── main.py # Application factory, startup/shutdown events
│ ├── config.py # Settings and configuration
│ ├── dependencies.py # Shared dependencies (auth, db session)
│ ├── database.py # Database engine and session setup
│ ├── models/ # SQLAlchemy ORM models
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── item.py
│ ├── schemas/ # Pydantic request/response models
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── item.py
│ ├── routers/ # API route handlers (grouped by domain)
│ │ ├── __init__.py
│ │ ├── users.py
│ │ └── items.py
│ ├── services/ # Business logic layer
│ │ ├── __init__.py
│ │ ├── user_service.py
│ │ └── item_service.py
│ └── middleware/ # Custom middleware
│ ├── __init__.py
│ └── logging.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Shared fixtures
│ ├── test_users.py
│ └── test_items.py
├── alembic/ # Database migrations
│ ├── env.py
│ └── versions/
├── alembic.ini
├── requirements.txt
├── .env
└── .gitignoreLarge Project (Domain-Driven Design)
For large applications with many domains and teams:
fastapi-project/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── core/ # Core application components
│ │ ├── __init__.py
│ │ ├── config.py # Pydantic Settings for configuration
│ │ ├── security.py # JWT, password hashing, OAuth2
│ │ ├── database.py # Database engine, session, base model
│ │ ├── dependencies.py # Shared dependencies
│ │ └── exceptions.py # Custom exception handlers
│ ├── features/ # Feature modules (domain-driven)
│ │ ├── auth/
│ │ │ ├── __init__.py
│ │ │ ├── router.py
│ │ │ ├── schemas.py
│ │ │ ├── models.py
│ │ │ ├── service.py
│ │ │ └── dependencies.py
│ │ ├── users/
│ │ │ ├── __init__.py
│ │ │ ├── router.py
│ │ │ ├── schemas.py
│ │ │ ├── models.py
│ │ │ ├── service.py
│ │ │ └── repository.py
│ │ └── products/
│ │ ├── __init__.py
│ │ ├── router.py
│ │ ├── schemas.py
│ │ ├── models.py
│ │ ├── service.py
│ │ └── repository.py
│ └── common/ # Shared utilities
│ ├── __init__.py
│ ├── pagination.py
│ ├── responses.py
│ └── validators.py
├── tests/
├── alembic/
├── docker/
│ ├── Dockerfile
│ └── docker-compose.yml
├── scripts/
│ ├── seed_db.py
│ └── create_admin.py
├── pyproject.toml # Modern Python project config
├── requirements/
│ ├── base.txt
│ ├── dev.txt
│ └── prod.txt
├── .env.example
└── .gitignoreImplementing Routers
The APIRouter is FastAPI’s way of organizing endpoints into separate modules. Think of it as a mini FastAPI application that you mount on your main app. Here is a practical example:
# app/routers/users.py
from fastapi import APIRouter, HTTPException, Depends
from app.schemas.user import UserCreate, UserResponse, UserUpdate
from app.services.user_service import UserService
from app.dependencies import get_db, get_current_user
router = APIRouter(
prefix="/users",
tags=["Users"],
responses={404: {"description": "User not found"}},
)
@router.get("/", response_model=list[UserResponse])
async def list_users(
skip: int = 0,
limit: int = 20,
db=Depends(get_db),
):
"""List all users with pagination."""
service = UserService(db)
return await service.get_users(skip=skip, limit=limit)
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db=Depends(get_db)):
"""Get a single user by ID."""
service = UserService(db)
user = await service.get_user(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
@router.post("/", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate, db=Depends(get_db)):
"""Create a new user."""
service = UserService(db)
return await service.create_user(user)
@router.put("/{user_id}", response_model=UserResponse)
async def update_user(
user_id: int,
user: UserUpdate,
db=Depends(get_db),
current_user=Depends(get_current_user),
):
"""Update a user. Requires authentication."""
service = UserService(db)
return await service.update_user(user_id, user)
@router.delete("/{user_id}", status_code=204)
async def delete_user(
user_id: int,
db=Depends(get_db),
current_user=Depends(get_current_user),
):
"""Delete a user. Requires authentication."""
service = UserService(db)
await service.delete_user(user_id)# app/routers/items.py
from fastapi import APIRouter, Depends
from app.schemas.item import ItemCreate, ItemResponse
from app.dependencies import get_db
router = APIRouter(
prefix="/items",
tags=["Items"],
)
@router.get("/", response_model=list[ItemResponse])
async def list_items(db=Depends(get_db)):
"""List all items."""
...
@router.post("/", response_model=ItemResponse, status_code=201)
async def create_item(item: ItemCreate, db=Depends(get_db)):
"""Create a new item."""
...# app/main.py
from fastapi import FastAPI
from app.routers import users, items
app = FastAPI(
title="My Application API",
version="1.0.0",
)
# Include routers
app.include_router(users.router)
app.include_router(items.router)
# You can also add a prefix when including
# app.include_router(users.router, prefix="/api/v1")
@app.get("/")
async def root():
return {"message": "Welcome to the API", "docs": "/docs"}The result is a clean separation of concerns. Each router file handles a single domain (users, items, etc.), the main application file is short and declarative, and you can easily add new domains by creating a new router file and including it.
Configuration with Pydantic Settings
FastAPI projects should use Pydantic’s BaseSettings for configuration management. It reads from environment variables and .env files with full type validation:
# app/core/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
"""Application settings loaded from environment variables."""
# Application
app_name: str = "FastAPI Tutorial"
debug: bool = False
api_version: str = "v1"
# Server
host: str = "0.0.0.0"
port: int = 8000
workers: int = 4
# Database
database_url: str = "sqlite:///./app.db"
# Security
secret_key: str = "change-me-in-production"
access_token_expire_minutes: int = 30
algorithm: str = "HS256"
# CORS
allowed_origins: list[str] = ["http://localhost:3000"]
model_config = {
"env_file": ".env",
"env_file_encoding": "utf-8",
"case_sensitive": False,
}
@lru_cache
def get_settings() -> Settings:
"""Cached settings instance. Call this instead of Settings() directly."""
return Settings()# .env file
APP_NAME=My Production API
DEBUG=false
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
SECRET_KEY=your-super-secret-key-here
ALLOWED_ORIGINS=["https://myapp.com","https://admin.myapp.com"]# Using settings in your application
from fastapi import FastAPI, Depends
from app.core.config import Settings, get_settings
app = FastAPI()
@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
return {
"app_name": settings.app_name,
"debug": settings.debug,
"api_version": settings.api_version,
}.env file to version control. Always add it to .gitignore. Create a .env.example file with placeholder values so other developers know which variables are needed.
Dependency Injection
FastAPI’s dependency injection system is one of its most powerful features. Dependencies are reusable functions that provide shared resources (database sessions, current user, settings) to your route handlers:
# app/dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from app.database import SessionLocal
# Database session dependency
def get_db():
"""Yield a database session, auto-close when done."""
db = SessionLocal()
try:
yield db
finally:
db.close()
# Authentication dependency
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db),
):
"""Validate JWT token and return current user."""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = decode_token(token)
user_id = payload.get("sub")
if user_id is None:
raise credentials_exception
except Exception:
raise credentials_exception
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise credentials_exception
return user
# Role-based access dependency
def require_role(required_role: str):
"""Factory that creates a dependency requiring a specific role."""
async def role_checker(current_user=Depends(get_current_user)):
if current_user.role != required_role:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Role '{required_role}' required",
)
return current_user
return role_checker
# Usage in routes:
# @router.delete("/users/{id}", dependencies=[Depends(require_role("admin"))])
# async def delete_user(id: int): ...Dependencies can depend on other dependencies, forming a dependency tree. FastAPI resolves this tree automatically, creates instances in the correct order, and handles cleanup (via yield) when the request completes.
Application Lifecycle Events
FastAPI supports startup and shutdown events using the modern lifespan context manager. This is where you initialize and clean up shared resources like database connection pools, HTTP clients, and caches:
from contextlib import asynccontextmanager
from fastapi import FastAPI
import httpx
# Shared resources
http_client: httpx.AsyncClient | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Manage application lifecycle - startup and shutdown."""
# --- STARTUP ---
global http_client
print("Starting up... initializing resources")
# Create a shared HTTP client with connection pooling
http_client = httpx.AsyncClient(
base_url="https://api.example.com",
timeout=30.0,
)
# Initialize database connection pool
# await database.connect()
# Load ML models, warm caches, etc.
print("Startup complete")
yield # Application runs here
# --- SHUTDOWN ---
print("Shutting down... cleaning up resources")
await http_client.aclose()
# await database.disconnect()
print("Shutdown complete")
app = FastAPI(lifespan=lifespan)
@app.get("/external-data")
async def get_external_data():
"""Use the shared HTTP client."""
response = await http_client.get("/data")
return response.json()lifespan context manager replaces the older @app.on_event("startup") and @app.on_event("shutdown") decorators, which are deprecated. Always use lifespan for new projects.
Error Handling
Proper error handling is critical for production APIs. FastAPI provides several mechanisms for handling errors gracefully.
HTTPException
The simplest way to return an HTTP error response:
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
@app.get("/items/{item_id}")
async def get_item(item_id: int):
if item_id < 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Item ID must be positive",
)
item = fake_db.get(item_id)
if not item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Item with ID {item_id} not found",
headers={"X-Error": "item-not-found"},
)
return itemCustom Exception Handlers
For consistent error formatting across your entire API:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
# Custom exception class
class AppException(Exception):
def __init__(self, status_code: int, detail: str, error_code: str):
self.status_code = status_code
self.detail = detail
self.error_code = error_code
# Handler for custom exceptions
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.error_code,
"message": exc.detail,
"path": str(request.url),
}
},
)
# Override default validation error handler for consistent format
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": exc.errors(),
}
},
)
# Override default HTTP exception handler
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": f"HTTP_{exc.status_code}",
"message": exc.detail,
}
},
)
# Usage
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = await find_user(user_id)
if not user:
raise AppException(
status_code=404,
detail=f"User {user_id} not found",
error_code="USER_NOT_FOUND",
)
return userThis pattern ensures every error response from your API has a consistent structure, making it easier for frontend developers and API consumers to handle errors programmatically.
Running in Production
While this is an introductory tutorial, it is worth understanding the basics of running FastAPI in production. We will cover this topic in depth in the FastAPI – Deployment tutorial later in this series.
Development vs Production
| Setting | Development | Production |
|---|---|---|
| Server Command | uvicorn main:app --reload |
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker |
| Workers | 1 (with reload) | 2-4x CPU cores |
| Auto-reload | Enabled | Disabled |
| Debug Mode | Enabled | Disabled |
| Docs | Enabled (/docs, /redoc) | Often disabled or restricted |
| HTTPS | Not needed | Required (via reverse proxy) |
| Logging | Console output | Structured JSON logging |
Quick Production Setup
# Install Gunicorn (Linux/macOS only)
pip install gunicorn
# Run with Gunicorn + Uvicorn workers
gunicorn app.main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--access-logfile - \
--error-logfile -# Dockerfile for FastAPI
# app/Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Run with Uvicorn (for single-container deployments)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]# Build and run the Docker container
docker build -t fastapi-app .
docker run -p 8000:8000 fastapi-appComplete Example: Putting It All Together
Let us build a complete, well-structured FastAPI application that demonstrates everything we have covered. This is a task management API with proper project organization:
# Project structure
task-manager/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── models.py
│ └── routers/
│ ├── __init__.py
│ └── tasks.py
├── requirements.txt
└── .env# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Task Manager API"
version: str = "1.0.0"
debug: bool = True
model_config = {"env_file": ".env"}
settings = Settings()# app/models.py
from pydantic import BaseModel, Field
from datetime import datetime
from enum import Enum
class Priority(str, Enum):
low = "low"
medium = "medium"
high = "high"
critical = "critical"
class TaskStatus(str, Enum):
todo = "todo"
in_progress = "in_progress"
done = "done"
class TaskCreate(BaseModel):
"""Schema for creating a new task."""
title: str = Field(..., min_length=1, max_length=200, examples=["Write unit tests"])
description: str | None = Field(None, max_length=2000)
priority: Priority = Field(default=Priority.medium)
assignee: str | None = None
class TaskUpdate(BaseModel):
"""Schema for updating an existing task (all fields optional)."""
title: str | None = Field(None, min_length=1, max_length=200)
description: str | None = Field(None, max_length=2000)
priority: Priority | None = None
status: TaskStatus | None = None
assignee: str | None = None
class TaskResponse(BaseModel):
"""Schema for task responses."""
id: int
title: str
description: str | None
priority: Priority
status: TaskStatus
assignee: str | None
created_at: str
updated_at: str# app/routers/tasks.py
from fastapi import APIRouter, HTTPException, Query
from app.models import TaskCreate, TaskUpdate, TaskResponse, TaskStatus, Priority
from datetime import datetime
router = APIRouter(prefix="/tasks", tags=["Tasks"])
# In-memory storage (use a real database in production)
tasks_db: dict[int, dict] = {}
next_id: int = 1
@router.get("/", response_model=list[TaskResponse])
async def list_tasks(
status: TaskStatus | None = Query(None, description="Filter by status"),
priority: Priority | None = Query(None, description="Filter by priority"),
assignee: str | None = Query(None, description="Filter by assignee"),
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
):
"""List all tasks with optional filters and pagination."""
results = list(tasks_db.values())
if status:
results = [t for t in results if t["status"] == status]
if priority:
results = [t for t in results if t["priority"] == priority]
if assignee:
results = [t for t in results if t["assignee"] == assignee]
return results[skip : skip + limit]
@router.get("/{task_id}", response_model=TaskResponse)
async def get_task(task_id: int):
"""Get a single task by ID."""
if task_id not in tasks_db:
raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
return tasks_db[task_id]
@router.post("/", response_model=TaskResponse, status_code=201)
async def create_task(task: TaskCreate):
"""Create a new task."""
global next_id
now = datetime.utcnow().isoformat()
task_data = {
"id": next_id,
**task.model_dump(),
"status": TaskStatus.todo,
"created_at": now,
"updated_at": now,
}
tasks_db[next_id] = task_data
next_id += 1
return task_data
@router.patch("/{task_id}", response_model=TaskResponse)
async def update_task(task_id: int, task: TaskUpdate):
"""Partially update a task."""
if task_id not in tasks_db:
raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
stored = tasks_db[task_id]
update_data = task.model_dump(exclude_unset=True)
stored.update(update_data)
stored["updated_at"] = datetime.utcnow().isoformat()
return stored
@router.delete("/{task_id}", status_code=204)
async def delete_task(task_id: int):
"""Delete a task."""
if task_id not in tasks_db:
raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
del tasks_db[task_id]
@router.get("/stats/summary")
async def task_stats():
"""Get task statistics."""
tasks = list(tasks_db.values())
return {
"total": len(tasks),
"by_status": {
s.value: len([t for t in tasks if t["status"] == s])
for s in TaskStatus
},
"by_priority": {
p.value: len([t for t in tasks if t["priority"] == p])
for p in Priority
},
}# app/main.py
import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
from app.routers import tasks
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifecycle manager."""
print(f"Starting {settings.app_name} v{settings.version}")
yield
print("Shutting down...")
app = FastAPI(
title=settings.app_name,
version=settings.version,
description="A task management API built with FastAPI",
lifespan=lifespan,
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Request timing middleware
@app.middleware("http")
async def add_process_time(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Process-Time"] = f"{duration:.4f}"
return response
# Include routers
app.include_router(tasks.router)
@app.get("/", tags=["Health"])
async def root():
"""API root - health check and documentation links."""
return {
"app": settings.app_name,
"version": settings.version,
"status": "healthy",
"docs": "/docs",
"redoc": "/redoc",
}
@app.get("/health", tags=["Health"])
async def health():
"""Detailed health check."""
return {"status": "healthy", "version": settings.version}# requirements.txt
fastapi[standard]>=0.115.0
pydantic-settings>=2.0.0
uvicorn[standard]>=0.30.0Run the application:
# From the project root
uvicorn app.main:app --reload
# Test the API
curl -X POST http://127.0.0.1:8000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Learn FastAPI", "priority": "high"}'
curl http://127.0.0.1:8000/tasks
curl http://127.0.0.1:8000/tasks?status=todo&priority=high
curl http://127.0.0.1:8000/tasks/stats/summary
curl http://127.0.0.1:8000/docsThis example demonstrates all the concepts from this tutorial: application setup, routers, Pydantic models, path parameters, query parameters, middleware, lifecycle events, error handling, and proper project structure.
Key Takeaways
Here is a summary of everything we covered in this introduction to FastAPI:
- FastAPI is built on Starlette and Pydantic. Starlette provides the high-performance ASGI foundation, and Pydantic provides automatic data validation and serialization. Understanding these two libraries helps you understand FastAPI at a deeper level.
- Performance is exceptional. FastAPI’s async-first architecture and Uvicorn’s event loop deliver throughput comparable to Node.js and Go. For I/O-bound workloads (database queries, external API calls), FastAPI significantly outperforms Flask and Django.
- Type hints drive everything. FastAPI uses Python type hints not just for documentation, but for request parsing, data validation, response serialization, and API documentation generation. Learning type hints well is the most valuable investment you can make when working with FastAPI.
- Documentation is automatic. Swagger UI and ReDoc are generated from your code with zero configuration. Every parameter, model, and constraint you define appears in the documentation immediately. Your docs never drift out of sync with your code.
- Path operations are the building blocks. Every endpoint is defined by an HTTP method decorator (
@app.get,@app.post, etc.) and a handler function. Path parameters, query parameters, and request bodies are all handled through function parameters and type hints. - Routers enable modular organization. Use
APIRouterto split your application into domain-specific modules. Each router handles a single concern and is included in the main application. - Dependency injection is powerful. FastAPI’s
Depends()system provides reusable, composable dependencies for database sessions, authentication, authorization, and any shared logic. - Error handling should be consistent. Use
HTTPExceptionfor simple errors and custom exception handlers for consistent error formatting across your entire API. - Project structure matters. Start simple and evolve. A single-file application is fine for learning, but production applications should use the router-based structure with separate layers for schemas, services, and data access.
- ASGI is the future of Python web development. ASGI supports async I/O, WebSockets, HTTP/2, and long-lived connections. If you are starting a new Python web project today, ASGI frameworks like FastAPI are the modern choice.
What is Next
In the next tutorial, FastAPI – Routes & Request Handling, we will dive deep into:
- Advanced path parameters and path converters
- Query parameter validation with
Query() - Request body handling with Pydantic models
- Headers, cookies, and form data
- File uploads (single and multiple)
- Request validation and custom validators
- Response models and status codes