# Pathrule Pattern: GraphQL API (1.0.0)
# ::pathrule:package:graphql-api

### [RULE] Batch every relation through a per-request loader  (path: /src/graphql/resolvers)
<!-- scope: folder | priority: high | strict -->

The N+1 problem is structural in GraphQL, not accidental: the engine calls a field resolver once per parent, so a list of 100 posts calls the author resolver 100 times.

- Put every by-id or by-foreign-key lookup behind a loader that batches keys collected during one tick and returns them in key order. Resolvers ask the loader; they never touch the database directly.
- Create loaders per request, in the context factory. A loader shared across requests is a cache that leaks one user's data into another user's response.
- Batch functions must return results in the same order as the keys they were given, with a null or an error placeholder for misses. Returning a filtered list silently misaligns every field.
- Loaders solve fan-out, not projection: a batched query that selects every column of a wide table is still slow. Select the fields the query actually asked for where your data layer supports it.
- Watch mutations too. A mutation that returns a payload triggering the same resolvers needs the loader primed or cleared, or it will serve the pre-mutation value.

---

### [RULE] Authorize inside the resolver that returns the data  (path: /src/graphql)
<!-- scope: folder | priority: high | strict -->

In a graph, the same object is reachable through many paths. An authorization check on the top-level query protects exactly one of them.

- Check permission where the data is produced: the field resolver (or the service it calls) verifies that the caller may see this object, not merely that they are logged in.
- Never trust an id from the query as proof of access. `node(id:)`, a nested relation, and a mutation input are all attacker-controlled.
- Put the authenticated identity in the request context and pass it down explicitly. A resolver that reads a global or re-parses the token is a resolver that will be called from a path you did not audit.
- Return an authorization failure as a typed error with a stable code, and be deliberate about whether a forbidden field nulls out or fails the whole query, since nullability decides that.
- Field-level directives or a policy layer are fine as a mechanism, but the guarantee must be enforced server-side per object, not in a client-selected shape.

See the web-security pattern for the deny-by-default posture this applies inside the graph.

---

### [RULE] Cap what a single operation can cost  (path: /src/server)
<!-- scope: folder | priority: high | strict -->

A GraphQL endpoint accepts arbitrary programs. Without limits, one request can traverse a recursive relation until the process dies.

- Reject queries past a maximum depth, and score complexity per field (higher for list fields, which multiply) with a rejection budget. Depth stops recursion; complexity stops wide, expensive selections.
- Prefer persisted (allow-listed) operations for first-party clients: the server executes only known operation hashes, which removes both arbitrary queries and most of the introspection attack surface, and shrinks request payloads.
- Disable introspection and field suggestions on public production endpoints. Treat this as defence in depth, not a substitute for authorization; a determined attacker can still probe.
- Set a server-side execution timeout and a request size limit, disable or bound query batching, and rate-limit by identity rather than by IP alone.
- Return errors that do not leak internals: a stable code and a safe message, with stack traces and database errors logged server-side only.

---

### [RULE] Evolve the schema additively and be intentional about nullability  (path: /src/graphql)
<!-- scope: folder | priority: medium | advisory -->

GraphQL has no versioned URLs, so the schema itself is the compatibility contract. Every breaking change is a breaking change for clients you cannot see.

- Additive changes are safe: new types, new optional arguments, new nullable fields. Renaming a field, removing one, changing its type, or making a nullable field non-null are all breaking.
- Mark the old field `@deprecated(reason: "...")` with a migration note, keep it working, and remove it only after usage tracking shows nobody asks for it.
- Choose nullability deliberately: a non-null field that fails propagates the null upward and can wipe out a whole response branch. Non-null for things that genuinely always exist (ids, timestamps); nullable for anything that can fail independently.
- Model expected failures in the schema (a result union or a payload with `errors`) rather than throwing for business outcomes. Reserve GraphQL errors for genuine faults.
- Run a schema diff in CI against the deployed schema and fail the build on a breaking change unless it is explicitly approved.

---

### [MEMORY] Schema conventions that survive growth  (path: /src/graphql)

A schema that mirrors your tables ages badly: every storage change becomes a client change, and every list becomes an unbounded read.

- Model the domain the client asks about. Fields exist because a screen needs them, not because a column exists. Keep storage details (join tables, denormalised columns) out of the graph.
- Paginate every list that can grow, with cursor-based connections (`edges`, `node`, `pageInfo`) and a maximum page size enforced server-side. An unpaginated list field is a future incident.
- Give each mutation a single input object and a payload type that returns the affected entities plus a typed error field. That shape stays compatible as the mutation grows.
- Name for the domain and stay consistent: verbs for mutations (`publishPost`), nouns for types, and no abbreviations that only the backend team knows. Enums for closed sets, so clients get exhaustiveness.
- If you federate, draw subgraph boundaries along ownership, keep entity keys stable, and let one service own each entity's canonical fields. A shared type that two teams both write is where federation goes wrong.

See /src/graphql/resolvers for the batching rule and /src/server for the limits that protect these lists.

---

### [SKILL] graphql-schema-review  (path: /)

---
name: graphql-schema-review
description: GraphQL review checklist covering resolver batching, per-object authorization, operation cost limits, and backward-compatible schema evolution. Run before merging a schema or resolver change.
---

# GraphQL review

## Resolvers
- [ ] Every relation resolves through a per-request loader; no query per parent.
- [ ] Batch functions return results in key order with placeholders for misses.
- [ ] Loaders are created per request, never shared across requests.
- [ ] Mutation payloads do not serve stale loader values.

## Authorization
- [ ] The resolver that returns protected data checks the caller against that object.
- [ ] No id from the query is treated as proof of access.
- [ ] Identity comes from the request context, not a global.

## Cost and exposure
- [ ] Depth and complexity limits still reject the worst query this change enables.
- [ ] New list fields are paginated with a server-enforced maximum page size.
- [ ] Introspection stays off in production; persisted operations cover new client queries.
- [ ] Errors expose a code and a safe message only.

## Compatibility
- [ ] The change is additive; nothing renamed, removed, or retyped.
- [ ] Nullability is deliberate, and non-null fields genuinely cannot fail alone.
- [ ] Anything being retired is `@deprecated` with a reason, not deleted.
- [ ] The CI schema diff passes.
