Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 50

Serving Non-JSON Responses

Recommended resources

Hello! Welcome to the final lesson in our "Advanced Features and Concurrency" module.

In our previous session, we explored how to build real-time, conversational applications using WebSockets. We saw how FastAPI's native support for asynchronous protocols makes it straightforward to create endpoints that maintain persistent connections, a sharp contrast to the typical request-response model you're used to from Django/DRF.

Today, we'll continue to move beyond the default JSON responses. This lesson addresses the learning outcome: Return non-JSON responses, such as FileResponse for file downloads or StreamingResponse for large data. You'll learn how to build endpoints that can serve images, generate downloadable reports, or stream massive datasets efficiently.

For someone with your Django background, the concept of serving files will be familiar—Django provides FileResponse and StreamingHttpResponse for these exact purposes. Today, you'll see how to achieve the same results in FastAPI, leveraging its async capabilities for enhanced performance.

Serving Files on Disk with FileResponse

The most direct way to offer a file for download is when the file already exists on your server's disk. For this, FastAPI provides the FileResponse class. It's efficient because it streams the file from the disk to the client without loading the entire file into memory first.

Let's watch a short video that walks through the fundamental usage of FileResponse.

How to Return Files in FastAPI

This clip from Pretty Printed will show you how to import and use FileResponse to serve a simple image file.

Watch from 0:53 to 1:30. Notice how you simply need to import FileResponse and return an instance of it with the path to the file.

As you saw, the basic implementation is simple:

from fastapi import FastAPI
from fastapi.responses import FileResponse

app = FastAPI()

@app.get("/cat-image")
async def get_cat_image():
    return FileResponse("path/to/your/cat.jpg")

However, there are a few important considerations for production-ready code.

Handling Missing Files

What happens if the file at the specified path doesn't exist? The application will raise an unhandled server error. To prevent this, you should always check if the file exists before attempting to serve it.

How to Return Files in FastAPI

Let's continue with the video to see how to properly handle cases where the file is not found.

Watch from 1:30 to 3:39. The key takeaway is to use a check like os.path.exists() before returning the FileResponse.

By performing this check, you can return a clean 404 Not Found error instead of a generic 500 Internal Server Error, which is much better practice.

Forcing a Download vs. Displaying Inline

When you return a FileResponse, the browser has to decide what to do with it. If it's an image, PDF, or text file, the default behavior is often to display it directly in the browser window (an "inline" display). If you want to force the browser to prompt the user to save the file, you need to set the Content-Disposition header to attachment.

FileResponse makes this easy with the filename parameter.

How to Return Files in FastAPI

This final clip demonstrates how to use the filename parameter to trigger a download prompt.

Watch from 6:46 to 7:56. Pay close attention to the effect of adding filename="mycat.jpg" to the FileResponse constructor.

Here’s a summary of the behavior:

  • return FileResponse("path/to/file.jpg"): Browser will likely display the image. The response header will be Content-Disposition: inline.
  • return FileResponse("path/to/file.jpg", filename="cute_cat.jpg"): Browser will prompt the user to download a file named cute_cat.jpg. The response header will be Content-Disposition: attachment; filename="cute_cat.jpg".

You can also explicitly set the media_type (also known as MIME type, e.g., image/jpeg, application/pdf) in the FileResponse to help the browser, though FastAPI is quite good at inferring it from the file extension.


Streaming Large or Generated Data with StreamingResponse

FileResponse is perfect for existing files, but what if you need to serve a file that's too large to handle at once, or one that you need to generate on the fly?

This is a common scenario in backend development. For example:

  • Exporting a large CSV report from millions of database records.
  • Generating a ZIP archive of multiple files.
  • Streaming a video file.

Loading the entire content into memory before sending the response is inefficient and can easily crash your server. The solution is StreamingResponse. It takes an iterator (or an async iterator) and sends the data to the client piece by piece, keeping memory usage low and constant.

This is the direct FastAPI equivalent of Django's StreamingHttpResponse.

Streaming a Large File from Disk

Let's start by using StreamingResponse to do what FileResponse does, but more explicitly, to understand the mechanism. We'll create a generator function that reads a large file in chunks.

Custom Response - HTML, Stream, File, others

The official FastAPI documentation has a clear, concise example of this. This will show you the core pattern for streaming from a file.

Please read the section titled 'Using StreamingResponse with file-like objects'. Focus on the iterfile generator function. Notice how it uses a with open(...) block and yield from to read the file chunk by chunk.

The key pattern is:

  1. Define a generator function that yields data in chunks.
  2. Pass an instance of this generator to StreamingResponse.
  3. Because standard file operations (open, read) are blocking, the path operation function should be defined with def, not async def. FastAPI is smart enough to run this synchronous code in a thread pool to avoid blocking the event loop.

If you need maximum concurrency, you can use a library like aiofiles to perform asynchronous file reads. The article from OneUptime has an excellent example of this.

How to Implement File Downloads in FastAPI - OneUptime

This article demonstrates the async version of file streaming, which is ideal for high-concurrency applications.

Read the code block under the 'Async File Streaming' section. Compare the async_file_iterator with the synchronous iterfile you saw before. Note the use of async with aiofiles.open(...) and await file.read(...).

Generating a CSV Report On-the-Fly

The true power of StreamingResponse is realized when generating content dynamically. Imagine exporting user data from your database to a CSV file.

How to Implement File Downloads in FastAPI - OneUptime

Let's look at a practical example of generating a CSV export. This is a very common task for a backend developer.

Read the code block under the 'CSV Export' section. Trace the logic within the generate_csv_rows async generator. It first yields the header, then enters a loop to fetch data in batches and yields each batch. This ensures the client starts receiving data almost instantly.

This pattern is incredibly powerful:

  • Low Time to First Byte (TTFB): The client receives the CSV header immediately, even before the first database query is complete.
  • Constant Memory Usage: The server's memory usage is only proportional to the batch size, not the entire dataset.
  • Scalability: You can export massive datasets that would be impossible to handle in memory.

FileResponse vs. StreamingResponse: A Quick Comparison

Here's a summary to help you decide which to use.

Approach Memory Usage Best For Key Characteristics
FileResponse Low (streams from disk) Serving static files that already exist on disk. Simple to use. Automatically sets headers like Content-Length, ETag, and Content-Type.
StreamingResponse Constant (chunked) Streaming very large files or dynamically generated content (e.g., reports, archives). Highly flexible. Requires a generator. You are responsible for setting headers like Content-Disposition and Content-Length manually.

This table is adapted from the "Comparison of Approaches" in the OneUptime article.


Conclusion

In this lesson, you've learned how to make your FastAPI applications serve more than just JSON. By mastering FileResponse and StreamingResponse, you can build APIs that handle file downloads, large data exports, and other complex response types with efficiency and control.

Key Takeaways:

  • FileResponse is for serving existing static files. It's simple and automatically handles many details. Use the filename parameter to force a download.
  • StreamingResponse is for large or dynamically generated data. It uses a generator to send data in chunks, keeping memory usage low and constant.
  • Control Browser Behavior with Headers: The Content-Disposition header (attachment vs. inline) is crucial for controlling whether a file is downloaded or displayed.
  • Async vs. Sync Matters: Use def for path operations with blocking I/O (like standard open()), and async def for non-blocking I/O (like with aiofiles).

Preview of the Next Lesson:

This lesson concludes the "Advanced Features and Concurrency" module. You've built a solid foundation in creating sophisticated FastAPI services. Now, how do you ensure they are reliable and correct?

The next module, "Testing and Production Deployment," will teach you just that. We'll kick it off by learning how to write synchronous and asynchronous tests for your API endpoints using TestClient and httpx.AsyncClient.