Skip to content
Skill · Cursor.cursorrulesCoding

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.

@libraryNewcomer
0 upvotes0 forks0 comments

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 repo

Save this to your project or home directory so Cursor can load it.

path./.cursorrules
Discussion

What people are saying

Forks

0 forks of this skill
Loading forks…