What is FastAPI?
FastAPI is a modern, high-performance Python web framework for building APIs. It's built on top of Starlette (for async web handling) and Pydantic (for data validation). FastAPI is as fast as Node.js and Go, while being the most pleasant Python web framework to use.
Why FastAPI?
Automatic API docs (Swagger UI), built-in data validation, async support, type hints everywhere, and incredible performance. It's the fastest-growing Python web framework and the default choice for new API projects.
# Install
pip install fastapi uvicorn
# Run your app
uvicorn main:app --reload
Basic Routes
A FastAPI app is a collection of route handlers — functions that respond to HTTP requests. Each route has a path, an HTTP method, and a handler function:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello, World!"}
@app.get("/users/{user_id}")
async def get_user(user_id: int):
"""Path parameter with automatic type validation."""
return {"user_id": user_id, "name": f"User {user_id}"}
@app.get("/items")
async def list_items(skip: int = 0, limit: int = 10):
"""Query parameters with defaults."""
return {"skip": skip, "limit": limit, "items": ["a", "b", "c"][skip:skip+limit]}
Notice the type hints: user_id: int tells FastAPI to automatically validate that the path parameter is an integer. If someone requests /users/abc, they get a clear error message without you writing any validation code.
Pydantic Models — Request & Response
For complex request/response bodies, define Pydantic models. They validate data automatically and generate clear error messages:
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: str
age: Optional[int] = Field(None, ge=0, le=150)
tags: list[str] = []
class UserResponse(BaseModel):
id: int
name: str
email: str
# In-memory "database"
users_db = {}
next_id = 1
@app.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate):
global next_id
new_user = {"id": next_id, **user.model_dump()}
users_db[next_id] = new_user
next_id += 1
return new_user
Send a POST with invalid data and FastAPI returns a detailed 422 error explaining exactly what's wrong — no manual validation needed.
Full CRUD Example
from fastapi import FastAPI, HTTPException
app = FastAPI(title="User API", version="1.0")
@app.get("/users")
async def list_users():
return list(users_db.values())
@app.get("/users/{user_id}")
async def get_user(user_id: int):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
return users_db[user_id]
@app.put("/users/{user_id}")
async def update_user(user_id: int, user: UserCreate):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
users_db[user_id] = {"id": user_id, **user.model_dump()}
return users_db[user_id]
@app.delete("/users/{user_id}")
async def delete_user(user_id: int):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
deleted = users_db.pop(user_id)
return {"message": "Deleted", "user": deleted}
Automatic API Docs
Visit /docs for interactive Swagger UI documentation. Visit /redoc for ReDoc documentation. Both are auto-generated from your route definitions and Pydantic models — always up to date.
Dependency Injection
FastAPI's dependency injection system is one of its most powerful features. It lets you declare what a route handler needs, and FastAPI provides it automatically:
from fastapi import Depends, Header, HTTPException
# Dependency: verify auth token
async def get_current_user(authorization: str = Header()):
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid token")
token = authorization.split(" ")[1]
# In reality, decode and verify the JWT here
return {"user_id": 1, "name": "Alice", "token": token}
# Dependency: database session
async def get_db():
db = SessionLocal()
try:
yield db # Provide to route handler
finally:
db.close() # Cleanup after request
@app.get("/profile")
async def get_profile(user=Depends(get_current_user)):
"""This route requires authentication."""
return {"message": f"Hello, {user['name']}!", "user": user}
@app.get("/items")
async def list_items(db=Depends(get_db), user=Depends(get_current_user)):
"""Multiple dependencies — both DB and auth."""
items = db.query(Item).filter(Item.owner_id == user["user_id"]).all()
return items
Middleware & CORS
from fastapi.middleware.cors import CORSMiddleware
import time
# CORS — allow frontend to call this API
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # Frontend URL
allow_methods=["*"],
allow_headers=["*"],
)
# Custom middleware — log request timing
@app.middleware("http")
async def add_timing(request, call_next):
start = time.time()
response = await call_next(request)
elapsed = time.time() - start
response.headers["X-Process-Time"] = f"{elapsed:.4f}"
return response
Testing FastAPI Apps
from fastapi.testclient import TestClient
client = TestClient(app)
def test_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, World!"}
def test_create_user():
response = client.post("/users", json={
"name": "Alice",
"email": "[email protected]"
})
assert response.status_code == 200
data = response.json()
assert data["name"] == "Alice"
assert "id" in data
def test_get_nonexistent_user():
response = client.get("/users/9999")
assert response.status_code == 404
TestClient lets you test your API without running a server. Combined with pytest, you get fast, reliable API tests.🔍 Deep Dive: Async in FastAPI
FastAPI supports both async def and regular def routes. Use async def when your route does I/O (database queries, API calls, file reads) — it lets the server handle other requests while waiting. Use regular def for CPU-bound work. FastAPI runs sync functions in a thread pool automatically, so both work correctly.
⚠️ Common Mistake: Blocking in Async Routes
Wrong:
@app.get("/data")
async def get_data():
import time
time.sleep(5) # BLOCKS the entire event loop!
return {"data": "result"}
Why: time.sleep() is a blocking call. In an async route, it blocks the entire server, not just this request.
Instead:
import asyncio
@app.get("/data")
async def get_data():
await asyncio.sleep(5) # Non-blocking!
return {"data": "result"}