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
typescript
import { Controller } from '@nestjs/common'; import { MessagePattern, Payload,
Manual RabbitMQ acks in a NestJS controller
microservices
message-queue
acknowledgement
Intermediate
8 steps
rust
use std::convert::TryFrom; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HttpStatus {
Converting HTTP codes with TryFrom in Rust
enums
error-handling
trait-implementation
Intermediate
8 steps
python
import random import click from faker import Faker
Building a Flask seed command with Click
cli
database seeding
orm
Intermediate
7 steps
python
import smtplib from email.message import EmailMessage from threading import Thread
Sending welcome emails off the request thread in Flask
background-threads
app-context
email
Intermediate
8 steps
python
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.shortcuts import get_object_or_404 from django.views.generic import DetailView
Team membership access control in Django
access-control
mixins
class-based-views
Intermediate
7 steps
rust
use chrono::NaiveDate; use serde::{Deserialize, Deserializer}; #[derive(Debug, Deserialize)]
Custom date parsing with serde in Rust
serde
deserialization
csv-parsing
Intermediate
7 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.