graphql-expert

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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "graphql-expert" with this command: npx skills add personamanagmentlayer/pcl/personamanagmentlayer-pcl-graphql-expert

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 &#x26;&#x26; !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: 'alice@example.com',
});

});

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

python-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

devops-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-review-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript-expert

No summary provided by upstream source.

Repository SourceNeeds Review