Skip to content
graphql-expert logo

GraphQL Expert

graphql-expert

Expert-level GraphQL API development with schema design, resolvers, and subscriptions

SKILL.md

Full skill instructions

GraphQL Expert

Expert guidance for GraphQL API development, schema design, resolvers, subscriptions, and best practices for building type-safe, efficient APIs.

Core Concepts

Schema Design

  • Type system and schema definition language (SDL)
  • Object types, interfaces, unions, and enums
  • Input types and custom scalars
  • Schema stitching and federation
  • Modular schema organization

Resolvers

  • Resolver functions and data sources
  • Context and info arguments
  • Field-level resolvers
  • Resolver chains and data loaders
  • Error handling in resolvers

Queries and Mutations

  • Query design and naming conventions
  • Mutation patterns and best practices
  • Input validation and sanitization
  • Pagination strategies (cursor-based, offset)
  • Filtering and sorting

Subscriptions

  • Real-time updates with WebSocket
  • Subscription resolvers
  • PubSub patterns
  • Subscription filtering
  • Connection management

Performance

  • N+1 query problem and DataLoader
  • Query complexity analysis
  • Depth limiting and query cost
  • Caching strategies (field-level, full response)
  • Batching and deduplication

Modern GraphQL Development

Apollo Server 4

// Apollo Server 4 setup
import { ApolloServer } from '@apollo/​server';
import { startStandaloneServer } from '@apollo/​server/​standalone';
import { ApolloServerPluginDrainHttpServer } from '@apollo/​server/​plugin/​drainHttpServer';

// Type definitions
const typeDefs = `#graphql
  type User {
    id: ID!
    email: String!
    name: String!
    posts: [Post!]!
    createdAt: DateTime!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    published: Boolean!
    tags: [String!]!
    createdAt: DateTime!
    updatedAt: DateTime!
  }

  type Query {
    users(limit: Int = 10, offset: Int = 0): UsersConnection!
    user(id: ID!): User
    posts(filter: PostFilter, sort: SortOrder): [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User!
    deleteUser(id: ID!): Boolean!

    createPost(input: CreatePostInput!): Post!
    updatePost(id: ID!, input: UpdatePostInput!): Post!
    publishPost(id: ID!): Post!
  }

  type Subscription {
    postPublished: Post!
    userCreated: User!
  }

  input CreateUserInput {
    email: String!
    name: String!
    password: String!
  }

  input UpdateUserInput {
    email: String
    name: String
  }

  input CreatePostInput {
    title: String!
    content: String!
    tags: [String!]
  }

  input UpdatePostInput {
    title: String
    content: String
    tags: [String!]
  }

  input PostFilter {
    published: Boolean
    authorId: ID
    tag: String
  }

  type UsersConnection {
    nodes: [User!]!
    totalCount: Int!
    pageInfo: PageInfo!
  }

  type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
  }

  enum SortOrder {
    NEWEST_FIRST
    OLDEST_FIRST
    TITLE_ASC
    TITLE_DESC
  }

  scalar DateTime
`;

// Resolvers
const resolvers = {
  Query: {
    users: async (_, { limit, offset }, { dataSources }) => {
      const users = await dataSources.userAPI.getUsers({ limit, offset });
      const totalCount = await dataSources.userAPI.getTotalCount();

      return {
        nodes: users,
        totalCount,
        pageInfo: {
          hasNextPage: offset + limit < totalCount,
          hasPreviousPage: offset > 0,
        },
      };
    },

    user: async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    },

    posts: async (_, { filter, sort }, { dataSources }) => {
      return dataSources.postAPI.getPosts({ filter, sort });
    },

    post: async (_, { id }, { dataSources }) => {
      return dataSources.postAPI.getPostById(id);
    },
  },

  Mutation: {
    createUser: async (_, { input }, { dataSources, user }) => {
      // Validate input
      if (!isValidEmail(input.email)) {
        throw new GraphQLError('Invalid email address', {
          extensions: { code: 'BAD_USER_INPUT' },
        });
      }

      return dataSources.userAPI.createUser(input);
    },

    updateUser: async (_, { id, input }, { dataSources, user }) => {
      // Check authorization
      if (user.id !== id && !user.isAdmin) {
        throw new GraphQLError('Not authorized', {
          extensions: { code: 'FORBIDDEN' },
        });
      }

      return dataSources.userAPI.updateUser(id, input);
    },

    createPost: async (_, { input }, { dataSources, user, pubsub }) => {
      if (!user) {
        throw new GraphQLError('Not authenticated', {
          extensions: { code: 'UNAUTHENTICATED' },
        });
      }

      const post = await dataSources.postAPI.createPost({
        ...input,
        authorId: user.id,
      });

      return post;
    },

    publishPost: async (_, { id }, { dataSources, user, pubsub }) => {
      const post = await dataSources.postAPI.publishPost(id);

      // Trigger subscription
      pubsub.publish('POST_PUBLISHED', { postPublished: post });

      return post;
    },
  },

  Subscription: {
    postPublished: {
      subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(['POST_PUBLISHED']),
    },

    userCreated: {
      subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(['USER_CREATED']),
    },
  },

  User: {
    posts: async (parent, _, { dataSources }) => {
      return dataSources.postAPI.getPostsByAuthorId(parent.id);
    },
  },

  Post: {
    author: async (parent, _, { dataSources }) => {
      return dataSources.userAPI.getUserById(parent.authorId);
    },
  },

  DateTime: new GraphQLScalarType({
    name: 'DateTime',
    description: 'ISO 8601 date-time string',
    serialize(value: Date) {
      return value.toISOString();
    },
    parseValue(value: string) {
      return new Date(value);
    },
    parseLiteral(ast) {
      if (ast.kind === Kind.STRING) {
        return new Date(ast.value);
      }
      return null;
    },
  }),
};

// Server setup
const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    ApolloServerPluginDrainHttpServer({ httpServer }),
  ],
});

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => {
    const token = req.headers.authorization || '';
    const user = await getUserFromToken(token);

    return {
      user,
      dataSources: {
        userAPI: new UserAPI(),
        postAPI: new PostAPI(),
      },
      pubsub,
    };
  },
  listen: { port: 4000 },
});

DataLoader for N+1 Prevention

import DataLoader from 'dataloader';

// Create DataLoaders
class UserAPI {
  private loader: DataLoader<string, User>;

  constructor() {
    this.loader = new DataLoader(async (ids: readonly string[]) => {
      // Batch fetch users
      const users = await db.user.findMany({
        where: { id: { in: [...ids] } },
      });

      // Return in same order as input ids
      const userMap = new Map(users.map(u => [u.id, u]));
      return ids.map(id => userMap.get(id) ?? null);
    });
  }

  async getUserById(id: string): Promise<User | null> {
    return this.loader.load(id);
  }

  async getUsersByIds(ids: string[]): Promise<(User | null)[]> {
    return this.loader.loadMany(ids);
  }
}

// Usage in resolvers
const resolvers = {
  Post: {
    author: async (parent, _, { dataSources }) => {
      // This will be batched with DataLoader
      return dataSources.userAPI.getUserById(parent.authorId);
    },
  },
};

GraphQL Codegen

# codegen.yml
schema: './​src/​schema.graphql'
documents: './​src/​**/​*.graphql'
generates:
  src/​generated/​graphql.ts:
    plugins:
      - typescript
      - typescript-resolvers
      - typescript-operations
    config:
      useIndexSignature: true
      contextType: '../​context#Context'
      mappers:
        User: '../​models#UserModel'
        Post: '../​models#PostModel'
// Generated types usage
import { Resolvers } from './​generated/​graphql';

const resolvers: Resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    },
  },
};

Error Handling

import { GraphQLError } from 'graphql';

class NotFoundError extends GraphQLError {
  constructor(resource: string, id: string) {
    super(`${resource} with id ${id} not found`, {
      extensions: {
        code: 'NOT_FOUND',
        resource,
        id,
      },
    });
  }
}

class ValidationError extends GraphQLError {
  constructor(message: string, field: string) {
    super(message, {
      extensions: {
        code: 'BAD_USER_INPUT',
        field,
      },
    });
  }
}

// Usage
const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      const user = await dataSources.userAPI.getUserById(id);
      if (!user) {
        throw new NotFoundError('User', id);
      }
      return user;
    },
  },
};

Authentication & Authorization

// Context with user
interface Context {
  user: User | null;
  dataSources: DataSources;
}

// Auth directive
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/​utils';
import { defaultFieldResolver, GraphQLSchema } from 'graphql';

function authDirective(directiveName: string) {
  return {
    authDirectiveTypeDefs: `directive @${directiveName}(requires: Role = USER) on OBJECT | FIELD_DEFINITION`,

    authDirectiveTransformer: (schema: GraphQLSchema) =>
      mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
          const directive = getDirective(schema, fieldConfig, directiveName)?.[0];

          if (directive) {
            const { resolve = defaultFieldResolver } = fieldConfig;
            const { requires } = directive;

            fieldConfig.resolve = async (source, args, context, info) => {
              if (!context.user) {
                throw new GraphQLError('Not authenticated', {
                  extensions: { code: 'UNAUTHENTICATED' },
                });
              }

              if (requires && !context.user.roles.includes(requires)) {
                throw new GraphQLError('Not authorized', {
                  extensions: { code: 'FORBIDDEN' },
                });
              }

              return resolve(source, args, context, info);
            };
          }

          return fieldConfig;
        },
      }),
  };
}

// Schema with directive
const typeDefs = `#graphql
  enum Role {
    USER
    ADMIN
  }

  type Query {
    user(id: ID!): User @auth
    adminData: AdminData @auth(requires: ADMIN)
  }
`;

Subscriptions with WebSocket

import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/​lib/​use/​ws';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

// Create WebSocket server
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/​graphql',
});

// Setup subscription server
useServer(
  {
    schema,
    context: async (ctx) => {
      const token = ctx.connectionParams?.authentication;
      const user = await getUserFromToken(token);
      return { user, pubsub };
    },
  },
  wsServer
);

// Subscription resolvers
const resolvers = {
  Subscription: {
    postPublished: {
      subscribe: (_, __, { pubsub }) =>
        pubsub.asyncIterator(['POST_PUBLISHED']),
    },

    messageAdded: {
      subscribe: withFilter(
        (_, __, { pubsub }) => pubsub.asyncIterator(['MESSAGE_ADDED']),
        (payload, variables) => {
          // Filter by channel
          return payload.messageAdded.channelId === variables.channelId;
        }
      ),
    },
  },
};

GraphQL Client (Apollo Client)

import { ApolloClient, InMemoryCache, gql, useQuery, useMutation } from '@apollo/​client';

const client = new ApolloClient({
  uri: 'http://localhost:4000/​graphql',
  cache: new InMemoryCache(),
});

// Query
const GET_USERS = gql`
  query GetUsers($limit: Int, $offset: Int) {
    users(limit: $limit, offset: $offset) {
      nodes {
        id
        name
        email
      }
      totalCount
      pageInfo {
        hasNextPage
      }
    }
  }
`;

function UserList() {
  const { loading, error, data } = useQuery(GET_USERS, {
    variables: { limit: 10, offset: 0 },
  });

  if (loading) return <p>Loading...</​p>;
  if (error) return <p>Error: {error.message}</​p>;

  return (
    <ul>
      {data.users.nodes.map(user => (
        <li key={user.id}>{user.name}</​li>
      ))}
    </​ul>
  );
}

// Mutation
const CREATE_POST = gql`
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      id
      title
      author {
        name
      }
    }
  }
`;

function CreatePostForm() {
  const [createPost, { loading, error }] = useMutation(CREATE_POST, {
    refetchQueries: ['GetPosts'],
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    await createPost({
      variables: {
        input: {
          title: 'New Post',
          content: 'Post content',
        },
      },
    });
  };

  return <form onSubmit={handleSubmit}>...</​form>;
}

// Subscription
const POST_PUBLISHED = gql`
  subscription OnPostPublished {
    postPublished {
      id
      title
      author {
        name
      }
    }
  }
`;

function PostFeed() {
  const { data, loading } = useSubscription(POST_PUBLISHED);

  if (loading) return <p>Waiting for posts...</​p>;

  return <div>New post: {data.postPublished.title}</​div>;
}

Query Complexity & Depth Limiting

import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    createComplexityLimitRule(1000, {
      scalarCost: 1,
      objectCost: 10,
      listFactor: 10,
    }),
  ],
  plugins: [
    {
      async requestDidStart() {
        return {
          async didResolveOperation({ request, document }) {
            const complexity = getComplexity({
              schema,
              query: document,
              variables: request.variables,
              estimators: [
                fieldExtensionsEstimator(),
                simpleEstimator({ defaultComplexity: 1 }),
              ],
            });

            if (complexity > 1000) {
              throw new GraphQLError(
                `Query is too complex: ${complexity}. Maximum allowed: 1000`
              );
            }
          },
        };
      },
    },
  ],
});

GraphQL Federation

Federated Schema

// Users service
import { buildSubgraphSchema } from '@apollo/​subgraph';

const typeDefs = gql`
  extend schema @link(url: "https://specs.apollo.dev/​federation/​v2.3")

  type User @key(fields: "id") {
    id: ID!
    email: String!
    name: String!
  }

  type Query {
    user(id: ID!): User
    users: [User!]!
  }
`;

const resolvers = {
  User: {
    __resolveReference: async (reference, { dataSources }) => {
      return dataSources.userAPI.getUserById(reference.id);
    },
  },
  Query: {
    user: (_, { id }, { dataSources }) => dataSources.userAPI.getUserById(id),
    users: (_, __, { dataSources }) => dataSources.userAPI.getUsers(),
  },
};

// Posts service
const typeDefs = gql`
  extend schema @link(url: "https://specs.apollo.dev/​federation/​v2.3")

  type Post @key(fields: "id") {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

  extend type User @key(fields: "id") {
    id: ID! @external
    posts: [Post!]!
  }

  type Query {
    post(id: ID!): Post
    posts: [Post!]!
  }
`;

const resolvers = {
  Post: {
    author: (post) => ({ __typename: 'User', id: post.authorId }),
  },
  User: {
    posts: (user, _, { dataSources }) =>
      dataSources.postAPI.getPostsByAuthorId(user.id),
  },
};

// Gateway
import { ApolloGateway, IntrospectAndCompose } from '@apollo/​gateway';

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'users', url: 'http://localhost:4001/​graphql' },
      { name: 'posts', url: 'http://localhost:4002/​graphql' },
    ],
  }),
});

const server = new ApolloServer({ gateway });

Best Practices

Schema Design

# Use clear, consistent naming
type User {
  id: ID!
  email: String!
  createdAt: DateTime!
}

# Prefer input types over many arguments
input CreateUserInput {
  email: String!
  name: String!
}

mutation {
  createUser(input: CreateUserInput!): User!
}

# Use enums for fixed sets
enum OrderStatus {
  PENDING
  CONFIRMED
  SHIPPED
  DELIVERED
}

# Design for pagination
type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
}

Performance Optimization

  • Use DataLoader for batching and caching
  • Implement query complexity analysis
  • Add depth limiting
  • Use persisted queries for production
  • Cache at multiple levels (CDN, field-level, full response)
  • Monitor query performance and slow fields

Security

  • Validate and sanitize all inputs
  • Implement rate limiting
  • Use query depth and complexity limits
  • Sanitize error messages in production
  • Implement proper authentication and authorization
  • Use HTTPS for all connections
  • Validate file uploads (type, size)

Anti-Patterns to Avoid

❌ Exposing internal IDs: Use opaque IDs or UUIDs ❌ Overly nested queries: Limit query depth ❌ No pagination: Always paginate lists ❌ Resolving in mutations: Keep mutations focused ❌ Exposing database schema directly: Design API-first ❌ No DataLoader: Leads to N+1 queries ❌ Generic error messages: Provide actionable errors ❌ No versioning strategy: Plan for schema evolution

Testing

import { ApolloServer } from '@apollo/​server';
import { describe, it, expect } from 'vitest';

describe('GraphQL Server', () => {
  it('should fetch user by id', async () => {
    const server = new ApolloServer({ typeDefs, resolvers });

    const response = await server.executeOperation({
      query: `
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            name
            email
          }
        }
      `,
      variables: { id: '1' },
    });

    expect(response.body.kind).toBe('single');
    expect(response.body.singleResult.data?.user).toEqual({
      id: '1',
      name: 'Alice',
      email: '[email protected]',
    });
  });

  it('should create post', async () => {
    const response = await server.executeOperation({
      query: `
        mutation CreatePost($input: CreatePostInput!) {
          createPost(input: $input) {
            id
            title
          }
        }
      `,
      variables: {
        input: {
          title: 'Test Post',
          content: 'Content',
        },
      },
    });

    expect(response.body.singleResult.data?.createPost).toHaveProperty('id');
  });
});

Common Patterns

Relay Cursor Pagination

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
}

type PostEdge {
  cursor: String!
  node: Post!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

File Upload

import { GraphQLUpload } from 'graphql-upload-ts';

const typeDefs = gql`
  scalar Upload

  type Mutation {
    uploadFile(file: Upload!): File!
  }
`;

const resolvers = {
  Upload: GraphQLUpload,

  Mutation: {
    uploadFile: async (_, { file }) => {
      const { createReadStream, filename, mimetype } = await file;
      const stream = createReadStream();

      // Process upload
      await saveFile(stream, filename);

      return { id: '1', filename, mimetype };
    },
  },
};

Resources

More Security skills

supabase logo
Security

supabase

Handles the full Supabase workflow from schema changes to deployment, with built-in security guardrails that catch common traps like RLS...

2.7K 308.9K
View

Comprehensive guides and best practices for Neon Serverless Postgres, covering setup, connection methods, authentication, and platform APIs.

98 216.2K
View

Guide for setting up and using Firebase Authentication. Use this skill when the user's app requires user sign-in, user management, or secure data access using auth rules.

462 163.8K
View

A skill to evaluate how secure Firestore security rules are. Use this when Firestore security rules are updated to ensure that the generated rules are extremely secure and robust.

462 127.7K
View

Official skill for integrating Firebase AI Logic (Gemini API) into web applications. Covers setup, multimodal inference, structured output, and security.

462 125.1K
View

Complete Better Auth server and client setup with database adapters, session management, plugins, and security configuration.

222 118.2K
View

Deploy and manage projects on Vercel using token-based authentication. Use when working with Vercel CLI using access tokens rather than interactive login — e.g. "deploy to vercel", "set up vercel", "add environment variables to vercel".

31.9K 116.1K
View
cloudflare logo
Security

cloudflare

Complete Cloudflare platform integration with decision trees for compute, storage, AI, networking, security, and infrastructure-as-code.

3K 110.7K
View

Run Azure compliance and security audits with azqr plus Key Vault expiration checks. Covers best-practice assessment, resource review, policy/compliance validation, and security posture checks. WHEN: compliance scan, security audit, BEFORE running azqr (compliance cli tool), Azure best practices, Key Vault expiration check, expired certificates, expiring secrets, orphaned resources, compliance assessment.

253 103K
View
gws-gmail logo
Security

gws-gmail

Send, read, and manage Gmail messages, drafts, labels, and account settings.

31.2K 78.9K
View

Comprehensive website auditing across 230+ rules in 21 categories including SEO, performance, security, and accessibility.

94 72.3K
View
gws-shared logo
Security

gws-shared

Shared authentication, CLI syntax, and output formatting patterns for gws Google Workspace commands.

31.2K 63.4K
View

Security AI tools

StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
GeoSpy logo
Security

GeoSpy

GeoSpy: Pricing, Features, FAQs, and Alternatives for AI Teams

Free
View

Detect AI-generated voices to protect against audio fraud.

Paid
View

Ensure Your Content's Originality with AI Plagiarism Checker

Freemium
View
Img Upscaler logo
Security

Img Upscaler

Upscale images by 400% without quality loss

Freemium
View
AICheatCheck logo
Security

AICheatCheck

Accurately Detect AI-Generated Content with TheChecker.AI

Free
View
D
Security

Detect GPT

Chrome extension that detects and flags AI-generated content

Free
View