Chapter 13, Intermediate
Dependency injection
A dependency is a function that your route **requests**, instead of calling. FastAPI executes, resolves, and delivers the result, and this is how you reuse authentication, pagination, and database access without copying code.
The idea
Instead of each route repeating "read the token, check the user, open the connection", you write this once, as a function, and declare in the parameter: Depends(a_funcao). FastAPI calls the function before from your route and injects the returned value. A classic first example is pagination, which several routes repeat:
from typing import Annotated
from fastapi import Depends, FastAPI, Query
from fastapi.testclient import TestClient
app = FastAPI()
def paginacao(
pagina: Annotated[int, Query(ge=1)] = 1,
limite: Annotated[int, Query(ge=1, le=100)] = 10,
) -> dict:
return {"pagina": pagina, "limite": limite}
Paginacao = Annotated[dict, Depends(paginacao)]
@app.get("/produtos")
def produtos(p: Paginacao):
return {"recurso": "produtos", **p}
@app.get("/clientes")
def clientes(p: Paginacao):
return {"recurso": "clientes", **p}
cliente = TestClient(app)
print(cliente.get("/produtos?pagina=2").json())
print(cliente.get("/clientes?limite=500").status_code)
{'recurso': 'produtos', 'pagina': 2, 'limite': 10}
422
The parameters pagina and limite and their rules now exist once, and apply to both routes, including in the documentation. The alias Paginacao = Annotated[dict, Depends(paginacao)] keeps the route signatures short.
Verify a token
The most common case is authentication. The token arrives in a header (never in the URL, which goes into logs). FastAPI converts the parameter name: x_token reads the header x-token:
import secrets
from fastapi import Header, HTTPException
def verificar_token(x_token: Annotated[str | None, Header()] = None) -> str:
if x_token is None or not secrets.compare_digest(x_token, "segredo"):
raise HTTPException(status_code=401, detail="Não autorizado")
return "usuário autorizado"
@app.get("/dados-secretos")
def dados_secretos(quem: Annotated[str, Depends(verificar_token)]):
return {"mensagem": "dados secretos", "para": quem}
print(cliente.get("/dados-secretos").status_code)
print(cliente.get("/dados-secretos", headers={"x-token": "errado"}).status_code)
print(cliente.get("/dados-secretos", headers={"x-token": "segredo"}).json())
401
401
{'mensagem': 'dados secretos', 'para': 'usuário autorizado'}
The comparison uses secrets.compare_digest, which takes the same time for any input, thus not allowing the secret to be discovered by measuring the response time. This fixed token is just to teach the mechanism: chapter 20 does this for real, with JWT.
The flow
- The request arrives.
- FastAPI validates the parameters and resolves the dependencies, in order.
- If a dependency raises an exception (like the
401), the route doesn't run. - If all pass, the route runs with the injected values.
Dependencies that depend on others
A dependency can request others. FastAPI builds the chain and executes each function once per request, even if several request it:
chamadas = []
def conexao() -> str:
chamadas.append("conexao")
return "conexão aberta"
def repositorio(c: Annotated[str, Depends(conexao)]) -> str:
return f"repositório usando {c}"
def servico(c: Annotated[str, Depends(conexao)], r: Annotated[str, Depends(repositorio)]) -> str:
return f"serviço com {r}"
@app.get("/cadeia")
def cadeia(s: Annotated[str, Depends(servico)]):
return {"resultado": s, "vezes_que_a_conexao_rodou": len(chamadas)}
print(cliente.get("/cadeia").json())
{'resultado': 'serviço com repositório usando conexão aberta', 'vezes_que_a_conexao_rodou': 1}
The conexao was requested by the repositorio and by servico, and ran once. It is this request cache that allows sharing, for example, the same database session among several dependencies.
`yield`: open and close a resource
A dependency with yield opens the resource before the route and closes after, even if an error occurs. It is the standard for database sessions and connections:
eventos = []
def recurso():
eventos.append("abre")
try:
yield "recurso"
finally:
eventos.append("fecha")
@app.get("/com-recurso")
def com_recurso(r: Annotated[str, Depends(recurso)]):
eventos.append("usa")
return {"recurso": r}
cliente.get("/com-recurso")
print(eventos)
['abre', 'usa', 'fecha']
Dependencies without using the value
When only the check matters (and not the value), declare the dependency in the decorator. In a APIRouter, it applies to all routes in the group:
from fastapi import APIRouter
admin = APIRouter(prefix="/admin", dependencies=[Depends(verificar_token)])
@admin.get("/usuarios")
def listar_usuarios():
return ["ana", "bia"]
@admin.get("/relatorio")
def relatorio():
return {"vendas": 10}
app.include_router(admin)
print(cliente.get("/admin/usuarios").status_code, cliente.get("/admin/relatorio").status_code)
print(cliente.get("/admin/usuarios", headers={"x-token": "segredo"}).json())
401 401
['ana', 'bia']
A class as a dependency
When parameters are grouped, a class organizes better. The Depends() without arguments uses the class itself:
class Filtros:
def __init__(self, q: str | None = None, ativo: bool = True):
self.q = q
self.ativo = ativo
@app.get("/busca")
def busca(f: Annotated[Filtros, Depends()]):
return {"q": f.q, "ativo": f.ativo}
print(cliente.get("/busca?q=caneta&ativo=false").json())
{'q': 'caneta', 'ativo': False}
| Resource | Usage |
|---|---|
Depends(funcao) | Reuse logic and validation between routes |
Dependency with yield | Open and close resources (database session) |
dependencies=[...] | Require something (authentication) without needing the value |
| Dependency of dependency | Set up layers (connection, repository, service) |
app.dependency_overrides | Change a dependency in tests (chapter 24) |
Exercise 1
Require the admin role
Create the dependency somente_admin, which reads the header x-papel and raises 403 if it is not "admin". Use it in GET /painel. Check that the absence of the header and a wrong role give 403, and that admin gives 200.