Introduction
FastAPI is a modern, fast (high-performance) web framework for building APIs with Python 3.7+ based on standard Python type hints. It's designed to be intuitive, robust, and highly efficient, leveraging cutting-edge technologies like Starlette for the web parts and Pydantic for data validation and serialization. The framework aims to provide developers with the best possible experience, making API development quick, easy, and enjoyable. It stands out due to its incredible speed, automatic interactive API documentation (Swagger UI and ReDoc), and its strong emphasis on type hints, which enables excellent editor support and data validation.
FastAPI exists to address several pain points in Python web development. Traditional frameworks often require more boilerplate code for API creation, lack automatic data validation, or don't provide built-in interactive documentation. FastAPI solves these by offering a declarative way to define API endpoints, automatically validating request data and serializing response data using Pydantic models, and generating OpenAPI (formerly Swagger) specifications and UI directly from your code. This means less time writing validation logic and documentation, and more time focusing on your application's core business logic. In real-life scenarios, FastAPI is used for building a wide array of applications, from microservices and backend APIs for web and mobile applications to machine learning model serving. Companies of all sizes, from startups to large enterprises, are adopting FastAPI for its performance, ease of use, and modern feature set. For instance, it's an excellent choice for developing high-throughput data processing APIs, real-time communication backends, and even complex GraphQL services, thanks to its asynchronous capabilities.
Step-by-Step Explanation
To get started with FastAPI, you'll first need Python 3.7+ installed. The core components you'll interact with are the `FastAPI` class itself, which is the main entry point for your application, and `uvicorn`, an ASGI server that runs your FastAPI application. You'll also frequently use `Pydantic` models for defining data schemas. The basic structure involves importing `FastAPI`, creating an instance of it, and then defining path operations using decorators like `@app.get('/'), @app.post('/item'),` etc. These decorators map HTTP methods and URLs to Python functions. Inside these functions, you write the logic for your API endpoint. FastAPI automatically handles request parsing, data validation (if you use Pydantic models as function parameters), and response serialization. For example, if a function parameter is type-hinted as a Pydantic model, FastAPI will expect a JSON body matching that model, validate it, and inject a Python object into your function.
Comprehensive Code Examples
Basic example
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
async def read_root():
return {'message': 'Hello, FastAPI!'}
@app.get('/items/{item_id}')
async def read_item(item_id: int, q: str | None = None):
return {'item_id': item_id, 'q': q}
# To run: uvicorn main:app --reload
This basic example demonstrates creating a FastAPI application, defining a root endpoint that returns a simple JSON message, and another endpoint that accepts a path parameter (`item_id`) and an optional query parameter (`q`).
Real-world example
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Dict
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
items_db: Dict[int, Item] = {}
next_item_id = 0
@app.post('/items/', response_model=Item)
async def create_item(item: Item):
global next_item_id
item_id = next_item_id
items_db[item_id] = item
next_item_id += 1
return item
@app.get('/items/{item_id}', response_model=Item)
async def read_item(item_id: int):
if item_id not in items_db:
raise HTTPException(status_code=404, detail='Item not found')
return items_db[item_id]
This example simulates a simple e-commerce API. It defines an `Item` model using Pydantic, allows creating new items via a POST request, and retrieves items by ID using a GET request, including error handling for non-existent items.
Advanced usage (Dependency Injection)
from fastapi import FastAPI, Depends, Header, HTTPException
from typing import Annotated
app = FastAPI()
async def get_current_user(x_token: Annotated[str, Header()]):
if x_token != 'fake-super-secret-token':
raise HTTPException(status_code=400, detail='X-Token header invalid')
return {'username': 'johndoe'}
@app.get('/users/me/')
async def read_users_me(current_user: Annotated[dict, Depends(get_current_user)]):
return current_user
This advanced example showcases FastAPI's powerful Dependency Injection system. The `get_current_user` function acts as a dependency that validates an `X-Token` header. If valid, it returns user data; otherwise, it raises an `HTTPException`. The `read_users_me` endpoint then depends on this function, meaning `get_current_user` will be called before `read_users_me`, and its return value will be passed as the `current_user` argument.
Common Mistakes
- Forgetting to install `uvicorn` or `uvicorn[standard]`: Many beginners install only `fastapi` and then try to run their app, getting a `ModuleNotFoundError`.
Fix: Always run `pip install 'uvicorn[standard]'` or `pip install uvicorn` alongside `pip install fastapi`. - Not using type hints correctly with Pydantic models: Expecting automatic validation when not defining request body parameters as Pydantic models.
Fix: Ensure your POST/PUT function parameters that represent request bodies are correctly type-hinted with your Pydantic `BaseModel` classes. - Blocking the event loop with synchronous operations: FastAPI is asynchronous by default. Performing long-running CPU-bound tasks directly in `async def` functions can block the event loop.
Fix: Use `async def` for I/O-bound tasks and `def` (normal functions) for CPU-bound tasks. FastAPI will automatically run `def` functions in a separate thread pool to avoid blocking the main event loop.
Best Practices
- Use Pydantic for everything: Leverage Pydantic for request body validation, response modeling, and even configuration settings. It ensures data consistency and provides excellent documentation.
- Organize your project structure: For larger applications, split your API into multiple files using `APIRouter`. This helps keep your codebase modular and maintainable.
- Implement Dependency Injection: Use `Depends` for common logic like database sessions, authentication, and authorization. It promotes reusability and testability.
- Document your API thoroughly: FastAPI automatically generates OpenAPI docs, but add `summary` and `description` to your path operations and Pydantic models for even richer documentation.
- Handle exceptions gracefully: Use `HTTPException` for standard HTTP error responses and consider custom exception handlers for application-specific errors.
Practice Exercises
- Create a new FastAPI application that has a `/status` endpoint returning a JSON object `{'status': 'ok'}`.
- Modify the previous application to include a `/greet/{name}` endpoint that takes a path parameter `name` and returns `{'message': 'Hello, {name}!'}`.
- Define a Pydantic model for a `Book` with fields `title` (string), `author` (string), and `year` (integer). Create a POST endpoint `/books/` that accepts this model and returns the received book data.
Mini Project / Task
Build a simple 'To-Do List' API. It should have the following endpoints:
- `POST /todos/`: Create a new To-Do item. The item should have a `title` (string) and an optional `description` (string). Return the created item.
- `GET /todos/`: Retrieve all To-Do items.
- `GET /todos/{item_id}`: Retrieve a single To-Do item by its ID.
Challenge (Optional)
Extend the 'To-Do List' API by adding a `PUT /todos/{item_id}` endpoint to update an existing To-Do item. The update should allow modifying the `title` and `description`. Also, implement an `is_completed` (boolean) field for the To-Do item, which defaults to `False` when created, and can be updated via the `PUT` endpoint. Ensure proper error handling if the `item_id` does not exist.
How it Works
FastAPI is a modern Python web framework built for creating APIs quickly while keeping performance high. It exists to solve common backend problems: defining routes, reading requests, validating data, returning responses, and documenting APIs automatically. In real projects, teams use FastAPI for web backends, mobile app services, machine learning endpoints, internal company tools, and microservices. What makes it powerful is that it combines Python type hints, Pydantic validation, and the ASGI standard. When a request reaches your app, FastAPI matches the URL to a path operation, extracts inputs from the path, query string, headers, or body, validates them, runs your Python function, and converts the returned value into JSON or another response type. It also builds OpenAPI documentation automatically, which is why developers can test endpoints in the browser without extra setup.
The main moving parts are routing, request parsing, validation, dependency injection, and response serialization. Routing decides which function runs for a given URL and HTTP method such as GET or POST. Validation checks incoming data using Python type annotations and Pydantic models. Dependency injection lets FastAPI run helper functions, such as database session creation or authentication checks, before your endpoint logic executes. Response serialization converts Python dictionaries, lists, and models into API responses. FastAPI runs on ASGI, usually through a server like Uvicorn, which allows asynchronous request handling. This is especially useful for I/O-heavy apps such as APIs calling databases, file systems, or external services.
Step-by-Step Explanation
First, you create an application object using FastAPI(). Second, you define path operations using decorators like @app.get() or @app.post(). Third, FastAPI reads your function signature. A plain parameter in the route path, such as /items/{item_id}, becomes a path parameter. A typed parameter not in the path often becomes a query parameter. A Pydantic model parameter becomes the request body. Fourth, FastAPI validates all inputs automatically. If validation fails, it returns a structured error response. Fifth, your function runs and returns data. Finally, FastAPI serializes the output to JSON and exposes it in generated docs at /docs and /redoc.
Comprehensive Code Examples
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello, FastAPI"}from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Product(BaseModel):
name: str
price: float
in_stock: bool
@app.post("/products")
def create_product(product: Product):
return {"received": product.model_dump(), "status": "created"}from fastapi import FastAPI, Depends, HTTPException
app = FastAPI()
def verify_token(token: str = ""):
if token != "secret123":
raise HTTPException(status_code=401, detail="Invalid token")
return token
@app.get("/reports/{year}")
async def get_report(year: int, published: bool = True, token: str = Depends(verify_token)):
return {
"year": year,
"published": published,
"token_used": token,
"summary": "Annual report data"
}Common Mistakes
- Ignoring type hints: Without clear types, validation and docs become weaker. Always annotate parameters and models.
- Mixing query and body assumptions: Beginners often expect every parameter to come from JSON. Use Pydantic models for request bodies.
- Returning unsupported objects directly: Raw database objects may fail serialization. Convert them to dictionaries or response models.
- Forgetting async context: Use
async deffor async operations and avoid blocking calls inside async routes.
Best Practices
- Use Pydantic models for all non-trivial input and output data.
- Keep endpoint functions small and move business logic into separate service functions.
- Use dependency injection for authentication, database sessions, and shared logic.
- Name routes clearly and follow REST-style conventions.
- Test endpoints through the generated docs and automated unit tests.
Practice Exercises
- Create a GET endpoint called
/hello/{name}that returns a greeting using a path parameter. - Create a POST endpoint that accepts a Pydantic model with
titleandcompletedfields and returns the submitted task. - Create a GET endpoint with query parameters
pageandlimitand return them in JSON.
Mini Project / Task
Build a small book API with one GET route to list books, one GET route to fetch a book by ID, and one POST route to add a new book using a Pydantic model.
Challenge (Optional)
Create an endpoint that uses a dependency to check an API key, validates a request body model, and returns a filtered response based on a query parameter.
Installation and Setup
FastAPI is a modern Python framework for building APIs quickly, with clean syntax, strong type support, automatic interactive documentation, and excellent performance. It exists to make backend API development easier while still supporting production-grade applications. In real life, teams use FastAPI for internal tools, mobile backends, machine learning services, microservices, and public REST APIs. Before building routes, models, and business logic, you need a correct development environment. Installation and setup include preparing Python, creating an isolated virtual environment, installing FastAPI and an ASGI server, and verifying that the application runs locally. The main setup pieces are Python itself, package management with pip, virtual environments for dependency isolation, FastAPI as the framework, and Uvicorn as the development server. Some developers also use editors like VS Code, a requirements file, and optional tools such as python-dotenv for configuration. A beginner should understand that FastAPI does not run by itself in the browser; it runs as a Python application served by an ASGI server such as Uvicorn.
Step-by-Step Explanation
First, install Python 3.9 or newer and verify it with python --version or python3 --version. Next, create a project folder such as fastapi_setup. Inside that folder, create a virtual environment using python -m venv venv. Activate it with venv\Scripts\activate on Windows or source venv/bin/activate on macOS and Linux. Once active, install dependencies using pip install fastapi uvicorn. Then create a file named main.py. Import FastAPI, create an app instance, and define at least one route. Run the server with uvicorn main:app --reload. The main:app part means the file is main.py and the FastAPI object is named app. The --reload flag restarts the server automatically when code changes, which is useful during development. After startup, visit http://127.0.0.1:8000 for the API response, /docs for Swagger UI, and /redoc for alternative documentation.
Comprehensive Code Examples
Basic example
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "FastAPI is running"}Real-world example
from fastapi import FastAPI
app = FastAPI(title="Inventory API", version="1.0.0")
@app.get("/health")
def health_check():
return {"status": "ok", "service": "inventory-api"}Advanced usage
from fastapi import FastAPI
app = FastAPI(docs_url="/documentation", redoc_url=None)
@app.get("/info")
def app_info():
return {"app": "FastAPI Setup Demo", "environment": "development"}To save dependencies for sharing, run pip freeze > requirements.txt. Another developer can recreate the environment with pip install -r requirements.txt after activating a virtual environment.
Common Mistakes
- Using the wrong Python version: Verify Python is modern enough before installing packages.
- Forgetting to activate the virtual environment: This installs packages globally instead of inside the project.
- Running the wrong Uvicorn target: Ensure the command matches the file and app object, such as
main:app. - Installing only FastAPI without a server: FastAPI needs Uvicorn or another ASGI server to run locally.
Best Practices
- Always use a virtual environment per project.
- Pin dependencies in
requirements.txtfor reproducible setups. - Use
--reloadonly in development, not production. - Name your application file and app object clearly, such as
main.pyandapp. - Test
/docsimmediately after setup to confirm everything works.
Practice Exercises
- Create a new FastAPI project folder, set up a virtual environment, and install FastAPI with Uvicorn.
- Build a
/helloroute that returns a JSON greeting message. - Change the application title and confirm the new title appears in the docs page.
Mini Project / Task
Set up a small local API called student-api with routes for / and /health, then run it with Uvicorn and verify the interactive documentation loads correctly.
Challenge (Optional)
Create a reusable setup checklist for new FastAPI projects that includes Python version verification, virtual environment creation, package installation, dependency export, and server startup commands for both Windows and Linux/macOS.
Your First FastAPI App
FastAPI is a modern Python web framework designed for building APIs quickly, clearly, and with excellent performance. It exists to reduce the amount of repetitive code developers write when creating web services, while still providing strong validation, automatic documentation, and async support. In real life, teams use FastAPI for backend services, internal tools, machine learning model endpoints, mobile app backends, and microservices. Your first FastAPI app is the foundation for everything else you build, because it teaches how the framework starts, how routes work, and how the server responds to browser or client requests.
At its core, a FastAPI application begins by creating an instance of the FastAPI class. You then define path operations, often called routes, using decorators such as @app.get() or @app.post(). These decorators connect a URL path and HTTP method to a Python function. For a beginner, the main sub-types to understand here are the common HTTP methods: GET for reading data, POST for creating data, PUT for replacing data, and DELETE for removing data. In your first app, you will usually start with simple GET endpoints because they are the easiest to test in a browser.
Step-by-Step Explanation
First, install FastAPI and a server such as Uvicorn. FastAPI defines the application, while Uvicorn runs it locally. Next, create a file such as main.py. Import FastAPI and create an app object with app = FastAPI(). Then define a route using a decorator like @app.get("/"). The slash means the root URL. The function below the decorator runs when that endpoint is requested. Return a Python dictionary, and FastAPI automatically converts it into JSON. Finally, start the app using a command like uvicorn main:app --reload. Here, main is the filename and app is the FastAPI instance. The --reload flag restarts the server whenever you save changes, which is very useful during development.
Comprehensive Code Examples
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello, FastAPI!"}from fastapi import FastAPI
app = FastAPI()
@app.get("/about")
def about():
return {"app": "Student API", "version": "1.0"}
@app.get("/status")
def status():
return {"status": "running"}from fastapi import FastAPI
app = FastAPI(title="Weather Service")
@app.get("/")
def home():
return {"service": "Weather API"}
@app.get("/city/{name}")
def get_city_weather(name: str):
return {"city": name, "forecast": "sunny"}The first example shows the minimum working app. The second looks more like a real service with multiple endpoints. The third adds app metadata and a path parameter, showing how route values can be captured from the URL.
Common Mistakes
- Forgetting to create the app instance: Without
app = FastAPI(), your decorators have nothing to attach to. Always define the app near the top of the file. - Running the wrong module name: If your file is
main.py, the command must usemain:app. Match the filename and variable name exactly. - Using browser-only testing for every route: Browsers mainly send
GETrequests. For other methods later, use the Swagger docs at/docsor a tool like Postman.
Best Practices
- Start with small, single-purpose routes so behavior is easy to test.
- Use clear endpoint names such as
/statusor/aboutinstead of vague paths. - Keep
--reloadonly for development, not production. - Return dictionaries or Pydantic models instead of manually building JSON strings.
- Open
/docsregularly to inspect and test your API interactively.
Practice Exercises
- Create a FastAPI app with a root endpoint that returns a welcome message.
- Add a
/helloendpoint that returns your name in JSON format. - Create a
/projectendpoint that returns the project title and version.
Mini Project / Task
Build a simple personal portfolio API with three routes: / for a welcome message, /about for your role, and /contact for an email address returned as JSON.
Challenge (Optional)
Create a FastAPI app with a dynamic route like /greet/{username} that returns a personalized greeting message for any name entered in the URL.
Running with Uvicorn
Uvicorn is a lightweight ASGI server used to run FastAPI applications. FastAPI does not serve HTTP requests by itself; it needs an ASGI server that listens for incoming connections and forwards them to your application. In real projects, Uvicorn is commonly used during development because it is fast, simple, and supports features such as auto-reload. You will use it when building local APIs, testing endpoints, and launching backend services before moving to staging or production. The most common pattern is creating a FastAPI app object and starting it with a command like uvicorn main:app --reload. Here, main is the Python file name, and app is the FastAPI instance inside that file.
Uvicorn works with the ASGI standard, which is designed for asynchronous Python web applications. This makes it a strong fit for FastAPI, especially when handling many concurrent requests. There are a few common ways to run it: command-line usage for day-to-day development, programmatic usage from Python, and production-style startup with explicit host, port, and worker settings. In development, --reload watches your files and restarts the server when code changes. In shared environments, --host 0.0.0.0 makes the app accessible from outside your machine. --port changes the default port if needed. While Uvicorn can run directly in production for smaller systems, many teams pair it with process managers or container platforms for reliability.
Step-by-Step Explanation
First, install FastAPI and Uvicorn using pip. Next, create a Python file such as main.py. Inside it, import FastAPI and create an app instance. Then define at least one route so the server has something to return. After that, open a terminal in the same folder and run uvicorn main:app --reload. The part before the colon is the file name without .py, and the part after the colon is the FastAPI variable. Once the server starts, open http://127.0.0.1:8000 in the browser. You can also visit /docs to see the automatic Swagger UI.
If you need a different port, add --port 8080. If you want the API reachable from another device, use --host 0.0.0.0. For script-based startup, call uvicorn.run() inside a Python block, usually protected by if __name__ == "__main__":. This avoids accidental server starts when importing the module elsewhere.
Comprehensive Code Examples
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello from FastAPI"}Run it with uvicorn main:app --reload.
from fastapi import FastAPI
app = FastAPI()
@app.get("/status")
def status():
return {"service": "inventory-api", "status": "running"}Real-world usage: uvicorn main:app --reload --host 0.0.0.0 --port 9000
from fastapi import FastAPI
import uvicorn
app = FastAPI()
@app.get("/health")
async def health_check():
return {"ok": True}
if __name__ == "__main__":
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)This advanced pattern is useful when startup is controlled from Python code.
Common Mistakes
- Wrong import path: Using
uvicorn app:mainwhen the file ismain.pyand the object isapp. Fix by matchingfilename:variableexactly. - Forgetting reload is development-only: Beginners often use
--reloadeverywhere. Fix by using it only during local development. - Binding to localhost unintentionally: Using the default host prevents access from other machines. Fix with
--host 0.0.0.0when needed. - Running from the wrong folder: The module may not be found. Fix by starting Uvicorn from the directory containing the target file or using the correct module path.
Best Practices
- Use
uvicorn main:app --reloadfor development and keep commands simple. - Store your FastAPI instance in a clearly named variable like
app. - Use explicit host and port values for team consistency.
- Check
/docsimmediately after startup to verify routes are loaded correctly. - Keep programmatic startup inside
if __name__ == "__main__":to avoid import side effects.
Practice Exercises
- Create a file named
main.pywith one/route and run it using Uvicorn with auto-reload. - Change the server to run on port
8080and verify the app opens in the browser. - Expose the app on
0.0.0.0and test access using another device or browser tab with the new host and port settings.
Mini Project / Task
Build a tiny service with routes /, /health, and /version, then run it with Uvicorn on port 9000 using auto-reload for development.
Challenge (Optional)
Create a startup script that launches the same FastAPI app programmatically with uvicorn.run() and compare that workflow to the command-line approach.
Project Structure
Understanding and implementing a well-organized project structure is fundamental to developing maintainable, scalable, and collaborative FastAPI applications. It's not just about aesthetics; a logical structure helps developers quickly locate files, understand dependencies, and onboard new team members efficiently. In real-world scenarios, a haphazard project layout can lead to 'spaghetti code,' making debugging a nightmare and feature development slow. FastAPI itself doesn't enforce a strict project structure, offering developers flexibility, but adopting common best practices is highly recommended. This section will guide you through establishing a robust and scalable project layout, which is crucial for applications ranging from simple microservices to complex, multi-service architectures.
At its core, a good project structure separates concerns. This means keeping your API endpoints, business logic, database models, configuration, and utility functions in distinct, logical locations. This separation promotes modularity, making it easier to test individual components, swap out implementations (e.g., changing database backends), and manage complexity as your application grows. For instance, all your API route definitions should reside in one area, while your data models reside in another. This prevents circular dependencies and makes the codebase more predictable.
Step-by-Step Explanation
Let's outline a common and effective project structure for a FastAPI application. We'll start with a basic layout and then discuss how to expand it for larger projects.
my_fastapi_project/
├── app/
│ ├── __init__.py
│ ├── main.py # Your main FastAPI application instance
│ ├── api/ # API routes (endpoints)
│ │ ├── __init__.py
│ │ ├── v1/ # API versioning (e.g., /v1/items, /v1/users)
│ │ │ ├── __init__.py
│ │ │ ├── endpoints/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── items.py
│ │ │ │ └── users.py
│ │ │ └── deps.py # Dependencies for endpoints (e.g., authentication)
│ ├── core/ # Core application settings and utilities
│ │ ├── __init__.py
│ │ ├── config.py # Configuration settings (e.g., database URL, API keys)
│ │ └── security.py # Security-related functions (e.g., password hashing)
│ ├── crud/ # Create, Read, Update, Delete operations (database interactions)
│ │ ├── __init__.py
│ │ ├── item.py
│ │ └── user.py
│ ├── db/ # Database-related files
│ │ ├── __init__.py
│ │ ├── base.py # Base for SQLAlchemy models
│ │ └── session.py # Database session management
│ ├── models/ # Database models (e.g., SQLAlchemy models)
│ │ ├── __init__.py
│ │ ├── item.py
│ │ └── user.py
│ ├── schemas/ # Pydantic models for request/response validation
│ │ ├── __init__.py
│ │ ├── item.py # ItemCreate, ItemUpdate, ItemInDB
│ │ └── user.py # UserCreate, UserUpdate, UserInDB
│ └── services/ # Business logic and complex operations
│ ├── __init__.py
│ └── item_service.py
├── tests/ # Unit and integration tests
│ ├── __init__.py
│ ├── api/
│ │ └── v1/
│ │ ├── test_items.py
│ │ └── test_users.py
│ └── conftest.py
├── migrations/ # Database migrations (e.g., Alembic scripts)
├── .env # Environment variables
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── requirements.txt # Project dependencies
└── README.md
Comprehensive Code Examples
Basic Example: Initializing the app/main.py
This is where your FastAPI application instance is created and where you typically include your routers.
# app/main.py
from fastapi import FastAPI
from app.api.v1.endpoints import items, users
from app.core.config import settings
app = FastAPI(title=settings.PROJECT_NAME, openapi_url=f"/api/v1/openapi.json")
app.include_router(items.router, prefix="/api/v1", tags=["items"])
app.include_router(users.router, prefix="/api/v1", tags=["users"])
@app.get("/")
async def root():
return {"message": "Welcome to FastAPI Project!"}
Real-world Example: Defining an endpoint and a Pydantic schema
This shows how an endpoint in app/api/v1/endpoints/items.py might use a Pydantic schema from app/schemas/item.py.
# app/schemas/item.py
from typing import Optional
from pydantic import BaseModel
class ItemBase(BaseModel):
title: str
description: Optional[str] = None
class ItemCreate(ItemBase):
pass
class Item(ItemBase):
id: int
owner_id: int
class Config:
orm_mode = True
# app/api/v1/endpoints/items.py
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app import crud, schemas
from app.db.session import get_db
router = APIRouter()
@router.post("/items/", response_model=schemas.Item, status_code=status.HTTP_201_CREATED)
def create_item(item: schemas.ItemCreate, db: Session = Depends(get_db)):
db_item = crud.item.get_item_by_title(db, title=item.title)
if db_item:
raise HTTPException(status_code=400, detail="Item with this title already exists")
return crud.item.create_item(db=db, item=item)
@router.get("/items/", response_model=List[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
items = crud.item.get_items(db, skip=skip, limit=limit)
return items
Advanced Usage: Configuration management
Using Pydantic's BaseSettings for environment variable management in app/core/config.py.
# app/core/config.py
from pydantic import BaseSettings
class Settings(BaseSettings):
PROJECT_NAME: str = "FastAPI Boilerplate"
API_V1_STR: str = "/api/v1"
SECRET_KEY: str = "your-super-secret-key"
DATABASE_URL: str = "sqlite:///./sql_app.db"
class Config:
case_sensitive = True
env_file = ".env"
settings = Settings()
Common Mistakes
- Monolithic
main.py: Beginners often put all routes, models, and logic directly intomain.py. This quickly becomes unmanageable.
Fix: UseAPIRouterto separate routes into different files (e.g.,api/v1/endpoints/) and import them intomain.py. - Mixing Concerns: Placing database queries directly within endpoint functions or mixing Pydantic models with SQLAlchemy models.
Fix: Separate Pydantic schemas (for request/response) from SQLAlchemy models (for database interaction). Use acrud/layer for database operations. - Hardcoding Configuration: Embedding sensitive information or environment-dependent values directly in the code.
Fix: Use a dedicatedconfig.pyfile, ideally leveraging Pydantic'sBaseSettingsto load values from environment variables or a.envfile.
Best Practices
- Use API Versioning: Implement
/v1/,/v2/prefixes for your API routes. This allows you to evolve your API without breaking existing clients. - Separate Models: Clearly distinguish between Pydantic schemas (for API request/response validation and serialization) and SQLAlchemy models (for database table definitions).
- Layered Architecture: Adopt a layered architecture (e.g., presentation/API layer, business logic/service layer, data access/CRUD layer). This improves testability and maintainability.
- Configuration Management: Centralize all application configuration in a single module (e.g.,
app/core/config.py) and load from environment variables. - Testing Directory: Always include a
tests/directory with a structure that mirrors your main application to facilitate unit and integration testing.
Practice Exercises
- Create a new FastAPI project and set up the basic directory structure as outlined above, including
app/main.py,app/api/v1/endpoints/, andapp/schemas/. - In your new project, define a Pydantic schema for a
Product(with fields likename,price,description) inapp/schemas/product.py. - Create a new endpoint file
app/api/v1/endpoints/products.pyand add a simple GET route that returns a hardcoded list ofProductobjects. Ensure it's properly included inapp/main.py.
Mini Project / Task
Extend your project structure to include a crud/ directory. Create a crud/product.py file that contains functions to simulate creating and retrieving products from a list (instead of a database for now). Modify your products.py endpoint to use these CRUD functions.
Challenge (Optional)
Implement basic configuration management using Pydantic's BaseSettings. Create an app/core/config.py. Define a setting for APP_VERSION and ensure it can be loaded from a .env file at the project root. Modify your main.py to display this version in the root endpoint's response.
Path Parameters
Path parameters are dynamic values placed directly inside a URL path so your FastAPI application can identify which resource a client wants to access. Instead of creating a separate route for every product, user, or order, you define one route with a variable segment such as /users/{user_id}. When a request arrives, FastAPI extracts the value from the path, converts it to the expected Python type, and passes it into your function. This exists to make APIs scalable, readable, and resource-oriented. In real projects, path parameters are used for fetching a user profile by ID, viewing a blog post by slug, loading an order by number, or opening a file by name in an internal tool.
FastAPI makes path parameters especially powerful because they work with Python type hints. If you declare item_id: int, FastAPI validates that the value in the URL is an integer. If the client sends text instead, FastAPI automatically returns a clear validation error. Common forms include numeric IDs, string usernames, slugs, and enum-based fixed choices like status names. Another important concept is route order. A specific route like /users/me should usually be declared before a dynamic route like /users/{user_id} so FastAPI does not interpret me as a parameter value.
Step-by-Step Explanation
To create a path parameter, define a route with curly braces in the URL and match that name in the function argument list. Then optionally add a Python type. FastAPI uses the parameter name in the path and the function name to bind the incoming value. For example, @app.get("/items/{item_id}") means FastAPI expects a value after /items/. If your function is def read_item(item_id: int):, the framework parses the URL part and converts it to an integer. You can return it in a dictionary, use it in database queries, or combine it with business logic. If needed, you can also restrict values using enums for predefined route options.
Comprehensive Code Examples
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id, "message": "Basic path parameter example"}from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{username}")
def get_user_profile(username: str):
return {"username": username, "profile": "Public profile data"}
@app.get("/orders/{order_id}")
def get_order(order_id: int):
return {"order_id": order_id, "status": "shipped"}from enum import Enum
from fastapi import FastAPI
app = FastAPI()
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
@app.get("/models/{model_name}")
def get_model(model_name: ModelName):
if model_name == ModelName.resnet:
return {"model_name": model_name, "type": "deep-learning"}
return {"model_name": model_name, "type": "classic"}
@app.get("/users/me")
def read_current_user():
return {"user": "current-user"}
@app.get("/users/{user_id}")
def read_user(user_id: str):
return {"user_id": user_id}Common Mistakes
- Using different names: If the path uses
{item_id}but the function usesid, FastAPI cannot bind correctly. Use matching names. - Wrong route order: Declaring
/users/{user_id}before/users/mecan causemeto be treated as a parameter. Put fixed routes first. - Ignoring types: Using
strfor everything removes useful validation. Add precise types likeintwhere appropriate.
Best Practices
- Use clear, meaningful parameter names such as
user_idorpost_slug. - Prefer integer IDs for database records and strings for readable slugs or usernames.
- Use enums when only a small set of values should be accepted.
- Keep route design resource-oriented, such as
/products/{product_id}instead of action-heavy naming. - Test invalid inputs to confirm validation errors are helpful.
Practice Exercises
- Create a route
/books/{book_id}that returns the book ID as an integer. - Create a route
/students/{name}that returns a welcome message using the student name. - Create two routes:
/account/meand/account/{account_id}, making sure the fixed route works correctly.
Mini Project / Task
Build a small inventory API with routes like /products/{product_id}, /categories/{category_name}, and /suppliers/{supplier_id} that each return simple mock JSON data based on the path parameter.
Challenge (Optional)
Create an enum-based route such as /reports/{report_type} where only a few report types are accepted, and return different mock responses depending on the selected type.
Query Parameters
Query parameters are values sent in the URL after a question mark, such as /items?limit=10&skip=20. In FastAPI, they are one of the most common ways to filter, search, sort, and paginate data without changing the path of the endpoint. They exist because many API operations need optional input that modifies the response rather than identifying a completely different resource. For example, an online store may use query parameters to filter products by category, price, or availability, while a blog API may use them for search terms, page numbers, and sorting options. FastAPI makes query parameters especially beginner-friendly because it reads Python function parameters and type hints to automatically parse, validate, and document them. Common forms include required query parameters, optional query parameters, parameters with default values, typed parameters like int and bool, and constrained parameters using validation helpers such as minimum length or numeric limits. This keeps APIs clear and safe because invalid input is rejected automatically.
Step-by-Step Explanation
To create a query parameter in FastAPI, define it as a function argument in a path operation that is not part of the path itself. If a route is /products and your function has limit: int = 10, FastAPI treats limit as a query parameter. If you omit the default value and write q: str, it becomes required. If you write q: str | None = None, it becomes optional. FastAPI converts types automatically, so strings from the URL become integers, booleans, or other supported types when possible. You can also use Query() to add validation rules, descriptions, aliases, and length constraints. This is useful when you want input like a search term to be at least three characters long or a page size to stay within a safe limit. In practice, use query parameters for operations such as filtering a list, paginating records, toggling extra details, or sorting output.
Comprehensive Code Examples
from fastapi import FastAPI
app = FastAPI()
@app.get("/items")
def list_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}from fastapi import FastAPI
app = FastAPI()
@app.get("/products")
def get_products(category: str | None = None, in_stock: bool = False):
return {"category": category, "in_stock": in_stock}from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/search")
def search_items(
q: str = Query(..., min_length=3, max_length=50),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100)
):
return {"query": q, "page": page, "page_size": page_size}The first example shows basic pagination values. The second reflects a real-world filtering scenario for a product catalog. The third is more advanced because it adds validation rules that protect your API and improve documentation.
Common Mistakes
- Mistake: Forgetting a default value and accidentally making a parameter required. Fix: Use
= Noneor another default if it should be optional. - Mistake: Treating all values as strings. Fix: Add proper type hints like
int,bool, orfloat. - Mistake: Allowing unsafe values like huge page sizes. Fix: Use
Query()with constraints such asle=100.
Best Practices
- Use query parameters for filtering, sorting, searching, and pagination, not for identifying the main resource.
- Always add clear type hints so FastAPI can validate and document input correctly.
- Set safe defaults for values like page number and page size.
- Use
Query()for validation and better API self-documentation. - Choose descriptive names such as
page,limit, andsort_by.
Practice Exercises
- Create a
/booksendpoint that acceptspageandlimitquery parameters with default values. - Build a
/usersendpoint that accepts an optionalrolequery parameter and returns it in the response. - Create a
/searchendpoint whereqis required and must contain at least 3 characters.
Mini Project / Task
Build a product listing API endpoint called /products that supports category filtering, stock status, page number, and page size using query parameters.
Challenge (Optional)
Create a movie API endpoint that supports optional search, minimum rating, sorting order, and pagination, while validating every query parameter with appropriate limits and defaults.
Request Body
In FastAPI, a request body is the data a client sends to an API, usually in JSON format, when creating or updating information. It exists because many operations need structured input such as user details, product information, or order data. In real applications, request bodies are used in registration forms, checkout systems, blog publishing tools, and mobile app backends. FastAPI makes request bodies especially powerful by combining Python type hints with Pydantic models, which automatically validate incoming data and generate clear API documentation. The most common form is a JSON object mapped to a Pydantic model, but request bodies can also contain optional fields, nested objects, lists, and mixed body parameters. This helps developers define exactly what data an endpoint expects while reducing manual parsing and error handling.
Step-by-Step Explanation
To use a request body, first create a Pydantic model that describes the expected data. Then pass that model as a parameter to a path operation function such as POST, PUT, or PATCH. FastAPI reads the type annotation, extracts JSON from the request body, validates each field, converts types when possible, and provides the result as a Python object. For example, a field declared as str must receive text, while float expects a number. Optional values can be declared using defaults like None. If the client sends invalid data, FastAPI automatically returns a helpful validation error. You can also access model data using dot notation, such as item.name. For updates, request bodies are often paired with path parameters so the API knows both which resource to update and what new data to apply.
Comprehensive Code Examples
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
in_stock: bool
@app.post("/items/")
def create_item(item: Item):
return {"message": "Item created", "item": item}This basic example accepts JSON data for a new item. FastAPI validates the fields and returns the parsed object.
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class CustomerOrder(BaseModel):
customer_name: str
product_id: int
quantity: int
note: Optional[str] = None
@app.post("/orders/")
def create_order(order: CustomerOrder):
total_items = order.quantity
return {
"status": "received",
"customer": order.customer_name,
"product_id": order.product_id,
"total_items": total_items,
"note": order.note
}This real-world example models an order submission, including an optional note field.
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import List, Optional
app = FastAPI()
class Address(BaseModel):
city: str
country: str
class UserProfile(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
age: int = Field(..., ge=13)
skills: List[str]
address: Address
bio: Optional[str] = None
@app.put("/profiles/{user_id}")
def update_profile(user_id: int, profile: UserProfile):
return {
"user_id": user_id,
"updated_profile": profile
}This advanced example shows nested models, list fields, validation rules, and combining a path parameter with a request body.
Common Mistakes
Using plain dictionaries instead of models: This works, but you lose automatic validation and documentation. Use
BaseModelfor structured input.Sending wrong JSON field names: If the client sends
productNamebut the model expectsname, validation fails. Match the schema exactly.Confusing query parameters with request body data: Query values go in the URL, while JSON body data is sent in the request payload. Use each in the correct place.
Forgetting optional defaults: If a field should not be required, assign a default such as
None.
Best Practices
Use Pydantic models for every meaningful request body.
Choose clear, business-focused field names like
email,quantity, andshipping_address.Add validation rules with
Field()to catch bad input early.Keep models small and reusable by separating nested data into dedicated classes.
Test endpoints with valid and invalid JSON to understand validation behavior.
Practice Exercises
Create a
Bookmodel withtitle,author, andprice, then build aPOSTendpoint that returns the submitted book.Create a
Studentmodel with one optional field and test how FastAPI behaves when that field is missing.Build an endpoint that accepts a nested request body for a customer and their address.
Mini Project / Task
Build a simple product registration API where users can submit product details through a request body, and the server validates and returns the stored product information.
Challenge (Optional)
Create an endpoint for updating a user profile that accepts nested JSON, validates age and username length, and returns a custom success message with the parsed body.
Pydantic Models
Pydantic models are Python classes used by FastAPI to define the shape, rules, and data types of incoming requests and outgoing responses. They exist to solve a very common backend problem: APIs often receive messy, incomplete, or incorrectly typed data from clients. Instead of manually checking every field, Pydantic validates the data automatically and converts compatible values when possible. In real applications, this is used for user registration forms, product creation, payment payloads, order records, and profile updates. In FastAPI, Pydantic models are central because they connect request parsing, validation, documentation, and serialization into one clean system.
The core idea is simple: you create a class that inherits from BaseModel, then define fields with Python type hints such as str, int, float, bool, list, and nested models. FastAPI reads those type hints and uses them to validate JSON request bodies. Common model variations include input models for creating data, response models for returning safe output, optional-field models for updates, and nested models for structured objects. For example, a UserCreate model may include password input, while a UserResponse model excludes it for security. This separation is extremely important in production APIs.
Step-by-Step Explanation
Start by importing BaseModel from Pydantic. Then define a class with attributes and types. Each attribute becomes a required field unless you provide a default value. Optional values can be declared with a default such as None. After that, use the model as a parameter in a FastAPI route. When a client sends JSON, FastAPI converts it into an instance of that model. If the payload does not match the rules, FastAPI returns a clear validation error automatically.
For beginners, think of the syntax in three parts: model declaration, route usage, and response handling. Model declaration defines what valid data looks like. Route usage tells FastAPI to expect that data. Response handling can also use a model to control what the API returns. This reduces bugs, improves API consistency, and generates accurate interactive docs automatically.
Comprehensive Code Examples
Basic example
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
in_stock: bool
@app.post("/items/")
def create_item(item: Item):
return {"message": "Item created", "item": item}Real-world example
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
class UserResponse(BaseModel):
username: str
email: EmailStr
@app.post("/users/", response_model=UserResponse)
def create_user(user: UserCreate):
return userAdvanced usage
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional, List
app = FastAPI()
class Address(BaseModel):
city: str
zip_code: str
class OrderItem(BaseModel):
product_name: str
quantity: int
class Order(BaseModel):
customer_name: str
address: Address
items: List[OrderItem]
notes: Optional[str] = None
@app.post("/orders/")
def create_order(order: Order):
total_items = sum(item.quantity for item in order.items)
return {"customer": order.customer_name, "total_items": total_items}Common Mistakes
- Using plain dictionaries instead of models: this removes validation benefits. Fix it by defining a proper
BaseModelclass. - Returning sensitive fields: beginners often return passwords accidentally. Fix it by using a separate response model.
- Confusing optional fields with empty strings:
Optional[str]means the field may be missing orNone, not automatically blank. Fix it by choosing defaults carefully.
Best Practices
- Use separate models for create, update, and response operations.
- Keep models small, focused, and reusable across routes.
- Use nested models for structured JSON instead of flat, unclear payloads.
- Prefer explicit types because clearer validation leads to better API docs.
Practice Exercises
- Create a
Bookmodel with title, author, price, and availability, then use it in a POST route. - Create separate models for
StudentCreateandStudentResponse, making sure the response excludes confidential data. - Build a nested
Customermodel that contains anAddressmodel and test it in a route.
Mini Project / Task
Build a small product API endpoint that accepts a product name, category, price, stock quantity, and supplier address using nested Pydantic models.
Challenge (Optional)
Create an order-checkout model with a list of items, customer information, and an optional discount code, then return the total quantity of all ordered items.
Field Validation
Field validation in FastAPI is a crucial aspect of building robust and reliable APIs. It ensures that the data received from clients conforms to expected types, formats, and constraints before it is processed by your application logic. This prevents common issues like incorrect data types, missing required fields, or values outside of an acceptable range, which can lead to server errors, security vulnerabilities, or corrupted data. FastAPI leverages Pydantic for its data validation and serialization, making it incredibly powerful and easy to use. Pydantic allows you to define data schemas using standard Python type hints, and it automatically validates incoming request bodies, query parameters, path parameters, and headers against these schemas. If the data doesn't match the defined schema, FastAPI automatically returns a clear and descriptive error message to the client, improving API usability and developer experience. This validation happens automatically, reducing boilerplate code and making your API more secure and predictable. In real-world applications, field validation is indispensable for handling user input, processing data from external systems, and maintaining data integrity across various services. Imagine an e-commerce platform where product prices must be positive numbers, or a user registration system where email addresses must be valid and passwords meet certain complexity requirements. Field validation handles all these scenarios gracefully, ensuring only valid data enters your system.
FastAPI's field validation is primarily driven by Pydantic models. Pydantic allows you to define models using standard Python classes that inherit from pydantic.BaseModel. Within these classes, you define fields using type hints, and Pydantic automatically infers the validation rules. For more advanced validation, Pydantic provides several built-in types and field options. You can specify default values, mark fields as optional, and add extra validation constraints. For instance, using Optional[str] from the typing module makes a string field optional. You can also use Pydantic's Field function from the pydantic library to add more granular validation rules like minimum length (min_length), maximum length (max_length), regular expressions (regex), minimum value (ge for greater than or equal to, gt for greater than), maximum value (le for less than or equal to, lt for less than), and even custom validators. These powerful features allow you to define highly specific validation logic without writing extensive manual checks. FastAPI integrates seamlessly with these Pydantic models, automatically performing validation when they are used as request body parameters, query parameters, or path parameters.
Step-by-Step Explanation
To implement field validation in FastAPI, you typically follow these steps:
- Define a Pydantic Model: Create a Python class that inherits from
pydantic.BaseModel. This class will represent the structure and validation rules for your data. - Specify Field Types: Use standard Python type hints (e.g.,
str,int,float,bool,list,dict) for each field in your Pydantic model. Pydantic will automatically validate the incoming data against these types. - Add Default Values (Optional Fields): For optional fields, you can assign a default value directly (e.g.,
name: str = 'John Doe') or useOptional[Type]from thetypingmodule with a default ofNone(e.g.,description: Optional[str] = None). - Apply Advanced Validation with
Field: ImportFieldfrompydantic. UseField(...)as the default value for a model attribute to add more specific validation constraints. For example,item_id: int = Field(..., gt=0)ensuresitem_idis an integer greater than 0. - Use the Model in FastAPI Endpoint: In your FastAPI path operation function, declare a parameter with the type hint of your Pydantic model. FastAPI will automatically read the request body (for POST/PUT) or query/path parameters and validate them against your model.
- Handle Validation Errors: If validation fails, FastAPI automatically returns an HTTP 422 Unprocessable Entity response with a detailed JSON error message, indicating which fields failed validation and why. You don't typically need to write explicit error handling for basic validation failures.
Comprehensive Code Examples
Basic Example
This example shows a simple Pydantic model for an item with basic type validation.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.post('/items/')
async def create_item(item: Item):
return item
Real-world Example
A more realistic example including required fields, optional fields, and basic numeric validation for a product.
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
from typing import List, Optional
app = FastAPI()
class Product(BaseModel):
product_id: str = Field(..., min_length=5, max_length=10, regex='^[a-zA-Z0-9]+$')
name: str = Field(..., min_length=3, description='Name of the product')
description: Optional[str] = Field(None, max_length=500)
price: float = Field(..., gt=0, description='Price must be greater than zero')
tax: Optional[float] = Field(None, ge=0, le=1, description='Tax rate between 0 and 1')
tags: List[str] = []
@app.post('/products/', response_model=Product)
async def create_product(product: Product):
# In a real application, you would save the product to a database
print(f'Product received: {product.dict()}')
return product
@app.get('/products/{product_id}', response_model=Product)
async def get_product(product_id: str):
# Simulate fetching from a database
if product_id == 'PROD123':
return Product(product_id='PROD123', name='Sample Product', price=29.99)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Product not found')
Advanced Usage
Demonstrates custom validation with Pydantic's validator decorator and nested models.
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field, validator
from typing import List, Optional
app = FastAPI()
class Address(BaseModel):
street: str
city: str
zip_code: str = Field(..., regex='^\d{5}(-\d{4})?$') # US Zip Code format
class User(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
email: str = Field(..., regex='^[^@]+@[^@]+\.[^@]+$') # Simple email regex
password: str = Field(..., min_length=8)
age: Optional[int] = Field(None, ge=18, le=100)
is_active: bool = True
shipping_address: Optional[Address] = None
@validator('username')
def username_cannot_be_admin(cls, v):
if v.lower() == 'admin':
raise ValueError('Username cannot be "admin"')
return v
@validator('password')
def password_strength(cls, v):
if not any(char.isdigit() for char in v):
raise ValueError('Password must contain at least one digit')
if not any(char.isupper() for char in v):
raise ValueError('Password must contain at least one uppercase letter')
return v
@app.post('/register/', response_model=User)
async def register_user(user: User):
# In a real app, you'd hash the password and save the user
print(f'User registered: {user.dict()}')
return user
Common Mistakes
- Forgetting to import
Field: Many beginners forget to importFieldfrompydanticwhen trying to add extra validation constraints likemin_lengthorgt, leading toNameError.
Fix: Always remember to addfrom pydantic import Field. - Incorrectly using
Optional: UsingOptional[Type]without providing a default value ofNonefor an optional field in a Pydantic model can make Pydantic treat it as a required field if not explicitly marked.
Fix: For optional fields, always define them asfield_name: Optional[Type] = Noneorfield_name: Type | None = None. - Misunderstanding
...vsNonevs default values:...(ellipsis) explicitly marks a field as required and without a default value.Noneas a default makes a field optional. A literal default value (e.g.,name: str = 'Default Name') makes it optional with that default. Confusing these can lead to unexpected validation behavior.
Fix: Use...for strictly required fields without defaults,NoneorType | None = Nonefor optional fields, and specific values for optional fields with defaults.
Best Practices
- Use Pydantic models extensively: Always define Pydantic models for incoming request bodies and outgoing responses (using
response_model). This ensures clear data contracts and automatic documentation. - Be explicit with validation rules: Don't rely solely on basic type hints. Use
Fieldto specify granular constraints likemin_length,max_length,gt,le, andregexto ensure data quality. - Organize models in separate files: For larger applications, group your Pydantic models into a
models.pyor similar file to keep your main application file clean and organized. - Use
response_model: Always specifyresponse_modelin your path operations. This validates the output data, serializes it correctly, and provides accurate API documentation. - Leverage custom validators for complex logic: For validation logic that cannot be expressed with simple
Fieldparameters (e.g., cross-field validation, specific business rules), use Pydantic's@validatordecorator.
Practice Exercises
- Exercise 1 (Basic Item Validation): Create a FastAPI application with an endpoint
/books/that accepts POST requests. Define a Pydantic modelBookwith fields:title(string, required),author(string, required),year(integer, required, must be greater than 1900). - Exercise 2 (User Registration): Extend the previous exercise or create a new one. Define a
UserRegistrationmodel with fields:username(string, required, min length 4, max length 15),email(string, required, must be a valid email format using regex),password(string, required, min length 8, max length 20). Create a POST endpoint/register/that uses this model. - Exercise 3 (Product Update with Optional Fields): Create a FastAPI endpoint
/products/{product_id}that accepts PUT requests. Define a Pydantic modelProductUpdatewith fields:name(optional string, min length 3),description(optional string, max length 500),price(optional float, must be greater than 0). The endpoint should acceptproduct_idas a path parameter andProductUpdateas the request body.
Mini Project / Task
Build a simple 'Task Management API'. Create a FastAPI application that allows users to create tasks. Define a Pydantic model TaskCreate with the following fields: title (string, required, min length 5), description (string, optional, max length 1000), due_date (string, optional, representing a date in 'YYYY-MM-DD' format, you can use datetime.date from Pydantic for proper date validation), and priority (integer, optional, must be between 1 and 5 inclusive). Implement a POST endpoint /tasks/ that accepts a TaskCreate object and returns the created task (you don't need a database, just return the received data). Ensure all validation rules are correctly applied.
Challenge (Optional)
Enhance the 'Task Management API' from the mini-project. Add a custom validator to the TaskCreate model to ensure that if a due_date is provided, it must be a date in the future (i.e., not today or in the past). You'll need to import date from datetime and use a Pydantic @validator decorator. Additionally, implement a GET endpoint /tasks/{task_id} that returns a sample task (again, no database, just hardcode a response) and ensure the task_id path parameter is an integer greater than 0 using Field.
Response Models
Response models in FastAPI define the exact shape of the data your API sends back to clients. They exist to make responses predictable, validated, and well documented. In real-world APIs, this is extremely important because clients such as web apps, mobile apps, and third-party integrations depend on consistent response formats. A response model acts like a contract: your route may work with database objects, internal dictionaries, or computed values, but FastAPI will convert and filter the output so the client only receives what the model allows.
In practice, response models are commonly created with Pydantic classes. They are used for hiding sensitive fields like passwords, standardizing public API output, and generating accurate OpenAPI docs automatically. A common pattern is to have one model for input and another for output. For example, a user creation request may include a password, but the response model should exclude it. FastAPI also supports response_model on path operations, optional fields, lists of models, and nested models for more complex responses.
The most common sub-types are: a single object response, a list response, nested response models, and filtered responses using include or exclude options. You can also use different models for create, update, and read operations. This separation helps maintain security and keeps your API stable as internal logic evolves.
Step-by-Step Explanation
First, define a Pydantic model representing the data you want to return. Next, attach it to a route using the response_model parameter in the decorator. Then return any compatible Python data such as a dict, list, or object. FastAPI validates the returned value against the model, converts types when possible, and removes fields not declared in the response model.
Basic syntax works like this: create a class that inherits from BaseModel, define fields with type hints, and reference that class in @app.get(..., response_model=YourModel). If you want a list, use something like list[YourModel]. If your returned object contains more fields than expected, FastAPI keeps only the ones defined by the response model.
Comprehensive Code Examples
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class UserOut(BaseModel):
id: int
username: str
email: str
@app.get("/user", response_model=UserOut)
def get_user():
return {
"id": 1,
"username": "alice",
"email": "[email protected]",
"password": "secret"
}In this basic example, the password is returned by the function but removed from the final API response because it is not part of UserOut.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class ProductOut(BaseModel):
id: int
name: str
price: float
@app.get("/products", response_model=list[ProductOut])
def list_products():
return [
{"id": 1, "name": "Keyboard", "price": 49.99, "stock": 20},
{"id": 2, "name": "Mouse", "price": 19.99, "stock": 50}
]This real-world example returns a list of products while hiding internal inventory fields.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Author(BaseModel):
id: int
name: str
class ArticleOut(BaseModel):
id: int
title: str
author: Author
tags: list[str]
@app.get("/articles/{article_id}", response_model=ArticleOut)
def get_article(article_id: int):
return {
"id": article_id,
"title": "Using Response Models",
"author": {"id": 10, "name": "Sam", "internal_note": "editor"},
"tags": ["fastapi", "api"],
"draft": False
}This advanced example shows nested models and automatic filtering at multiple levels.
Common Mistakes
- Using the same model for input and output: This may expose fields like passwords. Create separate request and response models.
- Returning incompatible data types: If the model expects a float or nested object, return matching data structures or convert them first.
- Forgetting list syntax: If the endpoint returns many items, use
response_model=list[ModelName]instead of a single model.
Best Practices
- Separate public and internal models: Keep database fields and private metadata out of client responses.
- Use clear naming: Names like
UserCreate,UserUpdate, andUserOutmake intent obvious. - Design stable contracts: Clients rely on your response schema, so avoid changing response models casually.
- Leverage automatic docs: Response models improve Swagger and help frontend teams understand the API quickly.
Practice Exercises
- Create a
BookOutmodel and return one book from a route while hiding an internal warehouse field. - Build an endpoint that returns a list of movies using
response_model=list[MovieOut]. - Create a nested response model for an order that includes customer details and a list of item names.
Mini Project / Task
Build a small employee directory API with one endpoint that returns public employee profiles. Include id, name, role, and email, but ensure salary and internal notes are never included in the response.
Challenge (Optional)
Create separate models for user registration and public user display, then build two routes: one that accepts full registration data and another that returns only safe public fields.
Status Codes
Status codes are the numeric results that an HTTP server sends back to tell the client what happened after a request. In FastAPI, they are essential because APIs are not just about returning data; they are also about clearly communicating success, failure, invalid input, missing resources, authentication problems, and server issues. In real applications, frontend apps, mobile apps, other microservices, and third-party integrations depend on these codes to decide what to do next. For example, a shopping app may show a success message after a 201 response, ask the user to log in after a 401, or display “product not found” after a 404.
HTTP status codes are grouped by meaning. 2xx codes mean success, such as 200 OK for normal successful requests and 201 Created when a new resource is created. 4xx codes mean the client sent a bad request or asked for something invalid, such as 400 Bad Request, 401 Unauthorized, 403 Forbidden, and 404 Not Found. 5xx codes mean the server failed while processing a valid request, like 500 Internal Server Error.
Step-by-Step Explanation
In FastAPI, you can set a status code directly in the route decorator using the status_code parameter. This is the most common way for successful responses. FastAPI also provides named constants through fastapi.status, which makes your code easier to read than hardcoding numbers. For error responses, you usually raise HTTPException and specify both the status code and a detail message.
A beginner should remember three common patterns. First, use status_code=200 or let FastAPI use the default for successful reads. Second, use 201 for successful creation of data. Third, raise HTTPException for failures like missing items or permission problems. This keeps API behavior predictable and aligns with REST conventions.
Comprehensive Code Examples
Basic example
from fastapi import FastAPI, status
app = FastAPI()
@app.get("/health", status_code=status.HTTP_200_OK)
def health_check():
return {"message": "API is running"}Real-world example
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
products = {1: {"name": "Laptop"}, 2: {"name": "Mouse"}}
@app.get("/products/{product_id}")
def get_product(product_id: int):
if product_id not in products:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found")
return products[product_id]
@app.post("/products", status_code=status.HTTP_201_CREATED)
def create_product(product: dict):
new_id = max(products.keys()) + 1
products[new_id] = product
return {"id": new_id, "product": product}Advanced usage
from fastapi import FastAPI, Response, status, HTTPException
app = FastAPI()
users = {"alice": {"active": True}}
@app.delete("/users/{username}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user(username: str):
if username not in users:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
del users[username]
return Response(status_code=status.HTTP_204_NO_CONTENT)Common Mistakes
- Using
200for everything: Use201for created resources,204for successful deletion without content, and proper error codes for failures. - Returning error messages without exceptions: If something fails, raise
HTTPExceptioninstead of returning a normal dictionary with an error string. - Sending a body with
204: A204 No Contentresponse should not include response content.
Best Practices
- Use constants from
fastapi.statusfor readability and maintainability. - Match status codes to action semantics: read, create, update, delete, authenticate, and validate correctly.
- Keep error messages short, clear, and useful for API consumers.
- Document expected status codes so frontend and backend teams share the same contract.
Practice Exercises
- Create a
GETendpoint that returns200 OKwith a simple welcome message. - Create a
POSTendpoint for adding a book and return201 Created. - Create a
GETendpoint that looks up a user by ID and raises404 Not Foundif the user does not exist.
Mini Project / Task
Build a small task API with three endpoints: list tasks with 200, create a task with 201, and delete a task with 204. If a task ID does not exist, return 404.
Challenge (Optional)
Design an endpoint that checks login credentials and returns different status codes for success, invalid credentials, and blocked users. Decide when to use 200, 401, and 403 correctly.
HTTP Methods GET POST PUT DELETE
HTTP methods define the action a client wants to perform on a server resource. In FastAPI, these methods are mapped to Python functions using decorators such as @app.get(), @app.post(), @app.put(), and @app.delete().
They exist to make APIs predictable and organized. In real life, a mobile app may use GET to load products, POST to create a new order, PUT to update a customer profile, and DELETE to remove a saved address. These methods are the foundation of REST-style API design, where URLs represent resources and HTTP methods represent actions.
Each method has a common purpose. GET retrieves data and should not change server state. POST creates new data or triggers a server-side action. PUT updates an existing resource, often by replacing all of its fields. DELETE removes a resource. Understanding these differences helps you build APIs that other developers can understand quickly and use correctly.
Step-by-Step Explanation
In FastAPI, you first create an application with FastAPI(). Then you define routes using decorators. A route combines a URL path and an HTTP method. For example, @app.get("/items") means the function below it will run when a client sends a GET request to /items.
Path parameters like /items/{item_id} let you target a specific record. POST and PUT often accept a request body, usually defined with a Pydantic model. DELETE commonly uses a path parameter to identify what should be removed. FastAPI automatically validates input data and generates API docs, which makes testing much easier for beginners.
Comprehensive Code Examples
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
def read_hello():
return {"message": "Hello from GET"}Basic GET example: returns data without changing anything.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
items = {}
class Item(BaseModel):
name: str
price: float
@app.post("/items/{item_id}")
def create_item(item_id: int, item: Item):
items[item_id] = item.dict()
return {"message": "Item created", "item": items[item_id]}
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
items[item_id] = item.dict()
return {"message": "Item updated", "item": items[item_id]}
@app.delete("/items/{item_id}")
def delete_item(item_id: int):
if item_id in items:
deleted = items.pop(item_id)
return {"message": "Item deleted", "item": deleted}
return {"message": "Item not found"}This real-world CRUD-style example shows how the four methods work together around one resource.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
users = {1: {"name": "Ava", "email": "[email protected]"}}
class User(BaseModel):
name: str
email: str
@app.get("/users/{user_id}")
def get_user(user_id: int):
if user_id not in users:
raise HTTPException(status_code=404, detail="User not found")
return users[user_id]
@app.put("/users/{user_id}")
def replace_user(user_id: int, user: User):
if user_id not in users:
raise HTTPException(status_code=404, detail="User not found")
users[user_id] = user.dict()
return users[user_id]Advanced usage includes proper error handling with HTTPException for clearer API behavior.
Common Mistakes
Using GET to change data. Fix: reserve GET for reading only.
Forgetting request models for POST and PUT. Fix: define a Pydantic model to validate body data.
Not handling missing records. Fix: return a clear 404 error with
HTTPException.
Best Practices
Use nouns in routes such as
/itemsand let the HTTP method describe the action.Keep GET endpoints safe and idempotent where possible.
Return meaningful messages and proper status codes for create, update, and delete operations.
Practice Exercises
Create a GET route called
/booksthat returns a list of three books.Create a POST route that accepts a product name and price using a Pydantic model.
Create a PUT route to update a student record by ID and a DELETE route to remove it.
Mini Project / Task
Build a small FastAPI inventory service with endpoints to list products, add a product, replace a product, and delete a product by ID.
Challenge (Optional)
Design a task manager API where GET returns all tasks, POST creates tasks, PUT updates a task by ID, and DELETE removes a task while returning a helpful error if the task does not exist.
Multiple Parameters
In FastAPI, handling multiple parameters in your path operation functions is a common and powerful feature that allows your API endpoints to be highly flexible and dynamic. This capability is fundamental for building robust APIs that can respond to various client requests by accepting different pieces of information. For instance, you might need an API endpoint that retrieves an item by its ID, but also allows filtering or sorting based on additional criteria provided in the query string. Or perhaps you're updating a resource, and you need both the resource's ID (from the path) and the updated data (from the request body). FastAPI intelligently differentiates between various types of parameters: path parameters, query parameters, and request body parameters, enabling you to combine them seamlessly within a single function signature. This design philosophy simplifies API development by allowing you to define complex endpoint behaviors with clear and concise Python type hints.
The existence of multiple parameter types stems from the different ways clients interact with web APIs. Path parameters are ideal for identifying a specific resource (e.g.,
/items/{item_id}). Query parameters are perfect for optional filtering, sorting, or pagination (e.g., /items/?skip=0&limit=10). Request body parameters, typically sent with POST, PUT, or PATCH requests, are used for sending larger, structured data, such as creating a new item or updating an existing one. FastAPI's ability to unify these different parameter sources into a single function signature, leveraging Python's type hinting system, is a cornerstone of its developer-friendly approach. In real-world scenarios, this translates to APIs that are easier to understand, maintain, and extend, as the function signature directly reflects the API's expected input.Step-by-Step Explanation
FastAPI uses standard Python type hints to declare parameters. When you define a function parameter, FastAPI inspects its type hint to determine how it should be treated. Let's break down how different types of parameters are handled:
- Path Parameters: These are defined in the path itself using curly braces (e.g.,
/items/{item_id}). In your function, you declare a parameter with the same name as the path parameter and provide a type hint. FastAPI will automatically extract the value from the URL path. - Query Parameters: These are declared as function parameters that are not part of the path and do not have a default value, or have a default value. If the parameter has a simple type (like
str,int,float,bool), FastAPI treats it as a query parameter. They appear in the URL after a?, separated by&(e.g.,/items?skip=0&limit=10). You can also useQueryfromfastapito add more validation and metadata. - Request Body Parameters: These are typically used with POST, PUT, and PATCH requests. If a function parameter is type-hinted with a Pydantic model, FastAPI automatically expects the incoming request body to match that model structure. This allows for complex data validation and serialization.
- Combining Parameters: The real power comes from combining these. You can have path parameters, query parameters, and a request body all in the same path operation function. FastAPI intelligently parses each part of the request and maps it to the corresponding function parameter.
Comprehensive Code Examples
Basic example: Path and Query Parameters
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
In this example,
item_id is a path parameter (required int), and q is an optional query parameter (str, defaults to None).Real-world example: Path, Query, and Body Parameters with Pydantic
from fastapi import FastAPI, Path, Query
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.put("/items/{item_id}")
async def update_item(
item_id: int = Path(..., title="The ID of the item to update"),
q: str | None = Query(None, alias="item-query", min_length=3, max_length=50),
item: Item
):
results = {"item_id": item_id, "item": item}
if q:
results.update({"q": q})
return results
Here,
item_id is a path parameter with additional validation and metadata using Path. q is an optional query parameter with an alias, min/max length validation, using Query. item is a request body parameter, expected to conform to the Item Pydantic model.Advanced usage: Multiple Body Parameters and Dependencies
from fastapi import FastAPI, Depends
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
class User(BaseModel):
username: str
email: str | None = None
async def get_current_user():
# In a real app, this would get the user from a token or session
return User(username="testuser", email="[email protected]")
@app.post("/items/")
async def create_item(
item: Item,
user: User,
current_user: User = Depends(get_current_user)
):
return {"item": item, "user": user, "current_user_from_dependency": current_user}
This example shows how to accept multiple body parameters (
item and user, both Pydantic models) and also integrate a dependency (get_current_user) which itself can provide a parameter (current_user). FastAPI automatically handles parsing and validating both body models and injecting the dependency's return value.Common Mistakes
- Confusing Path and Query Parameters: A common mistake is to define a parameter in the path (e.g.,
/items/{id}) but then not declareidas a function parameter, or vice-versa. Ensure the names match exactly. - Incorrect Type Hints: Forgetting to provide type hints, or using incorrect ones (e.g.,
strwhen anintis expected) can lead to validation errors or unexpected behavior. Always use correct Python type hints. - Mismatched Body Schema: When sending a request body, especially with Pydantic models, ensure the client sends JSON data that matches the model's structure. Missing required fields or providing incorrect data types will result in validation errors.
- Forgetting to import
QueryorPath: If you intend to add extra validation or metadata to query or path parameters, you must explicitly import and useQueryorPathfromfastapi.
Best Practices
- Use Type Hints Religiously: Always use Python type hints for all parameters. This is the foundation of FastAPI's automatic validation, documentation, and parameter extraction.
- Leverage Pydantic Models for Request Bodies: For any structured data sent in the request body, define a Pydantic model. This provides robust data validation, clear schema definition, and automatic serialization/deserialization.
- Add Validation with
PathandQuery: For path and query parameters, usePath(...)andQuery(...)to add useful validation (e.g.,min_length,gt,le) and metadata (title,description) which will appear in the OpenAPI documentation. - Use Dependencies for Common Logic: If you have parameters that are derived from common logic (like user authentication, database sessions), encapsulate that logic in a dependency function and use
Depends(). This keeps your path operation functions clean and promotes reusability. - Provide Meaningful Default Values: For optional query parameters, provide sensible default values (e.g.,
None, an empty string, or a default integer) to make your API more flexible.
Practice Exercises
- Exercise 1 (Beginner-friendly): Create a FastAPI application with a GET endpoint
/greet/{name}that accepts a path parametername(string) and an optional query parametermessage(string, default to "Hello"). It should return a greeting like "Hello, [name]! Your message: [message]". - Exercise 2: Implement a POST endpoint
/products/{product_id}/reviews. It should takeproduct_idas a path parameter (integer) and aratingas a query parameter (integer, between 1 and 5). The request body should contain the review text (a Pydantic model with a single fieldtext: str). Return a dictionary confirming the received data. - Exercise 3: Design a GET endpoint
/searchthat accepts two optional query parameters:query(string) andlimit(integer, default to 10). Ifqueryis provided, it should return a message indicating what was searched and the limit. If not, it should return a general search message.
Mini Project / Task
Build a simple "Task Manager" API. Create a POST endpoint
/tasks/ that accepts a task object (Pydantic model: Task(title: str, description: str | None = None, due_date: date | None = None)). Create a GET endpoint /tasks/{task_id} that retrieves a single task by its ID (path parameter, integer). Finally, create a PUT endpoint /tasks/{task_id} that updates an existing task, taking both task_id (path parameter) and an updated Task object (request body).Challenge (Optional)
Extend the "Task Manager" API. Implement a GET endpoint
/tasks/ that allows filtering by status (e.g., "pending", "completed") and pagination using skip and limit query parameters. All query parameters should be optional. For the status parameter, ensure that only allowed values are accepted (you can use an Enum for this). Additionally, add a dependency that simulates user authentication, ensuring only authenticated users can access the /tasks/ endpoints. Nested Models
Nested models in FastAPI are Pydantic models that contain other models as fields. They exist because real API data is rarely flat. In practical applications, a user may contain an address, an order may contain a list of products, and a blog post may include author details and comments. Instead of sending everything as loose dictionaries, nested models let you describe the exact structure of complex JSON data in a clean, reusable, and type-safe way.
FastAPI uses these nested Pydantic models to validate incoming request bodies, serialize response data, and generate clear API documentation automatically. This is very useful in real life for e-commerce systems, school portals, booking apps, payment services, and dashboards where one resource contains multiple related parts. Common nested patterns include one-to-one nesting such as a User with a single Profile, one-to-many nesting such as an Order with many Item objects, and optional nesting where a field may or may not be included.
The main idea is simple: define a model, then use that model as the type of a field inside another model. You can also nest lists of models, dictionaries, and optional sub-models. This makes your API contracts easier to understand and reduces bugs caused by malformed JSON.
Step-by-Step Explanation
Start by creating a small model that represents a reusable piece of data, such as an address. Next, create a larger model and assign the smaller model as a field type. If you need multiple related objects, wrap the nested model in a list. When a client sends JSON to your FastAPI endpoint, FastAPI checks that every nested field matches the declared structure. If something is missing or has the wrong type, FastAPI returns a validation error automatically.
For example, if a Customer contains an Address, the request body must include an object for address with the correct fields. If an Order contains a list of Item models, every item in the list must follow the Item schema.
Comprehensive Code Examples
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Address(BaseModel):
city: str
country: str
class User(BaseModel):
name: str
email: str
address: Address
@app.post("/users/")
def create_user(user: User):
return userThis basic example shows one model inside another. The User model requires a nested Address object.
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Product(BaseModel):
name: str
price: float
class Order(BaseModel):
order_id: int
products: List[Product]
@app.post("/orders/")
def create_order(order: Order):
total = sum(product.price for product in order.products)
return {"order_id": order.order_id, "total": total, "products": order.products}This real-world example uses a list of nested models for an order containing many products.
from typing import List, Optional
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Comment(BaseModel):
user: str
message: str
class Author(BaseModel):
name: str
website: Optional[HttpUrl] = None
class BlogPost(BaseModel):
title: str
content: str
author: Author
comments: List[Comment] = []
@app.post("/posts/")
def create_post(post: BlogPost):
return {
"title": post.title,
"author": post.author,
"comment_count": len(post.comments)
}This advanced example combines optional nested fields and a list of nested objects in one request model.
Common Mistakes
- Using plain dictionaries instead of models: Define reusable sub-models like
AddressorItemto get validation and better docs. - Wrong JSON shape: If a field expects an object, do not send a string or list. Match the nested structure exactly.
- Forgetting list types: If a field contains multiple nested objects, use
List[ModelName]instead of a single model. - Missing optional defaults: For fields that may not exist, use
Optional[...] = Nonewhen appropriate.
Best Practices
- Keep nested models small and focused so they can be reused across endpoints.
- Use descriptive names such as
ShippingAddress,OrderItem, andCustomerProfile. - Prefer explicit nested models over unstructured data for clarity and validation.
- Test sample request bodies in the FastAPI Swagger UI to verify the expected schema.
- Use response models when returning nested data so output stays consistent.
Practice Exercises
- Create a
Studentmodel with a nestedCoursemodel and build a POST endpoint. - Create a
Cartmodel containing a list of nestedCartItemmodels. - Create a
Companymodel with an optional nestedHeadOfficeaddress model.
Mini Project / Task
Build a small bookstore API endpoint where a BookOrder includes customer details, a shipping address, and a list of ordered books.
Challenge (Optional)
Create a food delivery request model where a restaurant order contains customer info, delivery address, multiple menu items, and optional special instructions, then calculate the total price in the endpoint.
Optional and Default Values
Optional and default values are essential in FastAPI because not every client request should be forced to send every possible input. In real APIs, some query parameters are optional filters, some request fields can be omitted, and some values should fall back to sensible defaults such as page size, sort order, or language. FastAPI makes this easy by combining Python type hints with default assignments. If you assign a normal default like limit: int = 10, FastAPI treats the parameter as optional and uses that value when the client does not provide one. If you use Optional[str] = None or str | None = None, the field may be missing and its value becomes None. This is especially useful for search filters, partial updates, and endpoints that should remain flexible. In practice, you will see this in product listing APIs where users may filter by category, price, rating, or availability without being required to send every filter each time. Understanding the difference between required values, optional values, and default values helps you design cleaner APIs and avoid confusing client behavior.
In FastAPI, there are two common ideas here. First, a parameter with no default is required. Second, a parameter with a default becomes optional from the client perspective because FastAPI already knows what to use if it is missing. These patterns appear in query parameters, path operation function arguments, and Pydantic models for request bodies. Optional values usually mean the input can be omitted or set to null-like behavior, while default values mean the server chooses a fallback automatically. Although they seem similar, they are used for different API design goals. Optional values are best when absence has meaning, such as no filter applied. Default values are best when a standard behavior is preferred, such as returning 20 results by default.
Step-by-Step Explanation
Start with a basic route function. If you write def read_items(limit: int = 10), the parameter is optional in the request because it has a default. If the client calls the endpoint without limit, FastAPI passes 10. If you write search: str | None = None, the client may omit it, and your code can check whether it is None before applying search logic. For body models, define fields with defaults inside a Pydantic model. A field like in_stock: bool = True gets a default value, while description: str | None = None becomes optional. FastAPI validates all of this automatically and documents it in Swagger UI.
Comprehensive Code Examples
from fastapi import FastAPI
app = FastAPI()
@app.get("/items")
def read_items(limit: int = 10, search: str | None = None):
return {"limit": limit, "search": search}from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class ProductFilter(BaseModel):
category: str | None = None
min_price: float | None = None
max_price: float | None = None
in_stock: bool = True
@app.post("/products/filter")
def filter_products(filters: ProductFilter):
return filters.dict()from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/users")
def list_users(page: int = 1, page_size: int = Query(default=20, ge=1, le=100), role: str | None = None):
return {"page": page, "page_size": page_size, "role": role}The first example shows a simple optional query. The second shows a realistic body model for filtering products. The third adds validation with defaults, which is common in paginated APIs.
Common Mistakes
- Confusing optional with required: Writing a type hint without a default makes the field required. Add
= Noneor another default if omission should be allowed. - Using the wrong default type: A parameter typed as
intshould not default to a string like"10". Match the declared type. - Not checking for None: If a value is optional, your logic must handle missing input safely before calling string or math operations.
Best Practices
- Use defaults for common behavior such as pagination, sorting, and status filters.
- Use
Nonewhen the absence of a value changes business logic. - Add validation with
Queryor model fields to keep default values safe and predictable. - Document intent through clear parameter names and sensible fallback values.
Practice Exercises
- Create a GET endpoint with an optional
categoryquery parameter and a defaultlimitof 5. - Build a Pydantic model with an optional
descriptionfield and a defaultavailablefield set toTrue. - Add a
page_sizequery parameter that defaults to 20 and only accepts values from 1 to 50.
Mini Project / Task
Build a product search API endpoint that supports optional filters like keyword and category, plus default pagination values such as page 1 and page size 10.
Challenge (Optional)
Create an endpoint for updating user preferences where some fields are optional, some have defaults, and your response clearly shows which values came from the client and which ones used fallback behavior.
Form Data Handling
Form data handling in FastAPI is the process of receiving values submitted from HTML forms, such as login fields, search boxes, profile editors, and contact forms. In real applications, browsers often send form values using the application/x-www-form-urlencoded or multipart/form-data content types instead of JSON. FastAPI supports this with the Form helper, which tells the framework to extract values from submitted form fields rather than from a JSON request body. This is especially useful in authentication flows, admin dashboards, classic server-rendered apps, and integrations where frontend forms submit directly to an API endpoint.
There are two common form submission styles to understand. The first is URL-encoded form data, which is typically used for simple text inputs like username, password, email, or search terms. The second is multipart form data, which is used when forms include files along with text fields. Even when no file is present, many browser-based forms may still send multipart data. In FastAPI, text form fields are declared using Form(...), while uploaded files are usually combined with File(...). This separation keeps endpoint declarations explicit and easy to read.
Step-by-Step Explanation
To handle form input, first import Form from fastapi. Then define an endpoint and mark each form field as a function parameter using Form. A required field uses Form(...), while an optional field can use a default such as Form(None) or a normal value. When a request arrives, FastAPI parses the incoming form body and passes each field into your function. If a required field is missing, FastAPI automatically returns a validation error. This works similarly to query and JSON validation, which is one reason FastAPI feels consistent.
Form handling is commonly used with POST endpoints because forms usually submit data that creates, updates, or verifies something. For example, a login form can send username and password, a registration form can send user details, and a profile update form can send editable account information. If you want to test such endpoints in Swagger UI, remember that FastAPI generates proper documentation for form parameters as long as they are declared with Form.
Comprehensive Code Examples
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login")
async def login(username: str = Form(...), password: str = Form(...)):
return {"message": f"Welcome, {username}"}from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/register")
async def register(
full_name: str = Form(...),
email: str = Form(...),
password: str = Form(...),
city: str = Form(None)
):
return {
"full_name": full_name,
"email": email,
"city": city,
"status": "registered"
}from fastapi import FastAPI, Form, File, UploadFile
app = FastAPI()
@app.post("/profile")
async def update_profile(
username: str = Form(...),
bio: str = Form(None),
avatar: UploadFile = File(None)
):
filename = avatar.filename if avatar else None
return {
"username": username,
"bio": bio,
"avatar_filename": filename
}Common Mistakes
- Using plain parameters instead of
Form: FastAPI may treat them as query values, not form fields. Fix: declare form inputs withForm(...). - Sending JSON from the client while expecting form data: The endpoint will not receive the values correctly. Fix: ensure the client submits proper form content types.
- Forgetting multipart support when uploading files: Files require
Fileand multipart form submission. Fix: combineFormandFilecorrectly.
Best Practices
- Use clear field names that match the frontend form inputs exactly.
- Validate required fields with
Form(...)and optional ones with safe defaults. - Keep authentication forms secure by using HTTPS and never logging raw passwords.
- Combine forms with Pydantic carefully when application logic grows, but keep simple forms readable.
Practice Exercises
- Create a
/feedbackendpoint that acceptsname,email, andmessageas form fields. - Build a
/signinendpoint with requiredusernameandpasswordfields and return a success message. - Create a
/edit-profileendpoint with requiredusernameand optionalbioform fields.
Mini Project / Task
Build a simple user registration API endpoint that accepts form-submitted full_name, email, password, and optional phone, then returns a confirmation response.
Challenge (Optional)
Create a profile update endpoint that accepts both form fields and an optional avatar upload, then returns different messages depending on whether a file was included.
File Uploads
File uploads let clients send binary content such as images, PDFs, CSV files, audio, and documents to an API. In FastAPI, this exists so applications can accept user-generated content efficiently without forcing everything into plain JSON. Real systems use file uploads for profile pictures, invoice processing, spreadsheet imports, medical reports, machine learning datasets, and document management workflows. FastAPI supports uploads through UploadFile and File, giving developers a clean way to receive multipart form data. The two main approaches are reading raw bytes with bytes = File(...) and streaming uploaded content with UploadFile. Using bytes is simple for very small files because the entire file is loaded into memory. Using UploadFile is better for most real applications because it provides a file-like object, metadata such as filename and content type, and better memory behavior for larger uploads. File uploads are usually sent with the request type multipart/form-data, not JSON. This matters because beginners often try to combine JSON body models directly with file parameters and get validation errors. FastAPI also supports multiple file uploads, optional uploads, and combining files with form fields such as usernames, categories, or document descriptions.
Step-by-Step Explanation
To receive an uploaded file, import FastAPI, File, and UploadFile. Then create a route and declare a parameter. If you use file: UploadFile = File(...), FastAPI expects a required file field named file. You can inspect file.filename for the original name, file.content_type for the MIME type, and read contents with await file.read(). After reading, you can save the file, validate its type, or process it. For very small uploads, file: bytes = File(...) returns raw bytes directly, but this is less flexible. For multiple files, use list[UploadFile]. If you need extra text data, add Form(...) parameters in the same endpoint. In practice, APIs often validate file extensions, limit file size, generate safe storage names, and avoid trusting the client-provided filename.
Comprehensive Code Examples
Basic example
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
content = await file.read()
return {
"filename": file.filename,
"content_type": file.content_type,
"size": len(content)
}Real-world example
from fastapi import FastAPI, File, UploadFile, HTTPException
from pathlib import Path
import shutil
app = FastAPI()
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
@app.post("/profile-photo")
async def upload_profile_photo(photo: UploadFile = File(...)):
allowed = {"image/png", "image/jpeg"}
if photo.content_type not in allowed:
raise HTTPException(status_code=400, detail="Only PNG and JPEG are allowed")
save_path = UPLOAD_DIR / photo.filename
with save_path.open("wb") as buffer:
shutil.copyfileobj(photo.file, buffer)
return {"message": "Upload successful", "path": str(save_path)}Advanced usage
from fastapi import FastAPI, File, UploadFile, Form, HTTPException
from pathlib import Path
import uuid
app = FastAPI()
UPLOAD_DIR = Path("documents")
UPLOAD_DIR.mkdir(exist_ok=True)
@app.post("/documents")
async def upload_document(
title: str = Form(...),
category: str = Form(...),
files: list[UploadFile] = File(...)
):
saved_files = []
for file in files:
if file.content_type not in {"application/pdf", "text/csv"}:
raise HTTPException(status_code=400, detail=f"Invalid type: {file.filename}")
suffix = Path(file.filename).suffix
safe_name = f"{uuid.uuid4()}{suffix}"
target = UPLOAD_DIR / safe_name
with target.open("wb") as out_file:
while chunk := await file.read(1024 * 1024):
out_file.write(chunk)
saved_files.append({"original": file.filename, "stored": safe_name})
return {"title": title, "category": category, "files": saved_files}Common Mistakes
- Using JSON instead of multipart: Files must usually be sent as
multipart/form-data. - Reading huge files fully into memory: Prefer
UploadFileand chunked reads for larger files. - Trusting filenames blindly: Generate safe names to avoid overwrites or unsafe paths.
- Skipping type checks: Validate MIME type and, if needed, file extension.
Best Practices
- Use
UploadFilefor most uploads because it is more scalable. - Store files outside application code directories when possible.
- Limit file size and allowed types to reduce security risk.
- Use unique generated filenames and keep original names as metadata only.
- Close or finish processing uploaded files promptly.
Practice Exercises
- Create an endpoint that accepts one text file and returns its filename and size.
- Build an endpoint that accepts multiple image files and returns how many were uploaded.
- Create an upload route that also accepts a
descriptionfield usingForm(...).
Mini Project / Task
Build a document upload API for a small office where users can upload PDF reports with a title and category, and the server stores them with unique filenames.
Challenge (Optional)
Add validation that rejects files larger than a chosen limit and returns a clear error message while still supporting multiple uploads.
Headers and Cookies
Headers and cookies are core parts of HTTP communication. Headers are key-value pairs sent with requests and responses to carry metadata such as authorization tokens, content type, language preferences, caching rules, and custom app information. Cookies are small pieces of data stored by the client and sent back to the server on later requests. They are commonly used for session management, remembering user preferences, and tracking logged-in users. In FastAPI, both are easy to read and set because the framework maps HTTP data into clear Python parameters and response objects.
In real applications, request headers help APIs identify clients, validate tokens, detect browser language, and trace requests across services. Cookies are useful when building login systems, dashboards, shopping carts, and websites that need persistent state between requests. FastAPI provides helpers such as Header and Cookie for incoming data, and response methods for setting outgoing cookies and headers. Headers may be standard like Authorization or custom like X-Request-ID. Cookies can be regular cookies or secure cookies with options like httponly, secure, and samesite.
Step-by-Step Explanation
To read a header in FastAPI, import Header from fastapi and declare it as a function parameter. FastAPI automatically extracts the value from the incoming HTTP request. To read a cookie, use Cookie in the same way. If a value is optional, assign None. If required, omit the default. For output, create a response object and use set_cookie() to send a cookie back to the client. You can also set response headers directly on the response object.
Header names usually contain hyphens such as User-Agent, but Python variables use underscores. FastAPI automatically converts underscores to hyphens, so user_agent maps to User-Agent. This makes header handling beginner-friendly. You can also read duplicate headers as lists when needed.
Comprehensive Code Examples
from fastapi import FastAPI, Header, Cookie
app = FastAPI()
@app.get("/basic")
def read_request_data(user_agent: str | None = Header(default=None), theme: str | None = Cookie(default=None)):
return {"user_agent": user_agent, "theme": theme}from fastapi import FastAPI, Response, Header
app = FastAPI()
@app.post("/login")
def login(response: Response, x_request_id: str | None = Header(default=None)):
response.set_cookie(key="session_id", value="abc123", httponly=True, samesite="lax")
response.headers["X-Request-ID"] = x_request_id or "generated-id"
return {"message": "Logged in"}from fastapi import FastAPI, Header, Cookie, HTTPException, Response
app = FastAPI()
@app.get("/dashboard")
def dashboard(authorization: str | None = Header(default=None), session_id: str | None = Cookie(default=None)):
if not authorization and not session_id:
raise HTTPException(status_code=401, detail="Missing authentication")
return {"status": "authorized"}
@app.post("/preferences")
def save_preferences(response: Response):
response.set_cookie(key="theme", value="dark", max_age=86400, secure=True, httponly=False, samesite="lax")
response.headers["Cache-Control"] = "no-store"
return {"message": "Preferences saved"}Common Mistakes
- Confusing request and response data:
Header()andCookie()read incoming values; use the response object to send headers or cookies back. - Forgetting security flags: Sensitive cookies should often use
httponly=Trueandsecure=Truein production. - Using wrong names: A Python parameter like
user_agentmaps toUser-Agent; beginners sometimes expect exact underscore names in HTTP tools.
Best Practices
- Validate authentication carefully: Never trust headers or cookies without verification.
- Use cookies only when appropriate: Prefer headers for token-based APIs and cookies for browser sessions.
- Set secure attributes: Use
Secure,HttpOnly, and properSameSitevalues for safer session cookies. - Add tracing headers: Custom IDs such as
X-Request-IDimprove debugging in distributed systems.
Practice Exercises
- Create an endpoint that reads the
User-Agentheader and returns it in JSON. - Create an endpoint that reads a cookie named
languageand returns a greeting based on its value. - Create a login-style endpoint that sets a cookie named
session_idand also adds a custom response header.
Mini Project / Task
Build a small preference API with one endpoint that stores a user theme in a cookie and another endpoint that reads the cookie and returns the current theme setting.
Challenge (Optional)
Create an endpoint that accepts either an Authorization header or a session_id cookie for access, then returns which authentication method was used.
Middleware
Middleware in FastAPI is a layer of code that runs before and after every request. It exists to handle cross-cutting concerns that should apply to many routes without repeating logic inside each endpoint. In real applications, middleware is commonly used for logging, timing requests, adding security headers, handling CORS, compression, authentication checks, and tracking request IDs. Think of it as a checkpoint around your API: a request enters, middleware can inspect or modify it, the route handler runs, and then middleware can inspect or modify the response before it is sent back.
FastAPI supports built-in middleware such as CORS middleware and also custom middleware. The most common custom pattern uses @app.middleware("http"), where you receive a request object and a call_next function. Calling call_next(request) passes control to the next middleware or route handler. You can also add class-based middleware for more structured behavior. Middleware is useful when the same logic applies globally, while dependencies are often better for route-specific rules.
Step-by-Step Explanation
To create middleware, define a function with the HTTP middleware decorator. The function must accept request and call_next. First, you can read request information such as headers, URL, method, or client IP. Next, call response = await call_next(request) so the request continues to the route. Finally, modify or inspect the response and return it. Order matters: middleware added first wraps middleware added later. This means outer middleware runs first on the request and last on the response.
Built-in middleware is usually added with app.add_middleware(...). For example, CORS middleware allows browsers to call your API from approved front-end origins. This is essential when a React, Vue, or mobile web client accesses your FastAPI backend.
Comprehensive Code Examples
from fastapi import FastAPI, Request
import time
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start = time.time()
response = await call_next(request)
duration = time.time() - start
response.headers["X-Process-Time"] = str(duration)
return response
@app.get("/")
async def home():
return {"message": "Hello"}from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def log_requests(request: Request, call_next):
print(f"Incoming: {request.method} {request.url.path}")
response = await call_next(request)
print(f"Completed: {response.status_code}")
return response
@app.get("/products")
async def get_products():
return [{"id": 1, "name": "Keyboard"}]from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/data")
async def get_data():
return {"status": "shared with frontend"}Common Mistakes
Forgetting to call
await call_next(request). Fix: always pass the request onward unless you intentionally want to block it.Putting route-specific logic in middleware. Fix: use dependencies when behavior should apply only to selected endpoints.
Reading the request body carelessly. Fix: be cautious because consuming the body in middleware can affect downstream handlers.
Misconfiguring CORS. Fix: specify correct origins, methods, and headers for your frontend.
Best Practices
Keep middleware focused on one responsibility, such as logging or timing.
Use middleware for global concerns, not business logic.
Add useful response headers like request timing or request IDs for debugging.
Be careful with performance; middleware runs on every request.
Test middleware behavior with successful and failing endpoints.
Practice Exercises
Create middleware that adds a custom header named
X-App-Nameto every response.Write middleware that prints the request method and path for each incoming request.
Add CORS middleware to allow requests from
http://localhost:8080.
Mini Project / Task
Build a small FastAPI app with two routes and middleware that logs requests, measures processing time, and adds both X-Process-Time and X-App-Name headers to every response.
Challenge (Optional)
Create middleware that generates a unique request ID for each request, stores it, and returns it in the response headers so logs can be traced across the application.
CORS Configuration
CORS, or Cross-Origin Resource Sharing, is a browser security mechanism that controls whether a web page from one origin can call an API hosted on another origin. An origin is defined by the combination of protocol, domain, and port. For example, a frontend running on http://localhost:3000 and an API running on http://localhost:8000 are considered different origins. Browsers block many cross-origin requests by default to protect users from malicious websites. In real applications, this matters whenever a JavaScript frontend, mobile web app, admin dashboard, or third-party client calls your FastAPI backend. FastAPI usually handles CORS through middleware, most commonly CORSMiddleware. The key ideas are allowed origins, allowed methods, allowed headers, credentials support, and preflight requests. A preflight request is an OPTIONS request sent by the browser before the real request when extra checks are needed. If your API does not answer correctly, the browser blocks the call even if the backend route itself works in tools like Postman. This is why CORS bugs often confuse beginners: the API seems healthy, but the browser refuses access. In development, teams often allow a local frontend origin. In production, you should restrict access to trusted domains only rather than allowing everything. You may also decide whether cookies or authorization headers are permitted. Understanding these options helps you build secure integrations instead of applying overly broad settings that weaken your API posture.
Step-by-Step Explanation
In FastAPI, CORS is configured by adding middleware to the application instance. First, import CORSMiddleware from fastapi.middleware.cors. Next, create your FastAPI app. Then call app.add_middleware() and pass the middleware class plus configuration values. allow_origins is a list of exact origins that may access the API. allow_methods defines which HTTP methods are permitted, such as GET, POST, and PUT. allow_headers controls which request headers may be sent, such as Authorization or Content-Type. allow_credentials=True is needed when requests include cookies or authenticated browser credentials. Be careful: when credentials are allowed, using a wildcard origin is not appropriate for secure setups. You can also use allow_origin_regex when multiple subdomains must be accepted. Once middleware is added, the browser receives CORS headers automatically for matching requests.
Comprehensive Code Examples
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/items")
def get_items():
return [{"id": 1, "name": "Keyboard"}]from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
origins = [
"https://app.example.com",
"https://admin.example.com"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
@app.post("/login")
def login():
return {"message": "CORS enabled for trusted frontends"}from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"https://.*\.example\.com",
allow_credentials=False,
allow_methods=["GET"],
allow_headers=["Accept", "Content-Type"],
)
@app.get("/public-data")
def public_data():
return {"status": "public read access for approved subdomains"}Common Mistakes
- Forgetting the frontend origin: adding
localhostwithout the correct port causes browser failures. Always match protocol, host, and port exactly. - Assuming Postman proves CORS works: CORS is a browser rule, so test in a browser-based frontend too.
- Using overly broad wildcards: allowing every origin, method, and header in production can expose your API unnecessarily. Limit access to trusted values.
Best Practices
- Keep development and production origin lists separate.
- Allow only required methods and headers.
- Use credentials only when your application truly needs cookies or browser-authenticated requests.
- Document approved frontend origins for your team.
- Review CORS whenever domains, environments, or authentication flows change.
Practice Exercises
- Configure a FastAPI app to allow requests only from
http://localhost:5173. - Create a route and allow only
GETandPOSTmethods through CORS settings. - Update a project so that only the
AuthorizationandContent-Typeheaders are allowed.
Mini Project / Task
Build a small FastAPI backend for a frontend dashboard running on a different localhost port, and configure CORS so the dashboard can read a list of products and submit a new product safely.
Challenge (Optional)
Set up CORS for multiple environment domains, such as local development, staging, and production, while keeping permissions as restrictive as possible.
Error Handling
Error handling in FastAPI is the process of returning clear, structured responses when something goes wrong. In real APIs, failures happen often: a user requests a missing record, sends invalid data, lacks permission, or triggers an unexpected server issue. FastAPI provides built-in tools for these situations so your application can respond with proper HTTP status codes and useful messages instead of crashing or exposing internal details. This matters in production because frontend apps, mobile clients, and other services depend on predictable error formats to guide users and retry safely.
The most common form is raising HTTPException. This lets you stop request processing and return a status code such as 404, 400, or 401 with a JSON error payload. FastAPI also performs automatic validation using Pydantic. If the request body, path, or query values do not match the expected type, FastAPI returns a validation error automatically. Beyond built-in behavior, you can define custom exception classes and register global exception handlers. This is useful when you want a consistent error format across the whole API, such as always returning fields like error, message, and code.
Step-by-Step Explanation
Start with HTTPException from fastapi. Inside a route, check a condition. If the condition fails, use raise HTTPException(status_code=..., detail=...). The raise keyword immediately stops the function and sends the response. The detail value becomes part of the JSON response.
For custom handling, create your own exception class, then decorate a function with @app.exception_handler(YourException). That handler receives the request and exception object and returns a response, usually JSONResponse. This pattern separates business logic from response formatting. FastAPI also allows overriding default handlers for request validation if you want to customize validation output.
Comprehensive Code Examples
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {1: {"name": "Keyboard"}, 2: {"name": "Mouse"}}
@app.get("/items/{item_id}")
def get_item(item_id: int):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return items[item_id]from fastapi import FastAPI, HTTPException, Header
app = FastAPI()
@app.get("/admin")
def admin_panel(x_token: str | None = Header(default=None)):
if x_token != "secret-token":
raise HTTPException(status_code=401, detail="Invalid or missing token")
return {"message": "Welcome, admin"}from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class InventoryException(Exception):
def __init__(self, item_id: int, message: str):
self.item_id = item_id
self.message = message
@app.exception_handler(InventoryException)
async def inventory_exception_handler(request: Request, exc: InventoryException):
return JSONResponse(
status_code=400,
content={"error": "inventory_error", "item_id": exc.item_id, "message": exc.message}
)
@app.get("/stock/{item_id}")
def check_stock(item_id: int):
if item_id < 1:
raise InventoryException(item_id=item_id, message="Item ID must be positive")
return {"item_id": item_id, "stock": 25}Common Mistakes
Using
return HTTPException(...)instead ofraise. Fix: always raise exceptions.Returning
500for user mistakes like bad input. Fix: use client-error codes such as400,404, or422when appropriate.Exposing internal stack traces or database errors. Fix: log detailed errors internally and send safe public messages to clients.
Best Practices
Use consistent error response structures across all endpoints.
Choose status codes carefully so clients can react correctly.
Create custom exception handlers for business-specific failures.
Keep sensitive implementation details out of API responses.
Practice Exercises
Create a route that returns a
404error when a user ID is not found in a dictionary.Build a protected endpoint that checks a header token and returns
401if it is missing or incorrect.Define a custom exception for invalid product prices and handle it with a global exception handler.
Mini Project / Task
Build a small book API with routes to fetch a book by ID and add a book. Return proper errors for missing books, duplicate IDs, and invalid input so every failure sends a clean JSON response.
Challenge (Optional)
Customize FastAPI validation error responses so all errors in your app follow one unified format with fields like success, error_type, and details.
Custom Exception Handlers
Custom exception handlers in FastAPI let you control how your application responds when errors happen. By default, FastAPI already returns useful JSON for common problems such as validation failures or explicit HTTPException errors. However, real-world APIs often need more consistency. For example, a company may want every error response to follow the same structure, include an internal error code, add request metadata, or hide sensitive details from users. Custom handlers solve this by intercepting exceptions and returning a response you design.
In practice, they are used in production APIs for centralized error formatting, security, debugging, logging, and better client integration. Instead of returning different error shapes from different routes, you can create one standard format for business errors, validation errors, and unexpected server failures. Common categories include built-in exceptions like HTTPException, request validation errors such as RequestValidationError, and your own custom exception classes for domain logic like ItemNotFoundError or PaymentFailedError.
Step-by-Step Explanation
To create a custom handler, first define an exception class if needed. Next, register a handler using FastAPI's @app.exception_handler(ExceptionType) decorator. The handler function usually accepts a Request object and the raised exception. Inside it, return a response such as JSONResponse with your chosen status code and message. Once registered, whenever that exception is raised anywhere in the app, FastAPI will call your handler automatically.
For built-in behavior changes, you can override handlers for HTTPException and RequestValidationError. This is useful when you want all errors to contain fields like success, message, and details. The route code stays clean because you raise exceptions normally, while formatting is handled globally.
Comprehensive Code Examples
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from fastapi import Request
app = FastAPI()
class ItemNotFoundError(Exception):
def __init__(self, item_id: int):
self.item_id = item_id
@app.exception_handler(ItemNotFoundError)
async def item_not_found_handler(request: Request, exc: ItemNotFoundError):
return JSONResponse(
status_code=404,
content={"error": "Item not found", "item_id": exc.item_id}
)
@app.get("/items/{item_id}")
async def get_item(item_id: int):
if item_id != 1:
raise ItemNotFoundError(item_id)
return {"item_id": item_id, "name": "Keyboard"}from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"success": False,
"message": "Invalid request data",
"details": exc.errors()
}
)@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"success": False,
"message": exc.detail,
"path": str(request.url.path)
}
)
@app.get("/admin")
async def admin_area():
raise HTTPException(status_code=403, detail="Access denied")The first example shows a basic custom exception. The second is a real-world validation format. The third is more advanced because it standardizes all HTTP errors and includes request context.
Common Mistakes
- Registering the wrong exception type: If you handle
Exceptioninstead of a specific class too early, you may hide useful details. Fix: start with targeted handlers. - Returning plain dictionaries: A handler should usually return
JSONResponseor another proper response object. Fix: always control status code explicitly. - Leaking internal details: Beginners sometimes expose stack traces or raw database messages. Fix: return safe client messages and log sensitive details separately.
Best Practices
- Use a consistent error response structure across the whole API.
- Create custom exception classes for business rules instead of hardcoding many
HTTPExceptionmessages in routes. - Override validation handling when frontend or mobile clients require predictable error formats.
- Log unexpected exceptions separately from user-caused errors.
- Keep handlers simple: format the response, avoid heavy business logic inside them.
Practice Exercises
- Create a custom exception called
UserNotFoundErrorand return a 404 JSON response. - Override the validation error handler so every 422 response includes
successanddetails. - Create a route that raises
HTTPExceptionand customize its output to include the request path.
Mini Project / Task
Build a small product API with one endpoint to fetch a product by ID. If the product does not exist, raise a custom exception and return a standardized JSON error response with status code, message, and product ID.
Challenge (Optional)
Create a global fallback handler for unexpected exceptions that returns a generic 500 message to clients while keeping the response format consistent with your other custom handlers.
Dependency Injection
Dependency Injection in FastAPI is a built-in system for providing shared logic, resources, and configuration to your path operations without manually creating them inside every function. In simple terms, instead of an endpoint directly building its own database connection, authentication checker, or settings object, FastAPI can inject those dependencies automatically. This exists to reduce repeated code, improve testability, and keep applications organized as they grow. In real projects, Dependency Injection is commonly used for database sessions, current user retrieval, API key validation, pagination rules, reusable query parameters, and service-layer objects. FastAPI makes this especially powerful through the Depends() helper, which can call functions, classes, and nested dependencies. A dependency can be synchronous or asynchronous, can validate incoming data, and can even clean up resources after the request completes using yield. Common forms include simple function dependencies, class-based dependencies, and dependencies with sub-dependencies. This system keeps endpoints small and focused on business logic while moving setup and validation into reusable units.
Step-by-Step Explanation
To use Dependency Injection, first create a function that returns a value your endpoint needs. Then, inside the route function, declare a parameter and assign it to Depends(your_dependency). FastAPI sees this and executes the dependency before the endpoint runs. If the dependency has its own parameters, FastAPI resolves those too, including query parameters, headers, cookies, and other dependencies. For resource management, use a dependency with yield. Code before yield prepares the resource, and code after it performs cleanup, such as closing a database session. You can also place dependencies in decorators for routes or routers when you need checks to run even if their return value is not used directly. This layered model helps you compose authentication, configuration, and shared services cleanly.
Comprehensive Code Examples
Basic example
from fastapi import FastAPI, Depends
app = FastAPI()
def get_message():
return "Hello from dependency"
@app.get("/welcome")
def welcome(message: str = Depends(get_message)):
return {"message": message}Real-world example
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
def verify_api_key(x_api_key: str = Header(...)):
if x_api_key != "secret-key":
raise HTTPException(status_code=401, detail="Invalid API key")
return x_api_key
@app.get("/reports")
def read_reports(api_key: str = Depends(verify_api_key)):
return {"status": "authorized", "key_used": api_key}Advanced usage
from fastapi import FastAPI, Depends
app = FastAPI()
class Settings:
def __init__(self):
self.app_name = "Inventory API"
def get_settings():
return Settings()
def get_db():
db = {"connected": True}
try:
yield db
finally:
db["connected"] = False
def get_service(settings: Settings = Depends(get_settings), db = Depends(get_db)):
return {"app": settings.app_name, "db": db}
@app.get("/items")
def list_items(service = Depends(get_service)):
return {"service": service}Common Mistakes
- Calling the dependency directly inside
Depends: useDepends(get_message), notDepends(get_message()). - Putting business logic everywhere: keep dependencies focused on setup, validation, or shared services instead of large endpoint workflows.
- Forgetting cleanup: for files, sessions, or connections, use
yieldso resources are released properly. - Ignoring nested dependencies: break complex requirements into smaller reusable dependencies instead of one huge function.
Best Practices
- Create small, single-purpose dependencies such as auth, settings, and database access.
- Use type hints to improve readability, validation, and editor support.
- Prefer reusable dependencies at router level for shared security or validation rules.
- Use
yielddependencies for request-scoped resources that need cleanup. - Design dependencies so they are easy to override during testing.
Practice Exercises
- Create a dependency that returns a fixed application name and inject it into an endpoint.
- Build a dependency that reads a custom header and rejects the request if the value is missing.
- Create two nested dependencies: one for settings and one for a fake database, then inject both into one route.
Mini Project / Task
Build a small protected notes API with one endpoint that returns notes only when a valid API key dependency passes. Add another dependency that provides application settings, and return both the notes and app name in the response.
Challenge (Optional)
Create a reusable dependency chain for a shop API where one dependency validates a token, another loads the current user, and a third checks whether the user has admin access before allowing product creation.
Dependencies with Parameters
Dependencies in FastAPI are a powerful feature that allows you to declare components that your path operations depend on. These components can be anything from database connections and authentication checks to complex business logic. When we talk about 'Dependencies with Parameters,' we are referring to the ability to pass arguments to your dependency functions, making them more dynamic and reusable. This concept is crucial for building modular, testable, and maintainable APIs. It helps in abstracting common logic, ensuring that your path operation functions remain clean and focused solely on handling the request-specific business logic.
In real-world applications, you'll often encounter scenarios where a dependency needs specific information to perform its task. For instance, an authentication dependency might need to know the 'scope' or 'role' required for a particular endpoint, or a database dependency might need a 'session ID' to retrieve user-specific data. By allowing dependencies to accept parameters, FastAPI provides a flexible mechanism to inject this contextual information. This capability significantly enhances the expressiveness and utility of the dependency injection system, moving beyond simple static dependencies to dynamic, configurable ones. It's used in almost every non-trivial FastAPI application for tasks like role-based access control, resource loading, and pagination.
FastAPI's dependency injection system works by calling your dependency functions, resolving their own dependencies (if any), and then passing their return values as arguments to the path operation function. When a dependency function itself needs parameters, FastAPI intelligently resolves these parameters. These parameters can be standard Python type-hinted arguments, just like in path operation functions. FastAPI will look for these parameters in various sources: path parameters, query parameters, headers, cookies, or even other dependencies. This seamless integration makes dependencies with parameters incredibly versatile and easy to use, allowing for complex logic to be encapsulated and reused across multiple endpoints without boilerplate.
Step-by-Step Explanation
To use dependencies with parameters, you follow these steps:
1. Define a regular Python function that will serve as your dependency. This function can accept parameters just like any other Python function.
2. Type-hint the parameters of your dependency function. FastAPI will automatically try to resolve these parameters from the request (e.g., query, path, header).
3. In your path operation function, declare an argument with a default value of
Depends(), passing your dependency function to it. If your dependency function takes parameters, these parameters will be resolved by FastAPI before the dependency function is called.4. The return value of your dependency function will be passed as the argument to your path operation function.
Let's consider an example where a dependency needs a query parameter to filter results. The dependency function would take an argument, say
skip: int = 0, and limit: int = 100. When this dependency is used, FastAPI will look for skip and limit in the query parameters of the incoming request and pass them to the dependency function. This allows for dynamic behavior within the dependency itself, controlled by the client's request.Comprehensive Code Examples
Basic example
Here, our dependency takes a query parameter
q and returns a modified string.from fastapi import FastAPI, Depends
app = FastAPI()
def get_query_param(q: str | None = None):
if q:
return f"Query parameter received: {q}"
return "No query parameter provided"
@app.get("/items/")
async def read_items(query_info: str = Depends(get_query_param)):
return {"message": query_info}
Real-world example
This example demonstrates pagination, where a dependency parses
skip and limit parameters for fetching items from a database.from fastapi import FastAPI, Depends, Query
from typing import Annotated
app = FastAPI()
class CommonQueryParams:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
self.q = q
self.skip = skip
self.limit = limit
@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):
response = {"skip": commons.skip, "limit": commons.limit}
if commons.q:
response["q"] = commons.q
return response
Advanced usage
Combining dependencies with parameters for authentication and role-based access control.
from fastapi import FastAPI, Depends, HTTPException, status
from typing import Annotated
app = FastAPI()
fake_users_db = {"john_doe": {"password": "secret", "role": "admin"}, "jane_smith": {"password": "secret", "role": "user"}}
def get_current_username(token: Annotated[str, Query()]):
# Simulate token validation and user retrieval
if token == "validtoken":
return "john_doe" # A real app would decode/validate the token and get the username
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
def verify_role(required_role: str):
def _verify_role(username: Annotated[str, Depends(get_current_username)]):
user_data = fake_users_db.get(username)
if not user_data or user_data["role"] != required_role:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"User does not have '{required_role}' role")
return username
return _verify_role
@app.get("/admin/dashboard")
async def get_admin_dashboard(username: Annotated[str, Depends(verify_role("admin"))]):
return {"message": f"Welcome, admin {username}!"}
@app.get("/user/profile")
async def get_user_profile(username: Annotated[str, Depends(verify_role("user"))]):
return {"message": f"Welcome, user {username}!"}
Common Mistakes
1. Forgetting to use
Depends(): If you define a dependency function but don't wrap it with Depends() in your path operation, FastAPI will treat it as a regular path or query parameter, not a dependency. Fix: Always use
your_arg: Type = Depends(your_dependency_function).2. Mismatched parameter names/types: If your dependency function expects a parameter named
user_id: int but the request only provides id: int, FastAPI won't be able to resolve it. Fix: Ensure parameter names and types in dependency functions match what's expected from the request or other dependencies.
3. Circular dependencies: A dependency function calls another dependency, which in turn calls the first one, leading to an infinite loop.
Fix: Design your dependencies carefully to ensure a clear, acyclic graph of calls.
Best Practices
- Keep dependencies granular: Each dependency should ideally have a single responsibility. This makes them easier to test and reuse.
- Use type hints extensively: This allows FastAPI to automatically validate and convert data, and provides excellent editor support.
- Document your dependencies: Add docstrings to your dependency functions to explain their purpose, expected parameters, and return values. This improves code readability and maintainability.
- Handle errors gracefully: Dependencies are a great place to implement error handling, such as raising
HTTPExceptionfor authentication failures or invalid input. - Reuse common logic: If multiple endpoints share similar setup or validation, encapsulate that logic in a dependency with parameters to avoid code duplication.
Practice Exercises
1. Create a dependency
get_current_user(token: str) that simulates user authentication. If the token is 'secret-token', return a user dictionary {'username': 'testuser'}, otherwise raise an HTTPException.2. Build a dependency
validate_item_id(item_id: int) that checks if item_id is positive. If not, raise an HTTPException. Use this dependency in a path operation /items/{item_id}.3. Implement a dependency
get_header_language(accept_language: str | None = None) that extracts the Accept-Language header. If not provided, default to 'en-US'. Use this to return a personalized message.Mini Project / Task
Create a simple FastAPI application where you have two endpoints:
/products/ and /orders/. Implement a dependency called get_db_session(session_id: str) that simulates fetching a database session based on a session_id query parameter. Both /products/ and /orders/ endpoints should use this dependency, and return a message confirming the session ID used.Challenge (Optional)
Enhance the authentication and role-based access control example. Create a dependency
get_user_permissions(username: str = Depends(get_current_username)) that returns a list of permissions for the given user. Then, modify the verify_role dependency (or create a new one, verify_permission) to accept a list of required permissions and check if the user has all of them. Use this for an endpoint that requires specific permissions beyond just a role. Database Setup with SQLAlchemy
Database setup with SQLAlchemy is the process of connecting your FastAPI application to a relational database such as SQLite, PostgreSQL, or MySQL using Python objects and models. SQLAlchemy exists to simplify database work by letting developers define tables as Python classes, manage connections safely, and write queries in a structured way instead of raw SQL for every operation. In real projects, this is used in user management systems, e-commerce apps, blogs, dashboards, inventory tools, and almost any API that must store persistent data. In FastAPI, SQLAlchemy is commonly used with a database engine, a session factory, and declarative models. A database engine handles the actual connection to the database. A session is the object that sends queries and saves changes. Models define the shape of tables. You will also see two common database choices during setup: SQLite for beginner-friendly local development and PostgreSQL for production-ready applications. Another important idea is sync versus async database access. Many beginner projects start with synchronous SQLAlchemy because it is easier to understand, while larger systems may move to async patterns later. The main goal of setup is to create a stable foundation so routes can create, read, update, and delete records without repeating connection logic everywhere.
Step-by-Step Explanation
Start by installing dependencies such as sqlalchemy and often a driver like psycopg2-binary for PostgreSQL. Next, define a database URL. For SQLite, it may look like sqlite:///./app.db. Then create an engine using create_engine(). After that, create a session factory with sessionmaker() so your app can open database sessions consistently. Then define a base class using declarative_base(). All table models inherit from this base. Each model maps a Python class to a database table using columns such as integers, strings, booleans, and timestamps. Once models are ready, call Base.metadata.create_all(bind=engine) to create tables. In FastAPI, the best practice is to provide a database session through dependency injection. A small function opens a session, yields it to the route, and closes it afterward. That way, every request gets clean access to the database without leaking connections.
Comprehensive Code Examples
Basic example
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker
DATABASE_URL = "sqlite:///./app.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
email = Column(String, unique=True, index=True)
Base.metadata.create_all(bind=engine)Real-world example
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
app = FastAPI()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/users")
def create_user(name: str, email: str, db: Session = Depends(get_db)):
user = User(name=name, email=email)
db.add(user)
db.commit()
db.refresh(user)
return {"id": user.id, "name": user.name, "email": user.email}Advanced usage
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User")Common Mistakes
- Forgetting to close sessions: Always use a dependency that closes the database session in
finally. - Using SQLite without
check_same_thread=False: FastAPI may fail during local development if this option is missing. - Not calling
commit(): Adding objects alone does not save them permanently. - Skipping
refresh()after insert: Use it when you need generated values like the new record ID.
Best Practices
- Keep database configuration in a separate file such as
database.py. - Separate SQLAlchemy models from FastAPI route logic for cleaner architecture.
- Use environment variables for database URLs instead of hardcoding secrets.
- Start with SQLite for learning, but design your code so PostgreSQL can replace it later.
- Use migrations with Alembic in real projects instead of relying only on
create_all().
Practice Exercises
- Create a
Productmodel withid,name, andprice, then generate the table using SQLAlchemy. - Write a FastAPI dependency called
get_dbthat opens and closes a session correctly. - Create a POST endpoint that stores a new user in the database and returns the saved record.
Mini Project / Task
Build a simple FastAPI contacts API with a SQLAlchemy-backed Contact table containing name, email, and phone number, and add an endpoint to create new contacts.
Challenge (Optional)
Extend your setup by adding a second table linked with a foreign key, then create an endpoint that stores related records correctly using one database session.
Creating Database Models
Creating database models is the process of defining how your application stores and organizes data in a database. In FastAPI projects, models are commonly created with SQLAlchemy or SQLModel and represent real entities such as users, products, orders, or blog posts. A model acts like a Python class mapped to a database table, where each class attribute becomes a column. This exists to give structure, consistency, and reusability to your data layer. In real life, database models are used in e-commerce apps to store products, in social platforms to store user profiles, and in business systems to track invoices or employees.
When working with FastAPI, it is important to understand that database models are different from Pydantic request and response schemas. Database models define how data is stored in the database, while Pydantic schemas define how data is validated and exposed through the API. Common concepts include tables, columns, primary keys, indexes, nullable fields, default values, and relationships. You will often create one base model class, then define specific models such as User or Item. Fields may use types like Integer, String, Boolean, and DateTime. Some projects also use foreign keys for linking tables, such as connecting an order to a user.
Step-by-Step Explanation
First, install SQLAlchemy if your project uses it. Then create a database base class using declarative_base(). Every model will inherit from that base. Next, define __tablename__ to specify the table name. After that, add columns with Column() and choose the correct data types. A primary key is usually an integer ID. You can also add index=True for commonly searched fields and unique=True for values like email addresses. If a value can be empty, set nullable=True; otherwise use nullable=False. Finally, create the database tables with metadata and connect them to your FastAPI app through a session.
The basic syntax looks like a normal Python class, but each attribute is a mapped database column. Keep names clear and predictable. Separate database models from API schemas so your project stays maintainable.
Comprehensive Code Examples
Basic example
from sqlalchemy import Column, Integer, String, Boolean
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
email = Column(String, unique=True, index=True, nullable=False)
is_active = Column(Boolean, default=True)Real-world example
from sqlalchemy import Column, Integer, String, Float, DateTime
from sqlalchemy.sql import func
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True, nullable=False)
description = Column(String, nullable=True)
price = Column(Float, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())Advanced usage
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
posts = relationship("Post", back_populates="owner")
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
owner = relationship("User", back_populates="posts")Common Mistakes
- Mixing Pydantic schemas with database models: Keep storage classes separate from request and response validation classes.
- Forgetting primary keys: Every model should have a clear primary key, usually
id. - Using wrong null settings: If a field is required, use
nullable=Falseto enforce it. - Skipping indexes on searchable fields: Add
index=Trueto fields like email or username when frequently queried.
Best Practices
- Use meaningful table and field names so the schema is easy to understand.
- Keep models focused on persistence logic only, not API formatting.
- Add constraints carefully such as unique emails or non-null required values.
- Prepare for relationships when your app contains linked data like users and orders.
- Use migrations with tools like Alembic instead of recreating tables manually in production.
Practice Exercises
- Create a
Customermodel withid,full_name,email, andis_active. - Create a
Bookmodel with a required title, optional description, and price field. - Create two related models:
AuthorandArticle, where each article belongs to one author.
Mini Project / Task
Design the database models for a simple blog application with users and posts. Include fields for user name, email, post title, and post content, and connect posts to their owner.
Challenge (Optional)
Create a small store schema with Category, Product, and Order models, then decide which fields should be indexed, unique, or required.
Database Sessions
Database sessions in FastAPI are the mechanism used to open a conversation with a database, perform queries or updates, and then close that conversation safely. In real applications, sessions are essential because APIs often need to create users, fetch products, update orders, or delete records. Without proper session management, connections can remain open too long, data may not be committed correctly, and errors can become difficult to trace. In FastAPI, sessions are commonly used with SQLAlchemy, where a session represents a unit of work. A session tracks changes to Python objects and translates them into SQL statements such as INSERT, UPDATE, SELECT, and DELETE. In practice, the most common pattern is to create a session per request, use it inside a path operation, then close it automatically. This approach keeps the API efficient, avoids connection leaks, and makes code easier to test. There are two common styles you will see: traditional synchronous sessions using SQLAlchemy Session and asynchronous sessions using AsyncSession. Synchronous sessions are simpler for beginners and are often used with standard database drivers. Asynchronous sessions are useful when the application stack is async and the database driver supports async behavior. Another important distinction is between committing and rolling back. Commit permanently saves changes, while rollback undoes pending changes after an error. Understanding this difference is critical when building APIs that must preserve correct business data.
Step-by-Step Explanation
Start by creating a database engine and a session factory. The engine knows how to connect to the database, and the session factory creates session objects when needed. In FastAPI, a common beginner-friendly pattern is to define a dependency function that yields a session. FastAPI injects that session into your route function, and after the request finishes, the session is closed. This makes session lifecycle management predictable. The usual flow is: create engine, create SessionLocal with sessionmaker, write get_db(), and inject it with Depends.
from fastapi import FastAPI, Depends
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
DATABASE_URL = "sqlite:///./app.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
app = FastAPI()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()A basic route can now receive the session and execute queries. For write operations, add objects, commit the transaction, and refresh the object if you want generated values like IDs.
@app.get("/health")
def health_check(db: Session = Depends(get_db)):
return {"message": "Database session available"}A real-world create example often stores data and handles persistence carefully.
@app.post("/users")
def create_user(name: str, db: Session = Depends(get_db)):
user = User(name=name)
db.add(user)
db.commit()
db.refresh(user)
return userAdvanced usage includes rollback on failure so the database remains consistent.
@app.put("/users/{user_id}")
def update_user(user_id: int, name: str, db: Session = Depends(get_db)):
try:
user = db.get(User, user_id)
if not user:
return {"error": "User not found"}
user.name = name
db.commit()
db.refresh(user)
return user
except Exception:
db.rollback()
return {"error": "Update failed"}Common Mistakes
- Forgetting to close the session: Always use a dependency with
yieldandfinallyso connections are released. - Calling commit too early or too often: Group related changes into a single transaction when possible.
- Not using rollback after errors: If a write fails, call
db.rollback()before continuing. - Confusing engine and session: The engine manages connections, while the session performs unit-of-work operations.
Best Practices
- Use one session per request for clean lifecycle management.
- Keep session creation in a dedicated dependency function.
- Commit only after validation and business rules pass.
- Use rollback in exception paths for safe recovery.
- Prefer repository or service layers in larger applications to avoid putting all database logic in route functions.
Practice Exercises
- Create a
get_db()dependency that opens and closes a SQLAlchemy session properly. - Build a route that inserts a single record into a table and returns the saved object.
- Write an update route that commits changes and rolls back if an exception occurs.
Mini Project / Task
Build a small FastAPI endpoint set for a notes app with routes to create, read, and update notes using one database session per request.
Challenge (Optional)
Refactor your CRUD routes so all database access happens inside a separate service layer that receives the session as a parameter.
CRUD Operations
CRUD stands for Create, Read, Update, and Delete. These are the four basic actions used to manage data in almost every application, from task managers and e-commerce systems to banking dashboards and inventory tools. In FastAPI, CRUD operations are typically implemented as HTTP endpoints: POST for creating records, GET for reading them, PUT or PATCH for updating them, and DELETE for removing them. FastAPI makes CRUD development efficient because it combines Python type hints, automatic validation with Pydantic models, and interactive API documentation.
In real projects, CRUD endpoints usually work with resources such as users, products, orders, blog posts, or support tickets. A common pattern is to define request models for incoming data, response models for outgoing data, and route handlers that perform operations against a database or an in-memory store. The two main update styles are full update with PUT, where the entire resource is replaced, and partial update with PATCH, where only selected fields are changed. Even when using a simple list or dictionary in examples, the same logic later applies to databases like PostgreSQL or MySQL.
Step-by-Step Explanation
Start by creating a FastAPI app and a Pydantic model that describes your resource. For example, a Task might have an id, title, and completed status. Next, create a temporary storage structure such as a dictionary. Then define routes: POST /tasks to add a task, GET /tasks to list tasks, GET /tasks/{task_id} to fetch one task, PUT /tasks/{task_id} to replace a task, and DELETE /tasks/{task_id} to remove it. Use path parameters to identify a resource and models to validate request bodies. If an item is missing, raise an HTTP exception with a suitable status code such as 404.
Comprehensive Code Examples
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
tasks = {}
class Task(BaseModel):
id: int
title: str
completed: bool = False
@app.post("/tasks")
def create_task(task: Task):
tasks[task.id] = task
return task
@app.get("/tasks")
def read_tasks():
return list(tasks.values())@app.get("/tasks/{task_id}")
def read_task(task_id: int):
if task_id not in tasks:
raise HTTPException(status_code=404, detail="Task not found")
return tasks[task_id]
@app.put("/tasks/{task_id}")
def update_task(task_id: int, task: Task):
if task_id not in tasks:
raise HTTPException(status_code=404, detail="Task not found")
tasks[task_id] = task
return task
@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
if task_id not in tasks:
raise HTTPException(status_code=404, detail="Task not found")
deleted = tasks.pop(task_id)
return {"message": "Deleted successfully", "task": deleted}from typing import Optional
from pydantic import BaseModel
class TaskUpdate(BaseModel):
title: Optional[str] = None
completed: Optional[bool] = None
@app.patch("/tasks/{task_id}")
def partial_update_task(task_id: int, task_update: TaskUpdate):
if task_id not in tasks:
raise HTTPException(status_code=404, detail="Task not found")
stored = tasks[task_id].dict()
updates = task_update.dict(exclude_unset=True)
stored.update(updates)
updated_task = Task(**stored)
tasks[task_id] = updated_task
return updated_taskThe basic example creates and lists tasks. The real-world pattern adds lookup, replacement, and deletion. The advanced example shows partial updates, which are common in production APIs because clients often update only one or two fields instead of resending a full object.
Common Mistakes
- Using the wrong HTTP method: sending updates with
GETinstead ofPUTorPATCH. Fix it by matching route purpose to HTTP semantics. - Skipping validation models: accepting raw dictionaries makes code fragile. Fix it by using Pydantic models for request and response data.
- Not handling missing records: returning empty data instead of a clear error. Fix it by raising
HTTPException(404).
Best Practices
- Use separate models for create, update, and response shapes when fields differ.
- Return meaningful status codes such as
201for creation and404for missing items. - Keep business logic separate from route functions as your project grows.
- Prefer database-backed persistence for real applications instead of in-memory dictionaries.
Practice Exercises
- Create a CRUD API for books with
id,title, andauthor. - Add a
PATCHendpoint that updates only thecompletedstatus of a task. - Modify the delete route so it returns only a success message and not the removed object.
Mini Project / Task
Build a small notes API with endpoints to create a note, list all notes, fetch one note by ID, update its content, and delete it.
Challenge (Optional)
Extend your CRUD API by adding simple search support, such as /tasks?completed=true, so users can filter records without retrieving everything.
Alembic Migrations
Alembic is the database migration tool most commonly used with SQLAlchemy-based FastAPI projects. A migration is a version-controlled change to your database structure, such as creating a table, adding a column, renaming an index, or updating constraints. Instead of manually editing a production database, you write or generate migration scripts and apply them in a predictable order. This matters in real projects because your database schema changes over time as features evolve. For example, an e-commerce API may begin with a users table, then later add is_active, last_login, or a new orders table. Alembic helps teams keep development, staging, and production databases synchronized.
In FastAPI, Alembic is usually paired with SQLAlchemy models. The key ideas are revision files, upgrade and downgrade functions, and autogeneration. A revision is a single migration script with a unique ID. The upgrade() function applies changes, while downgrade() reverses them. Autogenerate compares your SQLAlchemy models against the current database schema and creates a draft migration, but you must review it carefully. Alembic is used heavily in SaaS products, internal business systems, and any backend where schema changes must be safe, traceable, and repeatable.
Step-by-Step Explanation
First, install dependencies such as alembic, sqlalchemy, and your database driver. Next, initialize Alembic with alembic init alembic. This creates an alembic.ini file and an alembic folder. Then configure the database connection URL in alembic.ini or load it from environment settings. After that, open alembic/env.py and connect Alembic to your SQLAlchemy metadata using something like from app.db.base import Base and target_metadata = Base.metadata.
When your models are ready, create a migration with alembic revision --autogenerate -m "create users table". Alembic generates a file in the versions directory. Review the operations inside upgrade() and downgrade(). To apply the migration, run alembic upgrade head. If needed, move back one step with alembic downgrade -1. The word head means the latest revision. In beginner projects, the most common flow is: update models, autogenerate a revision, inspect it, then upgrade the database.
Comprehensive Code Examples
# Basic SQLAlchemy model
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, nullable=False)
# Example generated migration
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
"users",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("email", sa.String(), nullable=False),
)
op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False)
def downgrade():
op.drop_index(op.f("ix_users_id"), table_name="users")
op.drop_table("users")
# Real-world change: add a new column
def upgrade():
op.add_column("users", sa.Column("is_active", sa.Boolean(), server_default="true", nullable=False))
def downgrade():
op.drop_column("users", "is_active")
# Advanced: env.py target metadata setup
from app.models.user import Base
target_metadata = Base.metadata
Common Mistakes
Forgetting to import all models into the metadata path. Fix: ensure every model is loaded before Alembic autogenerates.
Trusting autogenerate without review. Fix: inspect every migration file before running it.
Changing old migration files after teammates already used them. Fix: create a new revision instead of rewriting history.
Using the wrong database URL. Fix: verify environment variables before upgrading.
Best Practices
Keep each migration focused on one logical schema change.
Name revisions clearly, such as
add_is_active_to_users.Test upgrades and downgrades in a local or staging database.
Commit migration files to version control with the related model changes.
Use manual edits for complex changes like data backfills or table splits.
Practice Exercises
Create a
productstable withid,name, andprice, then generate and apply a migration.Add a nullable
descriptioncolumn to theproductstable and create the matching downgrade.Create a second migration that adds a unique constraint to
name.
Mini Project / Task
Build a small FastAPI database setup for a blog with users and posts tables, then create Alembic migrations to initialize both tables and later add a published column to posts.
Challenge (Optional)
Create a migration that renames a column while preserving existing data, and verify that both the upgrade and downgrade paths work correctly.
Authentication Basics
Authentication is the process of verifying who a user or client is before allowing access to protected resources. In FastAPI, authentication is commonly used for login systems, private dashboards, mobile backends, admin panels, and APIs consumed by frontend applications or other services. It exists because not every endpoint should be public. For example, viewing a public product catalog may require no login, but creating an order, reading user profile data, or managing account settings should require proof of identity.
In real applications, authentication usually works together with authorization. Authentication answers, “Who are you?” while authorization answers, “What are you allowed to do?” In FastAPI, common authentication approaches include API keys, Basic Auth, and token-based authentication such as Bearer tokens. API keys are often used for service-to-service communication. Basic Auth sends a username and password with each request and is simple but less common for production web apps. Bearer token authentication, often combined with OAuth2 flows, is widely used because the client logs in once and then sends a token on future requests.
FastAPI provides helpful security utilities in fastapi.security. These tools do not magically authenticate users by themselves; instead, they help you extract credentials from requests in a standard way. Then your code verifies them. This design is clean and flexible because you can connect authentication to databases, external identity providers, or custom logic.
Step-by-Step Explanation
Start by deciding what credential your API expects. For beginner projects, a bearer token is a good starting point. The client sends an HTTP header like Authorization: Bearer mytoken. In FastAPI, you can define a security dependency using OAuth2PasswordBearer. Despite the name, it is often used simply to extract bearer tokens from requests.
Next, create a function that receives the token and validates it. At first, validation can be a simple string comparison. Later, it can check a database, decode a JWT, or verify expiration. If the token is invalid, raise an HTTP 401 error. If valid, return user information. Then use that function as a dependency in protected routes. This means every request to that route must pass authentication first.
Comprehensive Code Examples
Basic example
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
def get_current_user(token: str = Depends(oauth2_scheme)):
if token != "secret-token":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials"
)
return {"username": "alice"}
@app.get("/profile")
def read_profile(user: dict = Depends(get_current_user)):
return {"message": "Welcome", "user": user}Real-world example
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import APIKeyHeader
app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key")
VALID_KEYS = {"abc123": "reporting-service"}
def verify_api_key(api_key: str = Depends(api_key_header)):
if api_key not in VALID_KEYS:
raise HTTPException(status_code=401, detail="Invalid API key")
return {"client": VALID_KEYS[api_key]}
@app.get("/internal/reports")
def get_reports(client=Depends(verify_api_key)):
return {"access_granted_to": client["client"]}Advanced usage
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
app = FastAPI()
security = HTTPBearer()
def decode_token(token: str):
fake_tokens = {"token-admin": {"username": "admin", "role": "admin"}}
return fake_tokens.get(token)
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
user = decode_token(credentials.credentials)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
return user
def require_admin(user: dict = Depends(get_current_user)):
if user["role"] != "admin":
raise HTTPException(status_code=403, detail="Not enough permissions")
return user
@app.get("/admin")
def admin_dashboard(user: dict = Depends(require_admin)):
return {"message": "Admin access granted", "user": user}Common Mistakes
- Confusing authentication with authorization: verifying identity is not the same as checking permissions. Add role checks separately.
- Trusting any token format: extracting a token does not validate it. Always verify it before granting access.
- Returning 200 for invalid credentials: use
401 Unauthorizedfor failed authentication and403 Forbiddenfor insufficient permissions.
Best Practices
- Use dependencies: keep authentication logic reusable and clean across many routes.
- Store secrets safely: keep keys and signing secrets in environment variables, not hardcoded in source code.
- Prefer token-based auth for APIs: it scales better for web and mobile clients.
- Use HTTPS: credentials and tokens should never travel over insecure connections.
Practice Exercises
- Create a protected
/meendpoint that only works when the token equals a chosen secret value. - Build an API key check using the
X-API-Keyheader and protect a/statsroute. - Add a role field to a fake user object and allow only admins to access a
/settingsendpoint.
Mini Project / Task
Build a small FastAPI app for a team portal with one public route, one logged-in user route, and one admin-only route using dependency-based authentication.
Challenge (Optional)
Extend your authentication flow so different tokens map to different users, then return custom profile data based on the authenticated user.
Password Hashing with Passlib
Password hashing is a security technique used to store passwords safely without saving the original plain text value. In a real application, users create passwords during registration and send them again during login. If those passwords are stored directly in a database, anyone who gains access to that database can read them immediately. Hashing solves this by converting the password into a one-way encrypted-looking value that cannot be easily reversed. In FastAPI applications, Passlib is commonly used to hash and verify passwords during authentication workflows. It is often paired with login systems, JWT authentication, and user registration APIs. A hash is not the same as encryption. Encryption is designed to be reversed with a key, but hashing is designed to be one-way. Passlib provides secure algorithms such as bcrypt and helps developers avoid implementing password security incorrectly. In practice, the usual flow is simple: when a user registers, the password is hashed before saving it; when the user logs in, the plain password is checked against the stored hash using a verify function. Common password hashing algorithms include bcrypt, argon2, and pbkdf2_sha256. Bcrypt is widely used and beginner-friendly, while Argon2 is considered very strong and modern. Passlib makes it easy to switch between schemes if your project requirements change.
Step-by-Step Explanation
First, install the required packages in your FastAPI project. You typically use Passlib with bcrypt support. Then create a password context using CryptContext. This object defines which hashing schemes your application supports and which one should be used by default. Next, write one function to hash a plain password and another function to verify a plain password against a stored hash. During user registration, call the hash function before saving the password to the database. During login, fetch the user record and verify the submitted password against the stored hashed value. If verification succeeds, continue authentication; otherwise, reject the login request.
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)Comprehensive Code Examples
Basic example
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
password = "mysecret123"
hashed = pwd_context.hash(password)
print(hashed)
print(pwd_context.verify("mysecret123", hashed))Real-world example
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from passlib.context import CryptContext
app = FastAPI()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
fake_users_db = {}
class UserCreate(BaseModel):
username: str
password: str
class UserLogin(BaseModel):
username: str
password: str
@app.post("/register")
def register(user: UserCreate):
if user.username in fake_users_db:
raise HTTPException(status_code=400, detail="User already exists")
fake_users_db[user.username] = {
"username": user.username,
"hashed_password": pwd_context.hash(user.password)
}
return {"message": "User registered successfully"}
@app.post("/login")
def login(user: UserLogin):
db_user = fake_users_db.get(user.username)
if not db_user:
raise HTTPException(status_code=401, detail="Invalid credentials")
if not pwd_context.verify(user.password, db_user["hashed_password"]):
raise HTTPException(status_code=401, detail="Invalid credentials")
return {"message": "Login successful"}Advanced usage
from passlib.context import CryptContext
pwd_context = CryptContext(
schemes=["bcrypt", "pbkdf2_sha256"],
deprecated="auto"
)
def create_hash(password: str) -> str:
return pwd_context.hash(password)
def check_and_upgrade_password(password: str, stored_hash: str):
valid = pwd_context.verify(password, stored_hash)
new_hash = None
if valid and pwd_context.needs_update(stored_hash):
new_hash = pwd_context.hash(password)
return valid, new_hashCommon Mistakes
- Saving plain passwords: Always hash before storing user credentials.
- Comparing hashes manually: Use
verify()instead of hashing the input again and comparing strings yourself. - Using weak or outdated algorithms: Prefer bcrypt or argon2 through Passlib.
- Reusing one hardcoded hash: Let Passlib generate a unique salted hash for each password.
Best Practices
- Keep password hashing logic in dedicated utility functions.
- Use Passlib with a well-supported scheme like bcrypt or argon2.
- Never return hashed passwords in API responses.
- Combine hashing with proper authentication tokens such as JWT.
- Use HTTPS in production so passwords are protected during transmission.
Practice Exercises
- Create a function that hashes a password and prints the result.
- Write a second function that verifies whether a login password matches a stored hash.
- Build a small FastAPI route that accepts a password and returns whether it is valid against a sample stored hash.
Mini Project / Task
Build a small FastAPI authentication demo with two routes: /register to hash and store passwords, and /login to verify them before returning a success message.
Challenge (Optional)
Extend the login system so that if an old password hash format is detected, the password is verified and then automatically re-hashed using the current preferred scheme.
JWT Token Generation
JWT token generation is the process of creating a signed JSON Web Token that proves a user has successfully authenticated. A JWT is commonly used in FastAPI to support stateless authentication, meaning the server does not need to store login sessions in memory for every user. Instead, the client receives a token after login and sends it with later requests. This pattern is widely used in SPAs, mobile apps, dashboards, and microservices because it scales well and works cleanly with APIs.
A JWT usually contains three parts: header, payload, and signature. The header stores token metadata such as the signing algorithm. The payload contains claims like the username, user ID, role, and expiration time. The signature is created using a secret key or private key so the server can verify the token has not been altered. In FastAPI, JWT generation is often paired with OAuth2 password flow and libraries such as python-jose or PyJWT.
There are also important claim types to understand. Registered claims include sub for subject, exp for expiration, and iat for issued-at time. Public or custom claims can store values like roles, scopes, or tenant IDs. Access tokens are short-lived and used for normal API access. Refresh tokens are usually longer-lived and used to request new access tokens without forcing another login. Even if your app starts with access tokens only, understanding these sub-types helps you design secure systems later.
Step-by-Step Explanation
To generate a JWT in FastAPI, first define a secret key, a signing algorithm such as HS256, and a token expiration duration. Next, build a helper function that accepts data to encode, usually a dictionary containing a username or user ID. Then copy that data, attach an expiration time, and call the encoder from your JWT library. The result is a signed string token.
In a typical login flow, the user sends credentials to a login endpoint. Your app verifies the username and password, then calls the token creation function. The endpoint returns the token and token type, often bearer. Later, protected routes read the bearer token from the Authorization header and validate it before allowing access.
Comprehensive Code Examples
Basic example
from datetime import datetime, timedelta
from jose import jwt
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=30)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
token = create_access_token({"sub": "alice"})
print(token)Real-world example
from datetime import datetime, timedelta
from fastapi import FastAPI, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from jose import jwt
app = FastAPI()
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
fake_user = {"username": "admin", "password": "secret123", "role": "admin"}
def create_access_token(data: dict, expires_minutes: int = 30):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=expires_minutes)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
@app.post("/login")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
if form_data.username != fake_user["username"] or form_data.password != fake_user["password"]:
raise HTTPException(status_code=401, detail="Invalid credentials")
token = create_access_token({"sub": fake_user["username"], "role": fake_user["role"]})
return {"access_token": token, "token_type": "bearer"}Advanced usage
from datetime import datetime, timedelta, timezone
from jose import jwt
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
def create_access_token(user_id: int, username: str, scopes: list[str]):
now = datetime.now(timezone.utc)
payload = {
"sub": username,
"user_id": user_id,
"scopes": scopes,
"iat": now,
"exp": now + timedelta(minutes=15)
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)Common Mistakes
- Using a weak secret key: Use a long random secret stored in environment variables, not hardcoded demo text in production.
- Forgetting expiration: Always include
expso tokens do not last forever. - Putting sensitive data in payload: JWT payloads are encoded, not encrypted. Do not store passwords or private secrets inside them.
- Missing bearer format: Clients must send the token as
Authorization: Bearer.
Best Practices
- Keep access tokens short-lived to reduce risk if a token is stolen.
- Use environment variables for secret keys and rotate them when needed.
- Include only necessary claims such as
sub, role, or scopes. - Prefer UTC timestamps for consistent expiration handling.
- Separate access and refresh tokens in larger applications.
Practice Exercises
- Create a function that generates a JWT with only
subandexp. - Modify the token generator to include a custom
roleclaim. - Build a login endpoint that returns a bearer token when the correct username and password are submitted.
Mini Project / Task
Build a small FastAPI login service that accepts a username and password, generates a JWT access token for valid users, and returns the token in JSON format.
Challenge (Optional)
Extend your token generation logic to create both an access token and a refresh token with different expiration times and different payload contents.
JWT Token Verification
JWT token verification is the process of checking whether a JSON Web Token is authentic, unmodified, unexpired, and allowed to access a protected FastAPI endpoint. In real applications, users log in once, receive a token, and then send that token in the Authorization: Bearer ... header for later requests. FastAPI commonly uses JWTs for stateless authentication in dashboards, mobile backends, SaaS products, and microservices because the server can validate identity without storing a session for every user.
A JWT usually has three parts: header, payload, and signature. Verification means decoding the token with the expected secret key or public key, confirming the signing algorithm, checking claims such as exp for expiration, and reading identity fields like sub. In practice, JWT-related work includes token creation, token transport, and token verification. For this section, the focus is verification inside protected FastAPI routes. A token might be valid, expired, malformed, signed with the wrong key, or missing required claims. Your API must handle each case safely and return clear errors, usually 401 Unauthorized.
Step-by-Step Explanation
In FastAPI, verification is often implemented with dependencies. First, install a JWT library such as python-jose. Next, define constants like SECRET_KEY and ALGORITHM. Then use FastAPI's OAuth2PasswordBearer to extract the bearer token from incoming requests. After that, create a function such as verify_token() that decodes the token, validates claims, and returns the current user identity.
The common syntax flow is: get token from header, call jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]), read payload.get("sub"), and raise an HTTPException if anything fails. You should also catch decoding and expiration errors. Finally, inject the dependency into protected endpoints using Depends(verify_token) so only verified users can proceed.
Comprehensive Code Examples
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
SECRET_KEY = "super-secret-key"
ALGORITHM = "HS256"
def verify_token(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise credentials_exception
return {"username": username}
except JWTError:
raise credentials_exception
@app.get("/profile")
def read_profile(current_user: dict = Depends(verify_token)):
return {"message": "Access granted", "user": current_user}from datetime import datetime, timedelta
from jose import jwt
def create_access_token(data: dict):
to_encode = data.copy()
to_encode.update({"exp": datetime.utcnow() + timedelta(minutes=30)})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
token = create_access_token({"sub": "alice", "role": "admin"})from fastapi import Security
def verify_admin(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
if payload.get("role") != "admin":
raise HTTPException(status_code=403, detail="Admin access required")
return payload
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
@app.get("/admin")
def admin_dashboard(current_user = Depends(verify_admin)):
return {"message": "Welcome admin", "claims": current_user}Common Mistakes
- Not checking the signing algorithm: Always pass
algorithms=[ALGORITHM]when decoding. - Using a token without expiration: Add an
expclaim so old tokens cannot live forever. - Trusting missing claims: Verify required fields like
suband role values before granting access. - Hardcoding weak secrets: Use strong environment-based secrets in production.
Best Practices
- Store secrets in environment variables, not source code.
- Keep access tokens short-lived and issue refresh tokens separately.
- Use dependency functions to centralize verification logic.
- Return consistent
401and403responses for auth failures. - Validate both identity claims and authorization claims such as roles or scopes.
Practice Exercises
- Create a protected
/meendpoint that returns the username from the verified JWT. - Modify verification so requests fail if the token does not contain a
subclaim. - Add role checking so only users with
role=editorcan access an/editroute.
Mini Project / Task
Build a small FastAPI app with login, token generation, and two protected routes: one for any authenticated user and one restricted to admins through JWT claim verification.
Challenge (Optional)
Extend your verifier to support token revocation by checking whether the JWT's unique identifier exists in a blacklist before granting access.
OAuth2 with Password Flow
OAuth2 with Password Flow is an authentication approach where a user sends a username and password to a trusted backend, and the backend returns an access token. In FastAPI, this flow is commonly used for first-party applications such as your own web app, mobile app, or internal dashboard, where the frontend and backend are controlled by the same organization. The main goal is to avoid sending credentials on every request. Instead, the client logs in once, receives a token, and then includes that token in the Authorization: Bearer ... header for protected routes.
In real projects, this pattern is used for login systems, admin panels, internal tools, and APIs that need user identity. The important pieces are the OAuth2 password form, token generation, password hashing, and route protection. FastAPI provides OAuth2PasswordBearer to read bearer tokens from requests and OAuth2PasswordRequestForm to accept username/password form data at login. Although many tutorials show a simplified fake token, production systems usually issue JWTs with expiration times and validate them on every request.
There are two practical layers to understand. First, authentication verifies who the user is by checking a password. Second, authorization decides what that user can access after login. Password Flow handles the login exchange, while protected endpoints use the resulting token to identify the current user. In beginner examples, the token may just be the username. In real systems, it should be a signed JWT containing claims like subject, expiry, and scopes.
Step-by-Step Explanation
Start by creating a login endpoint, often /token. This endpoint accepts form fields named username and password. FastAPI reads them using OAuth2PasswordRequestForm. Next, verify the user exists and confirm the password using a hashing library such as pwdlib or passlib. If valid, create an access token. Then define oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token"). This tells FastAPI where tokens are obtained and how to extract bearer tokens from incoming requests. Finally, create a dependency like get_current_user that decodes the token, fetches the matching user, and returns that user for protected routes.
The basic syntax pattern is: login endpoint -> token creation -> bearer extraction dependency -> current user dependency -> protected path operation.
Comprehensive Code Examples
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
fake_users = {"alice": {"username": "alice", "password": "secret"}}
@app.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = fake_users.get(form_data.username)
if not user or user["password"] != form_data.password:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
return {"access_token": user["username"], "token_type": "bearer"}
def get_current_user(token: str = Depends(oauth2_scheme)):
user = fake_users.get(token)
if not user:
raise HTTPException(status_code=401, detail="Invalid token")
return user
@app.get("/me")
def read_me(current_user: dict = Depends(get_current_user)):
return current_userfrom datetime import datetime, timedelta, timezone
import jwt
SECRET_KEY = "change-this"
ALGORITHM = "HS256"
def create_access_token(data: dict, expires_minutes: int = 30):
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=expires_minutes)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise HTTPException(status_code=401, detail="Invalid token payload")
except Exception:
raise HTTPException(status_code=401, detail="Token validation failed")
user = fake_users.get(username)
if not user:
raise HTTPException(status_code=401, detail="User not found")
return userCommon Mistakes
Storing plain-text passwords instead of hashed passwords. Fix: always hash passwords and verify hashes securely.
Using the wrong login content type. Fix:
OAuth2PasswordRequestFormexpects form data, not raw JSON.Forgetting the
Bearerprefix in the authorization header. Fix: sendAuthorization: Bearer.Returning tokens that never expire. Fix: add expiration claims and reject expired tokens.
Best Practices
Use JWTs with
sub,exp, and optional scopes.Keep your secret key in environment variables, not source code.
Separate authentication logic into dependencies and utility functions.
Use HTTPS so credentials and tokens are protected in transit.
Add role or scope checks for sensitive endpoints.
Practice Exercises
Create a
/tokenroute that accepts a username and password and returns a bearer token.Build a
/profileroute that only returns data when a valid token is included.Modify your token creation logic to expire after 10 minutes and reject expired tokens.
Mini Project / Task
Build a small notes API where users log in through /token, receive a bearer token, and can only view their own notes through a protected /my-notes endpoint.
Challenge (Optional)
Extend the password flow by adding admin and regular-user roles, then protect one endpoint so only admin tokens are allowed to access it.
Role Based Authorization
Role Based Authorization, often called RBAC, is the process of controlling what a user can do after they have successfully logged in. Authentication answers the question “Who are you?” while authorization answers “What are you allowed to access?” In FastAPI, role-based authorization is commonly used to protect endpoints so that only users with specific roles such as admin, editor, or viewer can perform certain actions. This is used in dashboards, e-commerce admin panels, school management systems, banking portals, and internal company tools where different users need different levels of access.
The core idea is simple: every user is assigned one or more roles, and those roles are checked before an endpoint runs. A basic system may use a single role per user, while a more flexible one supports multiple roles. In FastAPI, this is usually implemented with dependencies. First, a dependency retrieves the current user from a token or session. Then another dependency checks whether that user has permission to access the route. This keeps authorization logic reusable and consistent across the application.
Common role patterns include fixed roles like user and admin, hierarchical roles where admins inherit lower permissions, and multi-role access where a route allows several roles. For example, a reporting endpoint may be available to both managers and admins. The important thing is to keep rules clear and centralized so they are easy to maintain.
Step-by-Step Explanation
Start by defining a user model or dictionary that contains role information. Next, create a dependency such as get_current_user that returns the logged-in user. Then build a role checker function that accepts allowed roles and raises an HTTPException with status 403 if access should be denied. Finally, attach that dependency to protected routes using Depends.
The syntax is beginner-friendly: a function returns another function, and FastAPI uses it as a dependency. This pattern is useful because you can write one authorization tool and reuse it on many endpoints without repeating code.
Comprehensive Code Examples
from fastapi import FastAPI, Depends, HTTPException, status
app = FastAPI()
def get_current_user():
return {"username": "alice", "role": "admin"}
def require_role(role: str):
def checker(user=Depends(get_current_user)):
if user["role"] != role:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
return user
return checker
@app.get("/admin")
def admin_dashboard(user=Depends(require_role("admin"))):
return {"message": "Welcome admin", "user": user}from fastapi import FastAPI, Depends, HTTPException
app = FastAPI()
def get_current_user():
return {"username": "maria", "roles": ["editor", "viewer"]}
def require_any_role(allowed_roles: list[str]):
def checker(user=Depends(get_current_user)):
if not any(role in user["roles"] for role in allowed_roles):
raise HTTPException(status_code=403, detail="Not enough permissions")
return user
return checker
@app.post("/articles")
def create_article(user=Depends(require_any_role(["editor", "admin"]))):
return {"message": "Article created by " + user["username"]}from fastapi import FastAPI, Depends, HTTPException
app = FastAPI()
fake_users = {
"john": {"username": "john", "role": "viewer"},
"sara": {"username": "sara", "role": "manager"},
"root": {"username": "root", "role": "admin"}
}
role_permissions = {
"viewer": ["read_reports"],
"manager": ["read_reports", "approve_budget"],
"admin": ["read_reports", "approve_budget", "delete_users"]
}
def get_current_user():
return fake_users["sara"]
def require_permission(permission: str):
def checker(user=Depends(get_current_user)):
permissions = role_permissions.get(user["role"], [])
if permission not in permissions:
raise HTTPException(status_code=403, detail="Permission denied")
return user
return checker
@app.get("/budget/approve")
def approve_budget(user=Depends(require_permission("approve_budget"))):
return {"message": "Budget approved", "by": user["username"]}Common Mistakes
- Mixing authentication and authorization: first identify the user, then check permissions.
- Hardcoding role checks inside every route: move checks into reusable dependencies.
- Returning 401 instead of 403: use
401for unauthenticated users and403for authenticated users without permission. - Ignoring multi-role scenarios: design for endpoints that allow more than one role when needed.
Best Practices
- Keep role names consistent across tokens, database records, and code.
- Centralize authorization rules in dependencies or service layers.
- Prefer permission-based checks for larger systems instead of many fragile role comparisons.
- Log access denials for auditing and debugging.
- Write tests for protected endpoints to verify both allowed and denied access.
Practice Exercises
- Create a FastAPI route called
/reportsthat only users with themanagerrole can access. - Modify a role checker so both
adminandeditorcan access an endpoint. - Build a route that returns
403when aviewertries to delete data.
Mini Project / Task
Build a small admin panel API with three routes: one public route, one route for editors to create posts, and one route for admins to delete users. Protect each route using reusable authorization dependencies.
Challenge (Optional)
Extend your RBAC system so one user can have multiple roles and permissions are calculated from all assigned roles without duplicating logic in each endpoint.
Background Tasks
Background tasks in FastAPI let you return a response to the client immediately while scheduling small follow-up work to run after the response is sent. This is useful when the user should not wait for non-critical operations such as sending a confirmation email, writing an audit log, notifying another service, or storing analytics data. In real applications, background tasks improve perceived performance and keep endpoints responsive. FastAPI provides this feature through BackgroundTasks, which is ideal for lightweight tasks that can safely run in the same application process. It is important to understand that background tasks are not a full job queue like Celery or RQ. They are best for short, simple actions, not long-running or mission-critical processing. You can add one or many tasks to a request, and FastAPI will execute them after the response has already been delivered. This makes them easy to use for practical API workflows where you need quick responses but still want extra work done behind the scenes.
The main concept is simple: define a normal Python function or async function that performs the follow-up action, then inject BackgroundTasks into your route and call add_task(). FastAPI stores these tasks and executes them after the main request cycle finishes. You will often use this pattern for email sending, temporary file cleanup, usage tracking, report generation triggers, and webhook notifications. A route can add multiple tasks, and each task can receive arguments just like a normal function call. While this is convenient, remember that if the process crashes, the task may be lost, so do not use it for guaranteed delivery requirements.
Step-by-Step Explanation
First, import BackgroundTasks from fastapi. Second, create the function that should run later. Third, add a parameter of type BackgroundTasks to the route function. Fourth, call background_tasks.add_task(function_name, arg1, arg2). Finally, return your response as usual. Syntax is beginner-friendly because you do not manually create threads or event loops. FastAPI manages task execution timing for you. If your task writes to a file, sends an email, or calls another helper function, keep the logic focused and short.
Comprehensive Code Examples
from fastapi import FastAPI, BackgroundTasks
app = FastAPI()
def write_log(message: str):
with open("app.log", "a") as f:
f.write(message + "\n")
@app.post("/notify")
def notify_user(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(write_log, f"Notification requested for {email}")
return {"message": "Request received"}from fastapi import FastAPI, BackgroundTasks
app = FastAPI()
def send_welcome_email(email: str, username: str):
with open("emails.log", "a") as f:
f.write(f"Sent welcome email to {username} at {email}\n")
def save_audit_event(action: str):
with open("audit.log", "a") as f:
f.write(action + "\n")
@app.post("/register")
def register(username: str, email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(send_welcome_email, email, username)
background_tasks.add_task(save_audit_event, f"New user registered: {username}")
return {"status": "created", "user": username}from fastapi import FastAPI, BackgroundTasks
from pathlib import Path
app = FastAPI()
async def cleanup_file(path: str):
file_path = Path(path)
if file_path.exists():
file_path.unlink()
@app.post("/export")
async def export_report(background_tasks: BackgroundTasks):
temp_file = "report.csv"
with open(temp_file, "w") as f:
f.write("id,name\n1,Ana\n2,Lee")
background_tasks.add_task(cleanup_file, temp_file)
return {"message": "Report generated", "file": temp_file}Common Mistakes
- Using background tasks for long jobs: Heavy processing can block resources. Use a task queue for large jobs.
- Assuming guaranteed execution: If the server stops, tasks may not finish. Use persistent workers for critical tasks.
- Putting response-dependent logic in the task: The client has already received the response, so task failure will not automatically update it. Log errors properly.
Best Practices
- Keep tasks short: Use them for lightweight post-response work.
- Separate task functions: Put background logic in reusable helper functions or service modules.
- Add logging and error handling: Silent failures are hard to debug.
- Use queues for critical workflows: Choose Celery or similar when retries and reliability matter.
Practice Exercises
- Create an endpoint that returns immediately and writes a username to a log file in the background.
- Build a route that adds two background tasks: one for logging and one for saving a timestamp to another file.
- Create a temporary text file in an endpoint, then schedule a background task to delete it after the response.
Mini Project / Task
Build a feedback API endpoint that accepts a user message, returns a success response instantly, and uses background tasks to store the feedback in a file and log the submission time.
Challenge (Optional)
Create a route that simulates user signup and schedules three background tasks: save an audit entry, send a welcome message, and clean up a temporary onboarding file. Organize the task functions cleanly in separate helpers.
WebSockets
WebSockets provide a persistent, two-way communication channel between a client and a server. Unlike traditional HTTP, where the client sends a request and waits for a response, WebSockets allow both sides to send messages at any time after a connection is established. This is useful in chat apps, live dashboards, multiplayer games, trading systems, notifications, and collaborative tools where data must update instantly without repeated polling. In FastAPI, WebSockets are supported directly, making it easy to create real-time endpoints alongside standard REST APIs.
A WebSocket connection begins with an HTTP handshake, then upgrades to a long-lived connection. In practice, this means a browser or app can connect once and continue exchanging messages efficiently. FastAPI exposes this through the WebSocket class. Common concepts include accepting a connection, receiving text or JSON, sending responses, handling disconnects, and managing multiple connected clients. Some applications use one client per connection, while others maintain a connection manager to broadcast updates to many users. You may also combine WebSockets with authentication, rooms, and background events.
Step-by-Step Explanation
To create a WebSocket route in FastAPI, import WebSocket and define an endpoint with @app.websocket("/ws"). Inside the function, call await websocket.accept() to complete the handshake. Then use methods such as receive_text(), receive_json(), send_text(), or send_json(). Since the connection stays open, you usually place receive/send logic inside a loop. If the client disconnects, FastAPI raises WebSocketDisconnect, which should be caught so cleanup can happen safely.
WebSocket paths can include parameters like /ws/{client_id}, which helps identify users or rooms. For multi-user systems, create a class that stores active connections in a list and offers methods like connect, disconnect, and broadcast. This pattern keeps your endpoint clean and makes scaling the logic easier. Because WebSockets are stateful, always think about connection lifecycle, error handling, and what happens when users leave unexpectedly.
Comprehensive Code Examples
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
message = await websocket.receive_text()
await websocket.send_text(f"Echo: {message}")
except WebSocketDisconnect:
print("Client disconnected")from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
class ConnectionManager:
def __init__(self):
self.active_connections = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@app.websocket("/chat/{username}")
async def chat_socket(websocket: WebSocket, username: str):
await manager.connect(websocket)
await manager.broadcast(f"{username} joined")
try:
while True:
data = await websocket.receive_text()
await manager.broadcast(f"{username}: {data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast(f"{username} left")from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/status")
async def status_socket(websocket: WebSocket):
await websocket.accept()
await websocket.send_json({"event": "connected", "ok": True})
while True:
payload = await websocket.receive_json()
if payload.get("action") == "ping":
await websocket.send_json({"action": "pong", "client": payload.get("client")})
else:
await websocket.send_json({"error": "unknown action"})Common Mistakes
Forgetting
await websocket.accept(). Fix: always accept the connection before sending or receiving data.Using normal HTTP decorators like
@app.get()for real-time communication. Fix: use@app.websocket()for persistent socket endpoints.Not handling disconnects. Fix: catch
WebSocketDisconnectand remove the client from active connection lists.Blocking the event loop with slow synchronous code. Fix: prefer async-friendly operations and offload heavy work when needed.
Best Practices
Keep connection management in a dedicated class for cleaner code.
Use structured JSON messages for real applications instead of plain text when events have multiple fields.
Validate user identity before accepting sensitive WebSocket connections.
Design clear message formats such as
type,payload, andtimestamp.Plan for disconnects, retries, and idle connection cleanup.
Practice Exercises
Create a WebSocket endpoint that accepts a message and returns the same message in uppercase.
Build a route with a path parameter for username and send a welcome message when the client connects.
Create a broadcast manager that sends every received message to all connected clients.
Mini Project / Task
Build a simple live support chat backend where multiple browser clients connect to one WebSocket endpoint and receive all incoming messages in real time.
Challenge (Optional)
Extend the chat system to support rooms so users only receive messages from the room they joined, and send all messages as JSON with fields for room, sender, and text.
API Versioning
API versioning is the practice of managing changes to an API without breaking existing client applications. In real projects, mobile apps, web frontends, partner integrations, and internal services may all depend on the same endpoints. If you rename fields, change response formats, or remove routes without a versioning strategy, older clients can fail immediately. Versioning solves this by allowing multiple API contracts to coexist while teams introduce improvements safely.
In FastAPI, versioning is commonly implemented through URL path prefixes such as /api/v1 and /api/v2. Other approaches include version headers or query parameters, but path-based versioning is easiest for beginners, clear in documentation, and simple to test. A common real-life example is when version 1 returns a simple product list, while version 2 adds pagination, filtering, or new field names. Keeping both available gives consumers time to migrate.
The main concepts are backward compatibility, deprecation, routing separation, and documentation clarity. Backward compatibility means old clients continue to work. Deprecation means warning users that an older version will eventually be removed. Routing separation means grouping endpoints for each version using FastAPI routers. Documentation clarity matters because FastAPI auto-generates OpenAPI docs, so versioned routes should be organized in a way developers can understand quickly.
Step-by-Step Explanation
Start by creating separate APIRouter objects for each version. Give each router a unique prefix such as /api/v1 and /api/v2. Then define routes inside each router. Finally, attach both routers to the main FastAPI application using include_router().
If version 2 introduces changes, do not overwrite version 1 logic unless you intentionally want to break clients. Instead, create a new route or a new schema. You can also mark older endpoints as deprecated in the route decorator to communicate planned phase-out clearly.
Comprehensive Code Examples
from fastapi import FastAPI, APIRouter
app = FastAPI()
v1 = APIRouter(prefix="/api/v1", tags=["v1"])
v2 = APIRouter(prefix="/api/v2", tags=["v2"])
@v1.get("/items")
def get_items_v1():
return [{"id": 1, "name": "Keyboard"}]
@v2.get("/items")
def get_items_v2():
return {"items": [{"id": 1, "name": "Keyboard", "in_stock": True}], "total": 1}
app.include_router(v1)
app.include_router(v2)from pydantic import BaseModel
class UserV1(BaseModel):
id: int
username: str
class UserV2(BaseModel):
id: int
username: str
email: str
@v1.get("/users/{user_id}", response_model=UserV1)
def get_user_v1(user_id: int):
return {"id": user_id, "username": "alice"}
@v2.get("/users/{user_id}", response_model=UserV2)
def get_user_v2(user_id: int):
return {"id": user_id, "username": "alice", "email": "[email protected]"}@v1.get("/orders", deprecated=True)
def old_orders_api():
return {"message": "This version is deprecated. Please migrate to v2."}
@v2.get("/orders")
def new_orders_api(limit: int = 10, offset: int = 0):
return {"limit": limit, "offset": offset, "orders": []}Common Mistakes
- Changing v1 directly: This breaks existing clients. Fix it by creating a new v2 route or schema.
- Mixing version logic in one function: This becomes hard to maintain. Fix it by separating routers and models by version.
- Forgetting documentation signals: Clients may not know an endpoint is outdated. Fix it by using clear tags, prefixes, and
deprecated=Truewhere needed.
Best Practices
- Prefer path-based versioning first because it is explicit and easy to test.
- Keep response models separate for each version when contracts differ.
- Deprecate older versions gradually instead of removing them suddenly.
- Document migration notes between versions for frontend and partner teams.
- Write tests for both active versions to avoid accidental regressions.
Practice Exercises
- Create
/api/v1/productsand/api/v2/productswhere v2 returns an extra field calledprice. - Build two user endpoints with different response models for v1 and v2.
- Mark one v1 endpoint as deprecated and confirm it appears that way in FastAPI docs.
Mini Project / Task
Build a small bookstore API with versioned routes where v1 lists books with title and author, while v2 adds ISBN, stock status, and pagination parameters.
Challenge (Optional)
Design a versioning strategy for an API used by mobile apps that cannot update immediately, and implement both v1 and v2 routes while sharing as much internal business logic as possible.
Testing with Pytest
Testing is a crucial aspect of software development, ensuring that your application behaves as expected, that new features don't break existing ones, and that your code is robust. For FastAPI applications, Pytest is the de-facto standard for writing and running tests due to its simplicity, powerful features, and extensive plugin ecosystem. It allows developers to write small, readable tests that can scale to complex scenarios. In real-world applications, testing with Pytest helps maintain code quality, facilitates refactoring, and boosts developer confidence, especially in large teams or when deploying to production. It's used in virtually every Python project, from small scripts to large enterprise-level services, to validate functionality, performance, and security.
Testing in FastAPI primarily involves using Pytest in conjunction with FastAPI's `TestClient`. The `TestClient` is a synchronous wrapper around `httpx.Client` (or `requests` for older versions) that allows you to make requests to your FastAPI application directly in memory, without needing to run a separate server. This makes tests fast and isolated. Key concepts include writing test functions (which typically start with `test_`), using `assert` statements to check expected outcomes, and leveraging Pytest fixtures for setup and teardown operations. The `TestClient` allows you to simulate HTTP methods like GET, POST, PUT, DELETE, and inspect the response status code, headers, and JSON body. This approach covers unit tests (testing individual components) and integration tests (testing how different components work together).
Step-by-Step Explanation
To get started with testing your FastAPI application using Pytest, follow these steps:
1. Install necessary libraries: You'll need `pytest` and `httpx` (which `TestClient` uses internally).
pip install pytest httpx2. Create your FastAPI application: Let's assume you have a simple `main.py` file.
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"message": "Hello, World!"}
@app.post("/items/")
async def create_item(name: str, price: float):
return {"name": name, "price": price}3. Create a test file: Pytest automatically discovers files named `test_*.py` or `*_test.py`. Create `test_main.py` in the same directory as `main.py` (or in a `tests/` directory).
4. Import `TestClient` and your app: Instantiate `TestClient` with your FastAPI app.
5. Write test functions: Each test function should start with `test_` and contain assertions.
6. Run tests: Navigate to your project directory in the terminal and run `pytest`.
Comprehensive Code Examples
Basic Example
This example tests the root endpoint of a simple FastAPI application.
# test_main.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, World!"}Real-world Example: Testing a CRUD API
Consider an API for managing items. This example demonstrates testing POST and GET requests with JSON bodies.
# main.py (continued)
from typing import List, Dict
items_db: List[Dict] = []
@app.post("/items/")
async def create_item(name: str, price: float):
item = {"name": name, "price": price}
items_db.append(item)
return item
@app.get("/items/")
async def read_items():
return items_db
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if 0 <= item_id < len(items_db):
return items_db[item_id]
return {"error": "Item not found"}# test_main.py (continued)
from fastapi.testclient import TestClient
from main import app, items_db
import pytest
client = TestClient(app)
@pytest.fixture(autouse=True)
def clear_items_db():
"""Fixture to clear the items_db before each test."""
items_db.clear()
yield
def test_create_item():
response = client.post("/items/", json={"name": "Laptop", "price": 1200.0})
assert response.status_code == 200
assert response.json() == {"name": "Laptop", "price": 1200.0}
assert len(items_db) == 1
def test_read_items_empty():
response = client.get("/items/")
assert response.status_code == 200
assert response.json() == []
def test_read_items_with_data():
client.post("/items/", json={"name": "Laptop", "price": 1200.0})
client.post("/items/", json={"name": "Mouse", "price": 25.0})
response = client.get("/items/")
assert response.status_code == 200
assert len(response.json()) == 2
assert response.json()[0]["name"] == "Laptop"
def test_read_single_item():
client.post("/items/", json={"name": "Keyboard", "price": 75.0})
response = client.get("/items/0")
assert response.status_code == 200
assert response.json() == {"name": "Keyboard", "price": 75.0}
def test_read_non_existent_item():
response = client.get("/items/99")
assert response.status_code == 200 # FastAPI returns 200 for custom errors like this
assert response.json() == {"error": "Item not found"}Advanced Usage: Testing Dependencies and Overrides
FastAPI's dependency injection system can be overridden during testing, which is incredibly powerful for isolating components or mocking external services.
# main.py (continued)
from fastapi import Depends, HTTPException
def get_current_user():
# Simulate a dependency that fetches a user
# In a real app, this would involve authentication logic
return {"username": "testuser", "id": 1}
@app.get("/users/me")
async def read_current_user(current_user: dict = Depends(get_current_user)):
return current_user
# test_main.py (continued)
from fastapi.testclient import TestClient
from main import app, get_current_user
client = TestClient(app)
def override_get_current_user():
return {"username": "mockuser", "id": 2}
def test_read_current_user_override():
app.dependency_overrides[get_current_user] = override_get_current_user
response = client.get("/users/me")
assert response.status_code == 200
assert response.json() == {"username": "mockuser", "id": 2}
app.dependency_overrides.clear() # Clear overrides after testCommon Mistakes
1. Forgetting to install `httpx` or `pytest`: The `TestClient` relies on `httpx` for making requests. Without it, tests will fail with import errors.
Fix: Always run `pip install pytest httpx`.
2. Not clearing database/state: If your tests modify global state (like `items_db` in the example), subsequent tests might fail due to leftover data.
Fix: Use Pytest fixtures with `autouse=True` and `yield` to set up and tear down test-specific data or clear shared state before each test.
3. Incorrectly using `async def` in test functions: Pytest test functions themselves should typically be synchronous (`def`) even when testing async FastAPI endpoints. The `TestClient` handles the async execution internally.
Fix: Ensure your test functions are `def test_something():` unless you are specifically using `pytest-asyncio` for more advanced async test scenarios.
4. Not clearing dependency overrides: If you override dependencies for a test and don't clear them, they might affect subsequent tests.
Fix: Always call `app.dependency_overrides.clear()` or use a fixture to manage the override lifecycle.
Best Practices
1. Use Fixtures for Setup/Teardown: Leverage Pytest fixtures (`@pytest.fixture`) for common setup (e.g., initializing `TestClient`, database connections, mock objects) and teardown operations. This keeps your tests DRY (Don't Repeat Yourself).
2. Isolate Tests: Each test should be independent and not rely on the order of execution or the state left by previous tests. Use fixtures to reset state (e.g., clear a database) between tests.
3. Test Edge Cases: Don't just test the happy path. Test invalid inputs, unauthorized access, missing data, and error conditions to ensure robustness.
4. Structure Your Tests: Organize your test files logically, mirroring your application's structure (e.g., `tests/routes/test_users.py`, `tests/services/test_auth.py`).
5. Use `parametrize` for Multiple Inputs: For tests that need to run with different sets of inputs and expected outputs, `pytest.mark.parametrize` is very useful.
6. Mock External Services: When your FastAPI application interacts with external APIs, databases, or message queues, use mocking libraries (like `unittest.mock`) within your tests to simulate their behavior and avoid making actual external calls, making tests faster and more reliable.
Practice Exercises
1. Basic Endpoint Test: Create a FastAPI app with a `GET /hello/{name}` endpoint that returns `{"message": "Hello, {name}!"}`. Write a Pytest test to verify this endpoint works correctly for a given name.
2. POST Data Validation Test: Extend your FastAPI app with a `POST /calculate` endpoint that accepts two integers (`num1`, `num2`) and returns their sum. Write tests to ensure it returns the correct sum and handles cases where non-integer inputs are provided (FastAPI's automatic validation will return a 422 Unprocessable Entity).
3. Dependency Override Test: Create a FastAPI endpoint `GET /protected-data` that has a dependency `get_api_key()` which returns a secret key. Write a test where you override `get_api_key` to return a mock key and verify the `protected-data` endpoint works with the overridden dependency.
Mini Project / Task
Build a small FastAPI application for managing a list of tasks. Implement the following endpoints:
- `POST /tasks/`: Create a new task with a `title` (string) and `completed` (boolean, default to `False`).
- `GET /tasks/`: Retrieve all tasks.
- `GET /tasks/{task_id}`: Retrieve a single task by its ID.
Then, write a comprehensive suite of Pytest tests for all these endpoints, including:
- Testing task creation and verifying the returned data.
- Testing retrieval of all tasks when the list is empty and when it contains multiple tasks.
- Testing retrieval of a specific task by ID and handling cases where the ID does not exist.
- Use a Pytest fixture to ensure the task list is cleared before each test.
Challenge (Optional)
Enhance your task management application and its tests. Add a `PUT /tasks/{task_id}` endpoint to update an existing task (allowing modification of `title` and `completed` status). Implement a `DELETE /tasks/{task_id}` endpoint to remove a task. Write Pytest tests for these new endpoints, ensuring proper updates, deletions, and error handling for non-existent task IDs. Consider using Pydantic models for request and response bodies to make your API more robust and testable.
Async and Await in FastAPI
FastAPI is built upon Starlette and Pydantic, making it inherently asynchronous. This means it leverages Python's async and await keywords to handle concurrent operations efficiently. In traditional synchronous programming, when a function performs an I/O-bound operation (like reading from a database, making an HTTP request, or accessing a file), the entire program thread blocks, waiting for that operation to complete. This can lead to significant performance bottlenecks, especially in web applications that need to serve many clients concurrently.
async and await, introduced in Python 3.5, provide a way to write concurrent code that looks sequential. An async function (or coroutine) can pause its execution at an await expression, allowing the program to switch to another task while the awaited operation completes. Once the awaited operation is done, the async function resumes from where it left off. This non-blocking I/O model is crucial for high-performance web servers like those built with FastAPI, as it allows a single process to handle many concurrent connections without needing to create a new thread or process for each, thus reducing overhead and improving scalability. This is particularly useful in real-world scenarios where API endpoints often interact with external services, databases, or message queues, all of which are I/O-bound operations.
FastAPI automatically detects if your path operation functions are standard (def) or asynchronous (async def). If you use async def, FastAPI expects you to await any I/O-bound operations within that function. If you use def, FastAPI will run your function in a separate thread pool, preventing it from blocking the main event loop. However, for true asynchronous benefits, especially when dealing with multiple I/O operations, using async def is preferred.
Step-by-Step Explanation
Understanding async and await in FastAPI involves knowing how to declare asynchronous functions and how to use await within them.
async deffor Asynchronous Functions: To define a function that can be paused and resumed, you declare it withasync definstead of justdef. These are called coroutines. In FastAPI, your path operation functions (e.g., functions decorated with@app.get(),@app.post()) can beasync def.from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
# This function is now a coroutine
return {"item_id": item_id}awaitfor Non-Blocking Operations: Theawaitkeyword can only be used inside anasync deffunction. It tells the Python interpreter to pause the execution of the current coroutine until the awaited object (another coroutine or an awaitable object) finishes its task. During this pause, the event loop can switch to other tasks, preventing the program from blocking.import asyncio
from fastapi import FastAPI
app = FastAPI()
async def simulate_db_call():
await asyncio.sleep(2) # Simulate an I/O-bound operation that takes 2 seconds
return {"message": "Data fetched from DB"}
@app.get("/data")
async def get_data():
db_result = await simulate_db_call() # Pause here, allow other tasks to run
return db_resultFastAPI's Handling of Synchronous Functions: If you define a path operation using a regular
deffunction, FastAPI is smart enough to run it in a separate thread pool. This prevents the synchronous function from blocking the main event loop, ensuring your API remains responsive. However, if thatdeffunction itself performs many I/O-bound operations synchronously, it might still block its dedicated thread for too long, potentially exhausting the thread pool if many such requests come in.import time
from fastapi import FastAPI
app = FastAPI()
def synchronous_blocking_task():
time.sleep(2) # This will block the thread it's running on
return {"message": "Synchronous task completed"}
@app.get("/sync-data")
def get_sync_data():
# FastAPI will run this in a thread pool
return synchronous_blocking_task()
Comprehensive Code Examples
Basic example
A simple asynchronous endpoint that simulates a delay.
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async-hello")
async def async_hello():
await asyncio.sleep(1) # Simulate a non-blocking 1-second delay
return {"message": "Hello from async FastAPI!"}
@app.get("/sync-hello")
def sync_hello():
import time
time.sleep(1) # This will block the thread
return {"message": "Hello from sync FastAPI!"}
To run this, save as main.py and use uvicorn main:app --reload. Test both endpoints. You'll notice that calling /async-hello multiple times concurrently will be handled more efficiently than /sync-hello.
Real-world example
Fetching data from an external API (simulated) and a database concurrently.
import asyncio
import httpx # A modern, async-friendly HTTP client
from fastapi import FastAPI
app = FastAPI()
async def fetch_external_api_data():
async with httpx.AsyncClient() as client:
# Simulate fetching data from an external API
response = await client.get("https://jsonplaceholder.typicode.com/todos/1")
return response.json()
async def fetch_database_data():
await asyncio.sleep(0.5) # Simulate a database query taking 0.5 seconds
return {"db_status": "connected", "users_count": 100}
@app.get("/dashboard")
async def get_dashboard_data():
# Run both I/O-bound operations concurrently
api_data_task = asyncio.create_task(fetch_external_api_data())
db_data_task = asyncio.create_task(fetch_database_data())
# Await their completion
api_data = await api_data_task
db_data = await db_data_task
return {"external_api": api_data, "database": db_data, "message": "Dashboard data loaded."}
This example demonstrates how asyncio.create_task and await can be used to perform multiple I/O-bound operations in parallel, significantly reducing the total response time compared to executing them sequentially.
Advanced usage
Using asyncio.gather for multiple concurrent tasks and error handling.
import asyncio
import httpx
from fastapi import FastAPI, HTTPException
app = FastAPI()
async def get_user_profile(user_id: int):
await asyncio.sleep(0.3) # Simulate DB call
if user_id % 2 == 0:
return {"id": user_id, "name": f"User {user_id}"}
raise ValueError(f"User {user_id} not found")
async def get_user_posts(user_id: int):
async with httpx.AsyncClient() as client:
# Simulate external API call for posts
response = await client.get(f"https://jsonplaceholder.typicode.com/posts?userId={user_id}")
response.raise_for_status() # Raise an exception for bad responses (4xx or 5xx)
return response.json()
@app.get("/user-details/{user_id}")
async def get_user_details(user_id: int):
try:
profile_task = get_user_profile(user_id)
posts_task = get_user_posts(user_id)
# Await multiple coroutines concurrently and gather their results
profile, posts = await asyncio.gather(profile_task, posts_task)
return {"user_profile": profile, "user_posts": posts}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except httpx.HTTPStatusError as e:
raise HTTPException(status_code=e.response.status_code, detail=f"External API error: {e.response.text}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {e}")
This advanced example uses asyncio.gather to run multiple coroutines concurrently and collect their results. It also demonstrates robust error handling for both internal logic and external API calls within an asynchronous context.
Common Mistakes
Using
awaitoutside anasync deffunction: This is a common Python syntax error.awaitcan only be used directly inside a function declared withasync def. If you try to use it in a regulardeffunction, Python will raise aSyntaxError.Forgetting to
awaitan awaitable: If you call anasync deffunction but forget to putawaitin front of it, you'll get a coroutine object that is never run. This won't raise an error immediately but will lead to unexpected behavior or tasks not completing.# INCORRECT
async def my_async_func():
await asyncio.sleep(1)
return "Done"
async def main_route():
result = my_async_func() # Missing await! result is a coroutine object, not its return value
return {"status": result}
# FIX
async def main_route_fixed():
result = await my_async_func() # Correct
return {"status": result}Mixing blocking I/O in
async defwithout proper handling: Directly calling blocking I/O functions (liketime.sleep(),requests.get(), or synchronous database drivers) inside anasync deffunction will block the entire event loop, defeating the purpose of asynchronous programming. If you must use blocking code, run it in a separate thread usingasyncio.to_thread()(Python 3.9+) or by passing it to FastAPI's thread pool by using adeffunction for your path operation.
Best Practices
Use
async deffor I/O-bound operations: Whenever your function performs network requests, database calls, file I/O, or any other operation that takes time waiting for an external resource, declare it asasync defand useawaitfor those operations.Use
deffor CPU-bound operations: If your function performs heavy computations that don't involve waiting (e.g., complex calculations, image processing), it's generally better to use a regulardeffunction for your path operation. FastAPI will automatically run these in a separate thread pool, preventing them from blocking the main event loop while they compute.Prefer async-native libraries: For I/O-bound tasks, use libraries that are designed for async Python, such as
httpxfor HTTP requests,asyncpgorSQLModel(with async drivers) for databases, andaiofilesfor file I/O. These libraries provide awaitable methods.Understand
asyncio.gatherandasyncio.create_task: Useasyncio.gatherwhen you need to run multiple awaitables concurrently and collect all their results. Useasyncio.create_taskwhen you want to schedule a coroutine to run in the background without immediately awaiting its result, or if you need more fine-grained control.Handle exceptions in async code: Just like synchronous code, asynchronous code needs proper error handling. Use
try...exceptblocks aroundawaitcalls that might fail (e.g., network requests, database operations).
Practice Exercises
Beginner-friendly: Create a FastAPI application with an endpoint
/delay/{seconds}. This endpoint should be anasync deffunction that takes an integerseconds, waits for that many seconds usingasyncio.sleep(), and then returns a message indicating the delay is over.Intermediate: Modify the previous exercise. Add another endpoint
/concurrent-delays. This endpoint should take two integer query parameters,delay1anddelay2. It should initiate two separateasyncio.sleep()calls concurrently (usingasyncio.gatherorasyncio.create_task), one for each delay, and return a message once both delays are complete. The total response time should be approximately the maximum ofdelay1anddelay2, not their sum.Advanced: Create an endpoint
/fetch-multiple-todos/{count}. This endpoint should fetchcountnumber of unique TODO items fromhttps://jsonplaceholder.typicode.com/todos/(e.g.,/todos/1,/todos/2, etc.) concurrently usinghttpx.AsyncClientandasyncio.gather. Return a list of the fetched TODOs. Handle potential network errors gracefully.
Mini Project / Task
Build a simple asynchronous product catalog API. Implement two endpoints:
GET /products/{product_id}: Anasync deffunction that simulates fetching product details from a database (useasyncio.sleep(0.2)for delay) and returns a dictionary with product ID, name, and price.GET /products/{product_id}/reviews: Anasync deffunction that simulates fetching product reviews from an external review service (useasyncio.sleep(0.3)for delay) and returns a list of review dictionaries.Create a third endpoint
GET /product-summary/{product_id}. This endpoint should concurrently call both/products/{product_id}and/products/{product_id}/reviewsfor the givenproduct_id. It should then combine the results and return a single dictionary containing the product details and its reviews. Ensure this combined endpoint takes approximately the maximum of the two simulated delays, not their sum.
Challenge (Optional)
Extend the /product-summary/{product_id} endpoint from the mini-project. Introduce a scenario where fetching product reviews might fail (e.g., for certain product_ids, simulate an HTTPStatusError from the external service). Modify your /product-summary/{product_id} endpoint to gracefully handle this failure: if review fetching fails, it should still return the product details but indicate that reviews could not be loaded (e.g., "reviews": "Failed to load reviews") instead of raising an HTTPException for the entire request, unless the product details fetch itself fails.
Environment Variables and Settings
Environment variables and settings are the standard way to configure a FastAPI application without hardcoding values directly in source code. They exist so you can run the same app in different environments such as local development, testing, staging, and production while changing only configuration values like database URLs, API keys, debug flags, or app names. In real life, this matters because secrets should not be committed to Git, and deployment platforms such as Docker, Kubernetes, GitHub Actions, and cloud hosts commonly inject configuration through environment variables. In FastAPI projects, settings are often managed with Pydantic-based configuration models, which give validation, type conversion, defaults, and a clean centralized structure.
The main concepts are simple. An environment variable is a key-value pair provided by the operating system or hosting platform. A settings class is a Python object that reads these values and converts them into typed fields such as str, int, bool, or URLs. Common sub-types of settings include application settings like project name and debug mode, database settings like connection strings, security settings like secret keys and token expiration, and service integration settings like email or third-party API credentials. A common pattern is to store local values in a .env file during development, while production values come from the actual environment. This separation keeps configuration flexible and safer.
Step-by-Step Explanation
First, define what values your app needs. Typical examples are APP_NAME, DEBUG, DATABASE_URL, and API_KEY. Next, create a settings class that loads and validates them. In modern FastAPI projects, this is commonly done with Pydantic settings support. Then create a single reusable settings object or a cached function so the app does not repeatedly rebuild configuration. Finally, use those settings inside routes, dependencies, database setup, or startup logic. The biggest beginner benefit is type safety: if PORT should be an integer and the environment provides invalid text, the app fails early with a clear error instead of breaking later.
Comprehensive Code Examples
from fastapi import FastAPI
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "FastAPI App"
debug: bool = False
settings = Settings()
app = FastAPI(title=settings.app_name)
@app.get("/info")
def read_info():
return {"app_name": settings.app_name, "debug": settings.debug}from fastapi import FastAPI, Depends
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
app_name: str
database_url: str
admin_email: str
def get_settings():
return Settings()
app = FastAPI()
@app.get("/config")
def get_config(settings: Settings = Depends(get_settings)):
return {
"app_name": settings.app_name,
"database_url": settings.database_url,
"admin_email": settings.admin_email
}from functools import lru_cache
from fastapi import FastAPI, Depends
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
app_name: str = "Production API"
debug: bool = False
database_url: str
secret_key: SecretStr
token_expire_minutes: int = 30
@lru_cache
def get_settings():
return Settings()
app = FastAPI()
@app.get("/health")
def health(settings: Settings = Depends(get_settings)):
return {
"app_name": settings.app_name,
"debug": settings.debug,
"token_expire_minutes": settings.token_expire_minutes
}Common Mistakes
- Hardcoding secrets: Beginners place API keys directly in Python files. Fix: move them to environment variables or a
.envfile excluded from version control. - Using wrong data types: Storing numbers or booleans as plain strings causes logic issues. Fix: define typed fields in the settings class so conversion is automatic.
- Recreating settings repeatedly: Building settings on every request can be wasteful. Fix: use a cached provider like
lru_cache. - Committing .env files: This exposes private values. Fix: add
.envto.gitignoreand use sample files like.env.example.
Best Practices
- Centralize configuration in one settings module.
- Use typed validation for booleans, integers, emails, and secrets.
- Separate development and production values without changing code.
- Cache settings for performance and consistency.
- Document required variables in a sample environment file.
Practice Exercises
- Create a settings class with
app_name,debug, andport, then return them from a FastAPI route. - Add a
.envfile and loaddatabase_urlinto your application. - Create a boolean setting named
maintenance_modeand expose its value through an endpoint.
Mini Project / Task
Build a small FastAPI service that reads APP_NAME, ADMIN_EMAIL, DATABASE_URL, and DEBUG from environment variables and returns safe non-secret configuration data from a /status endpoint.
Challenge (Optional)
Extend your settings design so the app uses one database URL for development and a different one for production, based on an environment variable such as ENVIRONMENT.
Dockerizing FastAPI
Dockerizing FastAPI means packaging your FastAPI application together with Python, dependencies, environment configuration, and the ASGI server into a portable container image. This exists to solve the classic problem of “it works on my machine” by ensuring the app runs the same way on a developer laptop, staging server, CI pipeline, or cloud platform. In real life, teams use Docker to deploy APIs to Kubernetes, ECS, Azure Container Apps, Railway, and many other environments. For FastAPI specifically, Docker is especially useful because apps often rely on exact Python versions, system packages, and process settings such as host, port, and worker count.
The main building blocks are the Dockerfile, which describes how the image is built, and the container, which is a running instance of that image. Common approaches include a simple single-stage image for learning and a more optimized image for production. You will also often use a .dockerignore file to keep unnecessary files out of the build context, and optionally Docker Compose when running FastAPI together with services such as PostgreSQL or Redis.
Step-by-Step Explanation
Start with a basic FastAPI app and make sure it runs locally with Uvicorn. Next, create a requirements.txt or pyproject.toml so Docker can install dependencies. Then write a Dockerfile. A common beginner-friendly flow is: choose a Python base image, set a working directory, copy dependency files, install packages, copy application code, expose the app port, and define the command that starts Uvicorn. Copying dependency files before application code helps Docker cache the package installation layer, making rebuilds faster.
You should usually bind Uvicorn to 0.0.0.0 inside a container, not 127.0.0.1, because the container must listen on all interfaces. You then map container port 8000 to a host port with docker run -p 8000:8000 .... Add a .dockerignore file to exclude __pycache__, .git, virtual environments, and local cache directories. For production, prefer a slim Python image, pin dependency versions, and run a single clear startup command. If your project uses environment variables, pass them with -e flags, --env-file, or your hosting platform’s secret manager.
Comprehensive Code Examples
Basic example
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello from Dockerized FastAPI"}fastapi==0.115.0
uvicorn[standard]==0.30.6FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]__pycache__
*.pyc
.git
.venv
venvdocker build -t fastapi-demo .
docker run -p 8000:8000 fastapi-demoReal-world example
from fastapi import FastAPI
import os
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok", "env": os.getenv("APP_ENV", "dev")}docker run -p 8000:8000 -e APP_ENV=production fastapi-demoAdvanced usage
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]Common Mistakes
- Using localhost inside the container: Use
0.0.0.0so the app is reachable from outside the container. - Copying everything too early: Copy dependency files first to improve Docker layer caching.
- Forgetting .dockerignore: Large folders make builds slower and images bigger; exclude unnecessary files.
- Missing pinned dependencies: Unpinned packages can break builds later; lock versions when possible.
Best Practices
- Use a small base image such as
python:3.12-slim. - Keep one responsibility per container and one clear startup command.
- Store configuration in environment variables, not hardcoded values.
- Test the container locally before pushing to production.
- Add a health endpoint so orchestrators can verify service status.
Practice Exercises
- Create a FastAPI app with a
/route and package it in Docker. - Add a
/healthendpoint and pass anAPP_ENVvariable into the container. - Create a
.dockerignorefile and verify that your image builds faster after excluding unnecessary folders.
Mini Project / Task
Build a small containerized FastAPI service with two routes: / and /health. The service should read an environment variable called APP_NAME and return it in one response.
Challenge (Optional)
Modify your Dockerized FastAPI app so it can run with multiple Uvicorn workers and compare startup behavior, logs, and response handling with the single-worker version.
Deployment to Production
Deployment to production is the process of taking a FastAPI application from local development and making it available for real users on a reliable server. In real life, production deployment means your API can handle external traffic, restart after failures, run behind a domain name, support HTTPS, and scale as usage grows. For FastAPI, this usually involves an ASGI server such as uvicorn or gunicorn with Uvicorn workers, environment-based configuration, reverse proxies like Nginx, containerization with Docker, and cloud hosting platforms. Common deployment styles include a single virtual machine, container deployment, and platform-as-a-service hosting. The main goal is not just to make the app run, but to make it secure, observable, repeatable, and easy to update.
Important ideas include the difference between development and production mode, process managers, worker processes, environment variables, health checks, logging, and graceful restarts. In development, --reload is useful, but in production it should not be used because it adds overhead and instability. A reverse proxy helps manage TLS termination, request buffering, and domain routing. Containers make deployments reproducible because the same image can run locally, in staging, and in production.
Step-by-Step Explanation
Start by preparing a production-ready FastAPI app with a clear entry point such as main:app. Install the server tools and define dependencies. Next, store secrets and configuration in environment variables instead of hardcoding values. Then choose a deployment model. For a simple Linux server, run FastAPI with Uvicorn or Gunicorn and place Nginx in front of it. For container-based deployment, write a Dockerfile, build an image, and run it with a fixed command. Finally, add health endpoints, logs, and startup checks so the service is easier to monitor.
Typical production command with Gunicorn is gunicorn -k uvicorn.workers.UvicornWorker -w 4 -b 0.0.0.0:8000 main:app. Here, -k selects the ASGI worker, -w 4 sets worker count, -b binds the host and port, and main:app points to the FastAPI instance. In containers, the exposed port usually matches the app port, and the hosting platform maps external traffic to it.
Comprehensive Code Examples
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok"}Basic example: a minimal health endpoint for uptime checks.
from fastapi import FastAPI
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "FastAPI Service"
debug: bool = False
database_url: str
class Config:
env_file = ".env"
settings = Settings()
app = FastAPI(title=settings.app_name)
@app.get("/info")
def info():
return {"app": settings.app_name, "debug": settings.debug}Real-world example: configuration loaded from environment variables for safer production setup.
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-w", "4", "-b", "0.0.0.0:8000", "main:app"]Advanced usage: a production-focused Dockerfile using Gunicorn with Uvicorn workers.
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}This reverse proxy example shows how public traffic can be forwarded to the FastAPI process.
Common Mistakes
- Using reload in production: Remove
--reloadand use worker processes instead. - Hardcoding secrets: Move API keys and database URLs into environment variables or a secret manager.
- No reverse proxy or HTTPS: Put Nginx, Caddy, or a cloud load balancer in front for TLS and routing.
- No health endpoint: Add
/healthfor monitoring and deployment checks.
Best Practices
- Use separate settings for development, staging, and production.
- Log application errors and access events in a structured format.
- Run multiple workers for CPU isolation and better throughput.
- Use Docker for consistent builds and easier rollback.
- Set up HTTPS, domain routing, and automated restarts.
- Monitor uptime, latency, memory, and error rates after release.
Practice Exercises
- Create a FastAPI app with a
/healthendpoint and run it with Gunicorn and Uvicorn workers. - Move app settings into environment variables and read them with a settings class.
- Write a Dockerfile that starts your FastAPI app on port
8000.
Mini Project / Task
Deploy a small FastAPI notes API to a Linux server or container platform with a reverse proxy, environment-based configuration, and a working health check endpoint.
Challenge (Optional)
Design a deployment workflow that supports zero-downtime updates, basic monitoring, and separate staging and production environments for the same FastAPI project.
Final Project
The final project is where you combine the major ideas of FastAPI into one complete application instead of learning features in isolation. In real work, developers rarely build a single endpoint and stop there; they create services with routing, request validation, business logic, persistence, error handling, authentication, and documentation. A final project exists to help you practice this full workflow and understand how separate pieces fit together. For this course, think of the project as building a small production-style API such as a task manager, note system, inventory service, or booking backend. The goal is not just to make something run, but to organize code clearly, validate data correctly, and expose a predictable API that other applications can consume. A strong FastAPI project usually includes app startup structure, routers for features, Pydantic schemas for input and output, dependency injection for shared resources, and database access through a clean service layer. Common project shapes include CRUD APIs, authenticated dashboards, internal microservices, and public APIs for mobile or web frontends. By completing a final project, you learn how to design endpoints, separate concerns, test behavior, and prepare an API for deployment.
Step-by-Step Explanation
Start by choosing one clear use case, such as a Task Management API. Define the resources first: users, tasks, tags, or projects. Next, list the operations you need, such as creating a task, reading all tasks, updating status, and deleting a task. Then structure the FastAPI app into files like main.py, routers/, schemas/, models/, and dependencies/. Create Pydantic models for request and response bodies so FastAPI can validate and document your API automatically. Add path operations with proper HTTP methods: GET, POST, PUT, PATCH, and DELETE. If your project needs authentication, add a reusable dependency that checks a token or current user. If it uses a database, keep direct database code out of route handlers whenever possible. Return consistent responses and raise HTTPException for invalid states such as missing records. Finally, test endpoints with Swagger UI, curl, or pytest. The project can be basic, real-world, or advanced depending on scope: a basic version may use in-memory storage, a real-world version may use SQLAlchemy, and an advanced version may include auth, pagination, filtering, and deployment-ready settings.
Comprehensive Code Examples
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
tasks = []
class Task(BaseModel):
title: str
completed: bool = False
@app.post("/tasks")
def create_task(task: Task):
tasks.append(task.dict())
return task
@app.get("/tasks")
def list_tasks():
return tasksfrom fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter(prefix="/tasks", tags=["Tasks"])
db = {1: {"id": 1, "title": "Write docs", "completed": False}}
class TaskCreate(BaseModel):
title: str
class TaskOut(BaseModel):
id: int
title: str
completed: bool
@router.get("/{task_id}", response_model=TaskOut)
def get_task(task_id: int):
task = db.get(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return taskfrom fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
def verify_token(x_token: str = Header(...)):
if x_token != "course-secret":
raise HTTPException(status_code=401, detail="Invalid token")
@app.delete("/tasks/{task_id}", dependencies=[Depends(verify_token)])
def delete_task(task_id: int):
return {"message": f"Task {task_id} deleted"}Common Mistakes
- Putting all code in one file: Split routes, schemas, and logic into separate modules.
- Skipping validation: Use Pydantic models instead of raw dictionaries for request bodies.
- Returning inconsistent data: Define response models so clients always get predictable fields.
- Mixing business logic into endpoints: Move reusable logic into helper or service functions.
- Ignoring errors: Use proper status codes and
HTTPExceptionmessages.
Best Practices
- Choose a small, focused project scope first, then extend it.
- Use routers and tags to group related endpoints.
- Keep naming consistent across paths, models, and functions.
- Write response models for public endpoints.
- Add authentication only where needed and keep it reusable.
- Test both success and failure cases before calling the project complete.
Practice Exercises
- Create a mini API with
/itemsendpoints for create and list operations. - Add a route that retrieves one record by ID and returns a 404 error if it does not exist.
- Refactor a single-file FastAPI app into routers and schema files.
Mini Project / Task
Build a Task Management API with endpoints to create, list, update, complete, and delete tasks, using Pydantic models and at least one protected route.
Challenge (Optional)
Extend the project by adding filtering, pagination, and user-specific task ownership so each user only sees their own records.