Learn / Frameworks / FastAPI / Models, Responses and Errors

Intermediate 15 min

Models, Responses and Errors

Response models, status codes and raising HTTP errors.

What you will learn

  • Declare response_model
  • Set status codes
  • Raise HTTPException

How an API replies matters as much as what it accepts: the right shape, the right status code and helpful errors. FastAPI makes each of these explicit.

Response models

Declaring a response_model filters the output to exactly the fields you specify. This is how you avoid accidentally leaking secrets, such as a password hash.

from pydantic import BaseModel

class UserIn(BaseModel):
    username: str
    password: str

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

@app.post("/users", response_model=UserOut, status_code=201)
def create_user(user: UserIn):
    saved = {"id": 1, "username": user.username, "password": "hashed..."}
    return saved            # password is removed by UserOut

Status codes

  • 200 OK: success (default).
  • 201 Created: a resource was created (POST).
  • 204 No Content: success with nothing to return (DELETE).
  • 400 / 422: bad input. 401 / 403: not authenticated / not allowed. 404: not found. 409: conflict.
from fastapi import HTTPException, status

users = {1: {"id": 1, "username": "ada"}}

@app.get("/users/{user_id}", response_model=UserOut)
def get_user(user_id: int):
    if user_id not in users:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
    return users[user_id]

@app.delete("/users/{user_id}", status_code=204)
def delete_user(user_id: int):
    if users.pop(user_id, None) is None:
        raise HTTPException(404, "User not found")
Output
// GET /users/99
{"detail":"User not found"}
from fastapi.responses import JSONResponse

class OutOfStock(Exception):
    def __init__(self, item): self.item = item

@app.exception_handler(OutOfStock)
def out_of_stock_handler(request, exc: OutOfStock):
    return JSONResponse(status_code=409, content={"error": f"{exc.item} is out of stock"})
from typing import List

@app.get("/users", response_model=List[UserOut])
def list_users(skip: int = 0, limit: int = 20):
    return list(users.values())[skip: skip + limit]
Consistency

Pick one error shape for the whole API and stick to it; front-end code becomes much simpler when every error looks the same.

Try it yourself

Build a tiny in-memory /todos API: POST (201), GET by id (404 if missing) and DELETE (204).

Show solution
todos, next_id = {}, 1

class TodoIn(BaseModel):
    title: str

@app.post("/todos", status_code=201)
def add(t: TodoIn):
    global next_id
    todos[next_id] = {"id": next_id, "title": t.title}
    next_id += 1
    return todos[next_id - 1]

@app.get("/todos/{tid}")
def get(tid: int):
    if tid not in todos:
        raise HTTPException(404, "Not found")
    return todos[tid]

@app.delete("/todos/{tid}", status_code=204)
def remove(tid: int):
    todos.pop(tid, None)