| name | graphql-expert |
| version | 2.0.0 |
| description | GraphQL API design with schema patterns, resolver implementation, real-time subscriptions, and Apollo federation for distributed graphs. Use when designing GraphQL schemas, implementing resolvers, configuring Apollo Gateway/Federation, or adding query depth/complexity limits. Do NOT use for REST API design (use rest-api-design) or gRPC services. |
| risk_level | HIGH |
| token_budget | 3500 |
GraphQL Expert - Code Generation Rules
0. Anti-Hallucination Protocol
0.2 Security Patterns (security rules)
CWE-400: Query Complexity DoS
- Do not: Allow unlimited query depth/complexity
- Instead: Depth limiting, complexity analysis, query cost limits
CWE-285: Authorization in Resolvers
- Do not: Trust query structure for authorization
- Instead: Check permissions in EVERY resolver, not just entry points
CWE-200: Introspection in Production
- Do not: Enable introspection in production (exposes schema)
- Instead: Disable introspection or restrict to authenticated users
CWE-943: N+1 Query Injection
- Do not: Unbounded nested queries
- Instead: Use DataLoader, limit nesting depth, query cost analysis
1. Security Principles
1.1 Query Complexity & Depth Limits (CWE-400)
Principle: Prevent denial of service through complex/deep queries.
const server = new ApolloServer({
typeDefs,
resolvers,
});
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5),
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 10,
listFactor: 10,
}),
],
});
1.2 Authorization Per Field (CWE-862)
Principle: Check authorization at resolver level, not just query level.
const resolvers = {
Query: {
user: async (_, { id }) => {
return db.users.findById(id);
},
},
};
const resolvers = {
Query: {
user: async (_, { id }, context) => {
if (!context.user) {
throw new AuthenticationError('Not authenticated');
}
if (context.user.id !== id && !context.user.isAdmin) {
throw new ForbiddenError('Access denied');
}
return db.users.findById(id);
},
},
User: {
email: async (parent, _, context) => {
if (context.user?.id !== parent.id && !context.user?.isAdmin) {
return null;
}
return parent.email;
},
},
};
1.3 Input Validation (CWE-20)
Principle: Validate all input at resolver boundaries.
import { z } from 'zod';
import { UserInputError } from 'apollo-server-errors';
const CreateUserSchema = z.object({
username: z.string().min(3).max(50).regex(/^[a-zA-Z0-9_]+$/),
email: z.string().email().max(255),
age: z.number().int().min(0).max(150).optional(),
});
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
return db.users.create(input);
},
},
};
const resolvers = {
Mutation: {
createUser: async (_, { input }) => {
const result = CreateUserSchema.safeParse(input);
if (!result.success) {
throw new UserInputError('Invalid input', {
validationErrors: result.error.flatten(),
});
}
return db.users.create(result.data);
},
},
};
1.4 SQL Injection Prevention (CWE-89)
Principle: Never construct queries from GraphQL arguments.
const resolvers = {
Query: {
users: async (_, { filter }) => {
return db.raw(`SELECT * FROM users WHERE name LIKE '%${filter}%'`);
},
},
};
const resolvers = {
Query: {
users: async (_, { filter }) => {
return db.users
.where('name', 'like', `%${filter}%`)
.limit(100);
},
},
};
1.5 Information Disclosure Prevention (CWE-209)
Principle: Never expose internal errors to clients.
const server = new ApolloServer({
formatError: (error) => error,
});
const server = new ApolloServer({
formatError: (error) => {
console.error('GraphQL Error:', error);
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return {
message: 'An internal error occurred',
extensions: { code: 'INTERNAL_SERVER_ERROR' },
};
}
return {
message: error.message,
extensions: { code: error.extensions?.code },
};
},
});
1.6 Rate Limiting (CWE-770)
Principle: Limit requests per user/IP to prevent abuse.
2. Version Requirements
Use these minimum versions:
{
"dependencies": {
"@apollo/server": "^4.10.0",
"graphql": "^16.8.0",
"graphql-depth-limit": "^1.1.0",
"graphql-validation-complexity": "^0.4.0",
"zod": "^3.22.0"
}
}
3. Code Patterns
3.1 WHEN creating GraphQL schema
type User {
id: ID!
username: String!
email: String
createdAt: DateTime!
}
input CreateUserInput {
username: String!
email: String!
age: Int
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
user(id: ID!): User
users(first: Int = 10, after: String): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
3.2 WHEN implementing resolvers with auth
import { ApolloServer } from '@apollo/server';
import { GraphQLError } from 'graphql';
import { z } from 'zod';
interface Context {
user: { id: string; role: string } | null;
db: Database;
}
function requireAuth(context: Context) {
if (!context.user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return context.user;
}
function requireRole(context: Context, role: string) {
const user = requireAuth(context);
if (user.role !== role && user.role !== 'admin') {
throw new GraphQLError('Access denied', {
extensions: { code: 'FORBIDDEN' },
});
}
return user;
}
const CreateUserSchema = z.object({
username: z.string().min(3).max(50).regex(/^[a-zA-Z0-9_]+$/),
email: z.string().email().max(255),
});
const resolvers = {
Query: {
user: async (_: unknown, { id }: { id: string }, context: Context) => {
requireAuth(context);
return context.db.users.findById(id);
},
users: async (
_: unknown,
{ first = 10, after }: { first?: number; after?: string },
context: Context
) => {
requireAuth(context);
const limit = Math.min(first, 100);
const users = await context.db.users
.findMany({ cursor: after, take: limit + 1 });
const hasNextPage = users.length > limit;
const edges = users.slice(0, limit).map(user => ({
node: user,
cursor: user.id,
}));
return {
edges,
pageInfo: {
hasNextPage,
hasPreviousPage: !!after,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount: await context.db.users.count(),
};
},
},
Mutation: {
createUser: async (
_: unknown,
{ input }: { input: unknown },
context: Context
) => {
requireRole(context, 'admin');
const result = CreateUserSchema.safeParse(input);
if (!result.success) {
throw new GraphQLError('Invalid input', {
extensions: {
code: 'BAD_USER_INPUT',
validationErrors: result.error.flatten(),
},
});
}
return context.db.users.create(result.data);
},
},
User: {
email: (parent: any, _: unknown, context: Context) => {
if (context.user?.id === parent.id || context.user?.role === 'admin') {
return parent.email;
}
return null;
},
},
};
3.3 WHEN setting up Apollo Server
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import express from 'express';
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
import { rateLimit } from 'express-rate-limit';
const app = express();
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { errors: [{ message: 'Too many requests' }] },
});
app.use('/graphql', limiter);
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5),
createComplexityLimitRule(1000),
],
formatError: (formattedError, error) => {
console.error('GraphQL Error:', error);
if (formattedError.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return {
message: 'Internal server error',
extensions: { code: 'INTERNAL_SERVER_ERROR' },
};
}
return formattedError;
},
introspection: process.env.NODE_ENV !== 'production',
});
await server.start();
app.use(
'/graphql',
express.json(),
expressMiddleware(server, {
context: async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = token ? await verifyToken(token) : null;
return {
user,
db: database,
};
},
})
);
3.4 WHEN implementing subscriptions securely
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
useServer(
{
schema,
context: async (ctx) => {
const token = ctx.connectionParams?.authToken;
if (!token) {
throw new Error('Not authenticated');
}
const user = await verifyToken(token as string);
if (!user) {
throw new Error('Invalid token');
}
return { user, db: database };
},
onSubscribe: async (ctx, msg) => {
const { user } = ctx.extra;
const activeSubscriptions = getActiveSubscriptions(user.id);
if (activeSubscriptions >= 10) {
throw new Error('Too many active subscriptions');
}
},
},
wsServer
);
4. Anti-Patterns
Do not:
- Allow unbounded query depth or complexity
- Skip authorization checks in resolvers
- Expose raw database errors to clients
- Trust GraphQL input without validation
- Allow introspection in production
- Skip rate limiting on GraphQL endpoints
- Construct SQL from GraphQL arguments
5. Testing
Write security tests:
import { createTestClient } from '@apollo/server/testing';
describe('GraphQL Security', () => {
test('rejects deep queries', async () => {
const { query } = createTestClient(server);
const result = await query({
# ... (additional test cases follow same pattern)
6. Pre-Generation Checklist
Before generating any GraphQL code: