FastAPI Form Data and File Uploads

Not all data arrives as JSON. HTML forms and file upload inputs send data in a different format called multipart or URL-encoded form data. FastAPI handles both with dedicated tools from the fastapi package.

JSON vs Form Data — The Key Difference

JSON request (typical API call):
  Content-Type: application/json
  Body: {"username": "priya", "password": "secret"}

Form request (HTML form submit):
  Content-Type: application/x-www-form-urlencoded
  Body: username=priya&password=secret

Browsers submit HTML forms as form data, not JSON. If you build a login page or registration form with a traditional HTML frontend, your FastAPI backend must accept form data.

Install the Required Package

pip install python-multipart

This library parses multipart and URL-encoded form bodies. FastAPI requires it for both form fields and file uploads.

Receiving Form Fields

from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/login")
def login(username: str = Form(), password: str = Form()):
    return {"username": username, "logged_in": True}
HTML form that sends to this route:
<form method="POST" action="/login">
  <input name="username" />
  <input name="password" type="password" />
  <button type="submit">Login</button>
</form>

Each Form() argument tells FastAPI to read that field from the form body instead of from JSON.

Receiving a Single File Upload

from fastapi import FastAPI, File, UploadFile

app = FastAPI()

@app.post("/upload")
async def upload_file(file: UploadFile):
    content = await file.read()
    return {
        "filename": file.filename,
        "size_bytes": len(content),
        "content_type": file.content_type
    }
UploadFile gives you:
  file.filename      → "resume.pdf"
  file.content_type  → "application/pdf"
  file.read()        → actual bytes (use await)
  file.size          → size in bytes

UploadFile vs File(bytes)

File(bytes):
  Reads entire file into memory at once.
  Simple but risky for large files.

UploadFile:
  Streams the file. Safe for large files.
  Gives you metadata (name, type).
  Recommended in almost all cases.

Uploading Multiple Files

from typing import List

@app.post("/upload-many")
async def upload_many(files: List[UploadFile]):
    results = []
    for file in files:
        content = await file.read()
        results.append({"filename": file.filename, "size": len(content)})
    return results

Combining Form Fields and a File in One Request

You cannot mix JSON body with form data in one request — but you can mix form fields with file uploads:

@app.post("/profile")
async def create_profile(
    name: str = Form(),
    bio: str = Form(),
    avatar: UploadFile = File()
):
    image_data = await avatar.read()
    return {
        "name": name,
        "bio": bio,
        "avatar_filename": avatar.filename,
        "avatar_size": len(image_data)
    }
Request format: multipart/form-data
Parts:
  name    = "Anjali"       ← text field
  bio     = "Developer"    ← text field
  avatar  = [file bytes]   ← file field

Saving an Uploaded File to Disk

import shutil
from pathlib import Path

@app.post("/save-file")
async def save_file(file: UploadFile):
    save_path = Path("uploads") / file.filename
    save_path.parent.mkdir(parents=True, exist_ok=True)

    with open(save_path, "wb") as buffer:
        shutil.copyfileobj(file.file, buffer)

    return {"saved_as": str(save_path)}

Key Points

  • Install python-multipart to use form data and file uploads.
  • Use Form() to read individual form fields.
  • Use UploadFile instead of raw bytes for file uploads — it streams content safely.
  • Combine Form() and File() in one endpoint to accept both text fields and files.
  • Use List[UploadFile] to accept multiple files in a single request.

Leave a Comment

Your email address will not be published. Required fields are marked *