GraphQL Cursor Rules: API Query Language
Cursor rules for GraphQL covering schema-first design, resolvers, queries/mutations, fragments, error handling with unions, subscriptions, and Apollo Server patterns.

Overview
GraphQL gives clients precise control over the data they request, replacing over-fetching REST endpoints with a typed query language. These cursor rules enforce schema-first design, resolver patterns, query/mutation structure, fragment composition, error handling with union types, subscription setup, and Apollo Server conventions to help AI assistants generate clean, type-safe GraphQL APIs.
Note:
Enforces schema-first GraphQL design, typed resolvers with codegen, query/mutation/subscription patterns, fragment-driven data requirements, error handling via union types, DataLoader for N+1 prevention, and Apollo Server v4 conventions.
Rules Configuration
---
description: Enforces GraphQL best practices including schema-first design, resolver patterns, query/mutation/subscription structure, fragment composition, error union types, and Apollo Server configuration. Provides guidelines for type-safe GraphQL APIs.
globs: **/*.graphql,**/*.gql,**/*.ts
---
# GraphQL Best Practices
You are an expert in GraphQL, Apollo Server, and type-safe API design.
You understand schema design, resolver patterns, N+1 prevention, and production deployment.
### Schema Design
- Use schema-first approach: define .graphql files, generate TypeScript types with GraphQL Codegen
- Use singular names for types: type Post, not type Posts
- Use descriptive names for queries: getUser(id: ID!): User, not user(id: ID!)
- Return nullable fields for data that may not exist; use Non-Null (!) for guaranteed fields
- Define inputs for mutations: input CreatePostInput { title: String!, body: String! }
- Use enums for constrained values: enum PostStatus { DRAFT PUBLISHED ARCHIVED }
- Use interfaces or unions for polymorphic responses: union SearchResult = Post | User
### Resolvers
- Keep resolvers thin: delegate to service layer, return typed results
- Use parent, args, context, info signature: (parent, args, context, info) => result
- Type context with authentication info, data loaders, and service instances
- Resolve relations in dedicated field resolvers, not in top-level query resolvers
- Use DataLoader for batching and caching to prevent N+1 queries
- Never expose database entities directly — map to GraphQL types
### Queries & Mutations
- Name queries for what they return: posts, not getPosts
- Name mutations as verbs: createPost, updatePost, deletePost
- Return the mutated object from mutations so clients can update their cache
- Use pagination with Relay-style connections or offset-based page/limit args
- Support filtering and sorting as input arguments
- Mutations should return a payload type: type CreatePostPayload { post: Post! }
### Errors & Unions
- Use union types for operation results: union CreatePostResult = Post | ValidationError
- Define error types with message, code, and field-level details
- Never throw errors from resolvers for expected failures — return them in the union
- Use formatError in Apollo Server to strip stack traces in production
- Log resolver errors centrally with context-aware logging
### Subscriptions
- Use subscriptions for real-time updates: type Subscription { postCreated: Post! }
- Return the full object from subscription resolvers, not just IDs
- Use withFilter for filtering subscription events by criteria
- Authenticate WebSocket connections in the context function
### Apollo Server Setup
- Use Apollo Server v4 with expressMiddleware or startStandaloneServer
- Define context function that attaches auth, loaders, and services per request
- Enable introspection in development, disable in production
- Use persisted queries and automatic persisted queries (APQ) for production
- Set up CSRF prevention for browser-based clients
Installation
Create graphql.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.
Examples
# schema.graphql — Schema-first definition
type Query {
posts(page: Int, perPage: Int): PostConnection!
post(id: ID!): Post
me: User
}
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload!
deletePost(id: ID!): DeletePostPayload!
}
type Subscription {
postCreated: Post!
}
type Post {
id: ID!
title: String!
body: String!
published: Boolean!
author: User!
comments: [Comment!]!
createdAt: String!
}
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
}
type Comment {
id: ID!
body: String!
author: User!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
input CreatePostInput {
title: String!
body: String!
}
input UpdatePostInput {
title: String
body: String
published: Boolean
}
union CreatePostPayload = CreatePostSuccess | ValidationError
union UpdatePostPayload = UpdatePostSuccess | NotFoundError | ValidationError
union DeletePostPayload = DeletePostSuccess | NotFoundError
type CreatePostSuccess { post: Post! }
type UpdatePostSuccess { post: Post! }
type DeletePostSuccess { id: ID! }
type ValidationError {
message: String!
code: String!
fields: [FieldError!]!
}
type FieldError {
field: String!
message: String!
}
type NotFoundError {
message: String!
code: String!
}
// resolvers/postResolvers.ts
import { PostService } from '../services/postService';
import { Context } from '../context';
export const postResolvers = {
Query: {
posts: async (_: unknown, args: { page?: number; perPage?: number }, ctx: Context) => {
const page = args.page ?? 1;
const perPage = args.perPage ?? 20;
const { posts, total } = await ctx.postService.findPublished(page, perPage);
return {
posts,
total,
edges: posts.map((post) => ({
node: post,
cursor: Buffer.from(`post:${post.id}`).toString('base64'),
})),
pageInfo: {
hasNextPage: page * perPage < total,
hasPreviousPage: page > 1,
startCursor: posts.length > 0 ? Buffer.from(`post:${posts[0].id}`).toString('base64') : null,
endCursor: posts.length > 0 ? Buffer.from(`post:${posts[posts.length - 1].id}`).toString('base64') : null,
},
};
},
post: async (_: unknown, { id }: { id: string }, ctx: Context) => {
return ctx.loaders.post.load(Number(id));
},
},
Mutation: {
createPost: async (_: unknown, { input }: { input: CreatePostInput }, ctx: Context) => {
if (!ctx.user) {
return { __typename: 'ValidationError', message: 'Unauthorized', code: 'UNAUTHORIZED', fields: [] };
}
const errors = validateCreatePost(input);
if (errors.length > 0) {
return { __typename: 'ValidationError', message: 'Invalid input', code: 'VALIDATION_ERROR', fields: errors };
}
const post = await ctx.postService.create(ctx.user.id, input);
ctx.pubsub.publish('POST_CREATED', { postCreated: post });
return { __typename: 'CreatePostSuccess', post };
},
},
Post: {
author: (parent: { authorId: number }, _: unknown, ctx: Context) => {
return ctx.loaders.user.load(parent.authorId);
},
comments: (parent: { id: number }, _: unknown, ctx: Context) => {
return ctx.loaders.commentsByPost.load(parent.id);
},
},
CreatePostPayload: {
__resolveType(obj: { __typename: string }) {
return obj.__typename;
},
},
};
// context.ts — Apollo Server context with DataLoader
import DataLoader from 'dataloader';
import { PubSub } from 'graphql-subscriptions';
import { PostService } from './services/postService';
import { prisma } from './lib/prisma';
export interface Context {
user: { id: number } | null;
postService: PostService;
pubsub: PubSub;
loaders: {
post: DataLoader<number, any>;
user: DataLoader<number, any>;
commentsByPost: DataLoader<number, any[]>;
};
}
export async function createContext({ req }: { req: any }): Promise<Context> {
const user = await getUserFromToken(req.headers.authorization);
return {
user,
postService: new PostService(prisma),
loaders: {
post: new DataLoader(async (ids) => {
const posts = await prisma.post.findMany({ where: { id: { in: [...ids] } } });
return ids.map((id) => posts.find((p) => p.id === id));
}),
user: new DataLoader(async (ids) => {
const users = await prisma.user.findMany({ where: { id: { in: [...ids] } } });
return ids.map((id) => users.find((u) => u.id === id));
}),
commentsByPost: new DataLoader(async (postIds) => {
const comments = await prisma.comment.findMany({ where: { postId: { in: [...postIds] } } });
return postIds.map((id) => comments.filter((c) => c.postId === id));
}),
},
};
}
Related Resources
Related Articles
MongoDB Cursor Rules: NoSQL Document Database
Cursor rules for MongoDB covering connection management, CRUD operations, aggregation pipeline, indexes, schema design, Mongoose ODM, transactions, and Atlas deployment.
AI Rules in Modern IDEs: Global and Project-Specific Configurations
AI rules customize AI assistants in modern IDEs like Cursor, Windsurf, and VSCode Copilot. Learn to configure global and project-specific rules for consistent, high-quality code.
Cypress Cursor Rules: End-to-End Component Testing
Cursor rules for Cypress covering cy.intercept network stubbing, data-cy selectors, custom commands, retry-ability patterns, component testing with cy.mount, fixtures, and CI/CD parallelization.