Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 23

Documenting Responses and Status Codes

Recommended resources

Hello! Welcome back to our course on FastAPI.

In the previous lesson, we established a robust, centralized error handling strategy using custom exceptions and global handlers. This allows our application to produce consistent error responses. Now, the logical next step is to communicate these possibilities to our API consumers.

This brings us to today's learning outcome: Declare different status codes and multiple possible responses for a single endpoint in the OpenAPI documentation.

An API is only as good as its documentation. If a developer using your API only knows about the "happy path" (the successful 200 OK response), they can't build resilient client applications that properly handle errors like "Not Found" or "Invalid Data". Today, you'll learn how FastAPI's first-class support for OpenAPI allows you to declaratively document every possible response, making your APIs truly self-describing.

For someone with your background, this is FastAPI's direct and, arguably, more integrated answer to using libraries like drf-spectacular with @extend_schema decorators in Django REST Framework to document API behavior.

1. Setting the Primary Status Code

By default, FastAPI assumes a successful response from a path operation will have a 200 OK status code. This is fine for most GET requests, but for other methods, different codes are more appropriate according to RESTful conventions. For instance, successfully creating a new resource via a POST request should return a 201 Created status code.

You can specify the success status code for an endpoint using the status_code parameter in the path operation decorator.

To see this in action, let's watch a segment from a tutorial you've seen before.

Python FastAPI Tutorial (Part 4): Pydantic Schemas - Request and Response Validation

In this clip from Corey Schafer's Python FastAPI Tutorial, he demonstrates how to create a POST endpoint and explicitly sets the success status code to 201.

Watch from 12:39 to 15:31. Focus on how he adds status_code=status.HTTP_201_CREATED to the @app.post decorator and explains why 201 is a best practice for resource creation.

As you saw, setting the primary status code is straightforward:

from fastapi import FastAPI, status
from pydantic import BaseModel




# Assume PostCreate and PostResponse schemas are defined
# as in the video.

app = FastAPI()

@app.post(
    "/posts",
    response_model=PostResponse,
    status_code=status.HTTP_201_CREATED  # This is the key part!
)
def create_post(post: PostCreate):



    # ... logic to create and save the post ...
    new_post = ... 
    return new_post

Using the status module (e.g., status.HTTP_201_CREATED) instead of the raw integer 201 is a best practice. It improves readability and helps avoid magic numbers in your code.


2. Documenting Multiple Responses with responses

Now for the main event: documenting all possible responses, including errors. FastAPI provides the responses parameter in the path operation decorator for this exact purpose. It takes a dictionary where keys are the HTTP status codes and the values are dictionaries describing the response.

Let's imagine a GET endpoint for retrieving an item. It has two main outcomes:

  1. 200 OK: The item is found and returned.
  2. 404 Not Found: An item with the given ID doesn't exist.

We've already seen how to use response_model to document the 200 OK response. We can use responses to document the 404 error.

The official FastAPI documentation has the perfect guide for this.

Additional Responses in OpenAPI - FastAPI

This document, 'Additional Responses in OpenAPI', is the definitive guide to using the responses parameter. It will show you how to declare responses with different status codes and even different Pydantic models.

Read the introduction, the section 'Additional Response with model', and the section 'Combining information'. Focus on the structure of the responses dictionary and how you can specify a Pydantic model for an error response.

Let's synthesize what you've just read. To document both the successful retrieval and the "not found" error for an endpoint, you combine response_model and responses:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel




# Schema for the successful response data
class Item(BaseModel):
    id: str
    value: str




# Schema for the error message
class Message(BaseModel):
    message: str

app = FastAPI()

@app.get(
    "/items/{item_id}",
    response_model=Item,  # Documents the 200 OK response
    responses={
        404: {"model": Message, "description": "The item was not found"}
    }  # Documents the 404 Not Found response
)
async def read_item(item_id: str):
    if item_id == "foo":
        return {"id": "foo", "value": "Found it!"}
    else:



        # As we learned last lesson, you would raise an exception here.
        # FastAPI's default handler or your custom one would create the response.
        # This documentation makes that behavior explicit.
        raise HTTPException(status_code=404, detail="Item not found")

When you run this and visit your /docs page, FastAPI generates a rich, interactive UI that shows both possibilities clearly.

FastAPI Swagger UI: Documenting Multiple Responses
This screenshot from the FastAPI documentation shows the end result: a clear listing of possible responses. The 200 OK response shows the `Item` schema, and the 404 Not Found response shows the `Message` schema and its description.

Analogy to Django REST Framework

In DRF with drf-spectacular, you'd achieve a similar result with a more verbose decorator:




# In DRF with drf-spectacular
from drf_spectacular.utils import extend_schema, OpenApiResponse

@extend_schema(
    responses={
        200: ItemSerializer,
        404: OpenApiResponse(response=MessageSerializer, description="The item was not found"),
    }
)
@api_view(['GET'])
def read_item(request, item_id):



    # ...

As you can see, FastAPI integrates this functionality more natively into its core decorator system, making it a bit more streamlined.


3. Applying Responses to an Entire Router

In any reasonably sized application, you'll have common responses that apply to many endpoints. For example, almost every endpoint in a "users" module might return a 404 Not Found if the user ID is invalid. Repeating the responses={404: ...} dictionary in every path operation decorator violates the DRY principle.

Just as you can set a common prefix or tags on an APIRouter, you can also set common responses.

Bigger Applications - Multiple Files - FastAPI

The documentation on building bigger applications shows how to add configuration, including default responses, directly to an APIRouter.

Read the section 'Another module with APIRouter' and 'Add some custom tags, responses, and dependencies'. Notice how responses={404: {"description": "Not found"}} is passed to the APIRouter constructor, and how a specific endpoint can add another response (403) on top of that.

This is a powerful pattern for maintaining consistency. Here's how it looks:

from fastapi import APIRouter, HTTPException

router = APIRouter(
    prefix="/items",
    tags=["items"],
    responses={404: {"description": "Not found"}}  # Applied to all routes in this router
)

@router.get("/{item_id}")
async def read_item(item_id: str):



    # ... logic that might raise a 404 error
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"]}

@router.put(
    "/{item_id}",



    # You can add more specific responses here
    responses={403: {"description": "Operation forbidden"}}
)
async def update_item(item_id: str):



    # ... logic that might raise 403 or 404
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    if item_id == "plumbus":
        raise HTTPException(status_code=403, detail="Plumbus cannot be updated")
    return {"item_id": item_id, "name": "The great Plumbus"}

In the example above:

  • read_item automatically gets the 404 Not Found documentation from the router.
  • update_item gets the 404 documentation from the router and has its own specific 403 Forbidden documentation. FastAPI intelligently merges them.

This separation of concerns—defining common behavior at the router level and specific behavior at the endpoint level—is a hallmark of well-structured FastAPI applications.


Conclusion

In this lesson, you have learned how to create a comprehensive and explicit API contract by documenting all possible success and error responses. This is a critical skill for building professional APIs that are easy for other developers to understand and consume.

Key Takeaways:

  • Use the status_code parameter in path operation decorators to define the primary success status code (e.g., status.HTTP_201_CREATED).
  • Use the responses parameter to declare additional responses, especially for error cases like 404 Not Found or 422 Unprocessable Content.
  • You can associate Pydantic models with your extra responses (e.g., {"model": MyErrorModel}) to fully document the structure of error payloads.
  • To keep your code DRY, apply common responses to an entire APIRouter, which will be inherited by all path operations within it.

Preview of the Next Lesson:
So far, all our data has been temporary and stored in memory. To build real-world applications, we need to persist data. In our next lesson, we will begin the "Database Integration" module and tackle the first outcome: Set up async SQLAlchemy with FastAPI, contrasting it with the Django ORM's synchronous model. This will be a significant step, moving us into the world of asynchronous database operations, one of FastAPI's core strengths.