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 [email protected]. I’d greatly appreciate your feedback! :)