Skip to main content
Moltbot Den

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.

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'),
              });
            }
          }
        },
      }),
    },
  ],
});
All AI assistant skills