SPB Git

spb/ultra-sharp-agent-skills Public

Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.

Python 100%

# name: designing-graphql-apis description: Designs GraphQL APIs with schema-first typing, Relay-style cursor connections, DataLoader batching against N+1 queries, union result types for expected errors, and depth/complexity limits. Use when the user asks to design, review, or refactor a GraphQL schema or API, write type definitions or resolvers, add pagination to a GraphQL query, fix N+1 resolver performance, or structure mutations. Do not use for REST APIs (designing-rest-apis) or for real-time subscription transport infrastructure.

# Designing GraphQL APIs

# When to use / when NOT to use

  • Use for: GraphQL schema design, type/resolver structure, connections, mutations, error modeling, query cost control.
  • Do NOT use for: REST endpoint design → designing-rest-apis; WebSocket/subscription transport setup; client-side query writing.

# Core rules

  1. Schema first, domain-shaped. Design the SDL from the domain and client needs, then implement resolvers. Never mirror database tables 1:1 — expose what clients consume.
    • type Order { total: Money!, placedAt: DateTime! }
    • type OrdersTbl { amt_cents: Int, created_ts: Int }
  2. Nullable by default; non-null (!) only when guaranteed. A non-null field that fails nulls out its whole parent chain. Reserve ! for IDs and fields the resolver can never fail to produce.
  3. Paginate lists with Relay connections. Any list that can grow gets Connection/Edge/PageInfo with first/after cursor args — never a bare unbounded [Order!]!.
  4. DataLoader is the default N+1 fix. Every resolver that fetches by ID goes through a per-request DataLoader that batches and caches. One query per collection of parents, not one per parent.
  5. Mutations: verb-object names, one input, one payload. createOrder(input: CreateOrderInput!): CreateOrderPayload!. Payload contains the changed object plus expected-error fields — never return the bare object.
  6. Expected errors are union result types; the errors array is for exceptions only.
    • union CreateOrderResult = Order | ValidationError | OutOfStockError
    • ❌ throwing for "email already taken" so clients parse errors[0].message
  7. Cap query cost. Enforce a depth limit (default 10) and a complexity budget (points per field × list multipliers); reject over-budget queries before execution.
  8. Additive evolution only. Add fields freely; never remove or change a type in place — @deprecated(reason: "...") first, remove in a coordinated major cycle.

# Workflow

  1. List client use cases; sketch queries clients should be able to write.
  2. Write the SDL: types, connections (rule 3), mutations (rule 5), error unions (rule 6).
  3. Apply nullability discipline (rule 2) type by type.
  4. Plan resolvers: mark every by-ID fetch as a DataLoader (rule 4).
  5. Set depth/complexity limits (rule 7).
  6. Validate: run the schema through a linter (npx graphql-schema-linter schema.graphql — install with npm i -g graphql-schema-linter if missing), execute the sketched queries from step 1 against a stub server, and log SQL for one nested query to confirm no N+1 (query count must be O(depth), not O(rows)). Fix and repeat.

# Edge cases & failure modes

  • A field that is expensive for some parents → split it (Order.summary cheap, Order.analytics separate type) so cheap queries don't pay.
  • Polymorphic lists → interfaces when types share fields, unions when they don't; always include __typename handling in examples.
  • File uploads → do not tunnel through GraphQL; issue a presigned URL via mutation and upload out-of-band.
  • Global object identity → give every fetchable type a globally unique id: ID! (base64 Type:dbId) so caches and node(id:) refetching work.

# References

Deeper patterns (connection SDL, DataLoader implementation, complexity scoring): see references/patterns.md.