FastAPI Dependency Injection: Replacing Globals With Depends

Module-level globals for database connections, email clients, and clocks make FastAPI handlers impossible to unit-test - every test hits real services. FastAPI's Depends() system turns those globals into swappable dependencies, so tests inject fakes without monkey-patching.

Backend Engineerpythonfastapidependency-injection

The problem with globals in FastAPI handlers

A common early-stage pattern is to initialise resources at module scope and have handlers reach for them directly:

import psycopg2
from datetime import datetime, timezone

DB_CONN = psycopg2.connect(DATABASE_URL)
EMAIL_CLIENT = FakeEmailClient()

@app.post("/api/orders/{order_id}/confirm")
def confirm_order(order_id: int):
    now = datetime.now(timezone.utc)   # global call, can't be pinned
    cur = DB_CONN.cursor()             # global, can't be swapped
    ...
    EMAIL_CLIENT.send(...)             # global, sends real mail in tests

Parallel tests stomp on shared state, and there is no seam to inject a fake without reaching into module.__dict__.

Define dependency factories

Replace each global with a plain function that returns the resource. FastAPI calls it once per request:

from fastapi import Depends

_db_conn = psycopg2.connect(DATABASE_URL)  # private - only the factory touches it
_email_client = FakeEmailClient()

def get_db_conn():
    return _db_conn

def get_email_client() -> FakeEmailClient:
    return _email_client

def get_clock() -> datetime:
    return datetime.now(timezone.utc)

Wire handlers with Depends()

Each handler declares what it needs as typed parameters. FastAPI resolves them:

@app.post("/api/orders/{order_id}/confirm")
def confirm_order(
    order_id: int,
    conn=Depends(get_db_conn),
    emailer: FakeEmailClient = Depends(get_email_client),
    now: datetime = Depends(get_clock),
):
    cur = conn.cursor()
    cur.execute(
        "UPDATE orders SET status='confirmed', confirmed_at=%s WHERE id=%s "
        "RETURNING customer_email, product",
        (now, order_id),
    )
    row = cur.fetchone()
    conn.commit()
    cur.close()
    if not row:
        raise HTTPException(status_code=404, detail="order not found")
    emailer.send(to=row[0], subject=f"Order #{order_id} confirmed",
                 body=f"Your {row[1]} is confirmed at {now.isoformat()}")
    return {"order_id": order_id, "confirmed_at": now.isoformat()}

Swap dependencies for testing with dependency_overrides

app.dependency_overrides is the canonical test seam - no monkey-patching needed:

@app.post("/_set_clock")
def set_clock(body: dict):
    iso = body.get("iso")
    if not iso:
        app.dependency_overrides.pop(get_clock, None)
        return {"cleared": True}
    fixed = datetime.fromisoformat(iso)
    app.dependency_overrides[get_clock] = lambda: fixed
    return {"set": fixed.isoformat()}

In a test client, pin the clock and assert the response reflects the fixed time - with no real database or email involved when you override those too.

Key rules

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

How does FastAPI dependency injection work?

You define a plain function that returns a resource (db connection, client, etc.), then declare it in a handler as a parameter annotated with Depends(your_function). FastAPI calls the function per request and passes the result to the handler automatically.

How do I replace a FastAPI dependency in tests?

Use app.dependency_overrides - a dict where the key is the original dependency function and the value is the replacement callable. For example: app.dependency_overrides[get_clock] = lambda: fixed_datetime. Clear it after the test.

When should I use yield in a FastAPI dependency?

Use yield when the dependency needs cleanup after the response is sent - for example, closing a database cursor or releasing a lock. Everything before the yield runs on the way in; everything after runs as teardown.

Keep learning

Return the Correct HTTP Status CodeBackend projectRepair Broken API PaginationBackend projectAdd CORS Middleware to an Express APIBackend projectBackend roadmapStep by step to hiredBackend interview questionsSTAR answersAll Backend projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →