Claude
Skills
Sign in
Back

graphql

Included with Lifetime
$97 forever

GraphQL API design. Covers schema, queries, mutations, and resolvers. Use when building or consuming GraphQL APIs. USE WHEN: user mentions "GraphQL", "schema definition", "resolvers", "mutations", "queries", "DataLoader", "N+1 problem", asks about "how to design GraphQL API", "GraphQL schema", "GraphQL authentication", "GraphQL pagination", "Apollo Server" DO NOT USE FOR: REST APIs - use `rest-api` instead; tRPC - use `trpc` instead; GraphQL code generation - use `graphql-codegen` instead

Design

What this skill does

# GraphQL Core Knowledge

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

## Schema Definition

```graphql
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

```typescript
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

```graphql
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

| Do | Don't |
|----|----|
| Use input types for mutations | N+1 queries (use DataLoader) |
| Implement pagination | Return unbounded lists |
| Add field-level auth | Expose sensitive data |
| Use fragments for reuse | Over-fetch data |

## Anti-Patterns

| Anti-Pattern | Why It's Bad | Solution |
|--------------|--------------|----------|
| N+1 queries | Causes performance issues, database overload | Use DataLoader for batching |
| Exposing implementation details in schema | Tight coupling, hard to refactor | Use domain-driven schema design |
| No pagination on lists | Memory issues, slow responses | Implement cursor or offset pagination |
| Allowing unbounded query depth | DoS vulnerability | Add depth limiting |
| No query complexity limits | Resource exhaustion | Add complexity analysis |
| Exposing sensitive fields without auth | Security vulnerability | Add field-level authorization |
| Using `String` for IDs | Type safety issues | Use `ID!` scalar type |
| Returning null instead of errors | Poor error handling | Use proper GraphQL error responses |

## Quick Troubleshooting

| Issue | Possible Cause | Solution |
|-------|----------------|----------|
| Slow query performance | N+1 queries | Implement DataLoader, check resolver patterns |
| High memory usage | Large unbounded lists | Add pagination, limit query depth |
| "Cannot return null for non-nullable field" | Missing data or resolver error | Check database queries, add error handling |
| Query rejected | Depth or complexity limit exceeded | Optimize query, reduce nesting |
| Authentication errors | Missing or invalid token | Check context creation, verify token |
| Type mismatch errors | Schema/resolver mismatch | Ensure resolver return types match schema |
| CORS errors | Server configuration issue | Configure CORS in Apollo Server |
| Introspection disabled | Production security setting | Enable for development, disable in production |

## Production Readiness

### Security Configuration

```typescript
// 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)

```typescript
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

```typescript
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

```typescript
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

```typescript
// 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

| Metric | Alert 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)

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

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

type UserEdge

Related in Design