Skip to content
graphql-reviewer logo

GraphQL Reviewer Skill

graphql-reviewer

WHEN: GraphQL schema review, resolver patterns, N+1 detection, query complexity, API security WHAT: Schema design + N+1 detection + Query complexity + Input validation + Error handling + DataLoader patterns WHEN NOT: REST API → api-documenter, Database schema → schema-reviewer, ORM → orm-reviewer

SKILL.md

Full skill instructions

GraphQL Reviewer Skill

Purpose

Reviews GraphQL schemas, resolvers, and operations for N+1 problems, query complexity limits, input validation, security best practices, and proper error handling.

When to Use

  • GraphQL schema or resolver review requests
  • "GraphQL", "N+1", "DataLoader", "query complexity" mentions
  • Schema design review
  • Projects with .graphql, .gql files
  • GraphQL library dependencies (Apollo, Relay, graphql-js)

Project Detection

  • .graphql or .gql schema files
  • schema.graphql or type-defs.ts
  • graphql package in dependencies
  • @apollo/​server, graphql-yoga, mercurius dependencies
  • @Query, @Mutation, @Resolver decorators (NestJS/​TypeGraphQL)

Workflow

Step 1: Analyze Project

**GraphQL Server**: Apollo Server 4.x / GraphQL Yoga
**Schema**: Code-first / SDL-first
**Language**: TypeScript / JavaScript
**ORM**: Prisma / TypeORM / Drizzle
**Key Features**:
  - DataLoader for batching
  - Query complexity plugin
  - Persisted queries

Step 2: Select Review Areas

AskUserQuestion:

"Which GraphQL areas to review?"
Options:
- Full GraphQL audit (recommended)
- N+1 / DataLoader patterns
- Schema design
- Query complexity / Security
- Error handling
- Input validation
multiSelect: true

Detection Rules

Critical: N+1 Query Problem

PatternIssueSeverity
Resolver per itemN+1 queriesCRITICAL
No DataLoaderUnbatched fetchesCRITICAL
ORM lazy load in resolverHidden N+1CRITICAL
// BAD: N+1 problem
// Schema
type Query {
  posts: [Post!]!
}

type Post {
  id: ID!
  author: User!  // N+1 here!
}

// Resolver - fetches author per post
const resolvers = {
  Query: {
    posts: () => db.post.findMany()
  },
  Post: {
    author: (post) => db.user.findUnique({ where: { id: post.authorId } })
    // If 100 posts → 1 + 100 queries!
  }
};

// GOOD: DataLoader for batching
import DataLoader from 'dataloader';

const createLoaders = () => ({
  userLoader: new DataLoader(async (ids: string[]) => {
    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);
  })
});

// Resolver with DataLoader
const resolvers = {
  Post: {
    author: (post, _, { loaders }) => loaders.userLoader.load(post.authorId)
    // Now: 1 + 1 queries (batched)
  }
};

// BEST: Prisma with includes (no N+1)
const resolvers = {
  Query: {
    posts: () => db.post.findMany({
      include: { author: true }  // Single query with JOIN
    })
  }
};

Critical: Excessive Fetching in Resolvers

PatternIssueSeverity
SELECT * in resolverOver-fetchingHIGH
No field selectionWasted resourcesMEDIUM
Ignoring selection setMissing optimizationHIGH
// BAD: Fetches all fields regardless of query
const resolvers = {
  Query: {
    user: (_, { id }) => db.user.findUnique({
      where: { id },
      include: {
        posts: true,      // Maybe not requested
        comments: true,   // Maybe not requested
        followers: true   // Maybe not requested
      }
    })
  }
};

// GOOD: Use info to select only requested fields
import { GraphQLResolveInfo } from 'graphql';
import graphqlFields from 'graphql-fields';

const resolvers = {
  Query: {
    user: (_, { id }, __, info: GraphQLResolveInfo) => {
      const requestedFields = graphqlFields(info);
      return db.user.findUnique({
        where: { id },
        include: {
          posts: 'posts' in requestedFields,
          comments: 'comments' in requestedFields
        }
      });
    }
  }
};

// BETTER: Use Prisma's select based on GraphQL query
import { PrismaSelect } from '@paljs/​plugins';

const resolvers = {
  Query: {
    user: (_, { id }, __, info) => {
      const select = new PrismaSelect(info).value;
      return db.user.findUnique({ where: { id }, ...select });
    }
  }
};

Critical: Mutation in Query

PatternIssueSeverity
Side effects in QueryViolates specCRITICAL
Write operation in QueryUnexpected behaviorCRITICAL
# BAD: Mutation disguised as Query
type Query {
  incrementViewCount(postId: ID!): Int!  # WRONG! This mutates data
  markAsRead(notificationId: ID!): Boolean!  # WRONG!
}

# GOOD: Mutations for side effects
type Mutation {
  incrementViewCount(postId: ID!): Post!
  markAsRead(notificationId: ID!): Notification!
}

# Query should be idempotent (read-only)
type Query {
  post(id: ID!): Post
  viewCount(postId: ID!): Int!
}

High: Missing Input Validation

PatternIssueSeverity
No validation in resolverBad data acceptedHIGH
Trusting client inputSecurity riskHIGH
No sanitizationInjection riskCRITICAL
// BAD: No validation
const resolvers = {
  Mutation: {
    createUser: (_, { input }) => {
      // input.email could be anything
      return db.user.create({ data: input });
    }
  }
};

// GOOD: Validate inputs
import { z } from 'zod';

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
  age: z.number().int().min(0).max(150).optional()
});

const resolvers = {
  Mutation: {
    createUser: (_, { input }) => {
      const validated = CreateUserSchema.parse(input);
      return db.user.create({ data: validated });
    }
  }
};

// Schema-level validation (GraphQL)
"""
User creation input
"""
input CreateUserInput {
  email: String! @constraint(format: "email")
  name: String! @constraint(minLength: 1, maxLength: 100)
  age: Int @constraint(min: 0, max: 150)
}

High: No Query Complexity Limit

PatternIssueSeverity
Unlimited depthDoS vectorHIGH
No complexity limitResource exhaustionHIGH
No rate limitingAbuse possibleMEDIUM
// BAD: Allows dangerous queries
// Can request: user.friends.friends.friends.friends...

// GOOD: Limit query depth
import depthLimit from 'graphql-depth-limit';

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

// GOOD: Query complexity plugin
import { createComplexityPlugin } from 'graphql-query-complexity';

const complexityPlugin = createComplexityPlugin({
  estimators: [
    fieldExtensionsEstimator(),
    simpleEstimator({ defaultComplexity: 1 })
  ],
  maximumComplexity: 1000,
  onComplete: (complexity) => {
    console.log('Query Complexity:', complexity);
  }
});

// Schema with complexity hints
type Query {
  users(first: Int!): [User!]! @complexity(multipliers: ["first"], value: 5)
  posts(first: Int!): [Post!]! @complexity(multipliers: ["first"], value: 3)
}

// GOOD: Rate limiting
import { rateLimitDirective } from 'graphql-rate-limit-directive';

const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } =
  rateLimitDirective();

type Query {
  expensiveQuery: Data! @rateLimit(limit: 10, duration: 60)
}

High: No List Pagination

PatternIssueSeverity
Unbounded listsMemory exhaustionHIGH
No cursor paginationPoor performanceMEDIUM
Missing total countBad UXLOW
# BAD: Unbounded list
type Query {
  posts: [Post!]!  # Could return millions!
}

# GOOD: Relay-style pagination
type Query {
  posts(
    first: Int
    after: String
    last: Int
    before: String
  ): PostConnection!
}

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

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

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

# SIMPLER: Offset pagination (for small datasets)
type Query {
  posts(offset: Int = 0, limit: Int = 20): PostPage!
}

type PostPage {
  items: [Post!]!
  totalCount: Int!
  hasMore: Boolean!
}

High: Missing Error Handling

PatternIssueSeverity
Throwing raw errorsLeaks infoHIGH
No error codesHard to handleMEDIUM
Stack traces in responseSecurity riskHIGH
// BAD: Raw error exposure
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      const user = await db.user.findUnique({ where: { id } });
      if (!user) {
        throw new Error('User not found');  // Generic error
      }
      return user;
    }
  }
};

// GOOD: Structured GraphQL errors
import { GraphQLError } from 'graphql';

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

const resolvers = {
  Query: {
    user: async (_, { id }) => {
      const user = await db.user.findUnique({ where: { id } });
      if (!user) {
        throw new NotFoundError('User', id);
      }
      return user;
    }
  }
};

// Error formatting plugin
const formatError = (error: GraphQLError) => {
  // Don't expose internal errors
  if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
    return new GraphQLError('Internal server error', {
      extensions: { code: 'INTERNAL_SERVER_ERROR' }
    });
  }
  return error;
};

Medium: Internal ID Exposure

PatternIssueSeverity
Database ID in schemaInformation leakMEDIUM
Sequential IDsEnumeration riskMEDIUM
No ID obfuscationPrivacy concernLOW
# BAD: Exposes database IDs
type User {
  id: Int!  # Sequential, guessable
}

# GOOD: Use opaque IDs
type User {
  id: ID!  # Could be UUID, hashid, etc.
}
// ID encoding/​decoding
import Hashids from 'hashids';
const hashids = new Hashids('secret-salt', 10);

const resolvers = {
  User: {
    id: (user) => hashids.encode(user.dbId)
  },
  Query: {
    user: (_, { id }) => {
      const [dbId] = hashids.decode(id);
      return db.user.findUnique({ where: { id: dbId } });
    }
  }
};

Medium: Missing Non-null Defaults

PatternIssueSeverity
Nullable without reasonConfusing APIMEDIUM
Everything nullableToo permissiveLOW
# BAD: Unnecessarily nullable
type User {
  id: ID          # Should always exist
  email: String   # Required for user
  name: String    # Should be required
  bio: String     # OK to be nullable
}

# GOOD: Clear nullability
type User {
  id: ID!          # Always present
  email: String!   # Required
  name: String!    # Required
  bio: String      # Optional (nullable)
  deletedAt: DateTime  # Optional
}

# For fields that may fail to resolve
type Post {
  id: ID!
  author: User  # Nullable if author deleted
  authorId: ID! # Always has the reference
}

Response Template

## GraphQL Code Review Results

**Project**: [name]
**Server**: Apollo Server 4.x
**Schema**: SDL-first / Code-first

### N+1 / DataLoader

#### CRITICAL
| File | Line | Issue |
|------|------|-------|
| resolvers/​post.ts | 23 | N+1 in author resolver - use DataLoader |
| resolvers/​user.ts | 45 | posts fetched per user without batching |

### Query Complexity / Security
| File | Line | Issue |
|------|------|-------|
| server.ts | 12 | No depth limit configured |
| schema.graphql | 34 | posts query unbounded - add pagination |

### Input Validation
| File | Line | Issue |
|------|------|-------|
| mutations/​user.ts | 56 | No email validation |
| mutations/​post.ts | 23 | Missing input sanitization |

### Error Handling
| File | Line | Issue |
|------|------|-------|
| resolvers/​query.ts | 78 | Raw error thrown - use GraphQLError |

### Schema Design
| File | Line | Issue |
|------|------|-------|
| schema.graphql | 12 | Query with side effect - move to Mutation |
| types/​user.graphql | 8 | Exposes sequential database ID |

### Recommendations
1. [ ] Implement DataLoader for all relationship resolvers
2. [ ] Add depth limit (max 5-7 levels)
3. [ ] Add query complexity plugin (max 1000)
4. [ ] Add pagination to all list fields
5. [ ] Validate all mutation inputs with Zod/​Yup

### Positive Patterns
- Good use of Relay connections for pagination
- Proper error codes in GraphQL errors

Best Practices

  1. DataLoader: Always batch relationship resolvers
  2. Complexity: Limit depth and complexity
  3. Pagination: Cursor-based for large lists
  4. Validation: Validate all inputs server-side
  5. Errors: Use structured GraphQL errors
  6. Security: Rate limit, no introspection in prod

Integration

  • schema-reviewer skill: Database schema
  • orm-reviewer skill: ORM patterns
  • typescript-reviewer skill: TS type safety
  • security-scanner skill: API security

Notes

  • Based on GraphQL best practices 2024
  • Works with Apollo, Yoga, Mercurius
  • Supports both SDL and code-first
  • Compatible with Prisma, TypeORM, Drizzle

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View