GraphQL Core Knowledge

SkillMedia

GraphQL API design. Covers schema, queries, mutations, and resolvers. Use when building or consuming GraphQL APIs.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the GraphQL Core Knowledge skill

What this skill tells your AI

The instructions your AI receives, as published by claude-dev-suite/claude-dev-suite in skills/api-design/graphql/SKILL.md and read by ahel’s review.

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: graphql for comprehensive documentation.

Schema Definition

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String
  author: User!
  published: Boolean!
}

type Query {
  user(id: ID!): User
  users(limit: Int, offset: Int): [User!]!
  post(id: ID!): Post
}

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

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

Resolvers

const resolvers = {
  Query: {
    user: (_, { id }, context) => {
      return context.db.users.findUnique({ where: { id } });
    },
    users: (_, { limit, offset }, context) => {
      return context.db.users.findMany({ take: limit, skip: offset });
    },
  },
  Mutation: {
    createUser: (_, { input }, context) => {
      return context.db.users.create({ data: input });
    },
  },
  User: {
    posts: (parent, _, context) => {
      return context.db.posts.findMany({ where: { authorId: parent.id } });
    },
  },
};

Queries

query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
    posts {
      title
      published
    }
  }
}

mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
  }
}

When NOT to Use This Skill

  • REST API design (use rest-api skill)
  • OpenAPI/Swagger documentation (use openapi skill)
  • tRPC type-safe APIs (use trpc skill)
  • Generating GraphQL types from schema (use graphql-codegen skill)
  • Simple CRUD operations where REST is sufficient

Best Practices

DoDon't
Use input types for mutationsN+1 queries (use DataLoader)
Implement paginationReturn unbounded lists
Add field-level authExpose sensitive data
Use fragments for reuseOver-fetch data

Anti-Patterns

Anti-PatternWhy It's BadSolution
N+1 queriesCauses performance issues, database overloadUse DataLoader for batching
Exposing implementation details in schemaTight coupling, hard to refactorUse domain-driven schema design
No pagination on listsMemory issues, slow responsesImplement cursor or offset pagination
Allowing unbounded query depthDoS vulnerabilityAdd depth limiting
No query complexity limitsResource exhaustionAdd complexity analysis
Exposing sensitive fields without authSecurity vulnerabilityAdd field-level authorization
Using String for IDsType safety issuesUse ID! scalar type
Returning null instead of errorsPoor error handlingUse proper GraphQL error responses

Quick Troubleshooting

IssuePossible CauseSolution
Slow query performanceN+1 queriesImplement DataLoader, check resolver patterns
High memory usageLarge unbounded listsAdd pagination, limit query depth
"Cannot return null for non-nullable field"Missing data or resolver errorCheck database queries, add error handling
Query rejectedDepth or complexity limit exceededOptimize query, reduce nesting
Authentication errorsMissing or invalid tokenCheck context creation, verify token
Type mismatch errorsSchema/resolver mismatchEnsure resolver return types match schema
CORS errorsServer configuration issueConfigure CORS in Apollo Server
Introspection disabledProduction security settingEnable for development, disable in production

Production Readiness

Security Configuration

// Query depth limiting
import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  schema,
  validationRules: [depthLimit(10)], // Max 10 levels deep
});

// Query complexity limiting
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const complexityLimitRule = createComplexityLimitRule(1000, {
  scalarCost: 1,
  objectCost: 10,
  listFactor: 10,
});

// Disable introspection in production
const server = new ApolloServer({
  introspection: process.env.NODE_ENV !== 'production',
  plugins: [
    process.env.NODE_ENV === 'production'
      ? ApolloServerPluginLandingPageDisabled()
      : ApolloServerPluginLandingPageLocalDefault(),
  ],
});

N+1 Query Prevention (DataLoader)

import DataLoader from 'dataloader';

// Create loader per request (in context)
function createLoaders(db: PrismaClient) {
  return {
    userLoader: new DataLoader<string, User>(async (ids) => {
      const users = await db.user.findMany({
        where: { id: { in: [...ids] } },
      });
      const userMap = new Map(users.map(u => [u.id, u]));
      return ids.map(id => userMap.get(id) || null);
    }),

    postsByUserLoader: new DataLoader<string, Post[]>(async (userIds) => {
      const posts = await db.post.findMany({
        where: { authorId: { in: [...userIds] } },
      });
      const postsByUser = new Map<string, Post[]>();
      posts.forEach(p => {
        const existing = postsByUser.get(p.authorId) || [];
        postsByUser.set(p.authorId, [...existing, p]);
      });
      return userIds.map(id => postsByUser.get(id) || []);
    }),
  };
}

// Use in resolvers
const resolvers = {
  User: {
    posts: (parent, _, context) => {
      return context.loaders.postsByUserLoader.load(parent.id);
    },
  },
};

Field-Level Authorization

import { rule, shield, and, or } from 'graphql-shield';

const isAuthenticated = rule()((parent, args, context) => {
  return context.user !== null;
});

const isAdmin = rule()((parent, args, context) => {
  return context.user?.role === 'ADMIN';
});

const isOwner = rule()((parent, args, context) => {
  return parent.authorId === context.user?.id;
});

const permissions = shield({
  Query: {
    users: isAuthenticated,
    user: isAuthenticated,
  },
  Mutation: {
    deleteUser: and(isAuthenticated, or(isAdmin, isOwner)),
    updateUser: and(isAuthenticated, or(isAdmin, isOwner)),
  },
  User: {
    email: or(isAdmin, isOwner), // Only owner or admin can see email
  },
});

const server = new ApolloServer({
  schema: applyMiddleware(schema, permissions),
});

Rate Limiting

import { rateLimitDirective } from 'graphql-rate-limit-directive';

const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } =
  rateLimitDirective();

const typeDefs = gql`
  ${rateLimitDirectiveTypeDefs}

  type Query {
    users: [User!]! @rateLimit(limit: 100, duration: 60)
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
      @rateLimit(limit: 10, duration: 60)
  }
`;

Error Handling

// Custom error formatting
const server = new ApolloServer({
  formatError: (formattedError, error) => {
    // Log original error
    logger.error(error);

    // Don't leak internal errors
    if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') {
      return {
        message: 'Internal server error',
        extensions: {
          code: 'INTERNAL_SERVER_ERROR',
        },
      };
    }

    // Remove stack trace in production
    if (process.env.NODE_ENV === 'production') {
      delete formattedError.extensions?.stacktrace;
    }

    return formattedError;
  },
});

Monitoring Metrics

MetricAlert Threshold
Query duration p99> 500ms
Error rate> 1%
Complexity score (avg)> 500
Depth exceeded errors> 10/min
DataLoader cache hit ratio< 50%

Pagination (Relay-style)

type Query {
  users(first: Int, after: String, last: Int, before: String): UserConnection!
}

type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  cursor: String!
  node: User!
}

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

Request Logging

const server = new ApolloServer({
  plugins: [
    {
      async requestDidStart(requestContext) {
        const start = Date.now();

        return {
          async willSendResponse(ctx) {
            logger.info({
              operationName: ctx.request.operationName,
              query: ctx.request.query,
              variables: ctx.request.variables,
              duration: Date.now() - start,
              errors: ctx.errors?.length || 0,
            });
          },
        };
      },
    },
  ],
});

Checklist

  • Query depth limiting
  • Query complexity limiting
  • Introspection disabled in production
  • DataLoader for N+1 prevention
  • Field-level authorization
  • Rate limiting on mutations
  • Custom error formatting
  • Relay-style pagination
  • Request logging with timing
  • Input validation
  • Persisted queries (optional)
  • APQ (Automatic Persisted Queries) enabled

Code Generation

GraphQL Codegen generates TypeScript types and hooks from your GraphQL schema and operations.

Quick Setup

npm install -D @graphql-codegen/cli @graphql-codegen/client-preset
// codegen.ts
import { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  schema: 'http://localhost:4000/graphql',
  documents: ['src/**/*.graphql', 'src/**/*.tsx'],
  generates: {
    './src/gql/': {
      preset: 'client',
      plugins: [],
    },
  },
};

export default config;

Generated Usage

import { graphql } from '@/gql';
import { useQuery } from '@tanstack/react-query';

const UserQuery = graphql(`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`);

function UserProfile({ id }: { id: string }) {
  const { data } = useQuery({
    queryKey: ['user', id],
    queryFn: () => request(endpoint, UserQuery, { id }),
  });

  return <div>{data?.user?.name}</div>;
}

Related Skills

SkillPurpose
GraphQL CodegenFull codegen setup
TanStack QueryData fetching hooks
React APIAlternative data patterns

Reference Documentation

Signals

GitHub stars
33
Forks
8
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
graphql
Source
github.com/claude-dev-suite/claude-dev-suite