Building APIs with FastAPI
FastAPI is a modern, high-performance Python web framework for building REST APIs. It is designed for speed, type safety, and automatic documentation — making it one of the most popular Python API frameworks in production today.
Why This Chapter Matters
APIs are the backbone of modern applications. A mobile app, a web frontend, and third-party integrations all use APIs to communicate. FastAPI makes building them in Python fast, safe, and developer-friendly.
What is an API?
An API (Application Programming Interface) allows different software systems to communicate. A REST API:
- receives HTTP requests (GET, POST, PUT, DELETE)
- processes them on the server
- returns JSON responses
Browser → HTTP Request → Your FastAPI Server → JSON Response → Browser
Installing FastAPI
pip install fastapi uvicorn[standard]
- FastAPI: the web framework
- Uvicorn: the ASGI server that runs your app
Your First FastAPI App
Create main.py:
from fastapi import FastAPI
app = FastAPI(title="My API", version="1.0.0")
@app.get("/")
def read_root():
return {"message": "Hello, World!"}
@app.get("/hello/{name}")
def greet(name: str):
return {"message": f"Hello, {name}!"}
Run it:
uvicorn main:app --reload
Visit:
http://localhost:8000/— your APIhttp://localhost:8000/docs— automatic interactive Swagger UIhttp://localhost:8000/redoc— alternative documentation
Path Parameters
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id, "name": "Asha"}
@app.get("/items/{category}/{item_id}")
def get_item(category: str, item_id: int):
return {"category": category, "item_id": item_id}
FastAPI automatically validates and converts types. If user_id is not an integer, it returns a proper 422 error.
Query Parameters
@app.get("/students")
def list_students(skip: int = 0, limit: int = 10, grade: str | None = None):
# URL: /students?skip=0&limit=5&grade=A
return {"skip": skip, "limit": limit, "grade": grade}
Optional parameters with | None = None (Python 3.10+) or Optional[str] = None.
Request Bodies with Pydantic
FastAPI uses Pydantic for data validation. Define models with type annotations:
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, field_validator
app = FastAPI()
class StudentIn(BaseModel):
name: str
email: str
score: float = 0.0
grade: str | None = None
class StudentOut(BaseModel):
id: int
name: str
email: str
score: float
@app.post("/students", response_model=StudentOut, status_code=201)
def create_student(student: StudentIn):
# In a real app: save to database
return StudentOut(id=1, **student.model_dump())
FastAPI automatically:
- Parses the incoming JSON body into
StudentIn - Validates types and required fields
- Returns proper error messages for invalid data
- Serializes the response as JSON
Pydantic Validators
from pydantic import BaseModel, field_validator
class Student(BaseModel):
name: str
score: float
@field_validator("score")
@classmethod
def score_must_be_valid(cls, v):
if not 0 <= v <= 100:
raise ValueError("Score must be between 0 and 100")
return v
@field_validator("name")
@classmethod
def name_must_not_be_empty(cls, v):
if not v.strip():
raise ValueError("Name cannot be empty")
return v.strip()
HTTP Methods
students_db = {}
@app.get("/students")
def list_students():
return list(students_db.values())
@app.get("/students/{student_id}")
def get_student(student_id: int):
if student_id not in students_db:
raise HTTPException(status_code=404, detail="Student not found")
return students_db[student_id]
@app.post("/students", status_code=201)
def create_student(student: StudentIn):
new_id = len(students_db) + 1
students_db[new_id] = {"id": new_id, **student.model_dump()}
return students_db[new_id]
@app.put("/students/{student_id}")
def update_student(student_id: int, student: StudentIn):
if student_id not in students_db:
raise HTTPException(status_code=404, detail="Student not found")
students_db[student_id].update(student.model_dump())
return students_db[student_id]
@app.delete("/students/{student_id}", status_code=204)
def delete_student(student_id: int):
if student_id not in students_db:
raise HTTPException(status_code=404, detail="Student not found")
del students_db[student_id]
HTTP Exceptions
from fastapi import HTTPException
@app.get("/users/{user_id}")
def get_user(user_id: int):
user = db.get(user_id)
if not user:
raise HTTPException(
status_code=404,
detail=f"User with id {user_id} not found"
)
return user
Dependency Injection
FastAPI has a powerful dependency injection system:
from fastapi import Depends
def get_current_user(token: str = ""):
if token != "valid-token":
raise HTTPException(status_code=401, detail="Unauthorized")
return {"id": 1, "name": "Asha"}
@app.get("/profile")
def get_profile(current_user = Depends(get_current_user)):
return current_user
Background Tasks
from fastapi import BackgroundTasks
def send_email(email: str, message: str):
# Simulate sending email
print(f"Sending email to {email}: {message}")
@app.post("/notify")
def notify(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(send_email, email, "Welcome!")
return {"status": "notification scheduled"}
Async Routes
FastAPI supports both sync and async functions. Use async def for I/O-bound work:
import asyncio
@app.get("/slow")
async def slow_endpoint():
await asyncio.sleep(1) # non-blocking wait
return {"status": "done"}
Middleware and CORS
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myfrontend.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Structuring a Real Project
myapi/
├── main.py
├── routers/
│ ├── students.py
│ └── courses.py
├── models/
│ ├── student.py
│ └── course.py
├── database.py
├── config.py
└── requirements.txt
routers/students.py:
from fastapi import APIRouter
router = APIRouter(prefix="/students", tags=["students"])
@router.get("/")
def list_students():
return []
main.py:
from fastapi import FastAPI
from routers import students, courses
app = FastAPI()
app.include_router(students.router)
app.include_router(courses.router)
Common Mistakes
- not using Pydantic models for request/response — relying on raw dicts
- loading environment secrets directly in code instead of using
python-dotenv - not returning proper HTTP status codes (using 200 for everything)
- not handling 404 and 422 explicitly
- mixing business logic directly into route handlers (keep routes thin)
Mini Exercises
- Build a FastAPI app with GET
/itemsthat returns a list of items. - Add POST
/itemsthat accepts a Pydantic model and returns the created item. - Add a path parameter to GET
/items/{item_id}and raise a 404 if not found. - Add a query parameter
?search=to filter the items list. - Add a dependency that checks for a bearer token in the
Authorizationheader.
Review Questions
- What is the difference between a path parameter and a query parameter?
- What is Pydantic and why does FastAPI use it?
- What is the difference between
async defanddefin a FastAPI route? - What does
response_model=do in a route decorator? - What command starts a FastAPI app with auto-reload?
Reference Checklist
- I can create a FastAPI app with GET, POST, PUT, DELETE routes
- I can define Pydantic models for request and response bodies
- I can use path parameters, query parameters, and request bodies correctly
- I can raise
HTTPExceptionwith appropriate status codes - I can use dependency injection
- I can structure a FastAPI project with routers