FastAPI Query Dependencies
Introduction
When building APIs with FastAPI, you'll often need to process query parameters - those values that appear after the question mark in URLs (like ?page=2&size=10
). While you can handle these directly in your route functions, FastAPI offers a more elegant solution through query dependencies.
Query dependencies allow you to:
- Extract and validate query parameters in reusable components
- Apply consistent parameter processing across multiple endpoints
- Keep your route function code clean and focused on business logic
- Apply automatic documentation for your API parameters
In this tutorial, we'll explore how to leverage FastAPI's dependency injection system specifically for query parameters.
Basic Query Dependencies
Let's start with a simple example of a query dependency. Imagine we're building an API that needs pagination in several endpoints.
from fastapi import FastAPI, Depends, Query
app = FastAPI()
# Define our dependency function
def pagination_params(
page: int = Query(1, ge=1, description="Page number"),
size: int = Query(10, ge=1, le=100, description="Items per page")
):
# We can add validation logic beyond what Query() provides
return {"skip": (page - 1) * size, "limit": size, "page": page}
@app.get("/items/")
def read_items(pagination: dict = Depends(pagination_params)):
# Use pagination parameters
skip = pagination["skip"]
limit = pagination["limit"]
# In a real application, you'd fetch items from a database
items = [f"Item {i}" for i in range(skip, skip + limit)]
return {
"items": items,
"page": pagination["page"],
"limit": limit
}
In this example:
- We define a dependency function
pagination_params
that processes common pagination parameters - We use FastAPI's
Query
to add validation and documentation - The function returns a dictionary with computed pagination values
- Our route function receives the processed parameters via
Depends()
Type Hinting with Classes
For more complex query dependencies, we can use Pydantic models or custom classes for better type hinting:
from fastapi import FastAPI, Depends, Query
from typing import Optional
app = FastAPI()
class PaginationParams:
def __init__(
self,
page: int = Query(1, ge=1, description="Page number"),
size: int = Query(10, ge=1, le=100, description="Items per page")
):
self.page = page
self.size = size
self.skip = (page - 1) * size
self.limit = size
class ProductFilterParams:
def __init__(
self,
category: Optional[str] = Query(None, description="Filter by category"),
min_price: Optional[float] = Query(None, ge=0, description="Minimum price"),
max_price: Optional[float] = Query(None, ge=0, description="Maximum price"),
in_stock: bool = Query(True, description="Filter by availability")
):
self.category = category
self.min_price = min_price
self.max_price = max_price
self.in_stock = in_stock
@app.get("/products/")
def read_products(
pagination: PaginationParams = Depends(),
filters: ProductFilterParams = Depends()
):
# In a real app, you would use these parameters to query a database
# Example of using pagination params
skip = pagination.skip
limit = pagination.limit
# Example of using filter params
category_filter = f"Category filter: {filters.category}" if filters.category else "No category filter"
price_range = f"Price range: {filters.min_price} to {filters.max_price}" if filters.min_price or filters.max_price else "No price range filter"
return {
"pagination": {
"page": pagination.page,
"size": pagination.size,
"skip": skip,
"limit": limit,
},
"filters_applied": {
"category": category_filter,
"price": price_range,
"in_stock": filters.in_stock
},
"results": [
{"id": 1, "name": "Example Product 1"},
{"id": 2, "name": "Example Product 2"},
]
}
Notice how we use classes to organize related parameters. This approach provides:
- Better code organization
- Type checking in your IDE
- Pre-computed values (like
skip
) - Clear documentation in the OpenAPI schema
Chaining Dependencies
One of the most powerful features of FastAPI's dependency system is the ability to chain dependencies. Let's see an example with query parameters:
from fastapi import FastAPI, Depends, Query, HTTPException
from typing import Optional, List
app = FastAPI()
# First-level dependency
def verify_api_key(api_key: str = Query(..., description="Your API key")):
# In a real app, you would validate against stored keys
if api_key != "valid_key":
raise HTTPException(status_code=401, detail="Invalid API key")
return api_key
# Second-level dependency
def get_user_permissions(api_key: str = Depends(verify_api_key)):
# In a real app, you would look up permissions based on the API key
return ["read", "list"]
# Third-level dependency that uses query parameters
def filter_by_permissions(
permissions: List[str] = Depends(get_user_permissions),
require_write: bool = Query(False, description="Requires write permission")
):
if require_write and "write" not in permissions:
raise HTTPException(
status_code=403,
detail="Write permission required for this operation"
)
return permissions
@app.get("/protected-resource/")
def access_protected_resource(
permissions: List[str] = Depends(filter_by_permissions),
resource_id: Optional[int] = Query(None, description="Specific resource ID")
):
# Use the permissions and query parameters
return {
"resource_id": resource_id or "all resources",
"permissions": permissions,
"message": "Access granted to protected resource"
}
In this example:
verify_api_key
validates the API key from query parametersget_user_permissions
depends on the API key and returns permissionsfilter_by_permissions
uses both the permissions and additional query parameters- The route function combines all of these
Real-World Example: Search API
Let's implement a more realistic example of a search API with various query parameters:
from fastapi import FastAPI, Depends, Query, HTTPException
from typing import Optional, List
from enum import Enum
import datetime
app = FastAPI()
# Sample data
products = [
{"id": 1, "name": "Laptop", "category": "electronics", "price": 999.99, "created_at": "2023-01-15"},
{"id": 2, "name": "Smartphone", "category": "electronics", "price": 699.99, "created_at": "2023-02-20"},
{"id": 3, "name": "Coffee Mug", "category": "kitchenware", "price": 12.99, "created_at": "2023-03-05"},
{"id": 4, "name": "Chair", "category": "furniture", "price": 149.99, "created_at": "2023-01-25"},
{"id": 5, "name": "Headphones", "category": "electronics", "price": 199.99, "created_at": "2023-04-10"},
]
class SortField(str, Enum):
NAME = "name"
PRICE = "price"
DATE = "created_at"
class SortOrder(str, Enum):
ASC = "asc"
DESC = "desc"
class SearchFilters:
def __init__(
self,
query: Optional[str] = Query(None, min_length=2, description="Search query string"),
category: Optional[str] = Query(None, description="Filter by category"),
min_price: Optional[float] = Query(None, ge=0, description="Minimum price"),
max_price: Optional[float] = Query(None, ge=0, description="Maximum price"),
date_from: Optional[str] = Query(None, description="Created from date (YYYY-MM-DD)"),
date_to: Optional[str] = Query(None, description="Created to date (YYYY-MM-DD)"),
):
self.query = query
self.category = category
self.min_price = min_price
self.max_price = max_price
self.date_from = None
self.date_to = None
# Parse dates if provided
if date_from:
try:
self.date_from = datetime.datetime.strptime(date_from, "%Y-%m-%d").date()
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date_from format. Use YYYY-MM-DD")
if date_to:
try:
self.date_to = datetime.datetime.strptime(date_to, "%Y-%m-%d").date()
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date_to format. Use YYYY-MM-DD")
class PaginationParams:
def __init__(
self,
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(10, ge=1, le=100, description="Items per page"),
sort_by: SortField = Query(SortField.NAME, description="Field to sort by"),
sort_order: SortOrder = Query(SortOrder.ASC, description="Sort order")
):
self.page = page
self.skip = (page - 1) * page_size
self.limit = page_size
self.sort_by = sort_by
self.sort_order = sort_order
@app.get("/search/")
def search_products(
filters: SearchFilters = Depends(),
pagination: PaginationParams = Depends()
):
# Filter logic
filtered_products = products.copy()
# Apply text search
if filters.query:
query_lower = filters.query.lower()
filtered_products = [p for p in filtered_products if query_lower in p["name"].lower()]
# Apply category filter
if filters.category:
filtered_products = [p for p in filtered_products if p["category"] == filters.category]
# Apply price range
if filters.min_price is not None:
filtered_products = [p for p in filtered_products if p["price"] >= filters.min_price]
if filters.max_price is not None:
filtered_products = [p for p in filtered_products if p["price"] <= filters.max_price]
# Apply date filters
if filters.date_from or filters.date_to:
for p in filtered_products[:]: # Create a copy to safely remove items
product_date = datetime.datetime.strptime(p["created_at"], "%Y-%m-%d").date()
if filters.date_from and product_date < filters.date_from:
filtered_products.remove(p)
continue
if filters.date_to and product_date > filters.date_to:
filtered_products.remove(p)
# Apply sorting
sort_field = pagination.sort_by.value
reverse = pagination.sort_order == SortOrder.DESC
filtered_products.sort(key=lambda x: x[sort_field], reverse=reverse)
# Apply pagination
total_count = len(filtered_products)
paginated_products = filtered_products[pagination.skip:pagination.skip + pagination.limit]
# Return results
return {
"total": total_count,
"page": pagination.page,
"page_size": pagination.limit,
"sort_by": pagination.sort_by,
"sort_order": pagination.sort_order,
"results": paginated_products
}
This example demonstrates:
- Enum classes for constrained option sets
- Data validation and transformation
- Multiple dependency classes for different parameter groups
- Error handling for invalid inputs
- Database-like filtering, sorting and pagination
Best Practices for Query Dependencies
-
Group related parameters: Create dependencies that group logically related parameters.
-
Use Pydantic for complex validations: For more complex validation logic, consider using Pydantic models.
from fastapi import Depends
from pydantic import BaseModel, validator
class DateRangeParams(BaseModel):
start_date: str
end_date: str
@validator('end_date')
def end_date_must_be_after_start(cls, v, values):
if 'start_date' in values and v < values['start_date']:
raise ValueError('end_date must be after start_date')
return v
def get_date_range_params(params: DateRangeParams = Depends()):
return params
-
Default values: Provide sensible defaults for optional parameters.
-
Documentation: Use the
description
parameter inQuery()
to document your parameters. -
Reuse dependencies: Create a library of common parameter dependencies to reuse across your application.
-
Cache expensive computations: If your dependency does expensive processing, consider caching the results.
from fastapi import Depends, Request
async def get_settings(request: Request):
# Check if we have settings in the app state
if not hasattr(request.app.state, "settings"):
# Expensive operation to load settings
request.app.state.settings = load_settings_from_file()
return request.app.state.settings
@app.get("/config/")
async def get_config(settings = Depends(get_settings)):
return {"settings": settings}
Summary
FastAPI's query dependencies provide a powerful way to handle query parameters in your API endpoints. By using dependency injection for query parameters, you can:
- Keep your route functions clean and focused
- Reuse parameter processing logic across endpoints
- Apply consistent validation rules
- Improve code organization with parameter grouping
- Build complex parameter processing pipelines
Query dependencies are a critical part of FastAPI's overall dependency injection system, helping you build more maintainable, scalable, and well-documented APIs.
Additional Resources
- FastAPI's official documentation on dependencies
- FastAPI's Query parameters documentation
- Advanced dependency injection techniques
Exercises
-
Create a query dependency that validates date ranges (start_date and end_date) and converts string dates to datetime objects.
-
Build a reusable filtering system for a blog API that handles filtering posts by author, category, tags, and publication date.
-
Implement a dependency that handles geographical search parameters (latitude, longitude, radius) and validates the inputs.
-
Create a dependency that processes a "fields" query parameter to implement field selection (similar to GraphQL) for your API responses.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)