On this page
Intro to FastAPI
Intro to FastAPI
FastAPI is a modern, high-performance Python web framework for building APIs. It is built on top of Starlette (the async web layer) and Pydantic (the data validation layer), and it leverages Python's type hints throughout. FastAPI generates interactive API documentation automatically, validates request and response data, and is one of the fastest Python web frameworks available — benchmarking close to Node.js and Go for I/O-bound workloads.
Why FastAPI?
- Speed: Built on ASGI with async/await support — handles thousands of concurrent requests.
- Type safety: Uses Python type hints to validate inputs and outputs automatically.
- Auto docs: Generates Swagger UI (
/docs) and ReDoc (/redoc) with zero configuration. - Pydantic: Data validation with clear error messages and automatic JSON serialization.
- Developer experience: Editor autocompletion and inline validation everywhere.
Installation
pip install fastapi uvicorn[standard]uvicorn is the ASGI server that runs your FastAPI app. The [standard] extra adds the uvloop and httptools for maximum performance.
Your First FastAPI App
Create a file called main.py:
from fastapi import FastAPI
app = FastAPI(title="My API", version="1.0.0")
@app.get("/")
def read_root() -> dict:
return {"message": "Hello from FastAPI!", "status": "ok"}
@app.get("/health")
def health_check() -> dict:
return {"status": "healthy"}Run it:
uvicorn main:app --reloadThe --reload flag restarts the server automatically when you change the code. Open your browser at:
http://127.0.0.1:8000/— your API roothttp://127.0.0.1:8000/docs— Swagger UI (interactive)http://127.0.0.1:8000/redoc— ReDoc documentation
Path Parameters
from fastapi import FastAPI, HTTPException
app = FastAPI()
ITEMS = {
1: {"name": "Laptop", "price": 999.99},
2: {"name": "Mouse", "price": 29.99},
3: {"name": "Keyboard", "price": 79.99},
}
@app.get("/items/{item_id}")
def get_item(item_id: int) -> dict:
"""Retrieve an item by its ID."""
if item_id not in ITEMS:
raise HTTPException(status_code=404, detail=f"Item {item_id} not found")
return ITEMS[item_id]
@app.get("/users/{username}/posts/{post_id}")
def get_user_post(username: str, post_id: int) -> dict:
return {"username": username, "post_id": post_id}FastAPI automatically converts item_id from a string (all URL parameters are strings) to int based on the type hint, and returns a 422 Unprocessable Entity if conversion fails (e.g., /items/abc).
Query Parameters
from fastapi import FastAPI, Query
app = FastAPI()
PRODUCTS = [
{"id": 1, "name": "Widget", "category": "tools", "price": 9.99},
{"id": 2, "name": "Gadget", "category": "electronics", "price": 49.99},
{"id": 3, "name": "Doohickey", "category": "tools", "price": 14.99},
{"id": 4, "name": "Thingamajig", "category": "electronics", "price": 99.99},
]
@app.get("/products")
def list_products(
category: str | None = None,
min_price: float = 0.0,
max_price: float = 10_000.0,
skip: int = Query(0, ge=0), # ge=0 means >= 0
limit: int = Query(10, ge=1, le=100), # between 1 and 100
) -> list[dict]:
"""List products with optional filtering and pagination."""
results = [
p for p in PRODUCTS
if (category is None or p["category"] == category)
and min_price <= p["price"] <= max_price
]
return results[skip: skip + limit]URL: GET /products?category=tools&min_price=5&limit=5
Pydantic Models — Request and Response Schemas
Pydantic models define the shape of your data. FastAPI uses them for request body validation and response serialization:
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, EmailStr, Field, field_validator
from datetime import datetime
app = FastAPI()
class UserCreate(BaseModel):
"""Schema for creating a user — this is what the client sends."""
username: str = Field(min_length=3, max_length=30, pattern=r"^[a-zA-Z0-9_]+$")
email: str
age: int = Field(ge=13, le=120, description="Age in years (must be at least 13)")
bio: str | None = Field(None, max_length=500)
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
if "@" not in v or "." not in v.split("@")[-1]:
raise ValueError("Invalid email address")
return v.lower()
class UserResponse(BaseModel):
"""Schema for the response — what the server returns."""
id: int
username: str
email: str
age: int
bio: str | None
created_at: datetime
is_active: bool = True
# In-memory "database"
_users: dict[int, dict] = {}
_next_id = 1
@app.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def create_user(user: UserCreate) -> dict:
"""Create a new user."""
global _next_id
new_user = {
"id": _next_id,
"username": user.username,
"email": user.email,
"age": user.age,
"bio": user.bio,
"created_at": datetime.utcnow(),
"is_active": True,
}
_users[_next_id] = new_user
_next_id += 1
return new_user
@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int) -> dict:
if user_id not in _users:
raise HTTPException(status_code=404, detail=f"User {user_id} not found")
return _users[user_id]
@app.get("/users", response_model=list[UserResponse])
def list_users(skip: int = 0, limit: int = 10) -> list[dict]:
all_users = list(_users.values())
return all_users[skip: skip + limit]tip type: tip title: "Separate request and response models"
Always use different Pydantic models for input (request body) and output (response). Input models contain
Fieldvalidators and only the fields the client is allowed to set. Response models define what the server returns and may include generated fields likeidandcreated_at. This separation prevents accidentally exposing internal fields.
Update and Delete Endpoints
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class ProductUpdate(BaseModel):
name: str | None = None
price: float | None = None
stock: int | None = None
INVENTORY: dict[int, dict] = {
1: {"id": 1, "name": "Laptop", "price": 999.99, "stock": 50},
2: {"id": 2, "name": "Mouse", "price": 29.99, "stock": 200},
}
@app.patch("/inventory/{product_id}")
def update_product(product_id: int, update: ProductUpdate) -> dict:
"""Partially update a product."""
if product_id not in INVENTORY:
raise HTTPException(status_code=404, detail="Product not found")
product = INVENTORY[product_id]
# model_dump(exclude_unset=True) returns only the fields the client actually sent
changes = update.model_dump(exclude_unset=True)
product.update(changes)
return product
@app.delete("/inventory/{product_id}", status_code=204)
def delete_product(product_id: int) -> None:
if product_id not in INVENTORY:
raise HTTPException(status_code=404, detail="Product not found")
del INVENTORY[product_id]Dependency Injection
FastAPI has a powerful dependency injection system perfect for shared logic like authentication, database connections, and pagination:
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
def get_current_user(x_api_key: str = Header(...)) -> dict:
"""Extract and validate the API key from the X-Api-Key header."""
valid_keys = {"secret123": {"user": "alice", "role": "admin"}}
if x_api_key not in valid_keys:
raise HTTPException(status_code=401, detail="Invalid API key")
return valid_keys[x_api_key]
def admin_only(current_user: dict = Depends(get_current_user)) -> dict:
if current_user["role"] != "admin":
raise HTTPException(status_code=403, detail="Admin access required")
return current_user
@app.get("/admin/stats")
def admin_stats(user: dict = Depends(admin_only)) -> dict:
"""Only accessible with a valid admin API key."""
return {"message": f"Hello, {user['user']}! Here are your stats.", "total_users": 42}Async Endpoints
FastAPI natively supports async route handlers — perfect for database queries, HTTP calls to other services, and I/O-bound work:
import asyncio
import httpx
from fastapi import FastAPI
app = FastAPI()
@app.get("/posts/{post_id}/with-user")
async def get_post_with_user(post_id: int) -> dict:
"""Fetch a post and its author concurrently."""
async with httpx.AsyncClient(timeout=10.0) as client:
# Run both requests concurrently
post_task = client.get(f"https://jsonplaceholder.typicode.com/posts/{post_id}")
user_id_task = asyncio.create_task(
client.get(f"https://jsonplaceholder.typicode.com/posts/{post_id}")
)
post_response = await post_task
post_response.raise_for_status()
post = post_response.json()
user_response = await client.get(
f"https://jsonplaceholder.typicode.com/users/{post['userId']}"
)
user_response.raise_for_status()
user = user_response.json()
return {"post": post, "author": {"name": user["name"], "email": user["email"]}}tip type: info title: "When to use async"
Use
async defroute handlers when your endpoint does I/O — database queries, HTTP calls to other services, reading files. Use plaindeffor CPU-bound operations (FastAPI runs them in a thread pool automatically). Mixingasyncand blocking calls (likerequests.get) inside an async function will block the event loop — usehttpx.AsyncClientinstead.
nextSteps
- decorators-and-type-hints
Sign in to track your progress