Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Schema design, resolver patterns, DataLoader, N+1 prevention, and subscription patterns for GraphQL APIs.
.claude/skills/graphql-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-14 | ✓→✓ | = Same ✓ | — | — |
| case-03 | ✓→✓ | = Same ✓ | — | — |
| case-19 | ✗→✗ | = Same ✗ | — | — |
Production-grade GraphQL API design with performance and type safety.
graphql# Use interfaces for shared fields interface Node { id: ID! createdAt: DateTime! updatedAt: DateTime! } type User implements Node { id: ID! createdAt: DateTime! updatedAt: DateTime! email: String! displayName: String! posts(first: Int, after: String): PostConnection! } # Relay-style pagination (cursor-based) type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! totalCount: Int! } type PostEdge { node: Post! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } # Input types for mutations input CreatePostInput { title: String! body: String! tags: [String!] } # Union for mutation results (error handling without exceptions) type CreatePostSuccess { post: Post! } type ValidationError { field: String! message: String! } union CreatePostResult = CreatePostSuccess | ValidationError
typescriptimport DataLoader from 'dataloader' // Batch function: receives array of keys, returns array of results in same order function createUserLoader(db: Database) { return new DataLoader<string, User | null>(async (userIds) => { const users = await db.user.findMany({ where: { id: { in: [...userIds] } } }) const userMap = new Map(users.map(u => [u.id, u])) // MUST return in same order as input keys return userIds.map(id => userMap.get(id) ?? null) }) } // Create per-request context (loaders are NOT shared across requests) function createContext(req: Request) { const db = getDatabase() return { db, loaders: { user: createUserLoader(db), post: createPostLoader(db), comment: createCommentLoader(db), } } } // Resolver uses loader instead of direct DB query const resolvers = { Post: { author: (post: Post, _args: unknown, ctx: Context) => { return ctx.loaders.user.load(post.authorId) // batched automatically } } }
typescriptimport { z } from 'zod' const CreatePostSchema = z.object({ title: z.string().min(1).max(200), body: z.string().min(10).max(50000), tags: z.array(z.string()).max(10).optional() }) const resolvers = { Mutation: { createPost: async (_parent: unknown, args: { input: unknown }, ctx: Context) => { // Auth guard if (!ctx.currentUser) { throw new AuthenticationError('Login required') } // Input validation const parsed = CreatePostSchema.safeParse(args.input) if (!parsed.success) { return { __typename: 'ValidationError', field: parsed.error.issues[0].path.join('.'), message: parsed.error.issues[0].message } } const post = await ctx.db.post.create({ data: { ...parsed.data, authorId: ctx.currentUser.id } }) return { __typename: 'CreatePostSuccess', post } } } }
typescriptimport { PubSub, withFilter } from 'graphql-subscriptions' const pubsub = new PubSub() // Use RedisPubSub in production const EVENTS = { POST_CREATED: 'POST_CREATED', COMMENT_ADDED: 'COMMENT_ADDED', } as const const resolvers = { Subscription: { commentAdded: { // Filter: only deliver to subscribers watching this post subscribe: withFilter( () => pubsub.asyncIterableIterator(EVENTS.COMMENT_ADDED), (payload, variables) => payload.commentAdded.postId === variables.postId ) } }, Mutation: { addComment: async (_p: unknown, args: { postId: string; body: string }, ctx: Context) => { const comment = await ctx.db.comment.create({ data: { postId: args.postId, body: args.body, authorId: ctx.currentUser!.id } }) await pubsub.publish(EVENTS.COMMENT_ADDED, { commentAdded: comment }) return comment } } }
typescriptimport depthLimit from 'graphql-depth-limit' import { createComplexityLimitRule } from 'graphql-validation-complexity' const server = new ApolloServer({ schema, validationRules: [ depthLimit(7), // Max 7 levels deep createComplexityLimitRule(1000, { // Max 1000 complexity points scalarCost: 1, objectCost: 2, listFactor: 10, }) ] })
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.