FASTAPI / 3. RESPONSE MODELS & STATUS CODES

Response Models, Status Codes & Error Handling

Controlling exactly what goes out, raising meaningful errors


EXPLANATION

Response models define the shape of what your API returns. This is separate from your internal models — you may store a password hash internally but never want it in the response.

response_model= on the decorator tells FastAPI to filter and validate the output through a Pydantic model. Even if your function returns extra fields, only what's in the response model gets serialized and sent. This prevents accidental data leaks.

response_model_exclude_unset=True means fields that weren't set (kept as defaults) are excluded from the response — great for PATCH endpoints where you only want changed fields.

Status codes — FastAPI defaults to 200. Override with status_code=:
• 201 Created → POST that creates a resource
• 204 No Content → DELETE with no body
• 400 Bad Request → client sent invalid data
• 401 Unauthorized → not authenticated
• 403 Forbidden → authenticated but not allowed
• 404 Not Found → resource doesn't exist
• 422 Unprocessable Entity → FastAPI raises this automatically for invalid bodies
• 500 Internal Server Error → something broke on the server

HTTPException raises an HTTP error with a status code and detail message. You can also create custom exception handlers with @app.exception_handler() to intercept any exception type and return a structured JSON error.

Union response types (response_model=UserCreate | Message) let one endpoint return different shapes depending on the situation.

ARCHITECTURE

Your function returns:
  {
    "id": 1,
    "username": "kamran",
    "email": "k@nii.ac.in",
    "hashed_password": "$2b$...",  ← sensitive!
    "is_admin": true
  }
                ↓  response_model=UserPublic filters it
  Client receives:
  {
    "id": 1,
    "username": "kamran",
    "email": "k@nii.ac.in"
    ← hashed_password GONE
    ← is_admin GONE
  }

  ERROR FLOW:
  raise HTTPException(status_code=404, detail="User not found")
        ↓ FastAPI catches it
  HTTP 404
  { "detail": "User not found" }

CODE

PYTHON
1from fastapi import FastAPI, HTTPException, status
2from fastapi.responses import JSONResponse
3from fastapi.requests import Request
4from pydantic import BaseModel
5
6app = FastAPI()
7
8# ── Separate input vs output models ────────────────────
9class UserCreate(BaseModel):
10 username: str
11 email: str
12 password: str # comes IN
13
14class UserPublic(BaseModel):
15 id: int
16 username: str
17 email: str # goes OUT — no password!
18
19fake_db: dict[int, dict] = {}
20counter = 0
21
22@app.post("/users/", response_model=UserPublic, status_code=status.HTTP_201_CREATED)
23def create_user(user: UserCreate):
24 global counter
25 counter += 1
26 fake_db[counter] = {"id": counter, "username": user.username, "email": user.email, "hashed_password": "hashed_" + user.password}
27 return fake_db[counter] # hashed_password is filtered out!
28
29@app.get("/users/{user_id}", response_model=UserPublic)
30def get_user(user_id: int):
31 if user_id not in fake_db:
32 raise HTTPException(
33 status_code=status.HTTP_404_NOT_FOUND,
34 detail=f"User {user_id} not found",
35 )
36 return fake_db[user_id]
37
38# ── Custom exception ───────────────────────────────────
39class ItemNotFoundError(Exception):
40 def __init__(self, item_id: int):
41 self.item_id = item_id
42
43@app.exception_handler(ItemNotFoundError)
44async def item_not_found_handler(request: Request, exc: ItemNotFoundError):
45 return JSONResponse(
46 status_code=404,
47 content={"message": f"Item {exc.item_id} not found", "type": "item_not_found"},
48 )
49
50@app.get("/items/{item_id}")
51def get_item(item_id: int):
52 if item_id > 100:
53 raise ItemNotFoundError(item_id)
54 return {"item_id": item_id}
← PREV2. Pydantic & Request BodiesNEXT →4. Dependency Injection