Skills graphql
install
source · Clone the upstream repo
git clone https://github.com/TerminalSkills/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/TerminalSkills/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/graphql" ~/.claude/skills/terminalskills-skills-graphql && rm -rf "$T"
manifest:
skills/graphql/SKILL.mdsource content
GraphQL
Overview
Design, build, and consume GraphQL APIs. This skill covers schema-first and code-first approaches, resolver patterns, real-time subscriptions, authentication, performance optimization with DataLoader, pagination, federation for microservices, and client-side consumption with Apollo Client.
Instructions
Step 1: Project Setup
Server (Apollo Server):
npm install @apollo/server graphql npm install @apollo/server express cors # With Express npm install dataloader # For N+1 prevention
Client (Apollo Client):
npm install @apollo/client graphql
Type generation:
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers
Step 2: Schema Design
type Query { user(id: ID!): User users(filter: UserFilter, pagination: PaginationInput): UserConnection! feed(cursor: String, limit: Int = 20): PostConnection! } type Mutation { createUser(input: CreateUserInput!): User! updateUser(id: ID!, input: UpdateUserInput!): User! createPost(input: CreatePostInput!): Post! likePost(id: ID!): Post! } type Subscription { postCreated: Post! } type User { id: ID! email: String! name: String! posts(limit: Int = 10): [Post!]! createdAt: DateTime! } type Post { id: ID! title: String! content: String! author: User! comments: [Comment!]! likes: Int! createdAt: DateTime! } # Relay-style cursor pagination type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! totalCount: Int! } type UserEdge { node: User!; cursor: String! } type PageInfo { hasNextPage: Boolean!; hasPreviousPage: Boolean!; endCursor: String } input CreateUserInput { email: String!; name: String! } input UpdateUserInput { name: String; avatar: String } input CreatePostInput { title: String!; content: String!; tags: [String!] } input PaginationInput { first: Int; after: String } scalar DateTime
Schema design rules: use
! for non-nullable fields, input types for mutations, Relay-style connections for pagination, descriptive verb names (createUser, not addUser).
Step 3: Resolvers
import { GraphQLError } from 'graphql'; export const resolvers = { Query: { user: async (_, { id }, { dataSources }) => dataSources.users.getById(id), users: async (_, { filter, pagination }, { dataSources }) => { const { first = 20, after } = pagination || {}; const result = await dataSources.users.getMany({ filter, first, after }); return { edges: result.items.map(item => ({ node: item, cursor: Buffer.from(item.id).toString('base64'), })), pageInfo: { hasNextPage: result.hasMore, hasPreviousPage: !!after, endCursor: result.items.at(-1) ? Buffer.from(result.items.at(-1).id).toString('base64') : null }, totalCount: result.totalCount, }; }, }, Mutation: { createUser: async (_, { input }, { dataSources, user }) => { if (!user) throw new GraphQLError('Not authenticated', { extensions: { code: 'UNAUTHENTICATED' } }); return dataSources.users.create(input); }, createPost: async (_, { input }, { dataSources, user }) => { if (!user) throw new GraphQLError('Not authenticated', { extensions: { code: 'UNAUTHENTICATED' } }); return dataSources.posts.create({ ...input, authorId: user.id }); }, }, User: { posts: async (parent, { limit }, { dataSources }) => dataSources.posts.getByAuthor(parent.id, limit) }, Post: { author: async (parent, _, { dataSources }) => dataSources.users.getById(parent.authorId) }, };
Step 4: Server Setup with Subscriptions
import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { WebSocketServer } from 'ws'; import { useServer } from 'graphql-ws/lib/use/ws'; import express from 'express'; import cors from 'cors'; const schema = makeExecutableSchema({ typeDefs, resolvers }); const app = express(); const httpServer = require('http').createServer(app); const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' }); useServer({ schema }, wsServer); const server = new ApolloServer({ schema }); await server.start(); app.use('/graphql', cors(), express.json(), expressMiddleware(server, { context: async ({ req }) => ({ user: await getUserFromToken(req.headers.authorization?.replace('Bearer ', '')), dataSources: createDataSources(), }), })); httpServer.listen(4000);
Step 5: DataLoader (N+1 Problem)
import DataLoader from 'dataloader'; export function createDataSources() { const userLoader = new DataLoader(async (ids) => { const users = await db.users.findMany({ where: { id: { in: ids } } }); const userMap = new Map(users.map(u => [u.id, u])); return ids.map(id => userMap.get(id) || null); // Must return in same order as input }); return { users: { getById: (id) => userLoader.load(id), create: (input) => db.users.create({ data: input }) }, posts: { getByAuthor: (authorId, limit) => db.posts.findMany({ where: { authorId }, take: limit }) }, }; }
Step 6: Apollo Client (React)
import { ApolloClient, InMemoryCache, gql, useQuery, useMutation } from '@apollo/client'; const client = new ApolloClient({ uri: '/graphql', cache: new InMemoryCache() }); const GET_FEED = gql` query GetFeed($cursor: String) { feed(cursor: $cursor, limit: 20) { edges { node { id title author { name } likes } cursor } pageInfo { hasNextPage endCursor } } } `; function Feed() { const { data, loading, fetchMore } = useQuery(GET_FEED); if (loading) return <p>Loading...</p>; return ( <div> {data.feed.edges.map(({ node }) => ( <article key={node.id}><h2>{node.title}</h2><p>By {node.author.name}</p></article> ))} {data.feed.pageInfo.hasNextPage && ( <button onClick={() => fetchMore({ variables: { cursor: data.feed.pageInfo.endCursor } })}> Load more </button> )} </div> ); }
Step 7: Security
import depthLimit from 'graphql-depth-limit'; const server = new ApolloServer({ schema, validationRules: [depthLimit(7)], // Prevent deeply nested abuse queries }); // Use @cacheControl directive for response caching // type Query { user(id: ID!): User @cacheControl(maxAge: 60) }
Examples
Example 1: Build a blog API with cursor pagination
User prompt: "Create a GraphQL API for a blog with users, posts, and comments. Include cursor-based pagination for the post feed and authentication for mutations."
The agent will:
- Define the schema with
,User
,Post
types,Comment
for Relay-style pagination, and input types for mutationsPostConnection - Set up Apollo Server with Express, configure JWT-based context extraction
- Implement resolvers with DataLoader to batch user lookups (preventing N+1 queries when loading post authors)
- Add cursor pagination using base64-encoded IDs as cursors in the
queryfeed - Protect mutation resolvers with authentication checks that throw
GraphQL errorsUNAUTHENTICATED
Example 2: Add real-time post notifications to a React app
User prompt: "Add a real-time subscription so users see new posts appear in the feed without refreshing the page."
The agent will:
- Add a
type to the schemaSubscription { postCreated: Post! } - Set up WebSocket server alongside the HTTP server using
graphql-ws - Implement a
instance and publish events in thePubSub
mutation resolvercreatePost - On the client, use
from Apollo Client with auseSubscription
subscription queryPOST_CREATED - Update the Apollo Client cache when new posts arrive via the subscription callback
Guidelines
- Schema-first design — agree on the schema before coding resolvers
- Always use DataLoader — the N+1 problem is GraphQL's biggest gotcha
- Cursor pagination — offset breaks on large datasets; cursors are stable
- Input types for mutations — cleaner, more evolvable than inline arguments
- Error codes in extensions —
,UNAUTHENTICATED
,FORBIDDEN
for client handlingNOT_FOUND - Depth and complexity limits — prevent abusive nested queries in production
- Codegen for types — hand-typing GraphQL types is error-prone and wastes time
- Cache normalization — Apollo Client's
deduplicates byInMemoryCache
+id__typename - Subscriptions only when needed — polling is simpler if real-time isn't critical
- Federation for microservices — split schema across services with Apollo Federation