FastAPI Request Extractors
When building APIs with FastAPI, you'll frequently need to extract data from incoming requests. FastAPI provides several elegant ways to retrieve data from different parts of an HTTP request. These "request extractors" help you access information like query parameters, path parameters, request bodies, headers, and cookies.
Introduction to Request Extractors
Request extractors are special function parameters that tell FastAPI where to look for specific values in incoming requests. By using type hints and parameter declarations, FastAPI automatically:
- Extracts the values from the correct request location
- Validates the data based on the type annotations
- Converts the data to the appropriate Python type
- Generates OpenAPI documentation
Let's explore the different types of request extractors available in FastAPI.
Path Parameters
Path parameters are parts of the URL path that are variable. They are essential for RESTful API design when identifying specific resources.
Basic Path Parameters
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
In this example, item_id
is a path parameter that will be extracted from the URL. If you access /items/42
, FastAPI will:
- Extract
42
from the URL - Convert it to an integer (since we declared
item_id: int
) - Pass it to the function
Multiple Path Parameters
You can have multiple path parameters in a single endpoint:
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}/items/{item_id}")
async def get_user_item(user_id: int, item_id: str):
return {"user_id": user_id, "item_id": item_id}
If you access /users/123/items/laptop
, FastAPI will extract 123
as user_id
and laptop
as item_id
.
Path Parameter with Predefined Values
You can restrict path parameters to a set of values using Pydantic's Enum
:
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}")
async def get_model(model_name: ModelName):
if model_name is ModelName.alexnet:
return {"model_name": model_name, "message": "Deep Learning FTW!"}
if model_name.value == "lenet":
return {"model_name": model_name, "message": "LeCNN all the images"}
return {"model_name": model_name, "message": "Have some residuals"}
Query Parameters
Query parameters are the key-value pairs that appear after the ?
in a URL. They're typically used for filtering, sorting, or pagination.
Basic Query Parameters
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
If you access /items/?skip=20&limit=50
, FastAPI will extract skip=20
and limit=50
.
Optional Query Parameters
You can make query parameters optional by setting a default value or using Optional
:
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(name: Optional[str] = None, price: float = None):
response = {}
if name:
response["name"] = name
if price:
response["price"] = price
return response
Required Query Parameters
If you don't provide a default value, the parameter becomes required:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(item_id: str):
return {"item_id": item_id}
Now, requests to /items/
without the item_id
query parameter will return an error.
Request Body
The request body is data sent by the client in the HTTP request. FastAPI uses Pydantic models to define, validate, and document request bodies.
Basic Request Body
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
return item
Example request:
{
"name": "Foo",
"price": 45.2,
"description": "Optional description"
}
FastAPI will:
- Read the request body as JSON
- Convert the corresponding types
- Validate the data
- Provide the data in the
item
parameter
Multiple Body Parameters
You can have multiple body parameters:
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
class User(BaseModel):
username: str
full_name: str = None
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item, user: User):
return {"item": item, "user": user}
FastAPI will expect a JSON body with both the item
and user
data.
Form Data
For handling HTML form data, you can use Form
fields:
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}
This requires installing python-multipart
:
pip install python-multipart
Headers
To extract data from HTTP headers:
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
@app.get("/items/")
async def read_items(user_agent: Optional[str] = Header(None)):
return {"User-Agent": user_agent}
FastAPI automatically converts header names from snake_case
to kebab-case
, so user_agent
will look for the User-Agent
header.
Cookies
To extract cookies from the request:
from fastapi import FastAPI, Cookie
from typing import Optional
app = FastAPI()
@app.get("/items/")
async def read_items(session_token: Optional[str] = Cookie(None)):
return {"session_token": session_token}
File Uploads
For handling file uploads:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
return {"filename": file.filename}
Combining Different Parameter Types
A real-world API endpoint often combines multiple parameter types:
from typing import Optional
from fastapi import FastAPI, Header, Path, Query
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
app = FastAPI()
@app.put("/users/{user_id}/items/{item_id}")
async def update_item(
*,
user_id: int = Path(..., title="The ID of the user"),
item_id: int = Path(..., title="The ID of the item to get"),
item: Item,
q: Optional[str] = Query(None, max_length=50),
user_agent: Optional[str] = Header(None)
):
result = {"user_id": user_id, "item_id": item_id, "item": item}
if q:
result.update({"q": q})
if user_agent:
result.update({"user_agent": user_agent})
return result
In this example, we're combining:
- Path parameters (
user_id
,item_id
) - Request body (
item
) - Query parameter (
q
) - Header (
user_agent
)
Request Object
Sometimes you might need access to the raw request object:
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/items/{item_id}")
async def read_root(item_id: str, request: Request):
client_host = request.client.host
request_headers = request.headers
return {
"item_id": item_id,
"client_host": client_host,
"request_headers": dict(request_headers)
}
Advanced Request Validation
FastAPI integrates with Pydantic for advanced validation:
from fastapi import FastAPI, Query
from typing import List
app = FastAPI()
@app.get("/items/")
async def read_items(
q: str = Query(
None,
title="Query string",
description="Query string for the items to search in the database",
min_length=3,
max_length=50,
regex="^fixedquery$",
deprecated=False,
)
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
Practical Example: Building a RESTful API
Let's build a simple TODO API that demonstrates how to use different request extractors together:
from fastapi import FastAPI, Path, Query, Body, HTTPException, Header
from typing import List, Optional
from pydantic import BaseModel, Field
from datetime import datetime
app = FastAPI()
# Models
class TodoItem(BaseModel):
title: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=500)
priority: int = Field(1, ge=1, le=5)
completed: bool = False
class TodoResponse(TodoItem):
id: int
created_at: datetime
# In-memory database
todos_db = {}
todo_counter = 0
@app.post("/todos/", response_model=TodoResponse, status_code=201)
async def create_todo(
item: TodoItem,
user_id: Optional[str] = Header(None, convert_underscores=False)
):
# Require user_id header for authentication
if not user_id:
raise HTTPException(status_code=401, detail="User ID header is required")
global todo_counter
todo_counter += 1
new_todo = TodoResponse(
**item.dict(),
id=todo_counter,
created_at=datetime.now()
)
todos_db[todo_counter] = new_todo
return new_todo
@app.get("/todos/", response_model=List[TodoResponse])
async def get_todos(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
completed: Optional[bool] = Query(None)
):
# Filter and paginate todos
filtered_todos = list(todos_db.values())
if completed is not None:
filtered_todos = [todo for todo in filtered_todos if todo.completed == completed]
return filtered_todos[skip: skip + limit]
@app.get("/todos/{todo_id}", response_model=TodoResponse)
async def get_todo(todo_id: int = Path(..., gt=0)):
if todo_id not in todos_db:
raise HTTPException(status_code=404, detail="Todo not found")
return todos_db[todo_id]
@app.put("/todos/{todo_id}", response_model=TodoResponse)
async def update_todo(
update_data: TodoItem,
todo_id: int = Path(..., gt=0)
):
if todo_id not in todos_db:
raise HTTPException(status_code=404, detail="Todo not found")
todo = todos_db[todo_id]
updated_todo = todo.copy(update=update_data.dict(exclude_unset=True))
todos_db[todo_id] = updated_todo
return updated_todo
@app.delete("/todos/{todo_id}", status_code=204)
async def delete_todo(todo_id: int = Path(..., gt=0)):
if todo_id not in todos_db:
raise HTTPException(status_code=404, detail="Todo not found")
del todos_db[todo_id]
return None
Summary
FastAPI's request extractors provide a powerful and type-safe way to extract data from HTTP requests:
- Path Parameters: Extract values from the URL path (
/items/{item_id}
) - Query Parameters: Extract values from URL query string (
/items/?skip=0&limit=10
) - Request Body: Extract JSON data sent in the request body
- Form Data: Extract data from form submissions
- Headers: Extract values from HTTP headers
- Cookies: Extract values from cookies
- Files: Handle file uploads
Using these extractors with appropriate type hints gives you:
- Automatic data validation
- Data conversion to Python types
- Clear API documentation
- Better code organization
By combining different extractors, you can create comprehensive API endpoints that handle data from various parts of the HTTP request in a clean, type-safe manner.
Additional Resources
- FastAPI Official Documentation on Path Parameters
- FastAPI Official Documentation on Query Parameters
- FastAPI Official Documentation on Request Body
- FastAPI Official Documentation on Headers
- Pydantic Documentation for data validation
Exercises
- Create an API endpoint that accepts a user ID as a path parameter, a sort direction as a query parameter, and user data in the request body.
- Build an endpoint that accepts file uploads along with metadata about the file in JSON format.
- Create an API endpoint that extracts authentication information from headers and returns different responses based on the user's role.
- Build a search API that accepts multiple search filters as query parameters with validation (min length, max length, etc.).
- Extend the TODO API example with the ability to assign tags to TODO items and search by tags.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)