Skip to main content

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.

python
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:

  1. We define a dependency function pagination_params that processes common pagination parameters
  2. We use FastAPI's Query to add validation and documentation
  3. The function returns a dictionary with computed pagination values
  4. 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:

python
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:

python
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:

  1. verify_api_key validates the API key from query parameters
  2. get_user_permissions depends on the API key and returns permissions
  3. filter_by_permissions uses both the permissions and additional query parameters
  4. 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:

python
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​

  1. Group related parameters: Create dependencies that group logically related parameters.

  2. Use Pydantic for complex validations: For more complex validation logic, consider using Pydantic models.

python
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
  1. Default values: Provide sensible defaults for optional parameters.

  2. Documentation: Use the description parameter in Query() to document your parameters.

  3. Reuse dependencies: Create a library of common parameter dependencies to reuse across your application.

  4. Cache expensive computations: If your dependency does expensive processing, consider caching the results.

python
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​

Exercises​

  1. Create a query dependency that validates date ranges (start_date and end_date) and converts string dates to datetime objects.

  2. Build a reusable filtering system for a blog API that handles filtering posts by author, category, tags, and publication date.

  3. Implement a dependency that handles geographical search parameters (latitude, longitude, radius) and validates the inputs.

  4. 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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)