Python Python

Sharing Logic Across Endpoints with FastAPI Dependency Injection

Dima Jul 26, 2026

Introduction

Every endpoint in a real API repeats the same handful of concerns: parse and validate pagination parameters, check the caller's credentials, obtain a database session, close it when done. Written inline, that boilerplate is copied into every route handler, so a fix to the auth check or the pagination limits must be made in a dozen places, and it is only a matter of time before one endpoint's copy drifts out of sync — an authentication check quietly missing from one route is exactly how security holes appear.

FastAPI's dependency injection system exists to declare these cross-cutting concerns once and attach them to any endpoint that needs them. A dependency is just a function; an endpoint requests it with Depends(...), and FastAPI runs the dependency before the handler, passing in its result. Because dependencies are ordinary functions, they can validate parameters, raise HTTP errors, manage resource lifecycles with setup and teardown, and depend on other dependencies — all resolved automatically. The route handler receives finished, validated inputs and contains only the logic unique to it.

This tutorial builds a small article API and factors its shared concerns into dependencies: a pagination dependency that parses and bounds query parameters, an API-key dependency that rejects unauthorized callers with a 401, and a database dependency that opens and reliably closes a session per request. It covers Depends, dependency-provided validation, dependencies that raise HTTPException, and yield dependencies for resource lifecycles.

Background

A dependency in FastAPI is a callable — usually a function — that FastAPI executes before a route handler and whose return value is injected into the handler. An endpoint declares a dependency by giving a parameter a default of Depends(the_dependency). FastAPI builds a dependency graph for each request: it resolves every declared dependency (and their sub-dependencies) first, then calls the handler with all results in place.

Because a dependency is a normal function, its own parameters participate in FastAPI's request parsing. A dependency parameter typed with Query(...) reads from the query string with validation; one typed with Header(...) reads from a request header. So a dependency can encapsulate and validate a whole category of input — the pagination dependency owns the limit/offset parameters and their bounds, and any endpoint that uses it inherits both.

Two behaviors make dependencies more than parameter helpers. A dependency may raise HTTPException, which FastAPI turns into an error response and which short-circuits the request before the handler runs — the basis for authentication and authorization checks. And a dependency may yield its value instead of returning it: code before the yield is setup, code after runs as teardown once the response is sent, which is how per-request resources like database sessions are opened and guaranteed to close.

Practical Scenario

A content platform exposes an article API. Its public endpoint lists articles with pagination, and an admin endpoint lists them for authenticated staff only. Both endpoints need the same pagination handling — a default page size, a maximum page size so a client cannot request ten thousand rows, and a non-negative offset — and both need a database session that must be closed after the request regardless of what happened. The admin endpoint additionally requires a valid API key, and an unauthenticated request must be rejected before any data is touched.

Written inline, the pagination parsing and bounds checking would be duplicated in both handlers, the session open/close would be copied and — inevitably — sometimes forgotten in an error path, and the auth check would live in the admin handler where a careless edit could remove it. Factoring each concern into a dependency puts the pagination rules, the session lifecycle, and the auth gate each in exactly one place, and every endpoint opts in by declaring Depends. This tutorial builds that structure.

The Problem

Consider the article listing written with its concerns inline. Create the file:

touch main.py


from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

ARTICLES = [{"id": i, "title": f"Article {i}"} for i in range(1, 11)]


@app.get("/articles")
def list_articles(request: Request):
    # pagination parsing and validation, inline
    try:
        limit = int(request.query_params.get("limit", 5))
        offset = int(request.query_params.get("offset", 0))
    except ValueError:
        raise HTTPException(status_code=400, detail="limit/offset must be integers")
    if limit < 1 or limit > 50 or offset < 0:
        raise HTTPException(status_code=400, detail="invalid pagination")

    items = ARTICLES[offset: offset + limit]
    return {"count": len(items), "items": items}


Run the server and query it:

uvicorn main:app --port 9000
curl -s "http://127.0.0.1:9000/articles?limit=3&offset=2"


{"count":3,"items":[{"id":3,"title":"Article 3"},{"id":4,"title":"Article 4"},{"id":5,"title":"Article 5"}]}


It works, but every concern is hand-rolled inside the handler. The pagination block reads raw strings from request.query_params, converts them by hand, catches the conversion error, and checks the bounds manually — logic that will be copied verbatim into the admin endpoint and every future listing endpoint. There is no database session management yet, and when it is added it will be another block to duplicate and another try/finally to remember. The handler's actual job — slicing the articles — is three lines buried under fifteen lines of plumbing. The rest of this tutorial lifts each concern into a dependency.

A Dependency for Pagination

The pagination logic becomes a function whose parameters FastAPI validates, returning a clean result. Replace the contents of main.py:

from fastapi import FastAPI, Depends, Query

app = FastAPI()

ARTICLES = [{"id": i, "title": f"Article {i}"} for i in range(1, 11)]


def pagination(limit: int = Query(5, le=50), offset: int = Query(0, ge=0)):
    return {"limit": limit, "offset": offset}


@app.get("/articles")
def list_articles(page=Depends(pagination)):
    items = ARTICLES[page["offset"]: page["offset"] + page["limit"]]
    return {
        "count": len(items),
        "limit": page["limit"],
        "offset": page["offset"],
        "items": items,
    }


curl -s "http://127.0.0.1:9000/articles"


{"count":5,"limit":5,"offset":0,"items":[{"id":1,"title":"Article 1"},{"id":2,"title":"Article 2"},{"id":3,"title":"Article 3"},{"id":4,"title":"Article 4"},{"id":5,"title":"Article 5"}]}


pagination is an ordinary function, but its parameters are declared with Query, so FastAPI parses them from the query string exactly as it would for a route handler. limit: int = Query(5, le=50) means the limit query parameter is an integer, defaults to 5, and must be less than or equal to 50 (le=50); offset: int = Query(0, ge=0) defaults to 0 and must be non-negative (ge=0). All the manual int(...) conversion, the try/except, and the bounds checking from the naive version are replaced by these declarations — FastAPI enforces the types and bounds before pagination even runs.

The route handler declares page=Depends(pagination). On each request, FastAPI resolves the dependency first: it parses and validates limit and offset, calls pagination, and injects the returned dict as page. The handler receives finished, validated values and does only its own work — slicing ARTICLES. The default request, with no query parameters, uses limit=5/offset=0 and returns the first five articles, exactly as the dependency's defaults specify.

Validation Comes for Free

Because the dependency's parameters carry constraints, invalid input is rejected automatically with a detailed error. Query with a negative offset:

curl -s "http://127.0.0.1:9000/articles?offset=-1"


{"detail":[{"type":"greater_than_equal","loc":["query","offset"],"msg":"Input should be greater than or equal to 0","input":"-1","ctx":{"ge":0}}]}


No code in either the dependency or the handler checks this — the ge=0 constraint on the offset parameter does it. FastAPI validates the query parameter against the constraint before running the dependency, and on failure returns 422 Unprocessable Entity with a structured error body: type is the machine-readable failure code (greater_than_equal), loc pinpoints the offending input (["query", "offset"] — the offset query parameter), msg is the human explanation, and ctx echoes the constraint (ge: 0). Every endpoint that uses the pagination dependency inherits this validation identically, so the bounds are defined once and enforced everywhere, with rich errors the naive hand-rolled 400 never produced.

An Authentication Dependency That Raises

Authentication is a dependency that reads a header and raises if it is wrong, short-circuiting the request. Add to main.py:

from fastapi import Header, HTTPException


def require_api_key(x_api_key: str = Header(default=None)):
    if x_api_key != "secret-key":
        raise HTTPException(status_code=401, detail="Invalid or missing API key")
    return x_api_key


@app.get("/admin/articles")
def admin_articles(key=Depends(require_api_key), page=Depends(pagination)):
    start = page["offset"]
    return {
        "authorized_as": key,
        "items": ARTICLES[start: start + page["limit"]],
    }


An unauthenticated request is rejected:

curl -s "http://127.0.0.1:9000/admin/articles"


{"detail":"Invalid or missing API key"}


A request with the correct key succeeds:

curl -s "http://127.0.0.1:9000/admin/articles?limit=2" -H "X-API-Key: secret-key"


{"authorized_as":"secret-key","items":[{"id":1,"title":"Article 1"},{"id":2,"title":"Article 2"}]}


require_api_key declares x_api_key: str = Header(default=None), so FastAPI reads the X-API-Key request header (header names are matched case-insensitively, with underscores mapping to hyphens) and passes its value in, or None if absent. If the value is not the expected key, the dependency raises HTTPException(status_code=401, ...). This raise is the key mechanism: FastAPI catches it and returns the 401 immediately, and — crucially — the route handler never runs. The unauthenticated request never reaches the data access, because the dependency short-circuited the request during resolution.

The admin handler declares two dependencies, Depends(require_api_key) and Depends(pagination). FastAPI resolves both before calling the handler, in the order needed, and the auth dependency's raise aborts the whole request if it fires. With the correct key, require_api_key returns the key (injected as key), pagination returns its dict, and the handler runs with both in hand. The auth check lives in one function, reused by every protected endpoint through one Depends — it cannot be accidentally omitted from a handler's body because it is not in the handler's body.

A yield Dependency for the Database Session

A per-request resource — a database session — must be opened before the handler and closed after, even on error. A yield dependency expresses exactly that. Add to main.py:

class Database:
    def __init__(self):
        self.opened = True

    def list_articles(self, limit, offset):
        return ARTICLES[offset: offset + limit]

    def close(self):
        self.opened = False


def get_db():
    db = Database()
    try:
        yield db
    finally:
        db.close()


@app.get("/articles")
def list_articles(page=Depends(pagination), db=Depends(get_db)):
    items = db.list_articles(page["limit"], page["offset"])
    return {
        "count": len(items),
        "limit": page["limit"],
        "offset": page["offset"],
        "items": items,
    }


curl -s "http://127.0.0.1:9000/articles?limit=3&offset=2"


{"count":3,"limit":3,"offset":2,"items":[{"id":3,"title":"Article 3"},{"id":4,"title":"Article 4"},{"id":5,"title":"Article 5"}]}


get_db uses yield rather than return, which splits it into setup and teardown. The code before yield db — creating the Database — is setup, run when the dependency is resolved. The yielded db is injected into the handler. The code after yield, inside the finally, is teardown: it runs after the response is sent, and the finally guarantees db.close() executes even if the handler raises. FastAPI drives this lifecycle: it runs the setup, suspends the generator at the yield, calls the handler with db, and then resumes the generator to run the teardown.

The handler now declares db=Depends(get_db) alongside pagination and delegates the data access to db.list_articles(...). Every request gets its own session, used for the request's duration and reliably closed afterward — the try/finally that the naive version would have had to copy into every handler now lives once in the dependency. This is the standard FastAPI pattern for database sessions, HTTP clients, file handles, and any resource whose lifetime is one request: open in the dependency's setup, yield it, close in the teardown.

Summary

This tutorial factored an article API's cross-cutting concerns — pagination, authentication, and database sessions — into FastAPI dependencies, so each is defined once and attached to endpoints with Depends.

  • A dependency is an ordinary callable that FastAPI runs before a handler; the handler requests it with a parameter defaulting to Depends(the_dependency), and FastAPI injects the dependency's result.
  • A dependency's own parameters participate in request parsing: typed with Query(default, le=..., ge=...) they read and validate query parameters, so the pagination rules live in the dependency and every endpoint that uses it inherits the same validation and the same structured 422 on violation.
  • A dependency may raise HTTPException, which short-circuits the request and returns the error before the handler runs — the basis for authentication (require_api_key returns a 401 and the protected handler never executes).
  • An endpoint can declare multiple dependencies; FastAPI resolves all of them (and their sub-dependencies) before calling the handler, so concerns compose cleanly.
  • A yield dependency splits into setup (before yield) and teardown (after, typically in a finally); FastAPI runs the teardown after the response, guaranteeing per-request resources like database sessions are closed even on error.

You need to be logged in to access the cloud lab.

Log in