| name | graphql-patterns |
| description | 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. |
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.
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!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
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)
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: {},
});
builder.queryField('user', (t) =>
t.field({
type: UserType,
nullable: true,
: { : },
: { : t..({ : }) },
: ctx...(id),
})
);
DataLoader — Solving the N+1 Problem
Without DataLoader, fetching 100 users' orders = 101 DB queries (1 + 100).
import DataLoader from 'dataloader';
export function createLoaders(db: Database) {
return {
user: new DataLoader<string, User | null>(async (ids) => {
const users = await db.query.users.findMany({
where: inArray(usersTable.id, ids as string[]),
});
const map = new Map(users.map(u => [u.id, u]));
return ids.map(id => map.get(id) ?? null);
}),
ordersByUser: new DataLoader<string, Order[]>(async (userIds) => {
orders = db...({
: (ordersTable., userIds []),
});
grouped = <, []>();
( order orders) {
list = grouped.(order.) ?? [];
list.(order);
grouped.(order., list);
}
userIds.( grouped.(id) ?? []);
}),
};
}
(): {
{
: req.,
db,
: (db),
};
}
Cursor Pagination
async function paginateUsers(args: {
first?: number;
after?: string;
}): Promise<UserConnection> {
const limit = Math.min(args.first ?? 10, 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,
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,
: !!afterId,
: edges[]?. ?? ,
: edges[edges. - ]?. ?? ,
},
: db.$count(users),
};
}
() {
.().();
}
() {
.(cursor, ).().(, );
}
Error Handling
import { GraphQLError } from 'graphql';
throw new GraphQLError('User not found', {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 },
},
});
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
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) => {
const token = ctx.connectionParams?.authorization;
const user = token ? await verifyToken(token) : null;
return { user, loaders: createLoaders(db) };
},
},
wsServer
);
const pubsub = new PubSub();
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
const user = await createUserInDb(input);
await pubsub.publish('USER_CREATED', { userCreated: user });
{ user, : [] };
},
},
: {
: {
: {
(!ctx.?.) ();
pubsub.();
},
},
},
};
Performance
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
schema,
validationRules: [
depthLimit(10),
createComplexityRule({ maxComplexity: 1000 }),
],
});
Checklist