Backend Development 8 min read

Master FastAPI Responses: JSON, HTML, Files, Streaming & Custom Classes

Learn how FastAPI handles various response types—including default JSON, custom status codes and headers, HTMLResponse, FileResponse, StreamingResponse, and user‑defined response classes—while covering best practices, common pitfalls, and code examples for building robust APIs.

Code Mala Tang
Code Mala Tang
Code Mala Tang
Master FastAPI Responses: JSON, HTML, Files, Streaming & Custom Classes

When building APIs with FastAPI, how the application responds to clients is a crucial aspect. Whether returning JSON data, HTML, or file downloads, FastAPI offers a concise and efficient way to customize responses.

Default behavior of FastAPI responses

Customizing response status codes and headers

Using Response and JSONResponse

Returning HTML and file responses

Streaming responses and custom response classes

🚀 Default Response Behavior

By default, FastAPI uses the JSONResponse class to automatically convert returned data to JSON.

<code>from fastapi import FastAPI
app = FastAPI()

@app.get("/hello")
def read_hello():
    return {"message": "Hello, World!"}
</code>

This endpoint returns:

<code>{
  "message": "Hello, World!"
}
</code>

✅ Custom Status Code and Headers

You can change the response status code using the status_code parameter.

<code>from fastapi import status
@app.post("/items", status_code=status.HTTP_201_CREATED)
def create_item(item: dict):
    return {"item": item}
</code>

For custom headers, use the Response object:

<code>from fastapi import Response
@app.get("/custom-header")
def custom_header(response: Response):
    response.headers["X-Custom-Header"] = "FastAPI"
    return {"msg": "Check the headers!"}
</code>

🧾 Using Response and JSONResponse

Sometimes you need finer control over how the response is sent:

<code>from fastapi.responses import JSONResponse
@app.get("/custom-response")
def custom_response():
    data = {"message": "Manually crafted"}
    return JSONResponse(content=data, status_code=200)
</code>

🌐 Return HTML

FastAPI can return HTML using HTMLResponse :

<code>from fastapi.responses import HTMLResponse
@app.get("/html", response_class=HTMLResponse)
def get_html():
    return """
    <html>
        <body>
            <h1>Hello from FastAPI</h1>
        </body>
    </html>
    """
</code>

📦 File Response

Use FileResponse to send files such as images, documents, or downloads:

<code>from fastapi.responses import FileResponse
@app.get("/download")
def download_file():
    return FileResponse("files/report.pdf", filename="report.pdf")
</code>

📡 Streaming Response

For large files or chunked data transfer, use StreamingResponse :

<code>from fastapi.responses import StreamingResponse
def generate_data():
    for i in range(10):
        yield f"Chunk {i}\n"

@app.get("/stream")
def stream_data():
    return StreamingResponse(generate_data(), media_type="text/plain")
</code>

🎯 Custom Response Class

You can create your own response class by inheriting Response :

<code>from fastapi.responses import Response
class CustomTextResponse(Response):
    media_type = "text/plain"

@app.get("/custom-text", response_class=CustomTextResponse)
def custom_text():
    return "Plain text response"
</code>

Key Considerations

1. Conflict between response content and response_model

If you use response_model together with a custom response class like JSONResponse , FastAPI will skip Pydantic validation.

<code>@app.get("/users", response_model=UserOut)
def get_user():
    return JSONResponse(content={"name": "Tom", "age": 30})  # No validation
</code>

Recommendation:

Return a plain Python object (dict or Pydantic model) when you need validation.

Use JSONResponse for scenarios where validation is not required, such as error handling.

2. File download path issues

When using FileResponse , ensure the file path exists, permissions are correct, and consider transfer efficiency for large files.

<code>return FileResponse("files/not_exist.pdf")  # May raise an exception if path is missing
</code>

Recommendation:

Check existence with os.path.exists or handle exceptions.

Avoid hard‑coded paths; use Pathlib or a configuration file.

3. StreamingResponse performance and memory

Streaming saves memory but generator exceptions can break the request.

<code>def generate():
    for i in range(100):
        if i == 50:
            raise Exception("error")
        yield str(i)
</code>

Recommendation: wrap generator logic in try...except and use streaming for large files, slow downloads, or real‑time data.

4. Media type mismatch in custom response classes

Always set the correct media_type to ensure clients parse the content properly.

<code>class MyCustomResponse(Response):
    media_type = "application/octet-stream"  # or "text/plain", "application/json", etc.
</code>

Recommendation: explicitly specify the MIME type to avoid incorrect rendering.

5. Header overriding

When setting headers via Response , do not return a new response object that overwrites them.

<code>@app.get("/wrong-header")
def wrong(response: Response):
    response.headers["X-Foo"] = "Bar"
    return JSONResponse(content={"msg": "hi"})  # Headers get overwritten
</code>

Correct approach:

<code>@app.get("/correct-header")
def correct(response: Response):
    response.headers["X-Foo"] = "Bar"
    return {"msg": "hi"}  # Uses default JSONResponse with headers intact
</code>

🧪 Summary

JSON (default) : JSONResponse

HTML : HTMLResponse

File : FileResponse

Streaming : StreamingResponse

Custom text : inherit from Response

This flexibility enables developers to create APIs for web applications, RESTful services, file servers, and more.

backendPythonStreamingWeb DevelopmentFastAPIfile handlingAPI responses
Code Mala Tang
Written by

Code Mala Tang

Read source code together, write articles together, and enjoy spicy hot pot together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.