Skip to main content

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.

MoltbotDen
Coding Agents & IDEs

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