FastAPI Custom Responses
When building APIs with FastAPI, the default response handling works well for most scenarios. However, there are cases where you need more control over your HTTP responses—specifying custom headers, setting specific status codes, or returning non-JSON content types. FastAPI's custom response features give you this flexibility while maintaining the framework's simplicity and type safety.
Introduction to Custom Responses
By default, FastAPI automatically converts your return values into JSON and wraps them in a proper HTTP response. But sometimes you need to:
- Return a different content type (like HTML, plain text, or a binary file)
- Set specific HTTP headers
- Customize the status code based on business logic
- Stream responses for large data
- Return custom formatted responses
In this article, we'll explore the various ways to customize your API responses in FastAPI.
Basic Response Types in FastAPI
FastAPI provides several response classes through Starlette, the ASGI framework it's built on. Let's look at the most common ones:
Response Class
The most basic response class is Response
from fastapi.responses
:
from fastapi import FastAPI
from fastapi.responses import Response
app = FastAPI()
@app.get("/basic-response")
def get_basic_response():
content = "This is a plain text response"
return Response(content=content, media_type="text/plain")
In this example:
- We return plain text instead of JSON
- We set the media type to
text/plain
- The status code defaults to 200 (OK)
Common Response Types
FastAPI includes pre-configured response classes for common scenarios:
from fastapi import FastAPI
from fastapi.responses import (
JSONResponse,
HTMLResponse,
PlainTextResponse,
RedirectResponse
)
app = FastAPI()
@app.get("/json", response_class=JSONResponse)
def get_json():
return {"message": "This is a JSON response"}
@app.get("/html", response_class=HTMLResponse)
def get_html():
html_content = """
<html>
<head>
<title>FastAPI HTML Response</title>
</head>
<body>
<h1>Hello from FastAPI!</h1>
</body>
</html>
"""
return html_content
@app.get("/text", response_class=PlainTextResponse)
def get_text():
return "This is plain text content"
@app.get("/redirect", response_class=RedirectResponse)
def get_redirect():
return "https://fastapi.tiangolo.com"
Specifying Response Class in Path Operation Decorator
You can declare the response class directly in your path operation decorator:
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/html-page", response_class=HTMLResponse)
def get_html_page():
return """
<html>
<head>
<title>FastAPI - HTML Response</title>
</head>
<body>
<h1>This is an HTML response</h1>
<p>FastAPI makes it easy to return HTML directly!</p>
</body>
</html>
"""
Custom Headers and Status Codes
You can specify custom headers and status codes in your responses:
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/custom-headers")
def get_custom_headers():
content = {"message": "This response has custom headers"}
headers = {"X-Custom-Header": "unique-value", "Cache-Control": "no-cache"}
return JSONResponse(
content=content,
headers=headers,
status_code=201 # Created
)
Returning Files and Streaming Responses
FastAPI makes it easy to return files and stream data:
FileResponse
from fastapi import FastAPI
from fastapi.responses import FileResponse
import os
app = FastAPI()
@app.get("/download-file")
def download_file():
file_path = "data/sample.pdf"
# Check if file exists
if not os.path.isfile(file_path):
return {"error": "File not found"}
# Return the file
return FileResponse(
path=file_path,
filename="report.pdf",
media_type="application/pdf"
)
StreamingResponse
For large files or long-running processes, you can use a streaming response:
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import io
app = FastAPI()
@app.get("/stream-csv")
def stream_csv():
def generate_csv():
# Create a file-like object in memory
csv_data = io.StringIO()
# Write headers
csv_data.write("id,name,value\n")
# Generate rows
for i in range(1, 1001):
csv_data.write(f"{i},item-{i},{i*10}\n")
# Return to beginning of "file"
csv_data.seek(0)
# Return content
return csv_data
return StreamingResponse(
generate_csv(),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=large_data.csv"}
)
Response Models with Type Annotations
One of FastAPI's powerful features is its use of type annotations to define response models:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
is_active: bool
bio: Optional[str] = None
@app.get("/users/{user_id}", response_model=User)
def get_user(user_id: int):
# In a real app, you would fetch this from a database
return {
"id": user_id,
"name": "John Doe",
"email": "[email protected]",
"is_active": True,
"bio": "Python developer and FastAPI enthusiast"
}
The response_model
parameter:
- Validates the output data
- Filters out unwanted fields
- Provides automatic documentation
- Enables automatic data serialization
Real-World Example: Building an API with Multiple Response Types
Let's create a more comprehensive example of an API that handles different types of responses:
from fastapi import FastAPI, HTTPException, status, Request
from fastapi.responses import (
JSONResponse,
HTMLResponse,
PlainTextResponse,
RedirectResponse,
FileResponse
)
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from typing import List, Optional
import os
app = FastAPI()
# Setup Jinja2 templates
templates = Jinja2Templates(directory="templates")
# Data models
class ErrorResponse(BaseModel):
message: str
details: Optional[str] = None
class Product(BaseModel):
id: int
name: str
price: float
in_stock: bool
# Custom exception handler
@app.exception_handler(HTTPException)
async def custom_http_exception_handler(request: Request, exc: HTTPException):
error = ErrorResponse(
message=exc.detail,
details=f"Status code: {exc.status_code}"
)
return JSONResponse(
status_code=exc.status_code,
content=error.dict()
)
# Routes with different response types
@app.get("/", response_class=HTMLResponse)
async def root():
return """
<html>
<head>
<title>FastAPI Custom Response Demo</title>
</head>
<body>
<h1>Welcome to FastAPI Custom Response Demo</h1>
<p>Check out the following endpoints:</p>
<ul>
<li><a href="/docs">/docs</a> - API documentation</li>
<li><a href="/api/products">/api/products</a> - JSON response example</li>
<li><a href="/view/products/1">/view/products/1</a> - HTML template response</li>
<li><a href="/download/report">/download/report</a> - File download</li>
<li><a href="/text/hello">/text/hello</a> - Plain text response</li>
<li><a href="/go-to-docs">/go-to-docs</a> - Redirect example</li>
</ul>
</body>
</html>
"""
# JSON response with data model
@app.get("/api/products", response_model=List[Product])
async def get_products():
return [
{"id": 1, "name": "Laptop", "price": 999.99, "in_stock": True},
{"id": 2, "name": "Smartphone", "price": 699.99, "in_stock": True},
{"id": 3, "name": "Headphones", "price": 149.99, "in_stock": False}
]
# HTML response with template
@app.get("/view/products/{product_id}", response_class=HTMLResponse)
async def view_product(request: Request, product_id: int):
products = {
1: {"id": 1, "name": "Laptop", "price": 999.99, "in_stock": True},
2: {"id": 2, "name": "Smartphone", "price": 699.99, "in_stock": True},
3: {"id": 3, "name": "Headphones", "price": 149.99, "in_stock": False}
}
if product_id not in products:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Product with ID {product_id} not found"
)
# In a real app, you'd return a template
# return templates.TemplateResponse(
# "product.html", {"request": request, "product": products[product_id]}
# )
# For this example, we'll just return some HTML
product = products[product_id]
return f"""
<html>
<head>
<title>Product: {product['name']}</title>
</head>
<body>
<h1>{product['name']}</h1>
<p>Price: ${product['price']}</p>
<p>Status: {"In Stock" if product['in_stock'] else "Out of Stock"}</p>
<a href="/">Back to home</a>
</body>
</html>
"""
# File download response
@app.get("/download/report")
async def download_report():
# In a real app, you might generate this file dynamically
report_path = os.path.join(os.path.dirname(__file__), "static", "sample_report.pdf")
# If the file doesn't exist for this example, handle the error
if not os.path.exists(report_path):
error_msg = "Report file not found. This is just an example endpoint."
return JSONResponse(
status_code=404,
content={"message": error_msg}
)
return FileResponse(
path=report_path,
filename="product_report.pdf",
media_type="application/pdf"
)
# Plain text response
@app.get("/text/{message}", response_class=PlainTextResponse)
async def get_text(message: str):
return f"You sent: {message}"
# Redirect response
@app.get("/go-to-docs", response_class=RedirectResponse, status_code=302)
async def redirect_to_docs():
return "/docs"
Advanced: Customizing Response with Dependency Injection
You can use FastAPI's dependency injection system to modify responses:
from fastapi import FastAPI, Depends, Response
app = FastAPI()
async def add_custom_header(response: Response):
response.headers["X-Custom-Header"] = "dependency-injected-value"
return response
@app.get("/with-dependency")
async def get_with_dependency(custom_header: Response = Depends(add_custom_header)):
return {"message": "This response has a header added by dependency injection"}
Summary
Custom responses in FastAPI give you fine-grained control over your API's HTTP responses while maintaining the framework's type safety and automatic documentation features. We've explored:
- Basic response types (JSON, HTML, plain text)
- Setting custom headers and status codes
- Returning files and streaming data
- Using response models for validation and documentation
- Practical examples of building APIs with different response types
- Advanced techniques like dependency injection for responses
With these tools, you can build robust APIs that handle various content types and provide appropriate responses for different scenarios.
Additional Resources
- FastAPI Official Documentation on Responses
- Starlette Response Documentation
- HTTP Status Codes Reference
Exercises
- Create an endpoint that returns a dynamically generated CSV file with 100 rows of sample data.
- Build an API that can return either JSON or XML based on an Accept header in the request.
- Create a file upload endpoint that responds with file metadata in JSON format.
- Implement a paginated API that returns a custom response with pagination metadata and links to next/previous pages.
- Build an endpoint that streams a large JSON array line by line to avoid loading everything into memory.
By mastering custom responses, you'll have the flexibility to build APIs that can handle any requirement while maintaining the simplicity and power of FastAPI.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)