FastAPI Path Dependencies
Introduction
In FastAPI, path dependencies are a specialized form of dependency injection that allows you to apply dependencies specifically to path parameters in your route handlers. This powerful feature enables you to validate, process, and extract information from URL path parameters before your endpoint function is called.
Path dependencies help you:
- Validate path parameters before processing the request
- Convert path parameters to appropriate Python objects
- Implement security checks based on path parameters
- Keep your code DRY (Don't Repeat Yourself) by reusing validation logic
- Improve error handling for malformed path parameters
Let's dive into how path dependencies work and how they can enhance your FastAPI applications.
Basic Path Dependencies
What Are Path Dependencies?
Path dependencies in FastAPI are dependencies that are specifically tied to path parameters in your route paths. They allow you to process these parameters separately before your endpoint function executes.
Here's a simple example of a path dependency in action:
from fastapi import FastAPI, Path, HTTPException, Depends
app = FastAPI()
async def validate_item_id(item_id: int = Path(..., title="The ID of the item")):
if item_id <= 0:
raise HTTPException(status_code=400, detail="Item ID must be positive")
return item_id
@app.get("/items/{item_id}")
async def read_item(item_id: int = Depends(validate_item_id)):
return {"item_id": item_id, "message": "Item found"}
In this example, validate_item_id
is a dependency function that validates the item_id
path parameter. If the ID is not positive, it raises an HTTP exception. Otherwise, it returns the validated ID for use in the endpoint function.
Path Dependencies vs. Regular Dependencies
While regular dependencies can be used for various purposes, path dependencies are specifically designed to work with path parameters. Here's how they differ:
# Regular dependency
async def common_parameters(q: str = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
# Path dependency
async def validate_item_id(item_id: int = Path(..., gt=0)):
return item_id
@app.get("/items/{item_id}")
async def read_item(
commons: dict = Depends(common_parameters),
item_id: int = Depends(validate_item_id)
):
return {"item_id": item_id, **commons}
The key difference is that path dependencies typically process and validate path parameters, while regular dependencies can handle any type of request data.
Advanced Path Dependencies
Type Conversion with Path Dependencies
Path dependencies can automatically convert path parameters to appropriate Python types:
from fastapi import FastAPI, Path, Depends
from uuid import UUID
app = FastAPI()
async def validate_user_id(user_id: UUID = Path(...)):
return user_id
@app.get("/users/{user_id}")
async def get_user(user_id: UUID = Depends(validate_user_id)):
return {"user_id": str(user_id)}
If someone tries to access /users/not-a-uuid
, FastAPI will automatically return a validation error before the dependency function is even called.
Path Dependencies for Parameter Validation
You can use path dependencies to implement complex validation rules for your path parameters:
from fastapi import FastAPI, Path, HTTPException, Depends
import re
app = FastAPI()
async def validate_product_code(
product_code: str = Path(..., min_length=8, max_length=12)
):
pattern = r'^[A-Z]{2}-\d{4}-[A-Z]{2}$'
if not re.match(pattern, product_code):
raise HTTPException(
status_code=400,
detail="Invalid product code format. Expected format: XX-0000-XX"
)
return product_code
@app.get("/products/{product_code}")
async def get_product(product_code: str = Depends(validate_product_code)):
return {"product_code": product_code, "is_valid": True}
This example ensures that product codes follow a specific format (e.g., AB-1234-CD
).
Practical Applications of Path Dependencies
Resource Existence Checking
One common use case for path dependencies is to check if a requested resource exists:
from fastapi import FastAPI, Path, HTTPException, Depends
from typing import Dict
app = FastAPI()
# Simulated database
fake_db: Dict[int, Dict] = {
1: {"name": "Laptop", "price": 999.99},
2: {"name": "Smartphone", "price": 599.99},
3: {"name": "Tablet", "price": 299.99},
}
async def get_item_or_404(item_id: int = Path(..., gt=0)):
if item_id not in fake_db:
raise HTTPException(status_code=404, detail="Item not found")
return fake_db[item_id]
@app.get("/items/{item_id}")
async def read_item(item: dict = Depends(get_item_or_404)):
return item
@app.put("/items/{item_id}")
async def update_item(
item_data: dict,
current_item: dict = Depends(get_item_or_404)
):
# Update the item
for key, value in item_data.items():
current_item[key] = value
return current_item
This pattern prevents code duplication by centralizing the existence check for items across different endpoints.
Hierarchical Resource Access
Path dependencies are especially useful for handling hierarchical resources:
from fastapi import FastAPI, Path, HTTPException, Depends
from typing import Dict, List
app = FastAPI()
# Simulated database
fake_users = {
1: {"name": "Alice"},
2: {"name": "Bob"},
}
fake_user_items = {
1: [{"item_id": 1, "name": "Laptop"}, {"item_id": 2, "name": "Phone"}],
2: [{"item_id": 1, "name": "Tablet"}],
}
async def get_user_or_404(user_id: int = Path(..., gt=0)):
if user_id not in fake_users:
raise HTTPException(status_code=404, detail="User not found")
return {"user_id": user_id, **fake_users[user_id]}
async def get_user_item_or_404(
user_id: int = Path(..., gt=0),
item_id: int = Path(..., gt=0),
user: dict = Depends(get_user_or_404)
):
if user_id not in fake_user_items:
raise HTTPException(status_code=404, detail="User has no items")
for item in fake_user_items[user_id]:
if item["item_id"] == item_id:
return item
raise HTTPException(status_code=404, detail="Item not found for this user")
@app.get("/users/{user_id}")
async def read_user(user: dict = Depends(get_user_or_404)):
return user
@app.get("/users/{user_id}/items")
async def read_user_items(user: dict = Depends(get_user_or_404)):
if user["user_id"] not in fake_user_items:
return {"items": []}
return {"items": fake_user_items[user["user_id"]]}
@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(item: dict = Depends(get_user_item_or_404)):
return item
This example demonstrates how path dependencies can be used to validate nested resources like user items.
Authorization Based on Path Parameters
Path dependencies can also implement authorization logic based on path parameters:
from fastapi import FastAPI, Path, HTTPException, Depends, Header
from typing import Optional
app = FastAPI()
async def verify_project_access(
project_id: int = Path(...),
x_api_key: Optional[str] = Header(None)
):
# In a real app, you would check against a database
allowed_projects = {
"user1_api_key": [1, 2, 3],
"user2_api_key": [2, 4, 6]
}
if not x_api_key:
raise HTTPException(status_code=401, detail="API key is required")
if x_api_key not in allowed_projects:
raise HTTPException(status_code=403, detail="Invalid API key")
if project_id not in allowed_projects[x_api_key]:
raise HTTPException(
status_code=403,
detail="You don't have access to this project"
)
return project_id
@app.get("/projects/{project_id}")
async def get_project(project_id: int = Depends(verify_project_access)):
# Process the request only if the user has access to the project
return {"project_id": project_id, "data": "Project data here"}
This example checks if the user has access to a specific project based on their API key before allowing access to the endpoint.
Path Dependencies with Dependency Classes
For more complex validation logic, you can use classes with the __call__
method:
from fastapi import FastAPI, Path, HTTPException, Depends
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class ItemValidator:
def __init__(self, min_id: int = 1, include_price: bool = True):
self.min_id = min_id
self.include_price = include_price
async def __call__(self, item_id: int = Path(...)):
if item_id < self.min_id:
raise HTTPException(status_code=400, detail=f"Item ID must be >= {self.min_id}")
# Simulate database lookup
item = {"id": item_id, "name": f"Item {item_id}"}
if self.include_price:
item["price"] = 19.99 * item_id
return item
# Create different validators with different configurations
standard_validator = ItemValidator(min_id=1)
premium_validator = ItemValidator(min_id=100)
basic_validator = ItemValidator(min_id=1, include_price=False)
@app.get("/items/{item_id}")
async def get_item(item: dict = Depends(standard_validator)):
return item
@app.get("/premium-items/{item_id}")
async def get_premium_item(item: dict = Depends(premium_validator)):
return item
@app.get("/basic-items/{item_id}")
async def get_basic_item(item: dict = Depends(basic_validator)):
return item
This approach allows you to create reusable validation components with different configurations.
Chaining Path Dependencies
You can chain multiple dependencies to build complex validation pipelines:
from fastapi import FastAPI, Path, Query, HTTPException, Depends
from typing import Optional
app = FastAPI()
async def verify_positive_id(item_id: int = Path(..., gt=0)):
return item_id
async def get_item_from_db(
item_id: int = Depends(verify_positive_id),
q: Optional[str] = Query(None)
):
# Simulate database lookup
item = {"id": item_id, "name": f"Item {item_id}"}
if q:
item["query"] = q
return item
@app.get("/items/{item_id}")
async def read_item(item: dict = Depends(get_item_from_db)):
return item
In this example, get_item_from_db
depends on verify_positive_id
, creating a chain of dependencies.
Summary
Path dependencies in FastAPI are a powerful tool for handling path parameters in your applications. They allow you to:
- Validate and transform path parameters before your endpoint logic runs
- Reuse validation logic across multiple endpoints
- Implement access control based on resource identifiers
- Organize code in a clean, maintainable way
- Build complex validation pipelines through dependency chaining
By leveraging path dependencies, you can create more robust, maintainable, and secure FastAPI applications.
Additional Resources
- FastAPI Official Documentation on Dependencies
- Path Parameters and Numeric Validations
- Advanced Dependencies in FastAPI
Exercises
-
Create a path dependency that validates a username path parameter to ensure it contains only alphanumeric characters and is between 3-16 characters long.
-
Implement a path dependency that takes a product ID and checks if the product exists in a database. If it does, return the product; if not, return a 404 error.
-
Build a chained dependency system for a blog post application where:
- The first dependency validates that a post ID is a valid integer
- The second dependency checks if the post exists
- The third dependency verifies if the current user has permission to view the post
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)