Building APIs with FastAPI

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 API
  • http://localhost:8000/docs — automatic interactive Swagger UI
  • http://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

  1. Build a FastAPI app with GET /items that returns a list of items.
  2. Add POST /items that accepts a Pydantic model and returns the created item.
  3. Add a path parameter to GET /items/{item_id} and raise a 404 if not found.
  4. Add a query parameter ?search= to filter the items list.
  5. Add a dependency that checks for a bearer token in the Authorization header.

Review Questions

  1. What is the difference between a path parameter and a query parameter?
  2. What is Pydantic and why does FastAPI use it?
  3. What is the difference between async def and def in a FastAPI route?
  4. What does response_model= do in a route decorator?
  5. 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 HTTPException with appropriate status codes
  • I can use dependency injection
  • I can structure a FastAPI project with routers