xiaohongshu
xiaohongshu
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
Full skill instructions
Reviews GraphQL schemas, resolvers, and operations for N+1 problems, query complexity limits, input validation, security best practices, and proper error handling.
.graphql, .gql files.graphql or .gql schema filesschema.graphql or type-defs.tsgraphql package in dependencies@apollo/server, graphql-yoga, mercurius dependencies@Query, @Mutation, @Resolver decorators (NestJS/TypeGraphQL)**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
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
| Pattern | Issue | Severity |
|---|---|---|
| Resolver per item | N+1 queries | CRITICAL |
| No DataLoader | Unbatched fetches | CRITICAL |
| ORM lazy load in resolver | Hidden N+1 | CRITICAL |
// 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
})
}
};
| Pattern | Issue | Severity |
|---|---|---|
| SELECT * in resolver | Over-fetching | HIGH |
| No field selection | Wasted resources | MEDIUM |
| Ignoring selection set | Missing optimization | HIGH |
// 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 });
}
}
};
| Pattern | Issue | Severity |
|---|---|---|
| Side effects in Query | Violates spec | CRITICAL |
| Write operation in Query | Unexpected behavior | CRITICAL |
# 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!
}
| Pattern | Issue | Severity |
|---|---|---|
| No validation in resolver | Bad data accepted | HIGH |
| Trusting client input | Security risk | HIGH |
| No sanitization | Injection risk | CRITICAL |
// 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)
}
| Pattern | Issue | Severity |
|---|---|---|
| Unlimited depth | DoS vector | HIGH |
| No complexity limit | Resource exhaustion | HIGH |
| No rate limiting | Abuse possible | MEDIUM |
// 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)
}
| Pattern | Issue | Severity |
|---|---|---|
| Unbounded lists | Memory exhaustion | HIGH |
| No cursor pagination | Poor performance | MEDIUM |
| Missing total count | Bad UX | LOW |
# 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!
}
| Pattern | Issue | Severity |
|---|---|---|
| Throwing raw errors | Leaks info | HIGH |
| No error codes | Hard to handle | MEDIUM |
| Stack traces in response | Security risk | HIGH |
// 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;
};
| Pattern | Issue | Severity |
|---|---|---|
| Database ID in schema | Information leak | MEDIUM |
| Sequential IDs | Enumeration risk | MEDIUM |
| No ID obfuscation | Privacy concern | LOW |
# 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 } });
}
}
};
| Pattern | Issue | Severity |
|---|---|---|
| Nullable without reason | Confusing API | MEDIUM |
| Everything nullable | Too permissive | LOW |
# 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
}
## 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
schema-reviewer skill: Database schemaorm-reviewer skill: ORM patternstypescript-reviewer skill: TS type safetysecurity-scanner skill: API securityxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
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...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
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...
Display project status, active tracks, and next actions
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.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience