Building on the task manager, this post covers image uploads and reliable static asset delivery. Static files are CSS/JS bundles that do not change after deployment; media files are user uploads that grow over time. UploadFile lets FastAPI stream big files without loading them entirely into memory. We will glance at the core ideas and move straight into practice.
Key vocabulary
- UploadFile: FastAPI’s efficient wrapper for multipart/form-data uploads that streams chunks instead of keeping whole files in memory.
- StaticFiles: A helper that mounts static or media directories (for example
/static,/media) directly into the FastAPI app. - Static assets: Versioned CSS, JS, and fonts you ship with each deployment. They emphasize caching and revision control.
- Media: User-generated uploads that keep growing. They require separate storage paths and access rules.
- Chunk: A slice of a large file processed sequentially so uploads do not exhaust RAM.
Practice card
- Estimated time: 45 minutes
- Prereqs: Project from Part 12, Pillow optional
- Goal: Accept files with UploadFile and split media/static paths
Define the upload form
File and UploadFile let the ASGI server handle multipart requests efficiently.
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/photos")
async def create_photo(file: UploadFile = File(...)):
contents = await file.read()
return {"filename": file.filename, "size": len(contents)}
File(...) marks the field as required. Under the hood UploadFile relies on SpooledTemporaryFile, so even large uploads avoid overusing memory. This structure also makes it easy to forward files to other services without buffering the entire payload.
Choose a storage path
In production you move uploads to a designated folder or cloud storage. A local example:
from pathlib import Path
MEDIA_ROOT = Path("media")
MEDIA_ROOT.mkdir(exist_ok=True)
@app.post("/photos", status_code=201)
async def create_photo(file: UploadFile = File("image/*")):
destination = MEDIA_ROOT / file.filename
with destination.open("wb") as buffer:
while chunk := await file.read(1024 * 1024):
buffer.write(chunk)
return {"url": f"/media/{file.filename}"}
chunk refers to the repeated 1 MB slices we read and write. Streaming in slices keeps memory low and translates easily to cloud storage uploads.
Separate static and media
- Static: version-controlled JS, CSS, fonts, etc.
- Media: user uploads that grow dynamically.
Mount each path with StaticFiles:
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="app/static"), name="static")
app.mount("/media", StaticFiles(directory="media"), name="media")
Static files go through your build pipeline, while media files often live on object storage (S3, etc.) or a CDN. This split protects the API server from heavy file transfer work.
Validate upload metadata
Guard against malicious files by checking extension, MIME type, and size:
MAX_SIZE = 5 * 1024 * 1024
async def validate_upload(file: UploadFile):
if not file.content_type.startswith("image/"):
raise HTTPException(status_code=400, detail="Images only.")
size = 0
while chunk := await file.read(1024 * 512):
size += len(chunk)
if size > MAX_SIZE:
raise HTTPException(status_code=413, detail="Max 5MB upload.")
await file.seek(0)
seek(0) rewinds the pointer so you can read the file again during storage. Perform validation first, then persist.
Quick thumbnail API
After uploading an image you can create a thumbnail with Pillow. Confirm MEDIA_ROOT exists and avoid filename collisions by adding UUIDs. Also sanitize input so paths like ../../secret.txt cannot appear.
from PIL import Image
@app.post("/photos/thumbnail")
async def create_thumbnail(file: UploadFile = File("image/*")):
with Image.open(file.file) as img:
img.thumbnail((300, 300))
thumb_path = MEDIA_ROOT / f"thumb_{file.filename}"
img.save(thumb_path)
return {"thumbnail": f"/media/{thumb_path.name}"}
file.file exposes the actual file object, so libraries like Pillow can operate without extra copies.
Visualizing the flow
Even if storage later lives in another container or cloud service, the same sequence applies. Once you grasp the data flow you can swap the storage backend confidently.
uvicorn tips
When testing uploads, run something like uvicorn app.main:app --reload --limit-concurrency 10 to prevent resource exhaustion locally. File handling and static/media separation are frequent pain points in production, so practice the structure now. Later, plugging in cloud storage or signed URLs becomes straightforward.
Practice
- Follow along: build the
/photosendpoint plus the/mediamount so uploads save and return a URL. - Extend: add the validation helper and thumbnail API.
- Debug: switch to UUID filenames, block path traversal attempts, and inspect logs when collisions occur.
- Done when: upload → store → return URL works in one flow and changing the limits yields the expected errors.
Wrap-up
Once you master UploadFile and StaticFiles you can bolt on images or attachments anytime. Real services still combine these concepts with CDNs and storage gateways, so keep each responsibility separate while you practice.
💬 댓글
이 글에 대한 의견을 남겨주세요