FastAPI Dependencies Introduction
When building web applications, we often need to reuse code for common operations like:
- Authenticating users
- Enforcing permissions
- Database connections
- Parameter validation
- Common processing logic
FastAPI's dependency injection system provides an elegant way to handle these scenarios, making your code more maintainable, testable, and cleaner.
What Are Dependencies in FastAPI?
Dependencies in FastAPI are functions (or callable objects) that can be "injected" into your path operation functions. When a request is made, FastAPI will:
- Identify the required dependencies
- Execute them in the right order
- Provide their results to your endpoint function
Think of dependencies as building blocks that handle specific tasks before your main endpoint logic runs.
Basic Dependency Example
Let's start with a simple example:
from fastapi import FastAPI, Depends
app = FastAPI()
# This is our dependency
def get_query_parameter(q: str = None):
return q or "No query parameter provided"
@app.get("/items/")
async def read_items(query_result: str = Depends(get_query_parameter)):
return {"query_result": query_result}
In this example:
get_query_parameter
is a dependency function- We inject it into our path operation using
Depends()
- The return value from
get_query_parameter
is passed toread_items
asquery_result
If you visit /items/?q=test
, you'll see:
{
"query_result": "test"
}
If you visit /items/
without a query parameter:
{
"query_result": "No query parameter provided"
}
How Dependency Injection Works
When someone makes a request to your endpoint, FastAPI:
- Calls the dependency function (e.g.,
get_query_parameter
) - Gets the result
- Assigns that result to the parameter in your path operation function (e.g.,
query_result
) - Then executes your endpoint function with that parameter
Types of Dependencies
1. Simple Dependencies
As we've already seen, simple dependencies are regular functions:
def get_db():
db = DBSession()
try:
yield db # This returns the database connection
finally:
db.close() # This ensures the connection is closed after the endpoint is processed
2. Class-based Dependencies
You can also use classes with a __call__
method:
class QueryChecker:
def __call__(self, q: str = None):
if q:
return q
return "No query provided"
query_checker = QueryChecker()
@app.get("/check/")
async def check_query(result: str = Depends(query_checker)):
return {"result": result}
3. Dependencies with Sub-dependencies
Dependencies can themselves depend on other dependencies:
def get_query(q: str = None):
return q or "default"
def process_query(query: str = Depends(get_query)):
return f"processed: {query}"
@app.get("/process/")
async def process_endpoint(final_result: str = Depends(process_query)):
return {"result": final_result}
Practical Example: Authentication System
Let's create a simple authentication dependency:
from fastapi import FastAPI, Depends, HTTPException, Header
from typing import Optional
app = FastAPI()
# Our "database" of API keys
API_KEYS = {
"valid_key123": "user1",
"another_key456": "user2"
}
def get_current_user(api_key: Optional[str] = Header(None)):
if api_key is None:
raise HTTPException(status_code=401, detail="API Key header is missing")
if api_key not in API_KEYS:
raise HTTPException(status_code=403, detail="Invalid API Key")
return {"username": API_KEYS[api_key]}
@app.get("/protected/")
async def protected_route(current_user: dict = Depends(get_current_user)):
return {
"message": "You accessed a protected route",
"user": current_user["username"]
}
In this example:
get_current_user
is our dependency function- It checks if an API key is present and valid
- If valid, it returns user information
- If invalid or missing, it raises an HTTP exception
- Our endpoint receives the user information when authentication succeeds
Benefits of Dependencies
- Code Reuse: Write once, use everywhere
- Separation of Concerns: Isolate authentication, validation, etc.
- Testability: Dependencies can be easily mocked for testing
- Maintainability: Centralized logic is easier to update
- Readability: Endpoint functions stay focused on their primary logic
Common Use Cases
- Database connections
- Authentication and authorization
- Logging
- Request validation
- Rate limiting
- Feature flags
- Caching mechanisms
Dependency Performance Considerations
When multiple endpoints use the same dependency, FastAPI optimizes performance by caching certain dependencies. This is controlled through the use_cache
parameter:
@app.get("/no-cache/")
async def no_cache_endpoint(fresh_data: dict = Depends(get_data, use_cache=False)):
return fresh_data
Summary
FastAPI's dependency injection system is a powerful tool that helps you:
- Create modular, reusable components
- Keep your code DRY (Don't Repeat Yourself)
- Separate concerns in your application
- Write cleaner path operation functions
By understanding dependencies, you're taking a significant step toward writing maintainable and professional APIs with FastAPI.
Exercises
- Create a dependency that validates a user's age from a query parameter and ensures it's at least 18
- Implement a dependency that logs all requests to an endpoint, including request method and path
- Create a chain of dependencies that: validates a token, fetches a user from a database, and checks if the user has admin privileges
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)