Getting Started
First API
FastAPI is built on Starlette and Pydantic. uvicorn is the ASGI server. --reload enables auto-reload during development.
# install FastAPI and uvicorn
pip install fastapi uvicorn
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
# run: uvicorn main:app --reloadRun with Uvicorn
The app instance 'main:app' means module 'main', variable 'app'. Use --reload only in development; in production run without it and use a process manager.
# development with auto-reload
uvicorn main:app --reload
# specify host and port
uvicorn main:app --host 0.0.0.0 --port 8000
# run programmatically
import uvicorn
if __name__ == "__main__":
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)Async Path Operations
You can declare path operations with either 'def' (run in threadpool) or 'async def'. Use async def when the function performs I/O via async libraries; mixing blocking calls in async def will block the event loop.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"message": "hello"}
@app.get("/sync")
def read_sync():
return {"message": "sync also works"}
# use async when calling async libs (httpx, databases)
@app.get("/data")
async def fetch_data():
import httpx
async with httpx.AsyncClient() as client:
r = await client.get("https://api.example.com")
return r.json()Path Operation Decorators
FastAPI supports all HTTP methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. Each maps to a decorator. Use status_code= on the decorator to override the default success status.
from fastapi import FastAPI
app = FastAPI()
@app.get("/items")
def list_items(): ...
@app.post("/items")
def create_item(): ...
@app.put("/items/{item_id}")
def replace_item(item_id: int): ...
@app.patch("/items/{item_id}")
def update_item(item_id: int): ...
@app.delete("/items/{item_id}")
def delete_item(item_id: int): ...
@app.options("/items")
def options_items(): ...
@app.head("/items")
def head_items(): ...JSON Response & Status
Returning a dict/list is auto-converted to JSON with 200 (or your status_code). For full control over status, headers, or content, return a JSONResponse or Response directly.
from fastapi import FastAPI, status
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/plain")
def plain():
return {"key": "value"} # auto JSON, 200 OK
@app.post("/created", status_code=status.HTTP_201_CREATED)
def created():
return {"id": 1}
@app.get("/custom")
def custom():
return JSONResponse(
status_code=418,
content={"error": "I'm a teapot"},
headers={"X-Custom": "yes"},
)Project Structure
Split a growing app into modules with APIRouter, then register them via app.include_router(). Keep Pydantic schemas, DB setup, and dependencies in separate files for maintainability.
myapp/
main.py # creates the FastAPI app
routers/
users.py # APIRouter for /users
items.py # APIRouter for /items
models/
schemas.py # Pydantic models
database.py # DB engine & session
dependencies.py # shared Depends functions
tests/
test_main.py
# main.py
from fastapi import FastAPI
from routers import users, items
app = FastAPI()
app.include_router(users.router)
app.include_router(items.router, prefix="/items")Path Parameters
Basic Path Parameters
Values captured from the path are strings unless you add a type annotation. The parameter name in the path must match the function argument name exactly.
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id):
return {"item_id": item_id}
# GET /items/42 -> {"item_id": "42"} (string by default)Type Conversion
Adding a type annotation (int, float, bool, str, Enum) makes FastAPI validate and convert the path value. Invalid values return a 422 response with a clear error message.
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
# GET /items/3 -> {"item_id": 3}
# GET /items/abc -> 422 Validation Error (not an integer)Path Validation
Path() adds metadata and constraints to a path parameter. Numeric constraints: ge (>=), gt (>), le (<=), lt (<). Path parameters are always required, so Path() cannot set a default.
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(
item_id: int = Path(
title="The ID of the item",
description="Must be a positive integer",
ge=1,
le=1000,
),
):
return {"item_id": item_id}Numeric Constraints
Use ge/gt/le/lt on int or float path parameters to enforce numeric ranges. Combined constraints give you precise bounds validation with automatic 422 errors.
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/ratio/{value}")
def ratio(
value: float = Path(gt=0, lt=1), # 0 < value < 1
):
return {"value": value}
@app.get("/page/{page}")
def page(
page: int = Path(ge=1), # >= 1
size: int = Path(le=100), # <= 100
):
return {"page": page, "size": size}Route Order Matters
Routes are matched in declaration order. Declare specific paths like /users/me before dynamic patterns like /users/{user_id}, or the dynamic route will shadow the specific one.
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/me")
def read_current_user():
return {"user": "me"}
@app.get("/users/{user_id}")
def read_user(user_id: str):
return {"user_id": user_id}
# /users/me MUST be declared before /users/{user_id}
# otherwise 'me' would match the {user_id} patternEnum Path Parameters
Using an Enum type for a path parameter restricts it to predefined values and generates an enum schema in the OpenAPI docs. Subclass (str, Enum) so values serialize as strings.
from enum import Enum
from fastapi import FastAPI
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
app = FastAPI()
@app.get("/models/{model_name}")
def get_model(model_name: ModelName):
if model_name is ModelName.alexnet:
return {"model": model_name, "layers": 5}
return {"model": model_name}
# GET /models/resnet -> {"model": "resnet"}Query Parameters
Basic Query Parameters
Function parameters that are NOT in the path become query parameters. Providing a default makes them optional; the default is used when the query key is absent.
from fastapi import FastAPI
app = FastAPI()
fake_items = [{"item": "a"}, {"item": "b"}, {"item": "c"}]
@app.get("/items")
def list_items(skip: int = 0, limit: int = 10):
return fake_items[skip : skip + limit]
# GET /items?skip=0&limit=2
# GET /items -> skip=0, limit=10Optional Query Parameters
Use Optional[str] = None (or str | None = None on Python 3.10+) for a query parameter that may be omitted. Required parameters simply have no default value.
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: str, q: Optional[str] = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
# GET /items/1?q=hello
# GET /items/1 (q is None)Query Validation
Query() adds validation: min_length/max_length for strings, ge/gt/le/lt for numbers, pattern (regex) for string format. Use default= to set a value while still applying constraints.
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/search")
def search(q: str = Query(min_length=3, max_length=50, pattern="^[a-zA-Z]+$")):
return {"q": q}
@app.get("/page")
def page(limit: int = Query(default=10, ge=1, le=100)):
return {"limit": limit}Boolean Conversion
Bool query parameters accept many truthy values: true, 1, yes, on (case-insensitive). Anything else is interpreted as false. Invalid types like ?active=maybe return 422.
from fastapi import FastAPI
app = FastAPI()
@app.get("/flags")
def flags(active: bool = False):
return {"active": active}
# GET /flags?active=true -> {"active": true}
# GET /flags?active=1 -> {"active": true}
# GET /flags?active=yes -> {"active": true}
# GET /flags?active=off -> {"active": false}List Query Parameters
A List[str] parameter accepts the same key repeated in the query string. Use Query(default=[]) for an empty list by default, or Query(default=None) to allow missing values.
from typing import List
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items")
def list_items(q: List[str] = Query(default=[])):
return {"q": q}
# GET /items?q=a&q=b&q=c -> {"q": ["a", "b", "c"]}
# declare default=None to accept a list or nullRequired Query Parameters
Use Query(...) (the Ellipsis) to declare a required query parameter that still has validation metadata. Without a default and without Query(...), a plain annotated param is also required.
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items")
def read_item(q: str = Query(...)):
return {"q": q}
# GET /items?q=hello -> ok
# GET /items -> 422 (q is required)
# Query(...) (Ellipsis) marks a parameter as requiredRequest Body (Pydantic)
BaseModel Basics
A Pydantic BaseModel declared as a function parameter becomes the JSON request body. FastAPI validates the payload, converts types, and returns 422 on invalid data. Fields with defaults are optional.
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
in_stock: bool = True
app = FastAPI()
@app.post("/items")
def create_item(item: Item):
return item
# POST /items body: {"name": "Apple", "price": 0.5}
# -> {"name": "Apple", "price": 0.5, "in_stock": true}Field Types & Constraints
Field() adds constraints and metadata to model attributes: min_length/max_length for strings, gt/ge/lt/le for numbers, default_factory for mutable defaults. model_config with json_schema_extra adds examples to the docs.
from pydantic import BaseModel, Field
from typing import Optional
class Item(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, description="must be positive")
tax: Optional[float] = None
tags: list[str] = Field(default_factory=list)
model_config = {
"json_schema_extra": {
"examples": [{"name": "Apple", "price": 0.5, "tags": ["fruit"]}]
}
}Nested Models
Models can be nested: declare another BaseModel as a field type. FastAPI validates the full nested structure recursively, and the generated OpenAPI schema reflects the nested objects.
from pydantic import BaseModel
from typing import Optional
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
description: Optional[str] = None
image: Optional[Image] = None
images: list[Image] = []
# valid body:
# {"name": "Phone", "image": {"url": "http://x/a.png", "name": "a"}}Field Validation
Use @field_validator (Pydantic v2) to add custom validation logic per field. Raise ValueError to reject input (FastAPI surfaces it as 422). Return the (possibly transformed) value to keep it.
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
email: str
@field_validator("email")
@classmethod
def email_must_contain_at(cls, v: str) -> str:
if "@" not in v:
raise ValueError("must contain @")
return v.lower()
@field_validator("name")
@classmethod
def name_stripped(cls, v: str) -> str:
return v.strip()Optional & Default Fields
A field is required only if it has no default and is not Optional. Optional[str] = None is the standard pattern for an optional nullable field; a plain default like 0.0 makes a field optional with that value.
from pydantic import BaseModel
from typing import Optional
class Item(BaseModel):
name: str # required
description: Optional[str] = None # optional, defaults to None
price: float = 0.0 # optional, defaults to 0.0
tags: list[str] = [] # optional, defaults to empty list
# All of these are valid:
# {"name": "X"}
# {"name": "X", "price": 9.99, "tags": ["a"]}Body with Path & Query
Path, query, and body parameters can coexist in one operation. FastAPI decides by: path param if in the path, Pydantic model (or Body()) for the body, otherwise query. Optional body needs a default of None.
from fastapi import FastAPI, Path, Query
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
app = FastAPI()
@app.put("/items/{item_id}")
def update_item(
item_id: int = Path(ge=1),
q: Optional[str] = Query(default=None),
item: Item = None,
):
result = {"item_id": item_id, **item.model_dump()}
if q:
result["q"] = q
return resultResponse Model
response_model Basics
response_model filters the returned data to only the fields defined in the model, even if you return an object with extra fields. This is the standard way to hide sensitive fields like passwords.
from fastapi import FastAPI
from pydantic import BaseModel
class User(BaseModel):
username: str
email: str
hashed_password: str
class UserOut(BaseModel):
username: str
email: str
app = FastAPI()
@app.get("/users/me", response_model=UserOut)
def read_user():
return User(username="bob", email="[email protected]", hashed_password="secret")
# response: {"username": "bob", "email": "[email protected]"} (no password)Exclude Fields
response_model_include and response_model_exclude let you whitelist or blacklist fields per route without defining new models. Pass a set of field names. These are applied in addition to the response_model.
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
secret: str
app = FastAPI()
@app.get("/items/{id}", response_model=Item, response_model_exclude={"secret"})
def read_item(id: int):
return {"name": "Apple", "price": 0.5, "secret": "hidden"}
@app.get("/items", response_model=list[Item], response_model_include={"name", "price"})
def list_items():
return [{"name": "Apple", "price": 0.5, "secret": "x"}]List Response
Use response_model=list[Model] (or List[Model]) to declare that an endpoint returns an array of objects. FastAPI validates and serializes each item in the list.
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
app = FastAPI()
@app.get("/items", response_model=list[Item])
def list_items():
return [
{"name": "Apple", "price": 0.5},
{"name": "Banana", "price": 0.3},
]
# response: [{"name": "Apple", "price": 0.5}, {"name": "Banana", "price": 0.3}]Exclude Unset/Defaults
exclude_unset omits fields the client never sent (only those explicitly set appear). exclude_defaults omits fields equal to their defaults; exclude_none omits fields that are None. Useful for sparse responses.
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
price: float = 0.0
tax: float = 0.0
app = FastAPI()
@app.get("/items/{id}", response_model=Item, response_model_exclude_unset=True)
def read_item(id: int):
return {"name": "Apple", "price": 0.5}
# -> {"name": "Apple", "price": 0.5} (description, tax omitted)
# also: response_model_exclude_defaults=True
# response_model_exclude_none=TrueResponse by Alias
Field(alias=...) lets incoming JSON use a different key name than the Python attribute. response_model_by_alias=True outputs using the alias too. populate_by_name=True also allows the original attribute name on input.
from fastapi import FastAPI
from pydantic import BaseModel, ConfigDict, Field
class Item(BaseModel):
model_config = ConfigDict(populate_by_name=True)
name: str = Field(alias="itemName")
price: float
app = FastAPI()
@app.post("/items", response_model=Item, response_model_by_alias=True)
def create(item: Item):
return item
# input: {"itemName": "Apple", "price": 0.5}
# output: {"itemName": "Apple", "price": 0.5}Plain Dict Response
Returning a dict/list auto-serializes to JSON. Use response_class to change the default response type (HTMLResponse, PlainTextResponse), or return a Response subclass directly for full control.
from fastapi import FastAPI
from fastapi.responses import JSONResponse, PlainTextResponse
app = FastAPI()
@app.get("/dict")
def dict_resp():
return {"a": 1, "b": 2} # auto JSON
@app.get("/text", response_class=PlainTextResponse)
def text_resp():
return "plain text response"
@app.get("/raw")
def raw_resp():
return JSONResponse(content={"a": 1}, status_code=200)Form & UploadFile
Form Data
Form() declares a parameter parsed from application/x-www-form-urlencoded or multipart form data. You cannot mix Form() with a JSON body (BaseModel) in the same operation — pick one content type.
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login")
def login(username: str = Form(), password: str = Form()):
return {"username": username}
# Content-Type: application/x-www-form-urlencoded
# body: username=bob&password=secretUploadFile Basic
UploadFile is a streaming file representation with .filename, .content_type, .read(), .write(), .close(). Use 'await file.read()' inside async def. For large files prefer file.file (SpooledTemporaryFile) to avoid loading all into memory.
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
contents = await file.read()
return {
"filename": file.filename,
"size": len(contents),
"content_type": file.content_type,
}Multiple Files
Declare list[UploadFile] to accept multiple files in one request. Each file is sent with the same field name. The order of files in the list matches the order they were sent.
from fastapi import FastAPI, UploadFile
app = FastAPI()
@app.post("/uploads")
async def upload_many(files: list[UploadFile]):
return [
{"filename": f.filename, "size": len(await f.read())}
for f in files
]
# curl: curl -F "[email protected]" -F "[email protected]" http://localhost:8000/uploadsFile and Form Together
Form() and UploadFile (File()) can be combined in the same endpoint because both use multipart/form-data. You still cannot combine them with a JSON BaseModel body.