For AI agentsA Markdown index of these docs is at /llms.txt. Every page is also Markdown: add .md to its address.

Agent Rules / Frameworks

How do I keep FastAPI routes visible to coding agents?

Rule AR-33

Declare dependencies as typed functions used through Annotated[..., Depends(fn)], and write routes as decorated functions on an APIRouter. Don't build routes in loops from strings.

FastAPI is built around ordinary Python functions with type annotations, which is why editors and agents read it well. A route written as @router.get("/items") over a named function is easy to find, and a dependency passed as a function object is a reference every tool follows. The pattern stops being readable when route functions are looked up by name from a table, or when dependencies are made from strings and lambdas. Keep to the plain form and the framework's strengths carry over to the tools.

Why it happens

FastAPI's docs describe a dependency as "just a function that can take all the same parameters that a _path operation function_ can take". You attach it with Depends, and with Annotated you can store the declaration under a name:

"because we are using Annotated, we can store that Annotated value in a variable and use it in multiple places ... This is just standard Python, it's called a 'type alias'."

The docs state the benefit: "the type information will be preserved, which means that your editor will be able to keep providing you with autocompletion, inline errors, etc. The same for other tools like mypy." And Depends takes the function itself: "You don't call it directly (don't add the parenthesis at the end), you just pass it as a parameter to Depends()."

That is the visible form. A name in Depends(get_db) is a reference a rename tool and a caller search follow. Routing has the same property. The bigger-applications page says "You can think of APIRouter as a 'mini FastAPI' class", with prefix, tags, responses and dependencies set on the router, and app.include_router() adding the router's routes to the app. Every route is a decorated function in a module, and every router is a named object included by name.

What hides routes and dependencies:

  • A route table. Registering handlers from a dict of strings in a loop (getattr(handlers, name)) is the dispatch pattern of AR-07 applied to routing. A search for list_items finds the definition, and nothing says it serves GET /items.
  • Dependencies that come from a factory or a string. A dependency chosen with globals()[name] or built per call in a function has no stable name. The testing docs say app.dependency_overrides is keyed by the original dependency function: "you put as a key the original dependency (a function), and as the value, your dependency override". A function object created inside a factory on each call is a different key each time (the page calls dependency_overrides "a simple dict" keyed by the function itself and never says "identity" or "equality"; a Python dict keyed by function objects matches the same object, which is the standard dict behaviour rather than a FastAPI statement; https://fastapi.tiangolo.com/advanced/testing-dependencies/).
  • The old default-value form. commons: dict = Depends(common_parameters) works, but the docs and ruff's FAST002 prefer Annotated, which also keeps the parameter's own type.
  • Dependencies hidden in decorators and routers. @app.get("/items/", dependencies=[Depends(verify_token)]) and APIRouter(dependencies=[...]) run for each request, and "their value (if they return any) won't be passed to your path operation function". A route function can look unauthenticated and not be. That is a feature, and the reason to keep the router declaration in the file the reader opens.

Bad and good

A service exposes read endpoints that all need a database session.

python# bad: app/main.py from fastapi import Depends, FastAPI from app import handlers app = FastAPI() ROUTES = { "list_items": "/items", "get_item": "/items/{item_id}", "list_users": "/users", } for name, path in ROUTES.items(): app.get(path)(getattr(handlers, name)) def get_db(kind: str = "primary"): return globals()["open_" + kind]() @app.get("/orders") def list_orders(db=Depends(get_db)): ...

An agent asked to add a parameter to handlers.get_item finds the definition and no route that serves it. The route table is data, getattr builds the handler name.

python# good: app/routers/items.py from typing import Annotated from fastapi import APIRouter, Depends from app.deps import Session, get_session router = APIRouter(prefix="/items", tags=["items"]) SessionDep = Annotated[Session, Depends(get_session)] @router.get("/") async def list_items(session: SessionDep) -> list[Item]: return await item_repo.list(session) @router.get("/{item_id}") async def get_item(item_id: int, session: SessionDep) -> Item: return await item_repo.get(session, item_id)
python# good: app/main.py from fastapi import FastAPI from app.routers import items, users app = FastAPI() app.include_router(items.router) app.include_router(users.router)

Each route is a decorated function, the dependency is the named get_session, and SessionDep carries the type. A search for get_session lists every route that uses it through SessionDep, and app.dependency_overrides[get_session] in tests replaces the same object the routes reference.

Trade-offs and when the dynamic form is fine

  • Loops are less typing. Fifty near-identical endpoints generated from a spec are shorter as a loop. If the routes come from an external spec, generate the Python files (AR-23) and commit them, so tools read decorated functions.
  • **include_router costs nothing at runtime.** The docs: "You don't have to worry about performance when including routers", so the split into modules carries no speed penalty.
  • Router-level dependencies move requirements out of the route. They save repetition for auth. Name the router's dependency in the router constructor, where the route's file reader sees it.
  • Including one router twice with different prefixes is documented, for /api/v1 and /api/latest. Mind that a route then has two URLs for one function.
  • **Annotated needs Python 3.9 or newer** (typing_extensions before that), per ruff's FAST002 page.

Enforce it

ToolSettingWhat it catchesChecked
ruffFAST002 fast-api-non-annotated-dependency (the FAST family; not marked preview in the rules index)Depends(...), Query(...) and similar used as default values instead of inside Annotated. The fix is always unsafe2026-10-30
ruffFAST001 fast-api-redundant-response-model, FAST003 fast-api-unused-path-parametera response_model "with the same type as the return type" (https://docs.astral.sh/ruff/rules/fast-api-redundant-response-model/), and a route that declares "path parameters in the route path that are not included in the function signature" (https://docs.astral.sh/ruff/rules/fast-api-unused-path-parameter/). Neither is marked preview and neither is in ruff's default set, so select FAST2026-10-30
FastAPIapp.dependency_overrides in testsa test that replaces the exact dependency function the routes use; overriding a different function silently changes nothing2026-10-30

No tool in the pages read flags a route registered in a loop. It is a review rule.

Rule block for AGENTS.md / CLAUDE.md

markdown## FastAPI - Routes are decorated functions (@router.get(...)) on an APIRouter, in the module named for the resource. Routers are included in main.py by name with app.include_router. - Don't register routes in loops, or look up handler functions by name (getattr, globals). If routes come from a spec, generate the .py files and commit them. - Dependencies are plain typed functions. Declare them as Annotated[Type, Depends(fn)] and give repeated ones a type alias (SessionDep). Don't use Depends(...) as a default value. - Don't create dependency functions inside other functions per call. Tests override dependencies with app.dependency_overrides[original_function], so the function must be one stable module-level name. - Before changing a dependency's parameters or return type, search for its name, its Annotated aliases and every dependency_overrides key that uses it. - Reset app.dependency_overrides at the end of each test.

Edge cases

  • Router-level and app-level dependencies. FastAPI(dependencies=[...]) and APIRouter(dependencies=[...]) apply to every route beneath. When changing one, check every route it reaches.
  • **Overrides in include_router.** The testing docs say an override works wherever the dependency is used, including "a .include_router() call".
  • Parameterised dependencies. A class with __call__, or a factory returning a function, is legitimate. If the factory runs at import, bind the result to a module-level name and use that name as the override key.
  • Sync and async. Docs: "You can use async def or normal def." Mixing is fine. Changing a dependency from def to async def doesn't change callers.
  • Return values of decorator dependencies are unused. Don't move a dependency whose value you read into the decorator list.
  • Reset overrides. Docs: app.dependency_overrides = {} resets them. A test that sets one and never clears it changes later tests, which is the fixture problem of AR-34.
  • Generated routes in chimera form. See CH-05 in the index (routes generated in a loop from a dict of string names), not yet published.

What LLMGRAPH shows

A handler registered in a loop prints 'plus 1 dynamic call nearby that could reach anything'. A decorated route handler prints 'no calls to it found in this repo'; LLMGRAPH does not mark route handlers as entry points or list the routes that use a dependency.

References

FastAPI (fetched 2026-10-30):

  • Dependencies tutorial (Depends, Annotated type alias, sync and async): https://fastapi.tiangolo.com/tutorial/dependencies/
  • Bigger applications (APIRouter, prefix, dependencies, include_router, performance): https://fastapi.tiangolo.com/tutorial/bigger-applications/
  • Dependencies in path operation decorators: https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/
  • Testing dependencies with overrides: https://fastapi.tiangolo.com/advanced/testing-dependencies/
  • APIRouter reference: https://fastapi.tiangolo.com/reference/apirouter/

Ruff (fetched 2026-10-30):

  • FAST002: https://docs.astral.sh/ruff/rules/fast-api-non-annotated-dependency/ · rule list: https://docs.astral.sh/ruff/rules/

Changelog

  • 2026-10-30: draft 1.
  • 2026-10-30: docs checks resolved
  • 2026-10-30: remaining checks closed (claims reduced to documented behaviour)
  • AR-07 Why does my coding agent miss calls made through getattr?
  • AR-08 How do I write dispatch that type checkers can follow?
  • AR-09 How should plugin registries be written for coding agents?
  • AR-13 Why is Any a blind spot for coding agents?

Last verified

For agentsthis page as Markdown/agent-readable/ar-33.mdllms.txt