graphql-expert
Design and implement production GraphQL APIs. Use when designing GraphQL schemas, implementing resolvers, solving N+1 problems with DataLoader, implementing subscriptions, building GraphQL federation, generating types from schemas, or optimizing GraphQL performance. Covers Apollo Server, GraphQL Yoga, Pothos schema builder, and persisted queries.
GraphQL Production Expert
Schema Design Best Practices
# Use Relay pagination spec throughout
# Use mutation payload types with errors array
# Name mutations as verb + noun
# Use enums for finite sets
# Make scalar types explicit
type Query {
node(id: ID!): Node # Global node interface (Relay spec)
users(
first: Int
after: String
last: Int
before: String
filter: UserFilter
orderBy: UserOrderBy = { field: CREATED_AT, direction: DESC }
): UserConnection!
user(id: ID!): User
}
interface Node {
id: ID!
}
type User implements Node {
id: ID!
email: String!
displayName: String!
createdAt: DateTime!
# Nested connections — implement with DataLoader
posts(first: Int, after: String): PostConnection!
followers(first: Int): UserConnection!
}
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 payload pattern — structured errors
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload!
deletePost(id: ID!): DeletePostPayload!
}
type CreatePostPayload {
post: Post # null if errors
errors: [UserError!]!
clientMutationId: String # Relay spec
}
type UserError {
field: [String!] # Which field (null = form-level)
code: String!
message: String!
}
input CreatePostInput {
title: String!
content: String!
tags: [String!]
status: PostStatus = DRAFT
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
scalar DateTime
scalar JSON
Resolvers with TypeScript
// Apollo Server 4 with TypeScript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { makeExecutableSchema } from '@graphql-tools/schema';
interface Context {
user: AuthUser | null;
loaders: ReturnType<typeof createLoaders>;
db: Database;
}
const resolvers = {
Query: {
users: async (_, { first = 20, after, filter }, ctx: Context) => {
const users = await ctx.db.users.findMany({
take: first + 1, // Fetch one extra to determine hasNextPage
cursor: after ? { id: decodeCursor(after) } : undefined,
where: buildUserFilter(filter),
orderBy: { createdAt: 'desc' },
});
const hasNextPage = users.length > first;
const nodes = hasNextPage ? users.slice(0, -1) : users;
return {
edges: nodes.map(user => ({
node: user,
cursor: encodeCursor(user.id),
})),
pageInfo: {
hasNextPage,
hasPreviousPage: !!after,
startCursor: nodes[0] ? encodeCursor(nodes[0].id) : null,
endCursor: nodes.at(-1) ? encodeCursor(nodes.at(-1)!.id) : null,
},
totalCount: () => ctx.db.users.count({ where: buildUserFilter(filter) }),
};
},
user: async (_, { id }, ctx: Context) => {
return ctx.loaders.user.load(id); // DataLoader for batching
},
},
User: {
// N+1 solution: use DataLoader
posts: async (user, { first = 10, after }, ctx: Context) => {
return ctx.loaders.userPosts.load({ userId: user.id, first, after });
},
// Computed field
displayName: (user) => user.name || user.email.split('@')[0],
},
Mutation: {
createPost: async (_, { input }, ctx: Context) => {
if (!ctx.user) {
return {
post: null,
errors: [{ code: 'UNAUTHENTICATED', message: 'Must be logged in', field: null }],
};
}
// Validate
const errors = validateCreatePost(input);
if (errors.length > 0) return { post: null, errors };
try {
const post = await ctx.db.posts.create({
data: { ...input, authorId: ctx.user.id }
});
return { post, errors: [] };
} catch (e) {
return {
post: null,
errors: [{ code: 'DATABASE_ERROR', message: 'Failed to create post', field: null }]
};
}
},
},
};
DataLoader (N+1 Prevention)
import DataLoader from 'dataloader';
import { PrismaClient } from '@prisma/client';
export function createLoaders(db: PrismaClient) {
return {
// Batch user lookups
user: new DataLoader<string, User | null>(
async (ids) => {
const users = await db.user.findMany({
where: { id: { in: ids as string[] } },
});
const map = new Map(users.map(u => [u.id, u]));
return ids.map(id => map.get(id) ?? null);
},
{
cache: true, // Cache per request (default)
maxBatchSize: 100, // Max batch size
batchScheduleFn: (callback) => setTimeout(callback, 0), // Next tick
}
),
// Batch load posts by userId
userPosts: new DataLoader<string, Post[]>(
async (userIds) => {
const posts = await db.post.findMany({
where: { authorId: { in: userIds as string[] } },
orderBy: { createdAt: 'desc' },
});
// Group by authorId, preserving order
const grouped = userIds.map(userId =>
posts.filter(p => p.authorId === userId)
);
return grouped;
},
{ cache: false } // No caching for time-sensitive data
),
};
}
// Create fresh loaders per request (prevents cache bleeding between users)
// In Apollo context:
const server = new ApolloServer({ resolvers, typeDefs });
startStandaloneServer(server, {
context: async ({ req }) => ({
user: await getUserFromToken(req.headers.authorization),
loaders: createLoaders(prisma), // NEW loaders per request
db: prisma,
}),
});
Subscriptions
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
// In production, use Redis-based PubSub for multi-server:
// import { RedisPubSub } from 'graphql-redis-subscriptions';
// const pubsub = new RedisPubSub({ publisher: redisClient, subscriber: redisClient });
const resolvers = {
Subscription: {
postCreated: {
subscribe: (_, args, ctx) => {
// Authorization check
if (!ctx.user) throw new Error('Unauthorized');
return pubsub.asyncIterator(['POST_CREATED']);
},
resolve: (payload) => payload.postCreated,
},
// Filter subscription events
postUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator(['POST_UPDATED']),
(payload, variables) => payload.postUpdated.authorId === variables.authorId
),
},
},
Mutation: {
createPost: async (_, { input }, ctx) => {
const post = await ctx.db.posts.create({ data: input });
// Publish to subscribers
await pubsub.publish('POST_CREATED', { postCreated: post });
return { post, errors: [] };
},
},
};
GraphQL Federation (Microservices)
// Subgraph: users service
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';
const userSchema = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3")
type Query {
me: User
user(id: ID!): User
}
type User @key(fields: "id") { # This type is "owned" by users service
id: ID!
email: String!
name: String!
}
`;
// Subgraph: posts service
const postSchema = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3")
type Query {
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User! # References User from users service
}
# Extend User to add posts field (without owning the User type)
extend type User @key(fields: "id") {
id: ID! @external # ID comes from users service
posts: [Post!]! # Added by posts service
}
`;
const resolvers = {
User: {
// Federation: resolve User from just its key
__resolveReference: async ({ id }) => {
return db.users.findUnique({ where: { id } });
},
posts: async (user) => {
return db.posts.findMany({ where: { authorId: user.id } });
},
},
};
Persisted Queries (Production Performance)
// Automatic Persisted Queries (APQ) reduce bandwidth
// Client sends hash first, server responds with error if not found, then client sends full query
import { ApolloServer } from '@apollo/server';
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
import { generatePersistedQueryIdsFromManifest } from "@apollo/persisted-query-lists";
import { sha256 } from 'crypto-hash';
// Server-side: allow only registered operations (safelisting)
const server = new ApolloServer({
typeDefs,
resolvers,
// Only allow pre-registered operations in production
allowBatchedHttpRequests: true,
csrfPrevention: true,
});
// Client-side APQ link
const link = createPersistedQueryLink({ sha256 }).concat(httpLink);
Security Patterns
// Depth limiting — prevent deeply nested queries (DoS protection)
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(10), // Max query depth
createComplexityLimitRule(1000, { // Max complexity score
scalarCost: 1,
objectCost: 2,
listFactor: 10,
}),
],
plugins: [
{
// Log expensive queries
requestDidStart: async () => ({
willSendResponse: async ({ response, metrics }) => {
if (metrics.executionStarted) {
const duration = Date.now() - metrics.executionStarted;
if (duration > 1000) {
logger.warn('Slow GraphQL query', {
duration,
operationName: response.http.headers.get('x-apollo-operation-name'),
});
}
}
},
}),
},
],
});Skill Information
- Source
- MoltbotDen
- Category
- Coding Agents & IDEs
- Repository
- View on GitHub
Related Skills
go-expert
Write idiomatic, production-quality Go code. Use when building Go APIs, CLIs, microservices, or systems code. Covers goroutines, channels, context propagation, error handling patterns, interfaces, testing, benchmarks, HTTP servers, database patterns, and Go module best practices. Expert-level Go idioms that senior engineers expect.
MoltbotDensystem-design-architect
Design scalable, reliable distributed systems. Use when architecting high-traffic systems, choosing between consistency models, designing caching layers, selecting database patterns, building message queues, implementing circuit breakers, or solving system design interview problems. Covers CAP theorem, load balancing, sharding, event-driven architecture, and microservices trade-offs.
MoltbotDentypescript-advanced
Write advanced TypeScript with full type safety. Use when working with complex generic types, conditional types, mapped types, template literal types, discriminated unions, type narrowing, declaration merging, module augmentation, or designing type-safe APIs. Covers TypeScript 5.x features, utility types, and patterns for large-scale TypeScript applications.
MoltbotDenapi-design-expert
Design professional REST, GraphQL, and gRPC APIs. Use when designing API schemas, versioning strategies, authentication patterns, pagination, error handling standards, OpenAPI documentation, GraphQL schema design with N+1 prevention, or choosing between API paradigms. Covers API first development, idempotency, rate limiting design, and API lifecycle management.
MoltbotDenrust-systems
Write safe, performant Rust systems code. Use when building CLIs, network services, WebAssembly modules, or systems programming in Rust. Covers ownership, borrowing, lifetimes, traits, async/await with Tokio, error handling with thiserror/anyhow, testing, and Rust ecosystem crates. Idiomatic Rust patterns that pass code review.
MoltbotDen