Status Codes

Difficulty: Intermediate

Proper use of HTTP status codes communicates the result of an API operation clearly. FastAPI makes it easy to set status codes both statically (in the decorator) and dynamically (in the handler), and to document all possible response codes in the OpenAPI specification.

Static status codes are set via the status_code parameter in the route decorator. This is the most common approach for the success case. For example, @app.post("/items", status_code=201) always returns 201 for successful creation. The starlette.status module provides named constants for all HTTP status codes.

Dynamic status codes are set by modifying the Response object within the handler. This is useful when the same endpoint can return different success codes depending on the logic. For example, creating a resource might return 201, while updating an existing one returns 200.

To document multiple possible responses in the OpenAPI spec, use the responses parameter in the decorator. This tells Swagger UI about all the possible response codes and their shapes, including error responses. Clients can then see all possible outcomes when using the endpoint.

FastAPI automatically documents 422 responses for endpoints with validated parameters. This is one of the standard responses that appears in every endpoint's documentation. You do not need to explicitly declare it.

A good practice is to use consistent status codes across your API. POST creates return 201, successful updates return 200, deletions return 204, client errors use 4xx, and server errors use 5xx. This consistency makes your API predictable for clients.

Code examples

Status Code Best Practices

from fastapi import FastAPI, HTTPException
from starlette import status
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float

items_db: dict[int, dict] = {}
next_id = 1

@app.post("/items", status_code=status.HTTP_201_CREATED)
def create_item(item: Item):
    """201 Created: Resource was successfully created"""
    global next_id
    items_db[next_id] = {"id": next_id, item.model_dump()}
    next_id += 1
    return items_db[next_id - 1]

@app.get("/items/{item_id}")
def get_item(item_id: int):
    """200 OK (default) or 404 Not Found"""
    if item_id not in items_db:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
    return items_db[item_id]

@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    """200 OK for update"""
    if item_id not in items_db:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
    items_db[item_id] = {"id": item_id, item.model_dump()}
    return items_db[item_id]

@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_item(item_id: int):
    """204 No Content: Successfully deleted, no body"""
    if item_id not in items_db:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
    del items_db[item_id]

Each operation uses the appropriate status code: 201 for creation, 200 (default) for reads and updates, 204 for deletion. Error cases use HTTPException with 404.

Dynamic Status Codes

from fastapi import FastAPI, Response
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float

items_db: dict[str, dict] = {}

@app.put("/items/{item_id}")
def upsert_item(item_id: str, item: Item, response: Response):
    """Create or update: 201 if created, 200 if updated"""
    if item_id in items_db:
        items_db[item_id] = {"id": item_id, item.model_dump()}
        response.status_code = 200
        return {"action": "updated", items_db[item_id]}
    else:
        items_db[item_id] = {"id": item_id, item.model_dump()}
        response.status_code = 201
        return {"action": "created", items_db[item_id]}

The Response parameter lets you set the status code dynamically. This upsert endpoint returns 201 when creating a new item and 200 when updating an existing one. The status code changes based on runtime logic.

Documenting Multiple Responses

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    id: int
    name: str
    price: float

class ErrorResponse(BaseModel):
    detail: str

@app.get(
    "/items/{item_id}",
    response_model=Item,
    responses={
        200: {"description": "Item found", "model": Item},
        404: {"description": "Item not found", "model": ErrorResponse},
        422: {"description": "Validation error"},
    },
)
def get_item(item_id: int):
    if item_id < 1:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"id": item_id, "name": "Widget", "price": 9.99}

# Swagger UI will show all three possible responses
# with their schemas and descriptions

The responses parameter documents all possible response codes in Swagger UI. Each response can have a description and an optional model for the response body schema. This helps clients understand all possible outcomes.

Key points

Concepts covered

HTTP Status Codes, status Module, Dynamic Status, Multiple Responses, OpenAPI Responses