NANDHOO.

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