KAHIBARO
Discord Login Register

8.9 File Uploads

Why File Uploads Matter in Backends

Uploading files is a very common requirement in backend applications. You might need to:

In a FastAPI backend, you need to handle file uploads in a way that is:

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:

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

ApproachType in functionMemory usageFeatures
bytes + Filefile: bytesKeeps entire file in memorySimple, not good for large files
UploadFilefile: UploadFileStreams to a temporary fileBetter for large files, file-like API

You will almost always want to use UploadFile in real applications.

To use them, you import from fastapi:

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

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

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 minimal example:

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

PropertyDescription
filenameOriginal filename sent by the client
content_typeMIME type, for example image/png
fileUnderlying file-like object (SpooledTemporaryFile)

Common methods:


MethodDescription
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.

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

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

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

For extra safety, you often want to generate your own filename, for example a UUID:

python
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

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

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

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

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

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

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

bash
curl -X POST "http://localhost:8000/upload-file/" \
  -F "file=@/path/to/myfile.txt"

For multiple files:

bash
curl -X POST "http://localhost:8000/upload-multiple/" \
  -F "files=@file1.txt" \
  -F "files=@file2.txt"

With additional form data:

bash
curl -X POST "http://localhost:8000/upload-with-data/" \
  -F "description=example files" \
  -F "files=@file1.txt" \
  -F "files=@file2.txt"

Using Python `requests`

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

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

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

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

Comments

Please login to add a comment.

Don't have an account? Register now!