Skip to main content

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:

python
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:

python
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:

python
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:

python
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

python
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:

python
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:

python
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:

python
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:

python
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

Exercises

  1. Create an endpoint that returns a dynamically generated CSV file with 100 rows of sample data.
  2. Build an API that can return either JSON or XML based on an Accept header in the request.
  3. Create a file upload endpoint that responds with file metadata in JSON format.
  4. Implement a paginated API that returns a custom response with pagination metadata and links to next/previous pages.
  5. 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! :)