Difficulty: Intermediate
In real APIs, you often need to accept data from multiple sources in a single request: path parameters for resource identification, query parameters for options, and a request body for the main data payload. FastAPI handles this combination seamlessly by automatically determining where each parameter comes from based on its type and declaration.
The rule is simple: if a parameter is declared in the path, it is a path parameter. If it is a Pydantic model or annotated with Body(), it comes from the request body. Everything else is a query parameter. This means you can freely mix all three in a single endpoint function.
When you have multiple Pydantic model parameters, FastAPI expects the request body to be a JSON object where each key corresponds to a model parameter name. For example, if you have def create(item: Item, user: User), FastAPI expects {"item": {...}, "user": {...}} in the request body.
The Body() function is used for singular values in the request body (not wrapped in a model). For example, importance: int = Body() tells FastAPI to read 'importance' from the JSON body rather than treating it as a query parameter. You can also use Body(embed=True) with a single model to wrap it in a key.
FastAPI also supports form data through the Form() function, which reads data from application/x-www-form-urlencoded format (the format used by HTML forms). File uploads use the File() and UploadFile types. These cannot be mixed with JSON body parameters in the same endpoint.
A common pattern is using path parameters for the resource ID, query parameters for options like 'fields' or 'expand', and the request body for the actual data. For example, PUT /users/{user_id}?notify=true with a JSON body containing the updated user data. This keeps URLs meaningful while providing rich data submission.
from fastapi import FastAPI, Query
from pydantic import BaseModel
app = FastAPI()
class ItemUpdate(BaseModel):
name: str
price: float
description: str | None = None
@app.put("/items/{item_id}")
def update_item(
item_id: int, # Path parameter
item: ItemUpdate, # Request body
notify: bool = False, # Query parameter
fields: str | None = None, # Query parameter
):
result = {"item_id": item_id, item.model_dump()}
if notify:
result["notification"] = "Item updated notification sent"
return result
# PUT /items/42?notify=true
# Body: {"name": "Widget Pro", "price": 19.99}
# -> {"item_id": 42, "name": "Widget Pro", "price": 19.99, ...}
FastAPI automatically determines each parameter's source: item_id is in the path, item is a Pydantic model (so it comes from the body), and notify/fields are plain types without path or body declarations (so they are query parameters).
from fastapi import FastAPI, Body
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
class User(BaseModel):
username: str
email: str
@app.post("/purchases")
def create_purchase(
item: Item,
user: User,
note: str = Body(default=""),
):
return {
"item": item.model_dump(),
"user": user.model_dump(),
"note": note,
}
# Expected request body:
# {
# "item": {"name": "Widget", "price": 9.99},
# "user": {"username": "alice", "email": "alice@example.com"},
# "note": "Gift wrapping please"
# }
# With a single model and embed=True:
@app.post("/items")
def create_item(item: Item = Body(embed=True)):
return item
# Expected body: {"item": {"name": "Widget", "price": 9.99}}
# Without embed: {"name": "Widget", "price": 9.99}
Multiple body parameters create a nested JSON structure keyed by parameter name. Body() declares singular values that come from the request body. embed=True wraps a single model in a key, changing the expected format.
from fastapi import FastAPI, Form, File, UploadFile
app = FastAPI()
# Form data (application/x-www-form-urlencoded)
@app.post("/login")
def login(
username: str = Form(),
password: str = Form(),
):
return {"username": username}
# File upload
@app.post("/upload")
async def upload_file(
file: UploadFile,
description: str = Form(default=""),
):
contents = await file.read()
return {
"filename": file.filename,
"size": len(contents),
"content_type": file.content_type,
"description": description,
}
# Multiple file upload
@app.post("/upload-multiple")
async def upload_multiple(files: list[UploadFile]):
return [
{"filename": f.filename, "size": f.size}
for f in files
]
Form() reads from URL-encoded form data. UploadFile handles file uploads with streaming support. You can combine Form() with UploadFile in the same endpoint, but you cannot mix Form/File with JSON Body parameters.
Body + Path, Body + Query, Multiple Bodies, Body Embed, Form Data