The Magic of Dependency Injection in FastAPI
Imagine building a LEGO masterpiece. Each brick represents a component of your project, meticulously placed to build something incredible. Now, think of dependency injection as the master blueprint that tells you where each LEGO piece goes. It’s that neat trick in software development which makes your code easier to manage, maintain, and test. And guess what? FastAPI has this built-in feature that’s as easy to use as snapping LEGO bricks together.
Understanding Dependency Injection
Dependency injection, in simple terms, is like outsourcing tasks. Instead of cooking the meal yourself, you hire a chef. The chef (dependency) does the cooking, while you (the main object) just reap the benefits of a gourmet meal. This separation makes your life easier and your code more adaptable.
FastAPI uses a nifty function called Depends
to handle dependency injection. Let’s dive right into it.
Basic Example of Dependency Injection
Imagine you have multiple endpoints that need the same query parameters. Continuously repeating the same code in every function is like reinventing the wheel. Instead, you could write a single dependency function to handle it all.
from fastapi import FastAPI, Depends
from typing import Union, Annotated
app = FastAPI()
async def common_parameters(q: Union[str, None] = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
return commons
@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
return commons
In this scenario, our common_parameters
function serves as our chef, preparing the query parameters. Instead of handling this in each endpoint, we use Depends(common_parameters)
to inject the prepped meal. This reduces redundancy and centralizes changes. Adjust one function, and all dependent endpoints evolve together.
Managing Database Connections
Who wants to deal with database connections in every single endpoint? Picture having to dial up your database for every query. Tedious, right? Here’s a way to get someone else to handle that call for you.
from fastapi import FastAPI, Depends
from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
app = FastAPI()
def get_db():
db = scoped_session(sessionmaker(bind=engine))
try:
yield db
finally:
db.close()
@app.get("/items/")
async def read_items(db: scoped_session = Depends(get_db)):
items = db.query(Item).all()
return items
Our get_db
function establishes the database connection, and it’s injected into the read_items
endpoint. This nifty piece of code ensures that every time a request comes in, it’s handled efficiently, like having a personal assistant managing your phone calls.
Staying Secure with Dependency Injection
Security is vital. It’s like having a bouncer at the club entrance, only letting in verified guests. Dependency injection can help manage this easily across multiple endpoints.
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
app = FastAPI()
oauth2_scheme = HTTPBearer()
async def get_current_user(token: HTTPAuthorizationCredentials = Depends(oauth2_scheme)):
if not token:
raise HTTPException(status_code=401, detail="Unauthorized")
# Validate the token and return the user
user = validate_token(token.credentials)
if not user:
raise HTTPException(status_code=401, detail="Unauthorized")
return user
@app.get("/users/me/")
async def read_users_me(current_user: User = Depends(get_current_user)):
return {"username": current_user.username}
Here, get_current_user
acts as our security detail, checking tokens and validating users before they access the VIP area. By using Depends(get_current_user)
in the read_users_me
endpoint, you ensure only the right people get through.
Reusing Dependencies Across Multiple Endpoints
Imagine you have a cooking technique you want to use in several recipes. Instead of rewriting it each time, you just refer back to it. Dependency injection lets you do exactly that.
from fastapi import FastAPI, Depends
from typing import Union, Annotated
app = FastAPI()
async def common_parameters(q: Union[str, None] = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
return commons
@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
return commons
@app.get("/products/")
async def read_products(commons: Annotated[dict, Depends(common_parameters)]):
return commons
The common_parameters
function is like a universal recipe. It’s reused in multiple endpoints - read_items
, read_users
, and read_products
. Any update to common_parameters
automatically reflects across all dependent endpoints, making your life much simpler.
Classes for Dependencies
Sometimes, a simple function won’t cut it—you need a chef with multiple skills. This is where classes come in handy.
from fastapi import FastAPI, Depends
app = FastAPI()
class DependencyClass:
def __init__(self, id: str, name: str, age: int):
self.id = id
self.name = name
self.age = age
@app.get("/user/")
async def read_user(dep: DependencyClass = Depends(DependencyClass)):
return dep.__dict__
@app.get("/admin/")
async def read_admin(dep: DependencyClass = Depends(DependencyClass)):
return dep.__dict__
The DependencyClass
here is our master chef with a plethora of culinary skills. It manages complex dependencies and then injects them neatly into read_user
and read_admin
endpoints.
Caching Dependencies
Imagine prepping a meal that takes an hour, but once prepared, you can serve it instantly to multiple guests. That’s exactly what caching does—saving time and resources.
from fastapi import FastAPI, Depends
from typing import Union, Annotated
app = FastAPI()
async def common_parameters(q: Union[str, None] = None, skip: int = 0, limit: int = 100):
# Simulate an expensive operation
import time
time.sleep(1)
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
return commons
@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
return commons
In this example, the common_parameters
function is our slow-cooked meal. By default, FastAPI caches this within a request, so the meal only gets cooked once, but can serve multiple endpoints instantly.
Wrapping Up
Dependency injection in FastAPI is a bit of magic that transforms how you handle shared logic, database connections, and security checks. It’s like having a team of specialized chefs, each handling their part of the meal, ensuring consistency and quality.
So, whether you’re juggling simple tasks, managing complex databases, or ensuring top-notch security, FastAPI’s dependency injection with Depends
makes it all seem like a walk in the park. Your code gets a touch of modularity, maintainability, and a good dose of efficiency. It’s like snapping together LEGO pieces—effortless, reliable, and endlessly adaptable.