Mastering Dependency Injection in FastAPI

FastAPI is a modern, fast (high-performance), web framework for building APIs with Python based on standard Python type hints. One of the most powerful features of FastAPI is its support for dependency injection. Dependency injection is a design pattern that allows you to provide the dependencies of a function or class from the outside, rather than having the function or class create them itself. This makes the code more modular, testable, and easier to maintain. In this blog post, we will explore the fundamental concepts of dependency injection in FastAPI, how to use it, common practices, and best practices.

Table of Contents

  1. Fundamental Concepts of Dependency Injection
  2. Usage Methods in FastAPI
  3. Common Practices
  4. Best Practices
  5. Conclusion
  6. References

Fundamental Concepts of Dependency Injection

What is Dependency Injection?

Dependency injection is a technique where objects receive their dependencies (objects they depend on) from an external source rather than creating them internally. For example, if a class A needs an instance of class B to function, instead of creating an instance of B inside A, an instance of B is passed to A as an argument.

Why Use Dependency Injection?

  • Modularity: It makes the code more modular by separating the concerns of creating objects and using them.
  • Testability: It is easier to write unit tests because you can easily replace real dependencies with mock objects.
  • Maintainability: Changes to the way dependencies are created or managed can be made in one place without affecting the code that uses the dependencies.

Usage Methods in FastAPI

Basic Dependency Injection

In FastAPI, dependencies are functions that are called before the route function. They can return values that are then passed as arguments to the route function.

from fastapi import Depends, FastAPI

app = FastAPI()

# Dependency function
def get_query_param(q: str = None):
    return q

@app.get("/items/")
async def read_items(q: str = Depends(get_query_param)):
    if q:
        return {"query": q}
    return {"message": "No query parameter provided"}

In this example, the get_query_param function is a dependency. It extracts the query parameter q from the request. The read_items route function uses this dependency by passing it to the Depends function.

Class-based Dependencies

You can also use classes as dependencies. The class should have a __call__ method that returns the value to be used as the dependency.

from fastapi import Depends, FastAPI

app = FastAPI()

class QueryParam:
    def __init__(self, q: str = None):
        self.q = q

    def __call__(self):
        return self.q

@app.get("/items/")
async def read_items(q: str = Depends(QueryParam())):
    if q:
        return {"query": q}
    return {"message": "No query parameter provided"}

Nested Dependencies

Dependencies can be nested. A dependency function can itself depend on other dependencies.

from fastapi import Depends, FastAPI

app = FastAPI()

def get_sub_dependency():
    return "sub_dependency_value"

def get_main_dependency(sub_dependency: str = Depends(get_sub_dependency)):
    return f"Main dependency with sub: {sub_dependency}"

@app.get("/items/")
async def read_items(main_dependency: str = Depends(get_main_dependency)):
    return {"dependency_result": main_dependency}

Common Practices

Reusing Dependencies

Dependencies can be reused across multiple route functions. This is useful when you have common functionality that needs to be executed before multiple routes.

from fastapi import Depends, FastAPI

app = FastAPI()

def common_dependency():
    return {"message": "Common dependency executed"}

@app.get("/items/")
async def read_items(common: dict = Depends(common_dependency)):
    return {"items": ["item1", "item2"], "common": common}

@app.get("/users/")
async def read_users(common: dict = Depends(common_dependency)):
    return {"users": ["user1", "user2"], "common": common}

Dependency for Authentication

One common use case of dependencies is for authentication. You can create a dependency function that checks if the user is authenticated and raises an exception if not.

from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()
security = HTTPBasic()

def authenticate_user(credentials: HTTPBasicCredentials = Depends(security)):
    # Here you would implement real authentication logic
    if credentials.username != "admin" or credentials.password != "password":
        raise HTTPException(status_code=401, detail="Invalid credentials")
    return credentials.username

@app.get("/protected/")
async def protected_route(user: str = Depends(authenticate_user)):
    return {"message": f"Hello, {user}! This is a protected route."}

Best Practices

Use Type Hints

Always use type hints in your dependency functions and route functions. This makes the code more readable and helps FastAPI generate accurate OpenAPI documentation.

Keep Dependencies Simple

Dependencies should have a single responsibility. Avoid creating overly complex dependencies that do too many things. This makes the code harder to understand and maintain.

Use Dependency Injection for Testing

When writing unit tests for your FastAPI application, use dependency injection to replace real dependencies with mock objects. This allows you to isolate the unit of code you are testing.

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

# Mock dependency function
def mock_get_query_param():
    return "test_query"

def test_read_items():
    response = client.get("/items/", params={"q": "test_query"}, dependencies=[Depends(mock_get_query_param)])
    assert response.status_code == 200
    assert response.json() == {"query": "test_query"}

Conclusion

Dependency injection is a powerful feature in FastAPI that can significantly improve the modularity, testability, and maintainability of your code. By understanding the fundamental concepts, usage methods, common practices, and best practices, you can effectively use dependency injection in your FastAPI applications. Whether it’s for extracting query parameters, performing authentication, or reusing common functionality, dependency injection provides a flexible and efficient way to manage dependencies in your web API.

References