GraphQL schema design

Have Agents follow consistent naming and evolution rules for types, federation subgraphs, and list fields; encode Connection and cursor strategy in the SKILL to avoid N+1, duplicate entities across services, and breaking changes in production.

Drive Object and Input boundaries from the domain: mutation inputs should be explicit, outputs stable; for public APIs prefer Connection or team-standard cursor pagination, and document default first caps with resolver enforcement in the SKILL.

With multiple subgraphs: one subgraph “owns” a field set per entity; others extend via federation directives; avoid conflicting scalar fields or non-key resolution paths for the same entity on two subgraphs.

  • Naming: types PascalCase, fields camelCase, enums SCREAMING_SNAKE_CASE.
  • Nullability and defaults: separate “unset” from explicit null to avoid client ambiguity.
  • Alongside REST: fix in the SKILL whether BFF aggregates or resolvers own which reads.

Schema design flow (skill-flow-block)

  [ Domain model / use cases ]
              │
              ▼
    [ Split subgraphs or monolith: Query entry, Mutation, Subscription ]
              │
              ▼
    [ Types: Object / Input / Enum; lists → Connection or explicit limit+offset+cap ]
              │
         ┌──────┴──────┐
         ▼             ▼
  [ Federation: @key / @external / resolver ownership ]   [ Resolvers: DataLoader, stable cursors, max first ]
         │             │
         └──────┬──────┘
                ▼
    [ @deprecated + reason; schema registry / contract tests block breaking in CI ]
                │
                ▼
    [ Depth / complexity / introspection policy; staging matches prod ]

When Agents emit SDL, self-check in this order: are entry fields too wide, do lists lack caps, does exactly one “primary” subgraph own key fields for federated entities, and is first ≤ max enforced in resolvers.

DataLoader: solving the N+1 problem

// DataLoader complete implementation: batch loading solves the N+1 problem
import DataLoader from 'dataloader';

// Batch loading function: merges N individual queries into 1 IN query
const userLoader = new DataLoader<string, User>(async (userIds) => {
  // userIds is the set of all ids requested within the same tick (auto-deduplicated)
  const users = await db.users.findMany({
    where: { id: { in: [...userIds] } }
  });
  // Must return results in the same order as userIds; missing entries get null
  return userIds.map(id => users.find(u => u.id === id) ?? null);
}, {
  cache: true,       // in-request cache (create a new loader instance per request)
  maxBatchSize: 100,  // max 100 per batch to avoid oversized IN queries
});

// Using in a resolver: each .load() call is automatically batched by DataLoader
const resolvers = {
  Post: {
    // Fetching 100 posts will NOT trigger 100 separate user queries
    author: (post, _, { loaders }) => loaders.user.load(post.authorId),
  },
};

// Create a new DataLoader instance per HTTP request (avoid cross-request cache pollution)
function createContext(req) {
  return {
    loaders: {
      user: new DataLoader(async (ids) => batchLoadUsers(ids)),
      post: new DataLoader(async (ids) => batchLoadPosts(ids)),
    }
  };
}
  • Entities and @key: types resolved across services use @key to declare primary fields; extending subgraphs use @external to reference foreign fields and add only incremental fields or resolver logic locally.
  • Coordinated change: changing keys or moving field ownership is a breaking change; the SKILL should require updating the subgraph inventory, running compatibility checks, and defining a rollback strategy.

Cursor pagination schema and query protection

# Cursor pagination Connection/Edge/PageInfo standard pattern (Relay spec)
"""Pagination info (defined once globally; reused by all Connections)"""
type PageInfo {
  hasNextPage:     Boolean!
  hasPreviousPage: Boolean!
  startCursor:     String
  endCursor:       String  # opaque cursor — clients should not parse its contents
}

type UserEdge {
  cursor: String!
  node:   User!
}

type UserConnection {
  edges:      [UserEdge!]!
  pageInfo:   PageInfo!
  totalCount: Int  # expensive — enable on demand; avoid triggering COUNT(*) by default
}

extend type Query {
  """List users (first max 100; over-limit returns BAD_USER_INPUT)"""
  users(
    first: Int!  # required; resolver enforces the cap
    after: String
    filter: UserFilter
  ): UserConnection!
}
// Query depth limiting (depth-limit middleware) + complexity analysis
import depthLimit from 'graphql-depth-limit';
import { createComplexityRule } from 'graphql-query-complexity';

const server = new ApolloServer({
  validationRules: [
    depthLimit(5),  // max nesting depth 5; prevents deep-nesting attacks
    createComplexityRule({
      maximumComplexity: 1000,  // max complexity score
      variables: {},
      onComplete: (complexity) => console.log(`Query complexity: ${complexity}`),
      estimators: [
        fieldExtensionsEstimator(),
        simpleEstimator({ defaultComplexity: 1 }),
      ],
    }),
  ],
  plugins: [
    {
      requestDidStart: () => ({
        didResolveOperation({ request, document }) {
          // Disable introspection in production (prevent schema leakage)
          if (process.env.NODE_ENV === 'production' &&
              request.operationName === 'IntrospectionQuery') {
            throw new ForbiddenError('Introspection disabled in production');
          }
        }
      })
    }
  ],
});

// Resolver-level authorization: check permissions inside the resolver, never rely on caller discipline
const resolvers = {
  Query: {
    adminUsers: (_, __, { user }) => {
      if (!user || user.role !== 'admin')
        throw new ForbiddenError('Admin access required');  // 403
      return db.users.findAll();
    },
  },
};

Deprecation, registry, and cost / security

Use @deprecated with a replacement for fields slated for removal; block major changes via schema registry or contract tests in CI. For multiple subgraphs, composition failures or version drift must fail the pipeline explicitly.

Security and cost: require query depth, complexity, and introspection policy in the skill text so open endpoints cannot be abused to overload resolvers.

Connection SDL lab (gql-page)

Enter node type and list field names to generate a pasteable Relay-style Connection skeleton for a subgraph (comments remind you to enforce first caps in resolvers). Generation runs only in the browser.


              

Resolvers must validate first: if callers pass more than 100, throw a domain error or clamp to the cap (pick one and document in the SKILL).

---
name: graphql-schema-design
description: Produce evolvable GraphQL SDL with security guards and N+1 solutions
---
# Schema design
1. Type naming: Object/Input/Enum PascalCase, fields camelCase, enums ALL_CAPS
2. List fields: use Connection pattern (edges/node/pageInfo); first is required with an enforced cap
3. Opaque cursors: clients must not parse cursor contents; server is free to change encoding strategy
4. totalCount is expensive: make it an opt-in field or feature flag; never trigger COUNT by default

# N+1 and performance
5. Each resolver uses DataLoader to batch-load related data
6. Create a new DataLoader instance per HTTP request to avoid cross-request cache pollution
7. Batch functions must return results in the same order as input ids; missing entries use null

# Query protection
8. Depth limiting: use graphql-depth-limit to set max nesting depth (recommended 5–10)
9. Complexity analysis: set maximumComplexity and log the score for every query
10. Disable introspection in production (prevents schema structure leakage)
11. Resolver-level auth: check permissions inside each sensitive resolver — never rely on caller discipline

# Evolution and deprecation
12. Deprecate fields: @deprecated(reason: "Use newField instead")
13. Schema Registry + CI blocks breaking changes
14. Federation: one subgraph owns the entity via @key; others reference it with @external
15. Before changing a @key or moving field ownership, audit all dependent subgraphs and define a rollback plan

Back to skills More skills