Not all requests are JSON. HTML forms send application/x-www-form-urlencoded or multipart/form-data (when files are involved). FastAPI handles both with Form and File/UploadFile.
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login/")
def login(username: str = Form(), password: str = Form()):
return {"username": username}
Requires pip install python-multipart. Unlike query/body params, a parameter declared with Form() is read from the form body, and the docs show a form-style request body.
from fastapi import FastAPI, UploadFile, File
from typing import List
app = FastAPI()
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
contents = await file.read()
return {
"filename": file.filename,
"content_type": file.content_type,
"size": len(contents),
}
@app.post("/upload-multiple/")
async def upload_multiple(files: List[UploadFile]):
return {"names": [f.filename for f in files]}
file: bytes = File(): loads the whole file into memory — fine for small files, dangerous for large ones.file: UploadFile: streamed; has metadata (filename, content_type) and async methods (await file.read(size), await file.seek(0), await file.close()). Preferred for anything real.import shutil, uuid
from pathlib import Path
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
@app.post("/save-file/")
async def save_file(file: UploadFile):
safe_name = f"{uuid.uuid4()}{Path(file.filename).suffix}" # never trust client filename
dest = UPLOAD_DIR / safe_name
with dest.open("wb") as out:
shutil.copyfileobj(file.file, out) # sync streaming copy
return {"saved_as": safe_name}
Security notes: never use the client-supplied filename directly (path traversal), validate content_type and size limits, and consider scanning uploads.
@app.post("/reports/")
async def upload_report(
title: str = Form(),
period: str = Form(),
attachment: UploadFile | None = None,
):
return {"title": title, "has_attachment": attachment is not None}
A: Form and file parsing are not part of FastAPI's core; install the parser with pip install python-multipart. It is included when you install fastapi[standard].
A: Read in chunks and abort when you exceed the limit: check file.size if available, or loop await file.read(1024 * 1024) counting bytes; alternatively enforce the limit at the proxy level (Nginx client_max_body_size).
A: Not directly inside a JSON model — files are multipart, not JSON. Declare the UploadFile as a separate parameter next to your Form() fields (or a BaseModel with model_config = ConfigDict(arbitrary_types_allowed=True) as a dependency).
Form() reads urlencoded fields and UploadFile streams files with metadata. Always prefer UploadFile over raw bytes, sanitize filenames, and enforce size limits — uploads are the classic attack surface of an API.