Skip to main content

FastAPI Response Status Codes

Introduction

When building APIs, communicating the result of an operation clearly to clients is essential. HTTP status codes are standardized codes that tell the client about the outcome of their request. FastAPI provides convenient ways to set and customize these status codes, helping you build more professional, standards-compliant APIs.

In this tutorial, we'll learn how to work with HTTP status codes in FastAPI, understanding their significance and implementing them properly in your applications.

Understanding HTTP Status Codes

HTTP status codes are three-digit numbers that indicate the result of an HTTP request. They're grouped into five categories:

  • 1xx (Informational): The request was received, and the process is continuing
  • 2xx (Success): The request was successfully received, understood, and accepted
  • 3xx (Redirection): Further action needs to be taken to complete the request
  • 4xx (Client Error): The request contains bad syntax or cannot be fulfilled
  • 5xx (Server Error): The server failed to fulfill a valid request

Common status codes include:

  • 200 OK - Request succeeded
  • 201 Created - Resource successfully created
  • 400 Bad Request - Invalid request format
  • 404 Not Found - Resource not found
  • 500 Internal Server Error - Server encountered an error

Setting Status Codes in FastAPI

Default Status Codes

FastAPI automatically assigns appropriate status codes based on your route operation:

python
from fastapi import FastAPI

app = FastAPI()

@app.get("/items/")
async def read_items():
return {"message": "This returns a 200 OK status code by default"}

@app.post("/items/")
async def create_item():
# POST operations return 201 Created by default
return {"message": "This returns a 201 Created status code by default"}

Manually Setting Status Codes

To explicitly set a status code for a response, use the status_code parameter in your path operation decorator:

python
from fastapi import FastAPI

app = FastAPI()

@app.get("/items/", status_code=202)
async def read_items():
return {"message": "This will have a 202 Accepted status code"}

Using Status Code Constants

Rather than using numeric codes directly, FastAPI provides status code constants through starlette.status for better code readability:

python
from fastapi import FastAPI
from starlette import status

app = FastAPI()

@app.get("/items/", status_code=status.HTTP_200_OK)
async def read_items():
return {"message": "Success"}

@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item():
return {"message": "Item created"}

@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int):
# Delete the item
return None # 204 responses typically have no body content

Status Codes in Error Handling

Raising HTTP Exceptions

FastAPI allows you to raise exceptions to return specific status codes and error messages:

python
from fastapi import FastAPI, HTTPException
from starlette import status

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id < 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Item ID must be a positive number"
)
if item_id == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Item not found"
)
return {"item_id": item_id, "name": f"Item {item_id}"}

When this endpoint is called with a negative ID, the response will be:

json
{
"detail": "Item ID must be a positive number"
}

With HTTP status code 400.

Custom Error Responses

You can customize error responses with additional headers:

python
from fastapi import FastAPI, HTTPException
from starlette import status

app = FastAPI()

@app.get("/protected-resource/{resource_id}")
async def get_protected_resource(resource_id: str):
if resource_id == "secret":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="You don't have access to this resource",
headers={"WWW-Authenticate": "Bearer"}
)
return {"resource_id": resource_id, "content": "Resource data"}

Practical Real-World Examples

REST API for a Todo Application

Let's build a simple TODO API with appropriate status codes:

python
from fastapi import FastAPI, HTTPException, Response
from pydantic import BaseModel
from starlette import status
from typing import List, Optional

app = FastAPI()

# Data model
class TodoItem(BaseModel):
id: Optional[int] = None
title: str
completed: bool = False

# In-memory database
todos = {}
counter = 0

@app.post("/todos/", status_code=status.HTTP_201_CREATED, response_model=TodoItem)
async def create_todo(todo: TodoItem):
global counter
counter += 1
new_todo = todo.dict()
new_todo["id"] = counter
todos[counter] = new_todo
return new_todo

@app.get("/todos/", status_code=status.HTTP_200_OK, response_model=List[TodoItem])
async def get_todos():
return list(todos.values())

@app.get("/todos/{todo_id}", response_model=TodoItem)
async def get_todo(todo_id: int):
if todo_id not in todos:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Todo with ID {todo_id} not found"
)
return todos[todo_id]

@app.put("/todos/{todo_id}", response_model=TodoItem)
async def update_todo(todo_id: int, todo: TodoItem):
if todo_id not in todos:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Todo with ID {todo_id} not found"
)

updated_todo = todo.dict()
updated_todo["id"] = todo_id
todos[todo_id] = updated_todo
return updated_todo

@app.delete("/todos/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_todo(todo_id: int):
if todo_id not in todos:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Todo with ID {todo_id} not found"
)

del todos[todo_id]
return Response(status_code=status.HTTP_204_NO_CONTENT)

User Registration API with Validation

Here's a user registration API example with status codes for different scenarios:

python
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel, EmailStr, validator
from starlette import status

app = FastAPI()

class UserRegistration(BaseModel):
username: str
email: EmailStr
password: str

@validator("username")
def username_must_be_valid(cls, v):
if len(v) < 3:
raise ValueError("Username must be at least 3 characters long")
if not v.isalnum():
raise ValueError("Username must contain only letters and numbers")
return v

@validator("password")
def password_must_be_strong(cls, v):
if len(v) < 8:
raise ValueError("Password must be at least 8 characters long")
return v

# Simulated user database
users_db = {}

@app.post("/register/", status_code=status.HTTP_201_CREATED)
async def register_user(user: UserRegistration):
# Check if username already exists
if user.username in users_db:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Username already registered"
)

# Check if email already exists
for existing_user in users_db.values():
if existing_user["email"] == user.email:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Email already registered"
)

# Store the user (in a real app, hash the password)
users_db[user.username] = {
"username": user.username,
"email": user.email,
"password": user.password # In a real app, hash this!
}

return {
"username": user.username,
"email": user.email,
"message": "User registered successfully"
}

@app.get("/users/{username}", status_code=status.HTTP_200_OK)
async def get_user(username: str):
if username not in users_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)

user_data = users_db[username].copy()
# Don't return the password
user_data.pop("password")
return user_data

Advanced Status Code Usage

Conditional Status Codes

Sometimes you need different status codes based on business logic:

python
from fastapi import FastAPI, Response, status

app = FastAPI()

@app.get("/products/{product_id}")
async def get_product(product_id: str, response: Response):
if product_id == "new":
# Product is brand new
response.status_code = status.HTTP_200_OK
return {"product_id": product_id, "status": "New Product"}
elif product_id.startswith("limited-"):
# Product is limited edition
response.status_code = status.HTTP_200_OK
return {"product_id": product_id, "status": "Limited Edition"}
elif product_id == "discontinued":
# Product is discontinued but we still return info
response.status_code = status.HTTP_410_GONE
return {"product_id": product_id, "status": "Discontinued"}
else:
# Regular product
response.status_code = status.HTTP_200_OK
return {"product_id": product_id, "status": "Regular Product"}

Redirections

Handling redirects with the appropriate status codes:

python
from fastapi import FastAPI, status
from fastapi.responses import RedirectResponse

app = FastAPI()

@app.get("/docs", status_code=status.HTTP_308_PERMANENT_REDIRECT)
async def docs_redirect():
return RedirectResponse(
url="/documentation",
status_code=status.HTTP_308_PERMANENT_REDIRECT
)

@app.get("/old-page")
async def old_page():
return RedirectResponse(
url="/new-page",
status_code=status.HTTP_307_TEMPORARY_REDIRECT
)

@app.get("/documentation")
async def documentation():
return {"message": "This is the documentation"}

@app.get("/new-page")
async def new_page():
return {"message": "This is the new page"}

Best Practices for Status Codes

  1. Use appropriate codes: Always return the most specific code for the situation
  2. Be consistent: Use similar codes for similar situations across your API
  3. Use constants: Use starlette.status constants instead of numeric codes
  4. Provide useful error messages: Include helpful details in error responses
  5. Follow HTTP standards: Adhere to standard meanings of status codes

Common code matches:

  • GET → 200 OK
  • POST (create) → 201 Created
  • PUT/PATCH (update) → 200 OK or 204 No Content
  • DELETE → 204 No Content
  • Resource not found → 404 Not Found
  • Validation errors → 400 Bad Request or 422 Unprocessable Entity
  • Authentication issues → 401 Unauthorized
  • Authorization issues → 403 Forbidden

Summary

Status codes are a critical part of building professional REST APIs with FastAPI. The framework makes it easy to set appropriate status codes through decorators, exception handling, and response manipulation. Using proper status codes improves your API's usability by providing clear communication about the outcome of operations.

Key takeaways:

  • FastAPI provides intuitive ways to set and control HTTP status codes
  • Use status code constants from starlette.status for better readability
  • Raise HTTPException with appropriate status codes for error conditions
  • Choose status codes that accurately reflect the result of operations
  • Be consistent in your status code usage across your API

Exercises

  1. Create a FastAPI endpoint that returns different status codes based on a query parameter value
  2. Build a simple blog API with proper status codes for creating, reading, updating, and deleting posts
  3. Implement custom exception handlers for common error codes in your API
  4. Create an endpoint that demonstrates conditional status codes based on database results
  5. Add proper authentication and authorization to an API with corresponding status codes (401, 403)

Additional Resources

Understanding HTTP status codes is an important aspect of API development that helps create more professional, standards-compliant, and user-friendly interfaces.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)