8.9 File Uploads
Table of Contents
Why File Uploads Matter in Backends
Uploading files is a very common requirement in backend applications. You might need to:
- Let users upload profile pictures.
- Accept PDFs or Word documents.
- Receive CSV files to import data.
- Store logs or reports generated by the system.
In a FastAPI backend, you need to handle file uploads in a way that is:
- Simple for clients to call.
- Safe and memory efficient.
- Easy to integrate with storage (local disk, cloud storage, etc.).
This chapter focuses only on FastAPI specific handling of file uploads. General security issues, object storage, presigned URLs, and similar topics are handled in other chapters.
How File Uploads Work in HTTP
When a browser uploads a file in a form, it usually sends a multipart/form-data request.
A simple HTML form:
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<button type="submit">Upload</button>
</form>
The important part is enctype="multipart/form-data". Without it, the file will not be sent correctly.
FastAPI is built on Starlette, which knows how to parse multipart/form-data and expose files to your endpoint.
FastAPI Tools: `File` and `UploadFile`
FastAPI provides two main ways to receive files:
| Approach | Type in function | Memory usage | Features |
|---|---|---|---|
bytes + File | file: bytes | Keeps entire file in memory | Simple, not good for large files |
UploadFile | file: UploadFile | Streams to a temporary file | Better for large files, file-like API |
You will almost always want to use UploadFile in real applications.
To use them, you import from fastapi:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()Accepting a Single File as Bytes
This is the simplest approach. FastAPI reads the entire uploaded file into memory as bytes.
from fastapi import FastAPI, File
app = FastAPI()
@app.post("/upload-bytes/")
async def upload_bytes(file: bytes = File(...)):
size = len(file)
return {"file_size": size}Explanation:
File(...)marks this parameter as a file field from the request body.file: bytesmeans: give me the content of the uploaded file as raw bytes.- The whole file must fit into memory.
This is fine for small files like avatars or icons, but risky for large files.
Rule: Do not use bytes for large or unbounded file uploads. It can easily exhaust server memory.
Using `UploadFile` for Better Performance
UploadFile is a special type provided by Starlette and used by FastAPI. It offers:
- A file-like interface (
read,write,seek,close). - Automatic handling with a temporary file on disk, not entirely in memory.
- Access to metadata like filename and content type.
A minimal example:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/upload-file/")
async def upload_file(file: UploadFile = File(...)):
return {
"filename": file.filename,
"content_type": file.content_type,
}
Properties of UploadFile:
| Property | Description |
|---|---|
filename | Original filename sent by the client |
content_type | MIME type, for example image/png |
file | Underlying file-like object (SpooledTemporaryFile) |
Common methods:
| Method | Description |
|---|---|
await file.read(size) | Read up to size bytes, or all if omitted |
await file.write(data) | Write bytes to file (rarely used in uploads) |
await file.seek(offset) | Move reading position |
await file.close() | Close and free resources |
Handling Metadata: Filenames and Content Types
You often need to know the filename and content type.
@app.post("/upload-info/")
async def upload_info(file: UploadFile = File(...)):
# This is the name the browser reports, not necessarily safe
original_name = file.filename
mime_type = file.content_type
return {
"original_filename": original_name,
"mime_type": mime_type,
}
You can use content_type to do simple checks, for example only allow images:
from fastapi import HTTPException, status
@app.post("/upload-image/")
async def upload_image(file: UploadFile = File(...)):
if not file.content_type.startswith("image/"):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only image uploads are allowed",
)
return {"filename": file.filename}
Important: filename and content_type come from the client and can be forged. Never rely on them alone for security checks or storage paths.
Saving Uploaded Files to Disk
In many backends you want to save the uploaded file to a directory.
Here is a straightforward implementation:
import os
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)
@app.post("/upload-save/")
async def upload_and_save(file: UploadFile = File(...)):
# Create a safe file path. In real apps, do not trust file.filename directly.
file_path = os.path.join(UPLOAD_DIR, file.filename)
# Read and save in chunks to avoid loading everything into memory
with open(file_path, "wb") as out_file:
while chunk := await file.read(1024 * 1024): # 1 MB chunks
out_file.write(chunk)
return {"stored_as": file_path}Notes:
os.makedirs(UPLOAD_DIR, exist_ok=True)ensures the directory exists.- We read and write in chunks of 1 MB.
- This keeps memory usage low for large files.
For extra safety, you often want to generate your own filename, for example a UUID:
import uuid
@app.post("/upload-save-safe/")
async def upload_and_save_safe(file: UploadFile = File(...)):
ext = os.path.splitext(file.filename)[1]
new_name = f"{uuid.uuid4().hex}{ext}"
file_path = os.path.join(UPLOAD_DIR, new_name)
with open(file_path, "wb") as out_file:
while chunk := await file.read(1024 * 1024):
out_file.write(chunk)
return {"stored_as": new_name}Receiving Multiple Files
FastAPI can also handle multiple files in one request. Clients send them as multiple entries with the same field name.
Multiple `UploadFile` objects
from typing import List
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/upload-multiple/")
async def upload_multiple(files: List[UploadFile] = File(...)):
return {
"count": len(files),
"filenames": [f.filename for f in files],
}
This expects form fields like files=file1, files=file2, etc.
You can also mix other fields with files:
from fastapi import Form
@app.post("/upload-with-data/")
async def upload_with_data(
description: str = Form(...),
files: List[UploadFile] = File(...),
):
return {
"description": description,
"files": [f.filename for f in files],
}Multiple files as bytes
You can also receive multiple small files as bytes:
@app.post("/upload-multiple-bytes/")
async def upload_multiple_bytes(files: List[bytes] = File(...)):
sizes = [len(content) for content in files]
return {"sizes": sizes}Again, this is only practical for small files.
Limiting File Size and Validating Uploads
FastAPI does not automatically limit file size. You must handle it yourself or configure your server.
A simple approach is to read only up to a maximum allowed size and reject larger uploads:
from fastapi import HTTPException, status
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
@app.post("/upload-with-limit/")
async def upload_with_limit(file: UploadFile = File(...)):
size = 0
while chunk := await file.read(1024 * 1024):
size += len(chunk)
if size > MAX_FILE_SIZE:
await file.close()
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail="File too large",
)
return {"file_size": size}You can combine size checking with content type validation:
ALLOWED_TYPES = {"image/jpeg", "image/png"}
@app.post("/upload-image-strict/")
async def upload_image_strict(file: UploadFile = File(...)):
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unsupported file type",
)
size = 0
while chunk := await file.read(1024 * 1024):
size += len(chunk)
if size > MAX_FILE_SIZE:
await file.close()
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail="File too large",
)
return {
"filename": file.filename,
"content_type": file.content_type,
"size": size,
}Practical rule: Always define a maximum allowed file size and at least basic type checks for file uploads.
Files Together With JSON or Form Fields
Browsers cannot send JSON and files in a single multipart/form-data request in a transparent way, but they can send form fields and files together.
In FastAPI you model this with Form and File parameters.
Example: upload a profile picture with a username:
from fastapi import Form
@app.post("/profile/")
async def upload_profile(
username: str = Form(...),
avatar: UploadFile = File(...),
):
# Process username
# Save avatar to disk or object storage
return {
"username": username,
"avatar_filename": avatar.filename,
}For more complex data you can send JSON as a string form field and parse it in the endpoint, but that is more advanced and is usually covered when you learn about file handling and object storage.
Testing File Uploads With `curl` and Python
Using `curl`
For a single file:
curl -X POST "http://localhost:8000/upload-file/" \
-F "file=@/path/to/myfile.txt"For multiple files:
curl -X POST "http://localhost:8000/upload-multiple/" \
-F "files=@file1.txt" \
-F "files=@file2.txt"With additional form data:
curl -X POST "http://localhost:8000/upload-with-data/" \
-F "description=example files" \
-F "files=@file1.txt" \
-F "files=@file2.txt"Using Python `requests`
import requests
url = "http://localhost:8000/upload-file/"
files = {"file": open("myfile.txt", "rb")}
response = requests.post(url, files=files)
print(response.json())Multiple files:
url = "http://localhost:8000/upload-multiple/"
files = [
("files", ("file1.txt", open("file1.txt", "rb"), "text/plain")),
("files", ("file2.txt", open("file2.txt", "rb"), "text/plain")),
]
response = requests.post(url, files=files)
print(response.json())Common Pitfalls and Good Practices
Avoid loading huge files into memory
Use UploadFile and chunked reading instead of bytes for anything that might be large.
Do not trust filenames
Never use file.filename directly to build paths like "/uploads/" + file.filename without sanitizing. Prefer generating your own safe names.
Always close files
FastAPI will usually clean up temporary files, but if you manually work with them, you can explicitly close:
await file.close()Use async-friendly patterns
FastAPI endpoints are async. When reading from UploadFile, use await file.read(). When writing to standard local files with open(...) you will usually still be fine, but for very high load systems you will later learn about async file operations.
Summary
In FastAPI, file uploads are handled primarily through File and UploadFile:
- Use
file: bytes = File(...)for very small files when you just need raw bytes. - Prefer
file: UploadFile = File(...)for real applications, it is more efficient and flexible. - You can get metadata like
filenameandcontent_type. - Use chunked reading to save files and to enforce size limits.
- Accept multiple files with
List[UploadFile]. - Combine files with other fields using
Form.
These patterns give you everything you need to receive files into your FastAPI backend and then plug them into storage solutions that you will explore in other chapters.
Views: 12
KAHIBARO