Course topics

By WebNest Studio

Python Tutorial

Response Models, Status Codes and Error Handling

A good API is predictable about what it returns. FastAPI lets you declare the response model of each endpoint so that output is validated, documented and — importantly — filtered, which stops internal fields such as password hashes from leaking. You also choose the right status code for each outcome and turn failures into clean JSON errors with HTTPException and custom exception handlers.

Response Models

Declare the return type (-> UserOut) or pass response_model=UserOut. FastAPI converts whatever you return into that model, drops fields that are not in it, validates the result and documents it in /docs. A common pattern is three models per resource: UserCreate (input with password), UserInDB (stored, with the hash) and UserOut (public). Use list[UserOut] for collections and response_model_exclude_none=True to omit empty fields.

Status Codes

  • 200 OK — successful read or update (the default).
  • 201 Created — a new resource was created: @app.post(..., status_code=201).
  • 204 No Content — success with no body, typical for DELETE.
  • 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict — client errors you raise yourself.
  • 422 Unprocessable Content — automatic validation errors. 500 — unhandled server errors.
  • Use the named constants in fastapi.status, e.g. status.HTTP_404_NOT_FOUND, for readability.

Raising and Handling Errors

raise HTTPException(status_code=404, detail="Book not found") stops the request and returns {"detail": "Book not found"}; headers= can add headers. For domain errors, define your own exception classes and register a handler with @app.exception_handler(MyError) that returns a JSONResponse — your business code stays free of HTTP details. You can also customise the format of validation errors by handling RequestValidationError.

Examples

Filtering sensitive fields with response models

Python
import hashlib
from fastapi import FastAPI, status
from fastapi.testclient import TestClient
from pydantic import BaseModel

app = FastAPI()
users = []

class UserCreate(BaseModel):
    username: str
    email: str
    password: str

class UserOut(BaseModel):
    id: int
    username: str
    email: str

@app.post("/users", status_code=status.HTTP_201_CREATED)
def register(user: UserCreate) -> UserOut:
    stored = {
        "id": len(users) + 1,
        "username": user.username,
        "email": user.email,
        "password_hash": hashlib.sha256(user.password.encode()).hexdigest(),  # demo only
        "is_admin": False,
    }
    users.append(stored)
    return stored              # extra keys are filtered out by UserOut

@app.get("/users", response_model=list[UserOut])
def list_users():
    return users

client = TestClient(app)
r = client.post("/users", json={"username": "asha", "email": "asha@example.com", "password": "s3cret!"})
print(r.status_code, r.json())
print(client.get("/users").json())
Output
201 {'id': 1, 'username': 'asha', 'email': 'asha@example.com'}
[{'id': 1, 'username': 'asha', 'email': 'asha@example.com'}]

HTTPException, 204 responses and custom headers

Python
from fastapi import FastAPI, HTTPException, Response, status
from fastapi.testclient import TestClient

app = FastAPI()
books = {1: "Python Basics", 2: "Deep Python"}

@app.get("/books/{book_id}")
def get_book(book_id: int):
    if book_id not in books:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Book {book_id} not found")
    return {"id": book_id, "title": books[book_id]}

@app.post("/books", status_code=status.HTTP_201_CREATED)
def add_book(title: str, response: Response):
    if title in books.values():
        raise HTTPException(status.HTTP_409_CONFLICT, detail="A book with this title already exists")
    book_id = max(books) + 1
    books[book_id] = title
    response.headers["Location"] = f"/books/{book_id}"
    return {"id": book_id, "title": title}

@app.delete("/books/{book_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_book(book_id: int):
    if books.pop(book_id, None) is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Book not found")

client = TestClient(app)
print(client.get("/books/1").json())
r = client.get("/books/9")
print(r.status_code, r.json())
r = client.post("/books?title=SQL Basics")
print(r.status_code, r.json(), r.headers["location"])
print(client.post("/books?title=Deep Python").json())
r = client.delete("/books/2")
print(r.status_code, repr(r.text))
print(client.delete("/books/2").status_code)
Output
{'id': 1, 'title': 'Python Basics'}
404 {'detail': 'Book 9 not found'}
201 {'id': 3, 'title': 'SQL Basics'} /books/3
{'detail': 'A book with this title already exists'}
204 ''
404

Custom exceptions with exception handlers

Python
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient

class InsufficientStock(Exception):
    def __init__(self, product, available):
        self.product = product
        self.available = available

app = FastAPI()
STOCK = {"pen": 5, "lamp": 0}

@app.exception_handler(InsufficientStock)
async def stock_handler(request: Request, exc: InsufficientStock):
    return JSONResponse(status_code=409, content={
        "error": "insufficient_stock", "product": exc.product, "available": exc.available,
    })

@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
    fields = [".".join(str(p) for p in e["loc"][1:]) for e in exc.errors()]
    return JSONResponse(status_code=422, content={"error": "invalid_input", "fields": fields})

def reserve(product, quantity):               # business logic: no HTTP details here
    if STOCK.get(product, 0) < quantity:
        raise InsufficientStock(product, STOCK.get(product, 0))
    STOCK[product] -= quantity
    return STOCK[product]

@app.post("/reserve/{product}")
def reserve_endpoint(product: str, quantity: int):
    return {"product": product, "left": reserve(product, quantity)}

client = TestClient(app)
print(client.post("/reserve/pen?quantity=2").json())
r = client.post("/reserve/lamp?quantity=1")
print(r.status_code, r.json())
r = client.post("/reserve/pen?quantity=lots")
print(r.status_code, r.json())
Output
{'product': 'pen', 'left': 3}
409 {'error': 'insufficient_stock', 'product': 'lamp', 'available': 0}
422 {'error': 'invalid_input', 'fields': ['quantity']}

Common Mistakes

  • Returning database rows directly without a response model and leaking password hashes or internal flags.
  • Returning 200 for everything, or 500 for client mistakes.
  • Returning an error dict with status 200 instead of raising HTTPException.
  • Sending a body with 204 No Content responses.
  • Scattering HTTP status logic through business code instead of using custom exceptions and handlers.

Key Points to Remember

  • Response models validate, document and filter output — separate input and output models.
  • Choose status codes deliberately: 201 for create, 204 for delete, 404/409 for client errors.
  • HTTPException returns {"detail": ...} with the status you choose.
  • Custom exceptions plus @app.exception_handler keep business logic independent of HTTP.
  • RequestValidationError handlers let you customise 422 responses.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local project environment for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.