graphql-patterns
GraphQL API patterns: schema-first design, resolvers, DataLoader for N+1 prevention, subscriptions, error handling, pagination, auth, and performance. For TypeScript with Apollo Server or Pothos.
Best use case
graphql-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
GraphQL API patterns: schema-first design, resolvers, DataLoader for N+1 prevention, subscriptions, error handling, pagination, auth, and performance. For TypeScript with Apollo Server or Pothos.
Teams using graphql-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/graphql-patterns/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How graphql-patterns Compares
| Feature / Agent | graphql-patterns | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
GraphQL API patterns: schema-first design, resolvers, DataLoader for N+1 prevention, subscriptions, error handling, pagination, auth, and performance. For TypeScript with Apollo Server or Pothos.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# GraphQL Patterns Skill
## When to Activate
- Building a GraphQL API (new or adding to existing)
- Solving N+1 query problems in resolvers
- Implementing real-time subscriptions
- Designing a type-safe schema with code generation
- Adding auth (field-level or operation-level) to GraphQL
- Migrating list endpoints from plain arrays to Relay-style cursor-paginated connections
- Choosing between Apollo Server with SDL-first resolvers and Pothos code-first schema builders
- Configuring query depth limits and complexity scoring to protect against expensive client-sent queries
---
## Schema-First Design
Write the schema first — it's your contract.
```graphql
# schema.graphql
type Query {
user(id: ID!): User
users(first: Int = 10, after: String): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): UserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UserPayload!
}
type Subscription {
userCreated: User!
}
type User {
id: ID!
email: String!
name: String!
orders(first: Int = 10): OrderConnection!
createdAt: DateTime!
}
# Always use Relay-style Connections for lists (enables cursor pagination)
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Mutation payloads: always return the mutated entity + errors
type UserPayload {
user: User
errors: [UserError!]!
}
type UserError {
field: String
message: String!
code: String!
}
input CreateUserInput {
email: String!
name: String!
}
scalar DateTime
```
---
## Schema with Pothos (TypeScript, type-safe)
```typescript
// schema/builder.ts
import SchemaBuilder from '@pothos/core';
import DataloaderPlugin from '@pothos/plugin-dataloader';
import RelayPlugin from '@pothos/plugin-relay';
import ScopeAuthPlugin from '@pothos/plugin-scope-auth';
export const builder = new SchemaBuilder<{
Context: GraphQLContext;
AuthScopes: { authenticated: boolean; admin: boolean };
}>({
plugins: [ScopeAuthPlugin, RelayPlugin, DataloaderPlugin],
authScopes: async (context) => ({
authenticated: !!context.user,
admin: context.user?.role === 'admin',
}),
relay: {},
});
// schema/types/user.ts
builder.queryField('user', (t) =>
t.field({
type: UserType,
nullable: true,
authScopes: { authenticated: true },
args: { id: t.arg.id({ required: true }) },
resolve: (_, { id }, ctx) => ctx.loaders.user.load(id),
})
);
```
---
## DataLoader — Solving the N+1 Problem
Without DataLoader, fetching 100 users' orders = 101 DB queries (1 + 100).
```typescript
// loaders/index.ts
import DataLoader from 'dataloader';
export function createLoaders(db: Database) {
return {
// Batch: instead of 100 individual queries, one WHERE IN query
user: new DataLoader<string, User | null>(async (ids) => {
const users = await db.query.users.findMany({
where: inArray(usersTable.id, ids as string[]),
});
// MUST return results in same order as input ids
const map = new Map(users.map(u => [u.id, u]));
return ids.map(id => map.get(id) ?? null);
}),
// Orders by userId: batch load
ordersByUser: new DataLoader<string, Order[]>(async (userIds) => {
const orders = await db.query.orders.findMany({
where: inArray(ordersTable.userId, userIds as string[]),
});
// Group by userId
const grouped = new Map<string, Order[]>();
for (const order of orders) {
const list = grouped.get(order.userId) ?? [];
list.push(order);
grouped.set(order.userId, list);
}
return userIds.map(id => grouped.get(id) ?? []);
}),
};
}
// Context setup (per request)
export function createContext(req: Request): GraphQLContext {
return {
user: req.user,
db,
loaders: createLoaders(db), // Fresh loaders per request (clear between requests)
};
}
```
---
## Cursor Pagination
```typescript
// Relay-compliant cursor pagination
async function paginateUsers(args: {
first?: number;
after?: string;
}): Promise<UserConnection> {
const limit = Math.min(args.first ?? 10, 100); // cap at 100
const afterId = args.after ? decodeCursor(args.after) : null;
const rows = await db.query.users.findMany({
where: afterId ? gt(users.id, afterId) : undefined,
limit: limit + 1, // fetch one extra to determine hasNextPage
orderBy: asc(users.id),
});
const hasNextPage = rows.length > limit;
const edges = rows.slice(0, limit).map(user => ({
node: user,
cursor: encodeCursor(user.id),
}));
return {
edges,
pageInfo: {
hasNextPage,
hasPreviousPage: !!afterId,
startCursor: edges[0]?.cursor ?? null,
endCursor: edges[edges.length - 1]?.cursor ?? null,
},
totalCount: await db.$count(users),
};
}
function encodeCursor(id: string) {
return Buffer.from(`cursor:${id}`).toString('base64');
}
function decodeCursor(cursor: string) {
return Buffer.from(cursor, 'base64').toString().replace('cursor:', '');
}
```
---
## Error Handling
```typescript
// GraphQL errors: use extensions for machine-readable codes
import { GraphQLError } from 'graphql';
// Throw typed errors from resolvers
throw new GraphQLError('User not found', {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 },
},
});
// Mutation payloads: return errors as data (not thrown)
// This allows partial success and field-level errors
const createUser = async (input: CreateUserInput): Promise<UserPayload> => {
const validation = validateUserInput(input);
if (!validation.ok) {
return {
user: null,
errors: validation.errors.map(e => ({
field: e.field,
message: e.message,
code: 'VALIDATION_ERROR',
})),
};
}
const user = await db.insert(users).values(input).returning();
return { user, errors: [] };
};
```
---
## Subscriptions
```typescript
// Apollo Server with WebSocket transport
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
const httpServer = createServer(app);
const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' });
useServer(
{
schema,
context: async (ctx) => {
// Auth for subscriptions via connection params
const token = ctx.connectionParams?.authorization;
const user = token ? await verifyToken(token) : null;
return { user, loaders: createLoaders(db) };
},
},
wsServer
);
// Resolver with PubSub
const pubsub = new PubSub();
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
const user = await createUserInDb(input);
await pubsub.publish('USER_CREATED', { userCreated: user });
return { user, errors: [] };
},
},
Subscription: {
userCreated: {
subscribe: (_, __, ctx) => {
if (!ctx.user?.isAdmin) throw new GraphQLError('Unauthorized');
return pubsub.asyncIterableIterator('USER_CREATED');
},
},
},
};
```
---
## Performance
```typescript
// Depth limiting — prevent deeply nested malicious queries
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
schema,
validationRules: [
depthLimit(10), // Max query depth
createComplexityRule({ maxComplexity: 1000 }), // query-complexity
],
});
// Persisted queries — send hash instead of full query text
// Apollo Client sends { extensions: { persistedQuery: { sha256Hash: "..." } } }
// Server looks up query by hash — reduces bandwidth, enables CDN caching of queries
```
---
## Checklist
- [ ] Schema written first, before any resolver code
- [ ] All list fields use Relay Connection pagination (not plain arrays)
- [ ] DataLoaders created per-request (not per-server) to prevent cross-request data leaking
- [ ] Every list resolver uses DataLoader (zero N+1 queries)
- [ ] Mutation payloads return `errors: []` field (never throw for user errors)
- [ ] Auth checked at operation level AND field level where needed
- [ ] Query depth limit configured (max 10)
- [ ] Subscriptions auth via connectionParams (not cookies)
- [ ] `graphql-codegen` generates types from schema for frontend consumptionRelated Skills
zero-trust-patterns
Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.
webrtc-patterns
WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.
webhook-patterns
Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.
wasm-patterns
WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).
ux-micro-patterns
UX micro-patterns for every product state: Empty States, Loading States (skeleton screens, spinners, optimistic UI), Error States, Success States, Confirmation Dialogs, Onboarding Flows, and Progressive Disclosure. These patterns apply to every feature — done wrong, they're the biggest source of user confusion.
typescript-patterns
TypeScript patterns — type system best practices, strict mode, utility types, generics, discriminated unions, error handling with Result types, and module organization. Core patterns for production TypeScript.
typescript-patterns-advanced
Advanced TypeScript — mapped types, template literal types, conditional types, infer, type guards, decorators, async patterns, testing with Vitest/Jest, and performance. Extends typescript-patterns.
typescript-monorepo-patterns
TypeScript monorepo patterns with Turborepo + pnpm workspaces. Covers package structure, shared configs, task pipeline caching, build orchestration, and publishing strategy.
terraform-patterns
Infrastructure as Code with Terraform — project structure, remote state, modules, workspace strategy, AWS/GCP patterns, CI/CD integration, and security hardening. The standard for managing production infrastructure.
swiftui-patterns
SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.
swift-patterns
Core Swift patterns — value vs reference types, protocols, generics, optionals, Result, error handling, Codable, and module organization. Foundation for all Swift development.
swift-patterns-advanced
Advanced Swift patterns — property wrappers, result builders, Combine basics, opaque & existential types, macro system, advanced generics, and performance optimization. Extends swift-patterns.