GraphQL Schema Design Rules
Schema-first GraphQL rules with a strict nullability policy, Relay-style connections, typed error unions, and naming conventions.
Use when: Use when designing or extending a GraphQL schema to keep nullability, pagination, mutations, and error handling consistent across the graph.
The skill file
Cursor · .cursorrules.cursorrules3551 chars
You are designing and evolving a GraphQL schema. The schema is a public contract: every decision below exists because changing it later breaks clients.
## Nullability policy
- Default to non-null (`!`) for scalars and enums on object types. A field is nullable only when one of these is true: the value is genuinely optional in the domain, it is resolved from a separate service that can fail independently, or it is being deprecated.
- List fields are `[Thing!]!` — non-null list of non-null items. Empty list means "none"; `null` lists and `null` holes inside lists are banned.
- Top-level lookup fields that can miss return nullable: `user(id: ID!): User` (null = not found). Do not throw for not-found on lookups.
- Nullability is the error boundary: a non-null field that errors nulls its whole parent selection. Anything backed by a flaky downstream gets nullable so one failure cannot blank an entire response.
- Input fields: required (`!`) only when the operation cannot proceed without them. In update inputs every field is optional — absent means "leave unchanged".
## Pagination: connections only
- Every list that can grow unbounded uses Relay-style connections: `invoices(first: Int!, after: String): InvoiceConnection!` with `edges { cursor node }`, `pageInfo { hasNextPage endCursor }`, and `totalCount: Int!` where counting is cheap.
- Cursors are opaque base64 strings encoding sort key + id — never raw offsets, never a bare id the client could fabricate.
- Plain lists (`[Tag!]!`) are allowed only for small bounded sets (≤ ~50, e.g. tags on one record, enum-like reference data). If you cannot state the bound, it's a connection.
- Cap `first` at 100 in the resolver and document the cap in the field description.
## Errors as unions, not exceptions
- Domain failures are data. Mutations return a union: `union CreateInvoiceResult = CreateInvoicePayload | ValidationError | DuplicateInvoiceError`, so clients must handle each case in the selection set.
- Error types implement a shared interface: `interface UserError { message: String! code: String! }`, with `field: [String!]` on validation errors pointing at the offending input path.
- The top-level GraphQL `errors` array is reserved for exceptional failures only: unauthenticated, internal errors, malformed queries. Business outcomes ("name taken", "insufficient balance") never go there.
- No `success: Boolean` flags and no stringly-typed `errorCode` fields on payloads — the union member IS the outcome.
## Mutations
- Named `verbNoun`: `createInvoice`, `archiveProject`, `addCartItem`. One mutation per user intent — no generic `updateEntity(json: String!)`.
- Exactly one argument: `input: CreateInvoiceInput!`. Payload types are mutation-specific (`CreateInvoicePayload`) and return the mutated entity plus anything the client must refetch (e.g. the parent whose counts changed).
## Naming and evolution
- Types `PascalCase`; fields/arguments `camelCase`; enum values `SCREAMING_SNAKE_CASE`. Enums over booleans whenever a third state is conceivable (`status: InvoiceStatus!`, not `isPaid: Boolean!`).
- No CRUD-leak names: the schema models the domain (`Customer.outstandingBalance`), not tables (`Customer.invoice_rows`).
- Never remove or retype a published field. Deprecate first — `@deprecated(reason: "Use totals.gross. Removal after 2026-12-01.")` — and only remove after usage telemetry reads zero.
- Every type and non-obvious field carries a description block (`"""..."""`). IDs are the `ID` type, globally unique, never exposed database integers.
Install
drop into your repoSave this to your project or home directory so Cursor can load it.
path./.cursorrules
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…