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 succeeded201 Created
- Resource successfully created400 Bad Request
- Invalid request format404 Not Found
- Resource not found500 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:
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:
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:
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:
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:
{
"detail": "Item ID must be a positive number"
}
With HTTP status code 400.
Custom Error Responses
You can customize error responses with additional headers:
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:
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:
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:
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:
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
- Use appropriate codes: Always return the most specific code for the situation
- Be consistent: Use similar codes for similar situations across your API
- Use constants: Use
starlette.status
constants instead of numeric codes - Provide useful error messages: Include helpful details in error responses
- Follow HTTP standards: Adhere to standard meanings of status codes
Common code matches:
GET
→ 200 OKPOST
(create) → 201 CreatedPUT
/PATCH
(update) → 200 OK or 204 No ContentDELETE
→ 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
- Create a FastAPI endpoint that returns different status codes based on a query parameter value
- Build a simple blog API with proper status codes for creating, reading, updating, and deleting posts
- Implement custom exception handlers for common error codes in your API
- Create an endpoint that demonstrates conditional status codes based on database results
- Add proper authentication and authorization to an API with corresponding status codes (401, 403)
Additional Resources
- FastAPI Official Documentation on Response Status Codes
- Mozilla HTTP Status Code Reference
- REST API Tutorial: HTTP Status Codes
- RFC 7231: HTTP/1.1 Semantics and Content
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! :)