File Uploads and Static Files

Introduction

APIs accept multipart file uploads for avatars, documents, and imports. FastAPI uses UploadFile for streaming uploads and StaticFiles to serve assets. This chapter validates uploads, saves safely, and notes production patterns with Nginx or object storage.

Prerequisites

Single File Upload

Code explanation:

  • UploadFile streams data—use await file.read() in async routes
  • Sync route: file.file.read() without await

Test:

bash
curl -X POST http://127.0.0.1:8000/upload \
  -F "file=@photo.jpg"

Save to Disk

Code explanation:

  • Do not trust client filename for path—generate uuid name
  • Store original name in database if needed

Compare Flask uploads.

Warning

Validate Content, Not Just Extension

Malware can rename extensions—inspect magic bytes in production.

File + Form Fields

python
from fastapi import Form
 
@app.post("/upload/meta")
async def upload_with_meta(
    title: str = Form(...),
    file: UploadFile = File(...),
):
    return {"title": title, "filename": file.filename}

Requires python-multipart.

Multiple Files

python
from typing import Annotated
 
@app.post("/upload/multi")
async def upload_multiple(files: list[UploadFile] = File(...)):
    return [{"filename": f.filename, "size": f.size} for f in files]

Download With FileResponse

python
from fastapi.responses import FileResponse
 
@app.get("/download/{stored_name}")
def download(stored_name: str):
    path = UPLOAD_DIR / stored_name
    if not path.is_file():
        raise HTTPException(status_code=404, detail="Not found")
    return FileResponse(path, filename=stored_name, media_type="application/octet-stream")

Protect with Depends(get_current_user)—do not expose upload directory listing publicly.

Static Files Mount

python
from fastapi.staticfiles import StaticFiles
 
app.mount("/static", StaticFiles(directory="static"), name="static")

Template reference (if using Jinja2 optionally):

html
<link rel="stylesheet" href="/static/css/style.css">

Production: serve /static via Nginx instead of Python process.

Settings for Upload Limits

Starlette/FastAPI body size—configure at server or reverse proxy:

python
# Concept: limit in middleware or nginx client_max_body_size

Nginx:

nginx
client_max_body_size 10M;

Match application MAX_BYTES check.

Object Storage (Concept)

text
Client → FastAPI validates → S3 API put_object → DB stores object key
Client ← presigned URL ← FastAPI

Same validation rules; offload bytes to S3/OSS/MinIO.

Async Write Pattern

For large files, stream to disk:

python
import shutil
 
@app.post("/upload/stream")
async def stream_upload(file: UploadFile = File(...)):
    dest = UPLOAD_DIR / f"{uuid.uuid4().hex}.bin"
    with dest.open("wb") as buffer:
        shutil.copyfileobj(file.file, buffer)
    return {"saved": dest.name}

FAQ

422 on upload?

Missing multipart/form-data or wrong field name file.

UploadFile sync route?

Use def route and read file.file synchronously.

Memory spike on read()?

Stream in chunks for large files instead of read() entire body.

Authenticated uploads?

Add Depends(get_current_user) to route.

Virus scan?

Queue scan job after save—background tasks chapter.

Web static vs API same app?

Mount StaticFiles on subpath; API under /api/v1.