FastAPI Response Metadata
When building APIs with FastAPI, you'll often need to control not just the content of your responses, but also important metadata that accompanies them. Response metadata includes elements like status codes, headers, cookies, and content types - all of which provide crucial context about the response your API sends back to clients.
In this guide, we'll explore how to manage response metadata in FastAPI to create more robust and communicative APIs.
Understanding Response Metadata
Response metadata consists of several components that accompany the actual data payload:
- Status Codes: Numeric codes that indicate the outcome of a request (e.g., 200 for success, 404 for not found)
- Headers: Key-value pairs that provide additional information about the response
- Cookies: Small pieces of data stored on the client side
- Media Types: Defines the format of the response content (JSON, XML, etc.)
Let's dive into each aspect and see how to control them in FastAPI.
Setting Status Codes
HTTP status codes tell clients about the result of their request. FastAPI allows you to easily set custom status codes for your endpoints.
Basic Status Code Setting
from fastapi import FastAPI, status
app = FastAPI()
@app.get("/items/", status_code=status.HTTP_200_OK)
async def read_items():
return {"message": "Here are your items"}
@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item():
return {"message": "Item created successfully"}
In this example:
- The first endpoint explicitly returns a 200 OK status
- The second endpoint returns a 201 CREATED status, which is more appropriate for resource creation operations
Dynamic Status Codes
Sometimes you need to determine the status code based on some logic:
from fastapi import FastAPI, Response, status
app = FastAPI()
@app.get("/items/{item_id}")
async def get_item(item_id: int, response: Response):
if item_id < 0:
response.status_code = status.HTTP_400_BAD_REQUEST
return {"error": "Item ID cannot be negative"}
elif item_id > 100:
response.status_code = status.HTTP_404_NOT_FOUND
return {"error": "Item not found"}
# Default success case
return {"item_id": item_id, "name": "Sample Item"}
Here, we're using the response
parameter to dynamically set different status codes based on the input value.
Managing Response Headers
Headers provide additional information about the response. FastAPI makes it easy to set custom headers.
Setting Basic Headers
from fastapi import FastAPI, Response
app = FastAPI()
@app.get("/items/")
async def read_items(response: Response):
response.headers["X-Custom-Header"] = "custom-value"
response.headers["Server-Timing"] = "db;dur=53, app;dur=47.2"
return {"message": "Headers have been set"}
Setting CORS Headers
Cross-Origin Resource Sharing (CORS) headers are especially important for web APIs:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com", "https://www.example.org"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/public-data")
async def get_public_data():
return {"message": "This is public data available cross-origin"}
In this example, we've configured CORS to allow requests from specific origins.
Working with Cookies
Cookies can be set to store information on the client's browser.
Setting a Simple Cookie
from fastapi import FastAPI, Response
app = FastAPI()
@app.get("/set-cookie/")
async def set_cookie(response: Response):
response.set_cookie(
key="user_id",
value="12345",
max_age=3600, # expires in 1 hour
httponly=True # not accessible via JavaScript
)
return {"message": "Cookie has been set"}
Reading Cookies from Requests
from fastapi import FastAPI, Cookie
app = FastAPI()
@app.get("/get-cookie/")
async def get_cookie(user_id: str = Cookie(default=None)):
if user_id:
return {"user_id": user_id}
return {"message": "No user_id cookie found"}
Content Type and Media Type Headers
The Content-Type
header specifies the format of the response data. FastAPI automatically sets it to application/json
for most responses, but you can customize it.
Custom Content Types
from fastapi import FastAPI, Response
from fastapi.responses import PlainTextResponse, HTMLResponse, JSONResponse
app = FastAPI()
@app.get("/text/", response_class=PlainTextResponse)
async def get_text():
return "This is plain text content"
@app.get("/html/", response_class=HTMLResponse)
async def get_html():
return """
<html>
<head>
<title>HTML Response</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
"""
@app.get("/json/")
async def get_json():
# Default response type is JSONResponse
return {"message": "This is JSON content"}
Custom Response with Multiple Metadata Elements
Here's a more comprehensive example combining various response metadata:
from fastapi import FastAPI, Response, status
from datetime import datetime
app = FastAPI()
@app.get("/api/report/{report_id}")
async def get_report(report_id: str, response: Response):
# Set status code
response.status_code = status.HTTP_200_OK
# Set custom headers
response.headers["X-Report-Generated"] = datetime.now().isoformat()
response.headers["Cache-Control"] = "max-age=3600"
# Set a cookie
response.set_cookie(
key="last_report_viewed",
value=report_id,
max_age=86400
)
# Return the actual response body
return {
"report_id": report_id,
"title": "Monthly Sales Report",
"data": [{"month": "January", "sales": 10000}, {"month": "February", "sales": 12000}]
}
This endpoint simultaneously sets a status code, multiple custom headers, and a cookie alongside the JSON response.
Real-World Application: API Versioning
Let's see how response metadata can be used for API versioning:
from fastapi import FastAPI, Response, Header
from typing import Optional
app = FastAPI()
@app.get("/api/products")
async def get_products(
response: Response,
api_version: Optional[str] = Header(None, alias="X-API-Version")
):
# Set header to inform client about supported versions
response.headers["X-API-Supported-Versions"] = "1.0, 1.1, 2.0"
if api_version == "1.0":
# Old version format
response.headers["X-API-Deprecated"] = "Upgrade to version 2.0 by January 2024"
return [{"id": 1, "name": "Product A", "price": 49.99}]
elif api_version == "2.0":
# New version with more fields
return [{"id": 1, "name": "Product A", "price": 49.99, "currency": "USD", "in_stock": True}]
else:
# Default to latest version with a hint
response.headers["X-API-Info"] = "Using latest API version. Specify version with X-API-Version header."
return [{"id": 1, "name": "Product A", "price": 49.99, "currency": "USD", "in_stock": True}]
In this example, we're using headers both to receive information about the client's requested API version and to send back metadata about supported versions.
Handling Errors with Proper Status Codes
Providing appropriate status codes and error details improves API usability:
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int):
if user_id <= 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User ID must be positive",
headers={"X-Error-Code": "INVALID_USER_ID"}
)
# Simulate user not found for certain IDs
if user_id > 1000:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
headers={"X-Error-Code": "USER_NOT_FOUND"}
)
return {"user_id": user_id, "name": "Test User", "email": f"user{user_id}@example.com"}
Notice how we include custom headers even in error responses to provide additional context.
Summary
Response metadata is a crucial aspect of API development that goes beyond just returning data. With FastAPI, you have full control over:
- Status codes to indicate the outcome of requests
- Headers to provide additional context and control
- Cookies for client-side state management
- Content types to specify the format of your responses
By mastering response metadata, you can create more communicative, standards-compliant, and user-friendly APIs.
Additional Resources
Exercises
- Create an API endpoint that returns different status codes based on a query parameter.
- Build an endpoint that sets a session cookie and then another endpoint that reads and validates that cookie.
- Implement a custom error response system with consistent error codes and formats across your API.
- Create an endpoint that serves different content types (JSON, XML, CSV) based on an Accept header.
- Build a simple rate-limiting system using custom headers to inform clients about their remaining quota.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)