FastAPI Essentials

Course Content

FastAPI Essentials

1 sections · 32 lessons

What features does FastAPI provide for form handling?


What you need to know

There are two form encodings:

  • application/x-www-form-urlencoded — simple key=value&key2=value2 text, sent by basic HTML forms.
  • multipart/form-data — the body is split into parts; needed when a form includes files.

FastAPI supports both through Form(), and adds files with File() / UploadFile.

Python
from typing import Annotatedfrom fastapi import Depends, FastAPI, File, Form, UploadFilefrom fastapi.security import OAuth2PasswordRequestFormfrom pydantic import BaseModel, Fieldapp = FastAPI()class Feedback(BaseModel):    run_id: str    rating: int = Field(ge=1, le=5)    comment: str = ""@app.post("/feedback")def feedback(form: Annotated[Feedback, Form()]):           # a whole model as a form    return form@app.post("/feedback-with-screenshot")def feedback_shot(rating: Annotated[int, Form(ge=1, le=5)],                  screenshot: Annotated[UploadFile | None, File()] = None):    return {"rating": rating, "screenshot": screenshot.filename if screenshot else None}@app.post("/token")def token(form: Annotated[OAuth2PasswordRequestForm, Depends()]):    return {"username": form.username}

Real responses:

Text
POST /feedback  form run_id=r-17 rating=4 comment=Good summary     -> {'run_id': 'r-17', 'rating': 4, 'comment': 'Good summary'}POST /feedback  form rating=9          -> 422 [(['body', 'rating'], 'Input should be less than or equal to 5')]POST /feedback  as JSON                -> 422   (the endpoint expects a form)POST /feedback-with-screenshot  rating=5 + bug.png  -> {'rating': 5, 'screenshot': 'bug.png'}POST /token     username=asha password=...           -> {'username': 'asha'}

The form value "4" arrived as text and became the integer 4; 9 broke the le=5 rule and got a normal 422. Sending JSON to a form endpoint fails, because the content type is wrong.

Mixing forms and JSON

A route declared as rating: Annotated[int, Form()], body: Question looks reasonable but cannot work. FastAPI saw a form field and documented the whole request body as application/x-www-form-urlencoded; the JSON model was then expected as a form field called body, and the real request failed with (['body', 'body'], 'missing'). One request has one body. If you need structured data plus a file, send the structured part as form fields, or as a JSON string in one form field that you parse with Model.model_validate_json.

A real-life example

An AI writing tool shows "Rate this answer" with 1–5 stars, a comment box and an optional screenshot. The front end posts it as multipart/form-data. The team first declared each field separately in the route signature; when they added model_version and latency_ms, the signature grew to eight parameters and the rules drifted between two endpoints.

They switched to form: Annotated[Feedback, Form()] with one Feedback model shared by both endpoints and by the analytics job that reads the feedback table. Validation messages now match across the product, and the /docs page shows a single, clear form.

Follow-up questions to expect

  • "Why is the OAuth2 token endpoint a form?" — The OAuth2 specification defines the password grant request as application/x-www-form-urlencoded, so OAuth2PasswordRequestForm reads username, password, scope and grant_type as form fields.
  • "What happens without python-multipart installed?" — FastAPI raises an error at startup telling you to install it, because it cannot parse forms.
  • "Are form fields limited in size?" — Yes. Starlette limits each non-file field to 1 MB by default and the number of fields to 1,000, which protects the server from huge form bodies.