FastAPI Parameters
When building APIs, one of the most common requirements is accepting and validating input data. FastAPI provides multiple ways to receive input parameters in your endpoints, each suited for different use cases. In this tutorial, we'll explore the various parameter types in FastAPI and how to effectively use them.
Introduction to FastAPI Parameters
Parameters in FastAPI are how your API receives data. They can come from different places:
- Path parameters: Part of the URL path
- Query parameters: After the
?
in the URL - Request body: Data sent in the request body (typically in JSON format)
- Form data: Data submitted through HTML forms
- Header parameters: Custom headers in the HTTP request
- Cookie parameters: Data from cookies
FastAPI's parameter handling is powerful because it:
- Automatically validates data
- Converts data to Python types
- Generates OpenAPI documentation
- Provides clear error messages when validation fails
Let's dive into each type of parameter.
Path Parameters
Path parameters are parts of the URL path that can change. They're defined in the route decorator using curly braces {}
.
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}
in the route defines a path parameteritem_id: int
in the function parameters tells FastAPI to convert the path parameter to an integer- If the conversion fails, FastAPI returns a clear error response
When you call /items/5
, you'll get:
{"item_id": 5}
If you call /items/abc
, you'll get an error because "abc" can't be converted to an integer.
Path Parameters with Predefined Values
You can restrict path parameters to a set of valid 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 == 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"}
This ensures that only "alexnet", "resnet", or "lenet" are accepted as valid values.
Query Parameters
Query parameters are the key-value pairs that appear after the ?
in a URL. In FastAPI, function parameters that aren't part of the path become query parameters.
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}
This endpoint accepts requests like /items/?skip=20&limit=50
.
If you call /items/?skip=20&limit=50
, you'll get:
{"skip": 20, "limit": 50}
If you call /items/
, you'll get the default values:
{"skip": 0, "limit": 10}
Optional Query Parameters
To make a query parameter optional, you can use Python's Optional
type hint:
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(name: Optional[str] = None):
if name is None:
return {"message": "No name provided"}
return {"name": name}
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: int):
return {"item_id": item_id}
Calling this endpoint without providing item_id
will result in an error.
Request Body Parameters
When you need to send data that's too complex or large for query parameters, you use the request body. In FastAPI, you define the structure of the body using Pydantic models.
Basic Request Body
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
item_dict = item.dict()
if item.tax:
price_with_tax = item.price + item.tax
item_dict.update({"price_with_tax": price_with_tax})
return item_dict
This endpoint expects a JSON body like:
{
"name": "Foo",
"description": "An optional description",
"price": 45.2,
"tax": 3.5
}
And would respond with:
{
"name": "Foo",
"description": "An optional description",
"price": 45.2,
"tax": 3.5,
"price_with_tax": 48.7
}
Combining Path, Query, and Body Parameters
FastAPI allows you to combine different types of parameters:
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
@app.put("/items/{item_id}")
async def update_item(
item_id: int,
item: Item,
q: Optional[str] = None
):
result = {"item_id": item_id, **item.dict()}
if q:
result.update({"q": q})
return result
This endpoint:
- Takes a path parameter
item_id
- Takes a request body defined by the
Item
model - Takes an optional query parameter
q
Form Data Parameters
When working with HTML forms or file uploads, you need to use form data. FastAPI provides the Form
class for this:
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}
Note: You need to install python-multipart
to use form data:
pip install python-multipart
File Upload Parameters
To handle file uploads, FastAPI provides the File
and UploadFile
classes:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: bytes = File(...)):
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
return {
"filename": file.filename,
"content_type": file.content_type
}
UploadFile
is generally recommended over bytes
for larger files as it doesn't load the entire file into memory.
Header and Cookie Parameters
You can also get data from headers and cookies:
from fastapi import FastAPI, Header, Cookie
app = FastAPI()
@app.get("/items/")
async def read_items(
user_agent: Optional[str] = Header(None),
session_id: Optional[str] = Cookie(None)
):
return {
"User-Agent": user_agent,
"Session ID": session_id
}
Parameter Validation
FastAPI provides powerful parameter validation through Pydantic:
from typing import List, Optional
from fastapi import FastAPI, Query
from pydantic import BaseModel
app = FastAPI()
@app.get("/items/")
async def read_items(
q: Optional[str] = Query(
None,
min_length=3,
max_length=50,
regex="^fixedquery$",
title="Query string",
description="Query string for the items to search in the database",
)
):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
Similar validation can be applied to Path
, Body
, and other parameter types.
Dependency Injection with Parameters
FastAPI's dependencies system works hand-in-hand with parameters, allowing you to create reusable components:
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
async def verify_token(x_token: str = Header(...)):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
return x_token
@app.get("/items/")
async def read_items(token: str = Depends(verify_token)):
return {"token": token, "items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
Real-World Example: Building a Product API
Let's build a more complete example that demonstrates various parameter types in a product API:
from typing import List, Optional
from fastapi import FastAPI, Query, Path, Body, HTTPException
from pydantic import BaseModel, Field
app = FastAPI()
class Product(BaseModel):
id: Optional[int] = None
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=1000)
price: float = Field(..., gt=0)
category: str
in_stock: bool = True
# In-memory database
products_db = {
1: Product(id=1, name="Laptop", price=999.99, category="Electronics",
description="A high-performance laptop"),
2: Product(id=2, name="Smartphone", price=499.99, category="Electronics",
description="Latest smartphone model"),
3: Product(id=3, name="Coffee Mug", price=11.99, category="Kitchen",
description="Ceramic coffee mug")
}
@app.get("/products/", response_model=List[Product])
async def get_products(
skip: int = Query(0, ge=0, description="Number of products to skip"),
limit: int = Query(10, ge=1, le=100, description="Max number of products to return"),
category: Optional[str] = Query(None, description="Filter by category")
):
"""Get a list of products with pagination and optional category filtering."""
products = list(products_db.values())
if category:
products = [p for p in products if p.category == category]
return products[skip:skip+limit]
@app.get("/products/{product_id}", response_model=Product)
async def get_product(
product_id: int = Path(..., ge=1, description="The ID of the product to retrieve")
):
"""Get a single product by its ID."""
if product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
return products_db[product_id]
@app.post("/products/", response_model=Product)
async def create_product(
product: Product = Body(..., description="The product data"),
):
"""Create a new product."""
# Generate a new ID
new_id = max(products_db.keys()) + 1
product.id = new_id
products_db[new_id] = product
return product
@app.put("/products/{product_id}", response_model=Product)
async def update_product(
product_id: int = Path(..., ge=1),
product_update: Product = Body(...),
partial: bool = Query(False, description="Whether to perform a partial update")
):
"""Update an existing product."""
if product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
existing_product = products_db[product_id]
update_data = product_update.dict(exclude_unset=partial)
update_data["id"] = product_id # Ensure we don't change the ID
updated_product = Product(**update_data)
products_db[product_id] = updated_product
return updated_product
@app.delete("/products/{product_id}")
async def delete_product(
product_id: int = Path(..., ge=1)
):
"""Delete a product."""
if product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
deleted_product = products_db.pop(product_id)
return {"message": f"Product '{deleted_product.name}' deleted"}
This example shows how to:
- Define a Pydantic model with validations
- Use path parameters with validation
- Use query parameters for pagination and filtering
- Work with request bodies for creating and updating data
- Use proper status codes and error handling
- Document your API with docstrings and parameter descriptions
Summary
FastAPI provides a comprehensive system for handling different types of parameters in your API endpoints. We've covered:
- Path parameters for variable parts of URL paths
- Query parameters for optional data and filtering
- Request body parameters for complex data structures
- Form data and file uploads for HTML forms and files
- Header and cookie parameters for HTTP headers and cookies
- Parameter validation using Pydantic
- Dependency injection with parameters
By understanding these concepts, you can build robust and well-documented APIs that are easy to use and maintain.
Additional Resources
- FastAPI official documentation on parameters
- Pydantic documentation for more advanced validation
- OpenAPI Specification to understand how FastAPI generates API documentation
Exercises
- Create an API endpoint that accepts a user ID in the path and returns user data
- Create an endpoint that accepts query parameters for filtering a list of items
- Add data validation to ensure a price parameter is always positive
- Create a form that uploads a file and returns information about the uploaded file
- Implement an endpoint that creates a new resource and returns appropriate HTTP status codes
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)