A comprehensive rule set for designing, evolving, and operating production-grade GraphQL schemas with Apollo Federation, GraphQL Yoga, and DGS.
Every backend developer knows the frustration: you ship a REST API, then spend months fielding requests for "just one more field" or "can you combine these three endpoints?" Meanwhile, your frontend team is drowning in overfetching, underfetching, and waterfall requests that make your app feel sluggish.
GraphQL promised to solve this. But most teams end up with GraphQL APIs that are just REST endpoints wearing a graph costume—brittle, hard to evolve, and missing the core benefits that made GraphQL revolutionary in the first place.
Here's what's really happening: teams jump straight into resolver code without designing their graph. They expose database tables as types, mirror REST endpoints as queries, and treat the schema as an afterthought. Six months later, they're stuck with:
The result? GraphQL that's harder to work with than the REST API it replaced.
These Cursor Rules transform how you build GraphQL APIs by enforcing schema-first design principles that scale from prototype to production. Instead of retrofitting a graph onto your existing architecture, you'll design expressive, evolvable schemas that actually unlock GraphQL's power.
Here's what changes:
Schema Evolution That Never Breaks Clients
# Before: Breaking change that breaks existing clients
type User {
name: String! # Removed firstName/lastName - breaking!
}
# After: Additive evolution with deprecation path
type User {
firstName: String! @deprecated(reason: "Use name instead")
lastName: String! @deprecated(reason: "Use name instead")
name: String! # New field, old ones still work
}
Expressive Design Over Database Mirrors
# Before: Exposes internal structure
type Order {
user_id: ID!
status_code: Int!
line_items: [OrderLineItem!]!
}
# After: Models the business domain
type Order {
"""The customer who placed this order"""
customer: Customer!
"""Current fulfillment status of the order"""
status: OrderStatus!
"""Products and quantities in this order"""
items: [OrderItem!]!
}
Eliminate Breaking Changes: Additive-only evolution means you never coordinate releases again. Deploy schema changes independently and deprecate gracefully over 2+ release cycles.
End Documentation Debt: Every type, field, and argument gets documented directly in the SDL. Your schema becomes self-documenting, eliminating the need for separate API docs that drift out of sync.
Federation at Scale: Proper @key design and boundary management means your federated graph stays performant as you add services. No more composition failures blocking deployments.
Performance by Design: Built-in DataLoader patterns, query complexity limits, and persisted operations prevent the N+1 problems and DoS vectors that plague most GraphQL APIs.
Type Safety Throughout: Generated TypeScript/Kotlin types from your schema eliminate runtime errors and catch breaking changes at compile time.
Before: Breaking schema changes require coordinated releases
# Deploy new schema
kubectl apply -f api-v2.yaml
# Wait for all clients to update
# Check client usage metrics
# Manually remove old endpoints
# Result: 3-week release cycle, coordination hell
After: Additive evolution with automated deprecation tracking
# Deploy new schema with deprecation
rover graph publish --schema schema.graphql
# Apollo Studio tracks field usage automatically
# Deprecation warnings guide client migration
# Remove fields only when usage hits zero
# Result: Deploy anytime, clients migrate on their schedule
Before: Composition failures block entire team
# Service A changes schema
git push origin main
# Gateway composition fails
# "Error: Cannot query field 'orders' on type 'User'"
# Block all deployments until fixed
# Result: 4-hour debugging session, frustrated team
After: Schema validation catches issues pre-merge
# Service A proposes schema change
git push origin feature/new-field
# CI runs composition check
# rover supergraph compose --config supergraph.yaml
# Green checkmark - safe to merge
# Result: Confident deployments, no surprises
Before: N+1 queries discovered in production
# Production alert: API response time > 5s
# Dig through logs to find the query
# Realize you're making 100 DB calls for 1 GraphQL query
# Emergency hotfix with manual batching
# Result: 2 AM debugging session, user impact
After: DataLoader patterns prevent N+1 queries by design
// Every resolver uses DataLoader batching
const userLoader = new DataLoader(async (userIds) => {
return await db.users.findMany({ id: { in: userIds } });
});
// Automatically batches queries
const resolvers = {
Order: {
customer: (order) => userLoader.load(order.customerId)
}
};
// Result: Consistent performance, no N+1 surprises
# Add GraphQL tooling
npm install graphql-codegen @graphql-eslint/eslint-plugin
npm install apollo-server-express dataloader
# Add CI schema checking
npm install @apollo/rover -g
Create your project structure:
src/
├── schema/
│ ├── user.graphql
│ └── order.graphql
├── resolvers/
│ ├── user.ts
│ └── order.ts
└── generated/
└── types.ts
Configure type generation:
// codegen.yml
generates:
src/generated/types.ts:
plugins:
- typescript
- typescript-resolvers
config:
useIndexSignature: true
contextType: './context#Context'
Transform your existing schema using the rules:
# user.graphql - Schema-first with documentation
"""
A user of the platform with authentication and profile information.
"""
type User @key(fields: "id") {
"""Unique identifier for the user"""
id: ID!
"""User's display name"""
name: String!
"""User's email address"""
email: String!
"""Orders placed by this user"""
orders: [Order!]!
}
// resolvers/user.ts
import { Resolvers } from '../generated/types';
import { userLoader } from '../loaders';
export const userResolvers: Resolvers = {
User: {
orders: async (user, _args, context) => {
return await context.dataSources.orders.findByUserId(user.id);
}
},
Query: {
user: async (_parent, { id }, context) => {
return await userLoader.load(id);
}
}
};
# .github/workflows/schema-check.yml
name: Schema Check
on: [pull_request]
jobs:
schema-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rover
run: curl -sSL https://rover.apollo.dev/nix/latest | sh
- name: Check schema changes
run: rover graph check --schema schema.graphql
Development Velocity: Teams report 40% faster feature development once they eliminate breaking change coordination and documentation debt.
API Quality: Schema-first design with built-in documentation means new developers can start consuming your API in minutes, not hours.
Production Reliability: Proper error handling, query complexity limits, and performance patterns prevent the runtime surprises that cause 3 AM pages.
Federation Success: Teams scale to 10+ federated services without composition headaches when they follow proper boundary design from day one.
Type Safety: Generated types catch breaking changes at compile time, eliminating an entire class of runtime errors that plague dynamically typed GraphQL implementations.
The difference is immediate and measurable. Your first schema refactor using these rules will save hours of debugging time. Your first federated service deployment will work on the first try. Your first breaking change will happen without breaking any clients.
Ready to build GraphQL APIs that actually unlock the graph advantage? These rules transform your schema design from database-driven to domain-driven, from breaking to evolutionary, from documented-nowhere to self-documenting.
Stop retrofitting graphs onto REST architectures. Start designing graphs that scale.
You are an expert in GraphQL, TypeScript (Node.js), Kotlin/Java, Apollo Federation, GraphQL Yoga, DGS, and Hasura.
Key Principles
- Design a graph, not a REST façade. Model relationships explicitly through edges.
- Schema-first: start with SDL, then generate types/resolvers.
- Expressiveness > brevity: prefer `TotalPrice` over `price2`.
- Additive evolution: only add fields/types; deprecate before removal (≥ 2 release cycles).
- Consistency across services: adopt a shared lint config and CI schema checks.
- Favour non-null (`!`) when the value is guaranteed; otherwise keep nullable to prevent runtime errors.
- Documentation lives in the SDL—write full-sentence `"""` descriptions for every public type, field, argument and enum value.
- Keep the public graph independent from internal service names or DB tables.
GraphQL SDL
- Type names: PascalCase nouns (e.g., `OrderItem`).
- Field/argument names: camelCase verbs or nouns (`totalCost`, `isPublished`).
- Enum values: ALL_CAPS (`PENDING_PAYMENT`).
- Use `ID` for opaque identifiers—never expose raw database IDs.
- Prefer explicit scalars (`Int`, `Float`, `Boolean`, `DateTime`) over generic `String`.
- Pagination: adopt the Relay Cursor spec (`Connection` / `Edge`) or a canonical offset pattern; do not combine both in the same schema.
- Input vs output separation: never reuse output types as inputs.
- Directives: confine custom directives to cross-cutting concerns (e.g. `@auth(role: ADMIN)`).
- Composition: for shared blocks, use schema extensions (`extend type User { ... }`) instead of massive root types.
- Federation-specific: mark boundaries with `@key`, `@external`, `@requires` only where necessary to keep resolve graphs shallow.
TypeScript Resolver Layer
- Each resolver file exports a single typed resolver map: `export const orderResolvers: Resolvers<Order> = { ... }`.
- Use `async/await`; never return raw promises in nested resolvers.
- Derive typings from generated code (`graphql-codegen`) to avoid `any`.
- Resolve batching: wrap DB calls with `DataLoader` to cut N+1 queries.
- Sort resolver keys alphabetically for git-diff hygiene.
Error Handling and Validation
- Validate user args in resolver entry; throw `UserInputError` (Apollo) or `GraphQLError` with extensions `{ code: "BAD_USER_INPUT" }`.
- Convert downstream service errors into graph-level errors with meaningful `extensions.code` (`UPSTREAM_TIMEOUT`, `RATE_LIMIT_EXCEEDED`).
- Surface partial failures via `errors[]` while still returning available data; avoid null-blanket patterns.
- Deprecation tag: `@deprecated(reason: "Use totalCost instead")` + CHANGELOG + Slack broadcast.
Framework-Specific Rules
Apollo Federation
- Single source of truth for shared types lives in the composition repo.
- At least one `@key` field must be stable and globally unique.
- Activate `fieldUsage` reporting to GraphOS; block composition on unknown @tagged directives.
GraphQL Yoga (Node.js)
- Enable envelop plugins: `useGraphQlJit` for execution speed, `useRateLimiter` for abuse protection.
- Keep `/graphql` route behind an auth middleware; expose `/sandbox` only in non-prod envs.
DGS (Spring Boot / Kotlin)
- Prefix DGS components with feature context (`@DgsComponent("Inventory")`).
- Collocate `*.graphqls`, `*.kt`, and test files under `src/main/graphql/inventory/`.
- Use `@InputArgument` for strong typing; avoid raw `Map<String, Any>`.
Additional Sections
Testing
- Contract tests: on every merge, diff composed schema against previous main; fail on breaking changes.
- Resolver tests: mock data sources; assert response shape and nullability.
- Integration tests: spin up supergraph with Rover CLI + Docker compose.
Performance
- Enable persisted operations; block ad-hoc introspection in production.
- Instrument field-level tracing; budget ≤ 10 ms per DB/data-source call.
- Prefer compiled queries (`graphql-jit` or `APQ`) in high-throughput services.
Security
- Enforce auth at field or directive level, not gateway route only.
- Depth-limit (<= 8) and complexity-limit (<= 1000) every query.
- Strip error stack traces from production responses; keep in logs only.
Documentation & Tooling
- Auto-publish schema to Apollo Studio / GraphQL Editor on every main push.
- Use GraphQL-Markdown or `doc:` comments to generate HTML docs.
- Provide runnable examples in SDL: `# Example: { user(id: "123") { name } }`.
Directory Layout (Node.js example)
```
├── src
│ ├── schema
│ │ ├── order.graphql
│ │ └── user.graphql
│ ├── resolvers
│ │ ├── order.ts
│ │ └── user.ts
│ ├── loaders
│ ├── context.ts
│ └── server.ts
└── tests
├── order.spec.ts
└── user.spec.ts
```
CI Checklist
- [ ] `graphql-eslint` passes
- [ ] Composition succeeds (`rover supergraph compose --config`)
- [ ] Breaking-change diff passes
- [ ] Unit & contract tests pass
- [ ] Coverage ≥ 90% on resolvers