pattern-reviewer-api
Skill0 starsChanged 5 months agoMITView on GitHub
Framework-agnostic HTTP wiring audit (Express/Fastify/NestJS, Gin, Axum, Spring/Ktor, Flask/Django) — one router per resource at the contracted prefix, path constants shared by route + tests, DI through the framework seam (no handler globals), request/response models separate, ONE error-envelope layer, request-id first on reject, auth as a route guard, status-only no body, env-driven knobs, in-process test client + DI-override factory. Activate on route, handler, middleware, or DI diffs.
In this repository74 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- create-bug-issueSkill
- create-enhancement-issueSkill
- create-feature-issuesSkill
- dream-summary-memorySkill
- memory-conventionSkill
- operation-gitSkill
- pattern-architect-api-endpointSkill
- pattern-architect-data-modelSkill
- pattern-architect-deep-moduleSkill
- pattern-e2e-coding-standardSkill
- pattern-engineer-apiSkill
- pattern-engineer-backend-standardSkill
- pattern-engineer-coding-standardSkill
- pattern-engineer-containerSkill
- pattern-engineer-databaseSkill
- pattern-engineer-fastapiSkill
- pattern-engineer-frontend-standardSkill
- pattern-engineer-goSkill
- pattern-engineer-javaSkill
- pattern-engineer-kotlinSkill
- pattern-engineer-nodeSkill
- pattern-engineer-non-functionalSkill
- pattern-engineer-observabilitySkill
- pattern-engineer-pythonSkill
- pattern-engineer-rustSkill
- pattern-engineer-securitySkill
- pattern-engineer-ssrSkill
- pattern-engineer-swiftSkill
- pattern-engineer-typescriptSkill
- pattern-engineer-viteSkill
- pattern-exploratory-testingSkill
- pattern-reviewer-backend-standardSkill
- pattern-reviewer-coding-standardSkill
- pattern-reviewer-containerSkill
- pattern-reviewer-contractSkill
- pattern-reviewer-databaseSkill
- pattern-reviewer-fastapiSkill
- pattern-reviewer-frontend-standardSkill
- pattern-reviewer-goSkill
- pattern-reviewer-javaSkill
- pattern-reviewer-kotlinSkill
- pattern-reviewer-nodeSkill
- pattern-reviewer-non-functionalSkill
- pattern-reviewer-observabilitySkill
- pattern-reviewer-pythonSkill
- pattern-reviewer-rustSkill
- pattern-reviewer-securitySkill
- pattern-reviewer-ssrSkill
- pattern-reviewer-swiftSkill
- pattern-reviewer-test-coverageSkill
- pattern-reviewer-typescriptSkill
- pattern-reviewer-viteSkill
- pattern-test-coverageSkill
- principle-engineer-tddSkill
- scaffold-projectSkill
- workflow-architect-interviewSkill
- workflow-designer-sample-pagesSkill
- workflow-design-interviewSkill
- workflow-e2e-authorSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 18
After loading this skill, also check $MAIN_ROOT/.claude/memory/patterns/pattern-reviewer-api.md in the consuming project (resolve …
- Distinctive: line 167
Where to look: test setup. Grep for tests spawning a real server / binding a port, for module-level shared app singletons imported by …
- Very specific: line 188
Use the shape in templates/review-comment.md.
≈ “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.
Files
pattern-reviewer-api
Framework-agnostic HTTP-service wiring best-practice audit — the cross-framework generalization of pattern-reviewer-fastapi. This skill focuses on framework wiring mechanics. Contract-conformance checks (path / verb / status code / response shape / error-envelope shape / idempotency policy / rate-limit policy) and language-agnostic backend mechanics (validation mechanics, envelope content, health/logging wiring) are out of scope — they belong to the api contract, pattern-reviewer-backend-standard, and pattern-reviewer-security respectively.
When to activate
- Reviewing a diff that includes HTTP routes, handlers/controllers, dependency injection, middleware, error handlers, lifecycle hooks, or app-factory /
mainwiring in Express / Fastify / NestJS / Hono, Gin / Echo / Chi, Axum / Actix, Spring Boot / Ktor, Vapor, or Flask / Django. - A user says "review the API / route / middleware / DI wiring".
- Skip when the framework is FastAPI —
pattern-reviewer-fastapicarries these same rules in FastAPI spelling.
Project memory overlay
After loading this skill, also check $MAIN_ROOT/.claude/memory/patterns/pattern-reviewer-api.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). - Stay in the wiring lane. Whether a value matches the api contract is out of scope; this audit is about the shape of the wiring.
Patterns to review
Router mounting (MEDIUM)
- Where to look: the mount/registration site (
app.use(...),@Controller(...),r.Group(...),Router::nest(...),@RequestMapping, Ktorroute(...)). Grep for multiple unrelated resource paths registered through one router/controller. - One router/handler module per resource, mounted with an explicit prefix. Multiple resources collapsed into one kitchen-sink router → flag.
- Prefix-explicit-vs-implicit is in scope; whether the prefix string equals the contract is not.
- False-positive guard: a deliberate aggregator that only
.use()s sub-routers (no handlers of its own) is fine.
Path constants shared by route + tests (MEDIUM)
- Where to look: route registration and the test that hits it. Grep for the same literal path string in both a handler/route file and a test file.
// BAD (Gin) — literal repeated in route and test, drift waiting to happen
r.POST("/api/v1/users", createUser) // handler
req := httptest.NewRequest("POST", "/api/v1/users", body) // test
// GOOD — one named constant imported by both
const UsersPath = "/api/v1/users"
r.POST(UsersPath, createUser)
req := httptest.NewRequest("POST", UsersPath, body)A path used in BOTH the route and a test, defined as a literal in each → flag. Whether the constant's value matches the contract is out of scope.
DI through the framework seam (HIGH)
- Where to look: handler bodies. Grep for module-level/global singletons referenced inside handlers —
db.,pool,getDb(), a package-levelvar client,GlobalSettings.
// BAD (Express) — handler reaches for a module global
import { pool } from "../db"; // module singleton
export async function getUser(req, res) {
const u = await pool.query("...", [req.params.id]);
res.json(u.rows[0]);
}
// GOOD — dep arrives through a request-scoped local / injected service
export const getUser = (deps: Deps) => async (req, res) => {
const u = await deps.users.byId(req.params.id);
res.json(u);
};Handler reaching for a global/singleton DB pool, settings, or current-user instead of the framework's DI seam (constructor injection, extractor, request local, fx/wire, Spring bean) → HIGH. Injection chains deeper than 2–3 levels → MEDIUM (the seam belongs in a service). False-positive guard: a genuinely process-wide immutable (logger, metrics registry) imported directly is acceptable.
Boundary validation + request/response separation (MEDIUM)
- Where to look: the model/DTO definitions and the handler signatures. Grep for one model type used as both the request body type and the response type.
// BAD (Axum) — one struct for request and response leaks write fields
#[derive(Deserialize, Serialize)]
struct User { id: Option<i64>, email: String, password: String }
// GOOD — distinct request/response shapes
#[derive(Deserialize)]
struct CreateUser { email: String, password: String }
#[derive(Serialize)]
struct UserView { id: i64, email: String }A single model used as BOTH request body AND response → MEDIUM (write-only fields leak into the response schema; response-only fields become optional on the request side). Wire/DTO model passed deep into the domain layer instead of converted at the seam → MEDIUM. No boundary schema validation at all (hand-rolled if-checks where the stack has Zod / binding tags / serde / Bean Validation) → MEDIUM.
ONE error-envelope mapping layer (HIGH)
- Where to look: handler bodies vs. the central error handler. Grep for inline status construction (
res.status(4xx|5xx),c.JSON(4xx, ...),c.AbortWithStatus,ResponseEntity.status(...)) on a project-wide error class.
// BAD (Fastify) — each route hand-builds the envelope for a shared error class
fastify.post("/login", async (req, reply) => {
try { return await login(req.body); }
catch (e) { if (e instanceof AuthError) return reply.code(401).send({ error: "bad creds" }); throw e; }
});
// GOOD — one mapping layer; handlers just throw the domain error
fastify.setErrorHandler((err, _req, reply) => {
if (err instanceof AuthError) return reply.code(401).send(toEnvelope(err));
reply.code(500).send(toEnvelope(err)); // logged server-side
});
fastify.post("/login", async (req) => login(req.body));Per-route status/envelope construction for a project-wide error class → HIGH (move to the single mapping layer: error middleware / @ControllerAdvice / IntoResponse / setErrorHandler). No generic catch-all for unhandled errors → HIGH. False-positive guard: a one-off route-local 4xx for a condition that is genuinely local (not a shared domain error class) is acceptable.
Request-id middleware ordered first on the reject path (HIGH)
- Where to look: the middleware/filter registration order and any rejection paths (rate-limit, auth, validation). Confirm a test pins it.
- The request-id middleware must run first on the rejection path so every 4xx/5xx body carries a non-null request id — including responses short-circuited by guards registered after it. Request-id registered after the auth/rate-limit guard (so early rejections get no id) → HIGH. Missing the order-pinning test → MEDIUM.
- False-positive guard: account for reverse-order stacks (Express/FastAPI-style where last-added runs first outbound vs. first-registered-runs-first frameworks) before flagging — verify actual execution order, not registration line number.
Auth as a route guard, not inline (HIGH)
- Where to look: handler bodies. Grep for
req.headers.authorization/decodeJwt/if user.role/getRole(inside handler functions rather than at the route boundary.
// BAD (Ktor) — auth decoded and checked inside the handler
post("/admin/users/{id}") {
val token = call.request.header("Authorization")
val user = decodeJwt(token)
if (user.role != "admin") return@post call.respond(HttpStatusCode.Forbidden)
deleteUser(call.parameters["id"]!!)
}
// GOOD — guard at the route boundary
authenticate("admin") {
post("/admin/users/{id}") { deleteUser(call.parameters["id"]!!) }
}Identity/role decode-and-check scattered inside handler bodies → HIGH (move to a guard/middleware/extractor/SecurityFilterChain at the boundary). False-positive guard: resource-ownership checks that need the loaded entity (e.g. "is this row owned by the caller") legitimately live in the handler/service — only route-level authN/role authZ belongs in the guard.
Status-only responses send no body (LOW)
- Where to look: handlers returning a contracted 204/304/205. Grep for a JSON body emitted alongside a no-content status.
// BAD (Echo) — invented body on a 204
return c.JSON(http.StatusNoContent, map[string]bool{"ok": true})
// GOOD — bare status, no body
return c.NoContent(http.StatusNoContent)A fabricated {"ok": true} / {} body on a status-only response → LOW.
Lifecycle hooks for startup/shutdown (MEDIUM)
- Where to look: bootstrap/
mainand module init. Grep for DB pool open / queue connect / OTel bootstrap done at import time or in a handler instead of a lifecycle hook; and for fire-and-forget of non-loseable work. - Startup/shutdown done outside the framework's lifecycle hook (
OnModuleInit/OnModuleDestroy,@PostConstruct/@PreDestroy, Axum graceful-shutdown future, KtorApplicationStopping, an Express SIGTERMserver.close) → MEDIUM. Fire-and-forget background work for persistent (non-loseable) work that should use a real queue → MEDIUM.
Env-driven operational knobs (MEDIUM)
- Where to look: rate limiters, timeouts, retry/backoff, worker tick cadences. Grep for hard-coded numeric literals in these spots.
- Hard-coded rate limit / timeout / tick cadence that gates external integrations or scheduling → MEDIUM (read from settings/env). In-memory limiter/scheduler state persisting across runs is a leading flaky-E2E cause. False-positive guard: a true constant of the algorithm (not an operational knob) is fine.
In-process test client + DI-override factory (MEDIUM)
- Where to look: test setup. Grep for tests spawning a real server / binding a port, for module-level shared app singletons imported by tests, and for monkeypatching module internals where an injection seam exists.
// BAD (Jest/supertest) — shared singleton app + monkeypatched internal
import { app } from "../app"; // module-level singleton, leaks state
jest.spyOn(require("../db"), "getUser").mockResolvedValue(fakeUser);
// GOOD — per-test factory with a DI override, in-process client
const app = createApp({ users: { byId: async () => fakeUser } });
await request(app).get("/api/v1/users/1").expect(200);Tests spawning a real server when an in-process client exists (supertest / httptest / tower oneshot / @SpringBootTest(MOCK) + MockMvc / Ktor testApplication) → MEDIUM. Module-level shared app singleton imported by tests → MEDIUM (leaks state). Monkeypatching module internals when a DI-override seam exists → MEDIUM.
Truthful docs (LOW)
- Where to look: route metadata. Grep for missing tags/groups and for internal routes exposed in public docs.
- Routes not tagged/grouped so generated docs render coherently → LOW. Internal routes (
/healthz, debug, admin ops) exposed in public docs instead of hidden (@ApiExcludeEndpoint/hidden = true/ excluded from the schema group) → LOW.
Constructing the finding
Use the shape in templates/review-comment.md.
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
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.

