Request and Response Handling
Explore how FastAPI handles requests and responses, including data validation and serialization.
Content
Form Data and Files
Versions:
Watch & Learn
AI-discovered learning video
Sign in to watch the learning video for this topic.
Form Data and Files — Because APIs Also Need to Accept Your Awful Selfie
"You can't send a file with JSON. Stop trying. Use multipart/form-data like a responsible developer." — Probably your future self
You're already comfortable creating routes and endpoints (remember that smooth Routing and Endpoints session?). You've validated JSON payloads with Pydantic and returned clean Response Models. Now it's time for the other half of real-world HTTP: form data and file uploads. This is where FastAPI stops pretending everything is JSON and starts dealing with the messy, human world of forms, photos, and PDFs named resume_final_v3_reallyfinal.pdf.
Why this matters (and when you'll cry without it)
- HTML forms submit as application/x-www-form-urlencoded or multipart/form-data (the latter when files are involved). Browsers and many clients use these; if your endpoint expects JSON, browsers will angrily give you 415 Unsupported Media Type.
- Uploading images, CSVs, or letting users fill a quick form is extremely common. You need to accept fields and files reliably and safely.
This topic builds on: Data Validation with Pydantic (useful for validating form fields) and Response Models (you'll still want to return structured data). But forms and files behave a bit differently, so read on.
The basics: Form() and File() — the dynamic duo
FastAPI provides two special dependency helpers: Form (for form fields) and File together with UploadFile (for files). Use them in your endpoint signatures like you'd use Query() or Body().
Simple form fields
from fastapi import FastAPI, Form
app = FastAPI()
@app.post('/login')
async def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}
- This expects either x-www-form-urlencoded or multipart/form-data (no file needed).
- Note: You can't pass a Pydantic model here directly from a form body — more on that below.
Single file upload
from fastapi import File, UploadFile
@app.post('/upload-file')
async def upload_file(file: UploadFile = File(...)):
contents = await file.read() # bytes — careful with big files
return {"filename": file.filename, "content_type": file.content_type}
- Use UploadFile for async-friendly file handling. It exposes
.filename,.content_type, and an async.read(). - For small files,
.read()is fine. For large files, stream to disk.
Multiple files
from typing import List
@app.post('/upload-multiple')
async def multi_upload(files: List[UploadFile] = File(...)):
return {"filenames": [f.filename for f in files]}
Combining form fields with files (the classic resume upload)
@app.post('/submit')
async def submit(name: str = Form(...), age: int = Form(...), resume: UploadFile = File(...)):
# validate age with Pydantic later or here
# store resume
return {"name": name, "age": age, "resume_filename": resume.filename}
Client must send multipart/form-data for this to work.
Pydantic + Forms: a little glue job
Pydantic models don't automatically read from form fields. But you can convert form fields into a Pydantic model via a dependency function.
from pydantic import BaseModel
from fastapi import Depends
class User(BaseModel):
name: str
age: int
async def user_form(name: str = Form(...), age: int = Form(...)) -> User:
return User(name=name, age=age)
@app.post('/users/')
async def create_user(user: User = Depends(user_form)):
return user
This keeps validation benefits of Pydantic while accepting form-encoded data.
Saving files safely + streaming large uploads
- Don't trust client filenames. A file named "../../../etc/passwd" is not a cute joke. Sanitize or generate server-side names.
- For big files, avoid reading entire content into memory.
Example streaming to disk (synchronous copy):
import shutil
from pathlib import Path
UPLOAD_DIR = Path('/tmp/uploads')
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
@app.post('/upload-stream')
async def upload_stream(file: UploadFile = File(...)):
dest = UPLOAD_DIR / file.filename # sanitize in production
with dest.open('wb') as buffer:
shutil.copyfileobj(file.file, buffer)
return {"filename": file.filename}
Note: file.file is a real file-like object which may be a SpooledTemporaryFile — good for streaming.
Security checklist (because you will be hacked if you don't)
- Validate file types (check content_type AND inspect bytes/signatures if critical).
- Enforce size limits (proxy or webserver-level, and also check file size in app if needed).
- Sanitize filenames or generate UUIDs for storage.
- Store uploads outside web root or serve them using secure mechanisms (like presigned URLs).
Responses: returning files to clients
You can return files using FileResponse or StreamingResponse.
from fastapi.responses import FileResponse
@app.get('/download/{filename}')
def download(filename: str):
path = UPLOAD_DIR / filename
return FileResponse(path, media_type='application/octet-stream', filename=filename)
You can't use response_model for raw file responses — response_model is for structured JSON-like responses.
Quick testing cheatsheet
- curl upload file:
curl -F "file=@/path/to/file.jpg" http://localhost:8000/upload-file - curl form + file:
curl -F "name=Alice" -F "age=30" -F "resume=@resume.pdf" http://localhost:8000/submit - Simple HTML form to test in-browser:
<form action="/submit" method="post" enctype="multipart/form-data">
<input name="name">
<input name="age" type="number">
<input type="file" name="resume">
<button type="submit">Send</button>
</form>
Common pitfalls & FAQs
- Q: "Why is my Pydantic model not populating from Form?"
- A: Forms are not JSON. Use a dependency that accepts Form() fields and returns a Pydantic model.
- Q: "Should I use bytes or UploadFile?"
- A: Use UploadFile for non-trivial file handling (async-friendly, streams). Use bytes (.read()) for tiny quick stuff.
- Q: "Can I validate file size/type with Pydantic?"
- A: Not directly. Validate manually in the endpoint or wrapper dependency.
TL;DR — Cheat-sheet Takeaways
- Use Form(...) for regular form fields; File(...)/UploadFile for files.
- Multipart/form-data is required when files are present; otherwise x-www-form-urlencoded is used for simple forms.
- For Pydantic validation, wrap form fields in a dependency that constructs a model.
- Stream to disk for large files; don't .read() megabytes into memory.
- Secure filenames, validate types, and set size limits.
Final thought: FastAPI treats forms and files like first-class citizens — but you still have to be a responsible human. Treat files like wild animals: don't let them roam uncontrolled, and definitely don't name them resume_final_v3.pdf.
Build on your routing knowledge by adding endpoints that take forms and files, then wire Pydantic validation via dependencies. You're now ready to accept that user's headshot, tax form, and heartfelt message — all in one multipart/form-data bouquet.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!