agentleFS
MartinKChenMartinKChen/harness-claude-code/skills/pattern-reviewer-fastapi/SKILL.md

pattern-reviewer-fastapi

Skill0 starsChanged 5 months agoMITView on GitHub

FastAPI best-practice audit — router-mount prefix discipline, `Depends()` injection (no inline auth in handlers), Pydantic at boundary only (not deep in domain), app-level exception handlers (every project exception class registered), middleware registration order (`RequestIdMiddleware` last so it runs first), named path constants shared by route + tests, `Settings()` instantiation footgun in `create_app()`, `dependency_overrides` in tests (not `monkeypatch`), per-test app factory.

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    After loading this skill, also check $MAIN_ROOT/.claude/memory/patterns/pattern-reviewer-fastapi.md in the consuming project (resolve …

    line 17
  2. Very specific:

    Every external dep enters via Depends() — DB session, settings, current-user resolver, rate limiter, idempotency-key extractor.

    line 36
  3. Very specific:

    Every 4xx / 5xx body must carry a non-null request_id — pin with a test that walks app.user_middleware and asserts the request-id …

    line 121
  4. Very specific:

    Use the shape in templates/review-comment.md.

    line 147

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

pattern-reviewer-fastapi

FastAPI implementation best-practice audit. This skill focuses on FastAPI-specific mechanics — contract-conformance checks (path / verb / status code / response shape / error envelope shape / idempotency policy / rate-limit policy) are out of scope here.

When to activate

  • Reviewing a diff that includes FastAPI routes, dependencies, middleware, exception handlers, or create_app() / main.py.
  • A user says "review the FastAPI wiring / dependencies / middleware order".

Project memory overlay

After loading this skill, also check $MAIN_ROOT/.claude/memory/patterns/pattern-reviewer-fastapi.md in the consuming project (resolve MAIN_ROOT="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")"). If present, load it as an additive overlay to the rules below; if absent, skip silently. See memory-convention for the full contract (additivity, severity floor, conflict surfacing).

Iron rules

  • >80% confidence filter. Report only when you are >80% confident. Consolidate similar findings.
  • Cite path/to/file.ext:line. Quote the offending snippet in a BAD block; show the fix in a GOOD block.
  • Severity is load-bearing. CRITICAL / HIGH block the gate; MEDIUM / LOW are informational. Use the per-pattern severity assigned below.
  • Never refer to a finding as #N — GitHub auto-links those to issues. Use a non-numeric handle (quoted title, F1 / F2, Finding 1).

Patterns to review

Router mounting (MEDIUM)

  • Each APIRouter mounts with an explicit prefix: app.include_router(users_router, prefix="/api/v1/users", tags=["users"]). This rule is "prefix explicit, not implicit" — contract-prefix conformance is out of scope here.
  • Routes from multiple resources collapsed into a single "kitchen-sink" router → flag.
  • Missing tags= → LOW (OpenAPI groups won't render right).

Depends() discipline (HIGH)

  • Every external dep enters via Depends() — DB session, settings, current-user resolver, rate limiter, idempotency-key extractor.
  • Auth guards inline in the handler body → flag; move to Depends(require_admin) or Depends(get_current_user).
  • Depends chains deeper than 2–3 levels → MEDIUM (the seam belongs in a service module).
# BAD — inline auth
@router.post("/admin/users/{user_id}")
def delete_user(user_id: int, request: Request, db: Session = Depends(get_db)):
    token = request.headers.get("Authorization")
    user = decode_jwt(token)
    if user.role != "admin": raise HTTPException(403)
    db.execute("DELETE FROM users WHERE id = :id", {"id": user_id})

# GOOD — Depends-based guard
@router.post("/admin/users/{user_id}", dependencies=[Depends(require_admin)])
def delete_user(user_id: int, db: Session = Depends(get_db)):
    db.execute("DELETE FROM users WHERE id = :id", {"id": user_id})

Settings() footgun in create_app() (HIGH)

# BAD — crashes any test fixture that builds the app without a full env
def create_app() -> FastAPI:
    settings = Settings()  # ValidationError if DATABASE_URL / APP_ORIGIN missing
    app = FastAPI()
    app.add_middleware(SessionMiddleware, https_only=settings.secure_cookies)
    return app

# GOOD — cookie knob readable independently of Settings
def _secure_cookies() -> bool:
    return os.getenv("SECURE_COOKIES", "true").lower() != "false"

def create_app(*, settings: Settings | None = None) -> FastAPI:
    app = FastAPI()
    app.add_middleware(SessionMiddleware, https_only=_secure_cookies())
    return app
  • Settings() instantiated unconditionally in create_app() purely to read one boolean → flag.

Pydantic at boundary only (MEDIUM)

  • Pydantic models on request bodies, response models, query params — yes.
  • Pydantic models passed deep into the domain layer / DB / business logic → flag; convert to dataclass at the seam.

Request vs response model separation (MEDIUM)

# BAD — same model used for both directions
class User(BaseModel):
    id: int | None = None             # only set on response
    email: str
    password: str                     # only set on request — and now in OpenAPI response schema

@router.post("", response_model=User)
def create_user(body: User) -> User: ...

# GOOD — distinct shapes
class UserCreate(BaseModel):
    email: str
    password: str

class UserRead(BaseModel):
    id: int
    email: str

@router.post("", response_model=UserRead)
def create_user(body: UserCreate) -> UserRead: ...

A single Pydantic model used as BOTH the route's request body and its response_model → MEDIUM. Write-side fields (passwords, raw tokens, internal write toggles) leak into the OpenAPI response schema; response-side fields (server-issued ids, derived values) silently become optional on the request side.

Exception handlers register at app level (HIGH)

  • App-level handlers register for project exceptions: @app.exception_handler(LoginError).
  • Inline HTTPException(status_code=..., detail=...) for a project-wide error class → flag (move to a handler).
  • Generic 500 handler exists; logs the full exception server-side.

(This rule is "handler exists at app level, not inline" — error-envelope-shape conformance is out of scope here.)

Middleware registration order (HIGH)

  • app.add_middleware(...) runs in reverse order on the response.
  • RequestIdMiddleware is the last add_middleware call so it runs first on rejection paths.
  • Every 4xx / 5xx body must carry a non-null request_id — pin with a test that walks app.user_middleware and asserts the request-id middleware is at the top.

Path constants (MEDIUM)

  • Paths used in BOTH the route decorator AND tests must be defined once as a module-level constant.
  • @router.post("/api/v1/users") + client.post("/api/v1/users") in a test → flag; both should import USERS_PATH.

(This rule is "constant exists; route + tests share it" — whether the constant's value matches the api contract is out of scope here.)

Lifespan + background tasks (MEDIUM)

  • Startup / shutdown hooks in lifespan context manager, not deprecated @app.on_event.
  • BackgroundTasks parameter only for fire-and-forget work that can lose to a crash; persistent work uses a real queue.

dependency_overrides in tests (MEDIUM)

  • Tests override deps via app.dependency_overrides[real_dep] = fake_dep.
  • monkeypatch / unittest.mock.patch on the module-level dep function → flag; use dependency_overrides.

Test app factory (MEDIUM)

  • Tests build the app per-test via create_app(settings=fake_settings); never share a singleton across tests.
  • Module-level app = create_app() imported by tests → flag; that shape leaks state across tests.

Constructing the finding

Use the shape in templates/review-comment.md.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.