python
44 lines · 6 steps
Handling multipart file uploads in FastAPI
A FastAPI endpoint that accepts a multipart form with a file, validates its type and size, then stores it under a random name.
Explained by
highlit
1import uuid
2from pathlib import Path
3
4from fastapi import APIRouter, File, Form, HTTPException, UploadFile
5from pydantic import EmailStr
6
7router = APIRouter()
8
9UPLOAD_DIR = Path("storage/uploads")
10ALLOWED_TYPES = {"image/jpeg", "image/png", "application/pdf"}
11MAX_BYTES = 5 * 1024 * 1024
12
13
14@router.post("/documents", status_code=201)
15async def upload_document(
16 title: str = Form(..., min_length=1, max_length=200),
17 owner_email: EmailStr = Form(...),
18 tags: list[str] = Form(default=[]),
19 is_public: bool = Form(False),
20 file: UploadFile = File(...),
21):
22 if file.content_type not in ALLOWED_TYPES:
23 raise HTTPException(415, f"Unsupported file type: {file.content_type}")
24
25 payload = await file.read()
26 if len(payload) > MAX_BYTES:
27 raise HTTPException(413, "File exceeds 5 MB limit")
28 if not payload:
29 raise HTTPException(422, "Uploaded file is empty")
30
31 suffix = Path(file.filename or "").suffix
32 stored_name = f"{uuid.uuid4().hex}{suffix}"
33 UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
34 (UPLOAD_DIR / stored_name).write_bytes(payload)
35
36 return {
37 "id": stored_name,
38 "title": title,
39 "owner_email": owner_email,
40 "tags": [t.strip() for t in tags if t.strip()],
41 "is_public": is_public,
42 "content_type": file.content_type,
43 "size": len(payload),
44 }
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Mixing Form and File parameters lets a single endpoint receive metadata and binary data in one multipart request.
- 2Validate content type and byte length before writing to disk so bad uploads never touch storage.
- 3Generating a random stored name from a UUID avoids collisions and keeps user-supplied filenames out of your filesystem.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/handling-multipart-file-uploads-in-fastapi-explained-python-6d66/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.