This lesson on Dependency Injection in FastAPI is hands-on and example-driven. You will refactor tightly coupled endpoint handlers by using FastAPI's dependency injection system with Depends and typing.Annotated. This approach isolates service construction logic, enabling clean unit testing, modular architecture, and easy swapping of dependencies.
What You'll Be Able To Do
- Identify tight coupling anti-patterns where service instances are created directly inside route handlers.
- Define reusable dependency factory functions that instantiate and return service objects.
- Inject dependencies into route functions using FastAPI's
Depends()helper. - Refactor dependency definitions using
typing.Annotatedfor clean and readable type declarations. - Implement multiple independent services across endpoints following Inversion of Control (IoC) patterns.
Detailed Concept Walkthrough
1. Inversion of Control and Loose Coupling
Inversion of Control (IoC) delegates object creation and lifecycle management to an external framework rather than forcing a consumer function to instantiate its own dependencies. This design pattern decouples application components, transforming rigid procedural code into modular, easily maintainable architecture.
- Architectural Shift: Instead of a route handler calling a constructor directly, the route declares what it needs via parameters, and the framework provides the ready-to-use instance. This separates the logic of using a service from the logic of configuring and creating it.
- Coupling Mechanics: Tight coupling binds an endpoint directly to a specific concrete class implementation, requiring edits to the route whenever constructor arguments or setup logic change. Loose coupling ensures the endpoint only cares about interacting with the injected interface.
- Maintainability & Testability: Decoupled systems allow you to swap real production services (like network loggers or email clients) with lightweight mocks or test doubles without modifying route code.
# Anti-Pattern: Tight Coupling
class Logger:
def log(self, message: str):
print(f"LOG: {message}")
# The endpoint is tightly bound to Logger's instantiation
def bad_route():
logger = Logger() # Manual instantiation inside route
logger.log("Processing request")
return {"status": "ok"}
Key Takeaway: Inject dependencies externally so route handlers focus exclusively on HTTP request handling rather than service lifecycle management.
2. Declaring Dependencies With Depends
fastapi.Depends acts as a marker telling FastAPI to execute a specified callable and inject its return value into the route parameter at runtime.
- Mechanism: You create a factory function (e.g.,
get_logger) that handles instantiation. When a route handler lists a parameter withDepends(get_logger)as its default value, FastAPI executesget_loggerfor incoming requests and assigns the result to that parameter. - Under the Hood: FastAPI inspects route parameter signatures during startup using Python's reflection features. When a request matches a route, FastAPI resolves all declared
Dependscallables before executing the route handler body. - Syntax Rule:
Dependstakes a callable (a function, class, or callable object) as its argument without calling it directly (passget_logger, notget_logger()).
from fastapi import FastAPI, Depends
app = FastAPI()
class Logger:
def log(self, message: str) -> None:
print(f"[INFO]: {message}")
def get_logger() -> Logger:
# Factory function responsible for instantiation
return Logger()
@app.get("/log-event")
def log_event(logger: Logger = Depends(get_logger)):
logger.log("Event triggered via Depends")
return {"message": "Logged successfully"}
Key Takeaway: Use
Depends(factory_func)in parameter defaults to delegate execution and injection to FastAPI's dependency resolver.
3. Type-Safe Dependencies With Annotated
typing.Annotated bundles type metadata with dependency declarations, enabling reusable, self-documenting type aliases that clean up endpoint signatures.
- Mechanism: Python's
Annotated[Type, Metadata]allows attaching framework-specific metadata likeDepends()directly to the type definition. FastAPI reads metadata insideAnnotatedto resolve dependencies identically to default parameter declarations. - Best Practice: By assigning
Annotated[Logger, Depends(get_logger)]to a custom type alias (e.g.,LoggerDep), you eliminate repeated boilerplate across multiple route signatures and keep default values clean for static analysis tools. - Refactoring Workflow: Creating type aliases standardizes dependency injection across the codebase and prevents accidental divergences in parameter definitions across routes.
from typing import Annotated
from fastapi import FastAPI, Depends
app = FastAPI()
class Logger:
def log(self, message: str) -> None:
print(f"LOG: {message}")
def get_logger() -> Logger:
return Logger()
# Reusable type alias containing dependency metadata
LoggerDep = Annotated[Logger, Depends(get_logger)]
@app.get("/items")
def read_items(logger: LoggerDep):
logger.log("Fetching items")
return ["item1", "item2"]
Key Takeaway: Declare reusable
Annotatedaliases to combine type safety with clean, DRY dependency injection syntax.
4. Injecting Multiple Distinct Services
FastAPI allows endpoints to declare multiple distinct dependencies simultaneously, managing the creation and delivery of all required services automatically.
- Execution Flow: When a route signature contains several injected parameters (e.g., a logger and an email client), FastAPI executes each associated factory function and injects the resulting objects in parallel before running the handler.
- Service Isolation: Each service remains fully isolated with its own factory logic, preventing cross-contamination of setup code and simplifying independent refactoring.
- Extensibility: Adding new system capabilities—such as an
EmailServicealongside aLogger—requires only writing a new factory and adding an annotated parameter to the target routes.
from typing import Annotated
from fastapi import FastAPI, Depends
app = FastAPI()
class EmailService:
def send(self, to: str, body: str) -> None:
print(f"Sending '{body}' to {to}")
def get_email_service() -> EmailService:
return EmailService()
EmailDep = Annotated[EmailService, Depends(get_email_service)]
@app.post("/notify")
def notify_user(email_service: EmailDep):
email_service.send("user@example.com", "Welcome to the platform!")
return {"status": "Notification sent"}
Key Takeaway: Compose complex endpoint behaviors cleanly by injecting multiple decoupled services via separate dependency factories.
Topics Covered in Dependency Injection in FastAPI
- Conceptual Framing & Definitions (0:00 - 1:56) — The instructor defines Dependency Injection and Inversion of Control, contrasting manual instantiation with external injection.
- Architectural Rationale: Coupling (1:56 - 4:07) — The lesson analyzes the architectural differences and maintainability benefits of loose coupling compared to tightly coupled legacy code.
- Initial Project Setup (4:07 - 5:01) — The development environment is configured by installing dependencies and importing core FastAPI components.
- Manual Instantiation Anti-Pattern (5:01 - 6:05) — The instructor writes a sample Logger class and demonstrates the anti-pattern of instantiating it directly inside a route.
- Implementing Depends and Annotated (6:05 - 7:51) — The route is refactored using a factory function with Depends and modernized using Python's Annotated type hints.
- Injecting Secondary Service (7:51 - 9:27) — The dependency injection pattern is reinforced by defining and injecting an EmailService into a new endpoint.
FastAPI Cheat Sheet
-
Depends(callable)— Declares a callable dependency for FastAPI to resolvelogger: Logger = Depends(get_logger) -
Annotated[Type, Depends(callable)]— Creates a typed dependency alias with injection metadataLoggerDep = Annotated[Logger, Depends(get_logger)] -
pip install fastapi uvicorn— Installs FastAPI framework and Uvicorn ASGI serverpip install fastapi uvicorn -
uvicorn main:app --reload— Starts development server with live reload enableduvicorn main:app --reload
Comparison Table
| Aspect | Manual Instantiation | Dependency Injection |
|---|---|---|
| Coupling | Tightly coupled to concrete classes | Loosely coupled via external factories |
| Creation Location | Inside the route handler body | External factory resolved by framework |
| Testability | Difficult to mock without monkeypatching | Trivially swappable with test doubles |
| Code Duplication | Setup duplicated across every endpoint | Centralized inside reusable factory functions |
Common Pitfalls
- Mistake: Calling the dependency function inside Depends like Depends(get_logger()). Avoid: Pass the callable reference without parentheses using Depends(get_logger).
- Mistake: Instantiating service classes directly inside the route function body. Avoid: Create a dedicated factory function and inject it using Depends.
- Mistake: Duplicating long Annotated type definitions across every endpoint. Avoid: Define a top-level type alias like LoggerDep for clean reuse.
FAQs
- Why should I use Annotated instead of setting Depends directly in default arguments?
Using
Annotatedkeeps parameter defaults clear, works better with standard type checkers, and lets you create reusable type aliases. - Does FastAPI call the dependency factory function on every incoming request? Yes, by default FastAPI executes the dependency callable for each request that hits an endpoint declaring that dependency.
- Can a dependency factory function itself take other dependencies? Yes, FastAPI resolves sub-dependencies recursively, building a full dependency tree before calling the endpoint.