Unlock Your Python Backend Career: Build 30 Projects in 30 Days. Join now for just $54

Handling forms and file uploads with FastAPI

by Jane Nkwor

.

Updated Wed Jul 30 2025

.
Handling forms and file uploads with FastAPI

Introduction

FastAPI is a modern web framework for building high-performance APIs in Python. It’s built for speed, ease of use, and developer productivity, making it an ideal choice for developing efficient and reliable backend systems. FastAPI is not only fast but also supports asynchronous programming and automatic interactive API documentation via Swagger UI.

Handling forms and file uploads is a common requirement in many APIs, especially in applications like resume builders, image galleries, and document submission platforms. These features allow users to send structured data (e.g., names, descriptions) alongside files (like PDFs or images), which the server must validate, store, and possibly process.

In this guide, we’ll walk through how to handle both form data and file uploads in FastAPI. You’ll see practical examples, learn best practices, and understand how to structure your endpoints to accept and process form data requests correctly.

Handling Regular Forms (Form Data)

Understanding Request Payloads: JSON vs. Form Data

When building APIs, it’s important to understand the difference between sending data as a JSON payload versus using form data (application/x-www-form-urlencoded or multipart/form-data)

JSON Payload

  • Sent with the header: Content-Type: application/json.

  • Commonly used for structured data like nested objects or arrays.

  • In FastAPI, you receive this using Pydantic models directly.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    name: str
    email: str

@app.post("/json-example")
async def receive_json(user: User):
    return {"message": f"Hello {user.name}"}

Form Data

Form data refers to the information sent to the server when a user submits an HTML form. In FastAPI, you receive this data using the Form() dependency. The data is sent with the header: Content-Type: application/x-www-form-urlencoded (or multipart/form-data for file uploads). The header tells the server how the data is formatted.

  • application/x-www-form-urlencoded is the default for HTML forms without file uploads as it send the form as key-value pairs.

  • multipart/form-data is used when the form includes files like PDFs, images or documents. The form data is split into parts - each part has its own headers (e.g., filename, content type)

from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/form-example")
async def receive_form(name: str = Form(...), email: str = Form(...)):
    return {"message": f"Hello {name}"}

When Should You Use Form() in FastAPI?

By default, FastAPI expects request data to come in as JSON, but when submitting data using an HTML form (e.g., using the <form> tag) the content isn’t JSON, and FastAPI won’t parse the data correctly unless you use the Form() dependency for text fields.

Form() is required when:

  • The client (usually a web form) is sending data using application/x-www-form-urlencoded.

  • You need to mix text fields with file uploads (i.e., multipart/form-data), since FastAPI requires form fields to be explicitly marked with Form() when used with UploadFile.

Example mixing file and form:

from fastapi import FastAPI, Form, File, UploadFile

app = FastAPI()

@app.post("/upload-resume")
async def upload_resume(
    name: str = Form(...),
    email: str = Form(...),
    file: UploadFile = File(...)
):
    return {"filename": file.filename, "submitted_by": name}

Handling File Uploads

File uploads are a common need in modern web applications, whether users are submitting resumes, profile pictures, scanned documents, or media files. FastAPI makes handling file uploads simple and efficient.

How to Receive Files in FastAPI

To receive uploaded files, FastAPI provides two built-in tools:

  • File() – This is used to declare a file input.

  • UploadFile – This is a class that gives you access to the uploaded file without loading it entirely into memory.

from fastapi import FastAPI, File, UploadFile

app = FastAPI()

@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    contents = await file.read()
    return {
        "filename": file.filename,
        "content_type": file.content_type,
        "size": len(contents)
    }

Combining Form Fields and File Uploads

from fastapi import FastAPI, Form, UploadFile, File

@app.post("/submit-resume/")
async def submit_resume(
    full_name: str = Form(...),
    email: str = Form(...),
    resume: UploadFile = File(...)
):
    return {
        "name": full_name,
        "email": email,
        "filename": resume.filename
    }

Common Errors and Fixes

When working with form data and file uploads in FastAPI, a few common mistakes can cause your API to break. Here’s how to identify and fix them:

Missing Form() or File()

Symptom: FastAPI returns a 422 Unprocessable Entity error.

Cause: FastAPI expects parameters like form fields or files to be explicitly declared using Form() and File(). If you leave them as plain function arguments, FastAPI assumes they should come from JSON or the query string.

Fix:

from fastapi import Form, File, UploadFile

@app.post("/submit")
async def submit_form(
    name: str = Form(...),
    resume: UploadFile = File(...)
):

Wrong Content-Type (e.g., sending JSON instead of Form Data)

Symptom: FastAPI doesn’t receive your data or shows validation errors.

Cause: If your frontend sends data as application/json, but the backend expects multipart/form-data (for files) or application/x-www-form-urlencoded (for regular forms), it will fail to parse the request.

Fix: Make sure your frontend sends the right content type:

  • For forms: Use application/x-www-form-urlencoded or multipart/form-data

  • For file uploads: Always use multipart/form-data

Example using FormData in JavaScript:

const form = new FormData();
form.append("name", "Jane Doe");
form.append("resume", file);  // file is a File object

fetch("/submit", {
  method: "POST",
  body: form,
});

Pro-Tip:

Always match how your frontend sends data with how FastAPI expects to receive it. If you’re using Form() or File() in the backend, use FormData on the frontend.

Real-World Application: Resume Builder

To bring these concepts to life, I built a Resume Builder using FastAPI on the backend and React on the frontend. This project combines form handling and file uploads in a practical way.

In the form, users fill out key sections like:

  • Personal information (name, email, phone)

  • Skills and professional summary

  • Education and work experience (with dynamic fields)

  • Volunteer experience and certifications

In addition to this structured data, users can upload their professional image.

To handle both parts, structured data and the file, I used a FormData object on the frontend. Here's how it worked:

const form = new FormData();
form.append("resume", JSON.stringify(formData)); // all form fields
form.append("file", fileInput); //image file

Note: Because FormData cannot send nested objects directly, we serialize the entire form data as a JSON string on the frontend. On the backend, we deserialize it using json.loads().

This allowed me to send the complete resume data and the attached file in one request using multipart/form-data.

Processing and Storing the File

On the FastAPI backend, I used Form() and UploadFile to receive both parts of the request:

from fastapi import FastAPI, File, Form, UploadFile

@app.post("/resume")
async def create_resume(
    resume: str = Form(...),  # JSON stringified form data
    file: UploadFile = File(...)
):
    resume_data = json.loads(resume)

    # Save resume info to the database
    resume_id = await save_resume_data(resume_data)

    # Optionally save the file
    if file:
        file_location = f"uploads/{file.filename}"
        with open(file_location, "wb") as f:
            f.write(await file.read())

    return {"id": resume_id, "message": "Resume saved successfully"}

Resume Preview Before Download

Another great user experience I added was previewing the resume before submission or download. For images, I used FileReader in React to show a preview. For PDFs, I displayed the file name and allowed users to confirm before submission.

Conclusion

The resume builder demonstrates how to handle mixed data types in APIs: validating complex nested form inputs alongside file uploads, a common requirement in many real-world applications like job portals, education platforms, and onboarding systems.

References

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board

Backend Tips, Every week

Backend Tips, Every week