Claude Skill

meta-reviewing-api-reviewing

Backend code review patterns. Use when reviewing API routes, database operations, auth middleware, and server utilities. Covers injection, boundary validation, authorization coverage, secret/PII exposure, error leakage, and query patterns.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_meta-reviewing-api-reviewing_skills_meta-reviewing-api-reviewing-3a51ef5.zip · 7 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/meta-reviewing-api-reviewing/skills/meta-reviewing-api-reviewing
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

API Code Review Patterns

Quick Guide: When a diff touches server code, trace every external input to where it is used - it must pass schema validation at the boundary and never reach a query or shell as a concatenated string. Verify every new route names its auth expectation and checks object-level access. Check what errors and logs expose. Security findings outrank everything else in the diff.


<critical_requirements>

CRITICAL: Before Reviewing API Code

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST trace every external input in the diff - body, params, query, headers - to its use, verifying schema validation at the boundary)

(You MUST verify no user input is concatenated into SQL, shell commands, or file paths - parameterized queries and validated paths only)

(You MUST verify every route the diff adds declares its authentication requirement and checks authorization for the object it touches)

(You MUST check that secrets, tokens, passwords, and PII do not reach logs, error responses, or client payloads)

(You MUST verify error handling in the diff returns intentional messages - no stack traces or raw driver errors to the client)

</critical_requirements>


Auto-detection: review API, backend PR review, route review, endpoint review, database query review, auth middleware review, server code review

When to use:

  • Reviewing diffs containing API routes or handlers
  • Reviewing database queries, schema changes, or ORM usage
  • Reviewing authentication/authorization middleware or session handling
  • Reviewing server utilities that touch external input, files, or child processes

When NOT to use:

  • When implementing backend code (use the relevant API implementation skill)
  • For UI components in the same diff (use the web reviewing skill)
  • For CI/CD pipelines and deployment configs (use the infra reviewing skill)

Key patterns covered:

  • Injection review: SQL, shell, and path traversal
  • Boundary validation with schemas
  • Authentication and object-level authorization coverage
  • Secret and PII exposure in logs and responses
  • Error responses that don't leak internals
  • Query patterns: N+1 and unbounded reads the diff introduces

Detailed Resources:




<decision_framework>

Decision Framework

Severity Classification for API Issues

Is this a security defect the diff introduces?
├─ User input concatenated into SQL/shell/path → MUST FIX
├─ Route missing auth, or query missing ownership scoping (IDOR) → MUST FIX
├─ Secrets/PII in logs, responses, or hardcoded in source → MUST FIX
├─ External input used with no boundary validation → MUST FIX
└─ NO → Is it a correctness or robustness gap?
    ├─ Raw internals in client-facing errors → SHOULD FIX
    ├─ N+1 or unbounded query on a growth path → SHOULD FIX
    ├─ Related writes without a transaction → SHOULD FIX
    ├─ Wrong status code for the failure's semantics → SHOULD FIX
    └─ NO → Is it a genuine enhancement?
        ├─ Narrowing an already-safe schema further → NICE TO HAVE
        ├─ Rate limiting/caching the spec never asked for → DON'T MENTION
        ├─ Layer/abstraction preference over working inline code → DON'T MENTION
        └─ Hypothetical scale concerns on internal tooling → DON'T MENTION

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues (Must Fix):

  • Template literals or string concatenation building SQL with request data
  • exec(userInput) or string-built shell commands
  • Route handlers reading req.body/params with no schema parse
  • findUnique({ where: { id: params.id } }) on user-owned resources with no ownership check
  • res.json(entity) where the entity carries hash/token/internal columns
  • Hardcoded credentials, tokens, or connection strings

Medium Priority Issues (Should Fix):

  • catch blocks that stringify the error into the response
  • Queries inside loops over query results
  • New list endpoints with no bound on a growing table
  • Sequential dependent writes with no transaction
  • console.log of request bodies on auth or payment paths

Common Mistakes:

  • Validating the body but not params or query
  • Checking authentication and calling it authorization
  • Trusting an id because it "comes from our own frontend"
  • Returning 200 with an error object in the body
  • Catch-and-continue that swallows the failure and corrupts later state

Gotchas & Edge Cases:

  • ORM raw-query escape hatches ($queryRawUnsafe, sequelize.query) reintroduce injection the ORM normally prevents
  • Zod .parse throws - a handler without the codebase's error boundary turns validation into a 500
  • Middleware order matters: a validator after the handler runs never
  • Soft-deleted rows still satisfy ownership checks unless the query filters them
  • JSON.stringify on circular DB entities throws at serialization, after the status was already sent

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST trace every external input in the diff - body, params, query, headers - to its use, verifying schema validation at the boundary)

(You MUST verify no user input is concatenated into SQL, shell commands, or file paths - parameterized queries and validated paths only)

(You MUST verify every route the diff adds declares its authentication requirement and checks authorization for the object it touches)

(You MUST check that secrets, tokens, passwords, and PII do not reach logs, error responses, or client payloads)

(You MUST verify error handling in the diff returns intentional messages - no stack traces or raw driver errors to the client)

Failure to catch these issues will result in APIs with injection vectors, cross-tenant data access, and credentials sitting in logs and client payloads.

</critical_reminders>

Files (skills)
  • examples
    • core.md 3.6 KB
      # API Reviewing - Core Examples
      
      > Good/bad backend patterns to look for during API code reviews. See [../SKILL.md](../SKILL.md) for checklists and the severity framework.
      
      ---
      
      ## Parameterized Queries vs String Building
      
      ```typescript
      // Must Fix: injection via the search box
      const results = await db.query(
        `SELECT id, title FROM posts WHERE title LIKE '%${req.query.q}%'`,
      );
      
      // Good: input travels as data, never as SQL
      const results = await db.query(
        "SELECT id, title FROM posts WHERE title LIKE $1",
        [`%${req.query.q}%`],
      );
      ```
      
      The same rule for shells: `execFile("git", ["clone", repoUrl])` passes the URL as one argument; ``exec(`git clone ${repoUrl}`)`` hands the request shell metacharacters.
      
      ---
      
      ## Boundary Validation With a Schema
      
      ```typescript
      // Must Fix: NaN page sizes, negative offsets, and arbitrary sort columns all flow through
      app.get("/items", async (c) => {
        const { limit, offset, sort } = c.req.query();
        return c.json(await listItems(Number(limit), Number(offset), sort));
      });
      
      // Good: the contract is explicit, and the handler reads the parsed output
      const listQuery = z.object({
        limit: z.coerce.number().int().min(1).max(100).default(20),
        offset: z.coerce.number().int().min(0).default(0),
        sort: z.enum(["createdAt", "title"]).default("createdAt"),
      });
      
      app.get("/items", zValidator("query", listQuery), async (c) => {
        const { limit, offset, sort } = c.req.valid("query");
        return c.json(await listItems(limit, offset, sort));
      });
      ```
      
      Note the `sort` enum: dynamic column names come from an allowlist, never from the wire.
      
      ---
      
      ## Object-Level Authorization (IDOR)
      
      ```typescript
      // Must Fix: authenticated ≠ authorized - any logged-in user reads any document
      app.get("/documents/:id", requireAuth, async (c) => {
        const doc = await db.document.findUnique({
          where: { id: c.req.param("id") },
        });
        return c.json(doc);
      });
      
      // Good: ownership is part of the query, and absence looks identical to forbidden
      app.get("/documents/:id", requireAuth, async (c) => {
        const doc = await db.document.findFirst({
          where: { id: c.req.param("id"), ownerId: c.get("userId") },
        });
        if (!doc) return c.json({ error: "Not found" }, 404);
        return c.json(doc);
      });
      ```
      
      ---
      
      ## Explicit Response Projection
      
      ```typescript
      // Must Fix: passwordHash, resetToken, and internal flags ship to the browser
      const user = await db.user.findFirst({ where: { id: session.userId } });
      return c.json(user);
      
      // Good: the response shape is chosen, not inherited from the table
      return c.json({ id: user.id, name: user.name, email: user.email });
      ```
      
      ---
      
      ## Error Mapping Without Leakage
      
      ```typescript
      // Should Fix: dialect, table names, and stack frames reach the client
      catch (error) {
        return c.json({ error: (error as Error).stack }, 500);
      }
      
      // Good: the client gets an actionable message; the log gets the truth
      catch (error) {
        if (isUniqueViolation(error)) {
          return c.json({ error: "That email is already registered" }, 409);
        }
        logger.error({ err: error }, "signup failed");
        return c.json({ error: "Could not create account" }, 500);
      }
      ```
      
      ---
      
      ## N+1 and Transactional Writes
      
      ```typescript
      // Should Fix: 1 + N round trips, and a partial failure strands the order
      const order = await db.order.create({ data: orderData });
      for (const line of lines) {
        await db.orderLine.create({ data: { ...line, orderId: order.id } });
      }
      
      // Good: one transaction, one shape
      const order = await db.$transaction(async (tx) => {
        const created = await tx.order.create({ data: orderData });
        await tx.orderLine.createMany({
          data: lines.map((line) => ({ ...line, orderId: created.id })),
        });
        return created;
      });
      ```
      
  • SKILL.md 13.8 KB
    ---
    name: meta-reviewing-api-reviewing
    description: Backend code review patterns. Use when reviewing API routes, database operations, auth middleware, and server utilities. Covers injection, boundary validation, authorization coverage, secret/PII exposure, error leakage, and query patterns.
    ---
    
    # API Code Review Patterns
    
    > **Quick Guide:** When a diff touches server code, trace every external input to where it is used - it must pass schema validation at the boundary and never reach a query or shell as a concatenated string. Verify every new route names its auth expectation and checks object-level access. Check what errors and logs expose. Security findings outrank everything else in the diff.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Reviewing API Code
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST trace every external input in the diff - body, params, query, headers - to its use, verifying schema validation at the boundary)**
    
    **(You MUST verify no user input is concatenated into SQL, shell commands, or file paths - parameterized queries and validated paths only)**
    
    **(You MUST verify every route the diff adds declares its authentication requirement and checks authorization for the object it touches)**
    
    **(You MUST check that secrets, tokens, passwords, and PII do not reach logs, error responses, or client payloads)**
    
    **(You MUST verify error handling in the diff returns intentional messages - no stack traces or raw driver errors to the client)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** review API, backend PR review, route review, endpoint review, database query review, auth middleware review, server code review
    
    **When to use:**
    
    - Reviewing diffs containing API routes or handlers
    - Reviewing database queries, schema changes, or ORM usage
    - Reviewing authentication/authorization middleware or session handling
    - Reviewing server utilities that touch external input, files, or child processes
    
    **When NOT to use:**
    
    - When implementing backend code (use the relevant API implementation skill)
    - For UI components in the same diff (use the web reviewing skill)
    - For CI/CD pipelines and deployment configs (use the infra reviewing skill)
    
    **Key patterns covered:**
    
    - Injection review: SQL, shell, and path traversal
    - Boundary validation with schemas
    - Authentication and object-level authorization coverage
    - Secret and PII exposure in logs and responses
    - Error responses that don't leak internals
    - Query patterns: N+1 and unbounded reads the diff introduces
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Good/bad backend patterns to look for during review
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    **Server code is the trust boundary.** A UI bug annoys one user; an injection or authorization gap exposes every user's data. Review the diff's inputs and outputs before its style: what enters unvalidated, and what leaves that shouldn't.
    
    **When reviewing API code:**
    
    - Follow the data: entry point → validation → use → response, for each input the diff adds
    - Assume every request is hostile until a schema says otherwise
    - Ask "who may call this?" and "may they touch THIS row?" for every new route - the second question is the one that gets missed
    - Read the error paths as carefully as the happy path; that is where internals leak
    
    **When NOT to flag:**
    
    - Don't demand rate limiting, caching, or pagination the spec never asked for on an internal or low-traffic endpoint
    - Don't demand a repository/service layer around a query the codebase writes inline everywhere else
    - Don't flag missing observability on code following the file's existing logging pattern
    - Don't rank a hypothetical scale problem above a real correctness issue in the same diff
    
    **Core principles:**
    
    - **Validate at the boundary**: inside the handler, data is typed and trusted because the schema ran, not because the client is polite
    - **Authorization is per-object**: authentication says who you are; the query must still scope to what you own
    - **Errors are API surface**: what a failure returns is part of the contract
    - **Performance findings need a workload**: an N+1 in a loop over user data is real; a missing index on a ten-row table is not
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Injection Review
    
    No external input reaches an interpreter as a string fragment.
    
    ```markdown
    ## Injection Review
    
    For EACH place the diff sends data to SQL, a shell, or the filesystem:
    
    - [ ] SQL uses parameterized queries or the ORM's binding - no template literals with user input
    - [ ] Shell commands use argument arrays (execFile/spawn), never string-built exec with input
    - [ ] File paths derived from input are validated against a base directory (no ../ traversal)
    - [ ] Dynamic column/table names come from an allowlist, not from the request
    ```
    
    ```typescript
    // Must Fix: classic injection
    const rows = await db.query(
      `SELECT * FROM users WHERE name = '${req.query.name}'`,
    );
    
    // Good: parameterized
    const rows = await db.query("SELECT * FROM users WHERE name = $1", [
      req.query.name,
    ]);
    ```
    
    **Why this matters:** String-built queries and commands turn any input field into an execution vector. This is always a blocking finding, regardless of how internal the endpoint seems.
    
    ---
    
    ### Pattern 2: Boundary Validation
    
    Every input the diff reads gets a schema before it gets used.
    
    ```markdown
    ## Validation Review
    
    For EACH route or handler in the diff:
    
    - [ ] Body, params, and query are parsed through a schema (Zod or the codebase's equivalent) before use
    - [ ] Validation failures return 400 with a safe message - not a 500 from downstream
    - [ ] The schema is as narrow as the contract: enums for enums, bounds on numbers, formats on ids
    - [ ] Handler code reads the schema's OUTPUT type, not the raw request
    ```
    
    ```typescript
    // Should Fix: trusts the wire shape
    const { limit } = req.query;
    const items = await listItems(Number(limit));
    
    // Good: contract enforced at the edge
    const { limit } = listQuerySchema.parse(req.query); // z.coerce.number().int().min(1).max(100)
    const items = await listItems(limit);
    ```
    
    **Why this matters:** Unvalidated input surfaces as NaN limits, negative offsets, and type confusion deep in the stack, where the error message no longer names the cause.
    
    ---
    
    ### Pattern 3: Authentication and Object-Level Authorization
    
    Who may call this - and may they touch this row?
    
    ```markdown
    ## Auth Review
    
    For EACH route the diff adds or changes:
    
    - [ ] The route's auth requirement is explicit (middleware or guard) - public routes are deliberately public
    - [ ] Queries for user-owned resources scope by the session's user id, not by an id the client sent
    - [ ] Mutations verify the resource belongs to the caller before writing (IDOR check)
    - [ ] Role/permission checks happen server-side even when the UI hides the action
    ```
    
    ```typescript
    // Must Fix: any authenticated user can read any invoice (IDOR)
    const invoice = await db.invoice.findUnique({ where: { id: req.params.id } });
    
    // Good: ownership is part of the query
    const invoice = await db.invoice.findFirst({
      where: { id: req.params.id, userId: session.userId },
    });
    ```
    
    **Why this matters:** Object-level authorization is the most common real-world API vulnerability. Authentication middleware passing does not mean the caller owns the row.
    
    ---
    
    ### Pattern 4: Secret and PII Exposure
    
    What leaves the server is as important as what enters it.
    
    ```markdown
    ## Exposure Review
    
    - [ ] No credentials, API keys, or connection strings hardcoded in the diff
    - [ ] Logs added by the diff exclude passwords, tokens, session ids, and PII
    - [ ] Response payloads select fields explicitly - no serializing whole DB entities with hash/token columns
    - [ ] Env vars are read through the codebase's config module, not scattered process.env reads
    ```
    
    ```typescript
    // Must Fix: password hash and reset token ride along to the client
    return res.json(user);
    
    // Good: explicit projection
    return res.json({ id: user.id, name: user.name, email: user.email });
    ```
    
    **Why this matters:** Serialize-the-entity is how hashes, tokens, and internal flags end up in browser devtools. Logging the request body "for debugging" is how credentials end up in log aggregators.
    
    ---
    
    ### Pattern 5: Error Handling Without Leakage
    
    Errors are caught, mapped, and intentional.
    
    ```markdown
    ## Error Path Review
    
    - [ ] Async handlers cannot reject unhandled - errors reach the error middleware or a catch
    - [ ] Client-facing messages are written for the client; internals (stack, SQL, paths) stay in server logs
    - [ ] Status codes match semantics: 400 invalid, 401 unauthenticated, 403 forbidden, 404 absent, 409 conflict
    - [ ] Failures the caller can act on (duplicate email) are distinguished from failures they cannot (DB down)
    ```
    
    ```typescript
    // Should Fix: raw driver error to the client - leaks schema and dialect
    catch (error) {
      res.status(500).json({ error: String(error) });
    }
    
    // Good: intentional message out, full detail logged
    catch (error) {
      logger.error({ err: error }, "createUser failed");
      res.status(500).json({ error: "Could not create user" });
    }
    ```
    
    **Why this matters:** Raw errors hand attackers a map of the schema and stack, and hand legitimate clients a message they can't act on.
    
    ---
    
    ### Pattern 6: Query Patterns the Diff Introduces
    
    Review the shape of data access the change creates.
    
    ```markdown
    ## Query Review
    
    When the diff adds queries or loops around them:
    
    - [ ] No query inside a loop over records that a join/include or IN-list would satisfy (N+1)
    - [ ] List endpoints the diff adds bound their result set when the table grows with usage
    - [ ] Multi-write operations that must succeed together run in a transaction
    - [ ] When the diff adds a WHERE on a new column of a large, growing table, an index accompanies it
    ```
    
    ```typescript
    // Should Fix: one query per order - N+1
    const orders = await db.order.findMany({ where: { userId } });
    for (const order of orders) {
      order.items = await db.item.findMany({ where: { orderId: order.id } });
    }
    
    // Good: one round trip
    const orders = await db.order.findMany({
      where: { userId },
      include: { items: true },
    });
    ```
    
    **Why this matters:** N+1s and unbounded reads pass every test on seed data and fall over on production volume - the review is the last place the shape is visible.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Severity Classification for API Issues
    
    ```
    Is this a security defect the diff introduces?
    ├─ User input concatenated into SQL/shell/path → MUST FIX
    ├─ Route missing auth, or query missing ownership scoping (IDOR) → MUST FIX
    ├─ Secrets/PII in logs, responses, or hardcoded in source → MUST FIX
    ├─ External input used with no boundary validation → MUST FIX
    └─ NO → Is it a correctness or robustness gap?
        ├─ Raw internals in client-facing errors → SHOULD FIX
        ├─ N+1 or unbounded query on a growth path → SHOULD FIX
        ├─ Related writes without a transaction → SHOULD FIX
        ├─ Wrong status code for the failure's semantics → SHOULD FIX
        └─ NO → Is it a genuine enhancement?
            ├─ Narrowing an already-safe schema further → NICE TO HAVE
            ├─ Rate limiting/caching the spec never asked for → DON'T MENTION
            ├─ Layer/abstraction preference over working inline code → DON'T MENTION
            └─ Hypothetical scale concerns on internal tooling → DON'T MENTION
    ```
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues (Must Fix):**
    
    - Template literals or string concatenation building SQL with request data
    - `exec(userInput)` or string-built shell commands
    - Route handlers reading `req.body`/params with no schema parse
    - `findUnique({ where: { id: params.id } })` on user-owned resources with no ownership check
    - `res.json(entity)` where the entity carries hash/token/internal columns
    - Hardcoded credentials, tokens, or connection strings
    
    **Medium Priority Issues (Should Fix):**
    
    - `catch` blocks that stringify the error into the response
    - Queries inside loops over query results
    - New list endpoints with no bound on a growing table
    - Sequential dependent writes with no transaction
    - `console.log` of request bodies on auth or payment paths
    
    **Common Mistakes:**
    
    - Validating the body but not params or query
    - Checking authentication and calling it authorization
    - Trusting an id because it "comes from our own frontend"
    - Returning 200 with an error object in the body
    - Catch-and-continue that swallows the failure and corrupts later state
    
    **Gotchas & Edge Cases:**
    
    - ORM raw-query escape hatches (`$queryRawUnsafe`, `sequelize.query`) reintroduce injection the ORM normally prevents
    - Zod `.parse` throws - a handler without the codebase's error boundary turns validation into a 500
    - Middleware order matters: a validator after the handler runs never
    - Soft-deleted rows still satisfy ownership checks unless the query filters them
    - JSON.stringify on circular DB entities throws at serialization, after the status was already sent
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST trace every external input in the diff - body, params, query, headers - to its use, verifying schema validation at the boundary)**
    
    **(You MUST verify no user input is concatenated into SQL, shell commands, or file paths - parameterized queries and validated paths only)**
    
    **(You MUST verify every route the diff adds declares its authentication requirement and checks authorization for the object it touches)**
    
    **(You MUST check that secrets, tokens, passwords, and PII do not reach logs, error responses, or client payloads)**
    
    **(You MUST verify error handling in the diff returns intentional messages - no stack traces or raw driver errors to the client)**
    
    **Failure to catch these issues will result in APIs with injection vectors, cross-tenant data access, and credentials sitting in logs and client payloads.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related