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_paramsthat processes common pagination parameters
- We use FastAPI's Queryto 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_keyvalidates the API key from query parameters
- get_user_permissionsdepends on the API key and returns permissions
- filter_by_permissionsuses 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 descriptionparameter 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. 
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!