Claude-skill-registry graphql-api-development
Comprehensive guide for building GraphQL APIs including schema design, queries, mutations, subscriptions, resolvers, type system, error handling, authentication, authorization, caching strategies, and production best practices
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/graphql-api-development" ~/.claude/skills/majiayu000-claude-skill-registry-graphql-api-development && rm -rf "$T"
skills/data/graphql-api-development/SKILL.mdGraphQL API Development
A comprehensive skill for building production-ready GraphQL APIs using graphql-js. Master schema design, type systems, resolvers, queries, mutations, subscriptions, authentication, authorization, caching, testing, and deployment strategies.
When to Use This Skill
Use this skill when:
- Building a new API that requires flexible data fetching for web or mobile clients
- Replacing or augmenting REST APIs with more efficient data access patterns
- Developing APIs for applications with complex, nested data relationships
- Creating APIs that serve multiple client types (web, mobile, desktop) with different data needs
- Building real-time applications requiring subscriptions and live updates
- Designing APIs where clients need to specify exactly what data they need
- Developing GraphQL servers with Node.js and Express
- Implementing type-safe APIs with strong schema validation
- Creating self-documenting APIs with built-in introspection
- Building microservices that need to be composed into a unified API
When GraphQL Excels Over REST
GraphQL Advantages
- Precise Data Fetching: Clients request exactly what they need, no over/under-fetching
- Single Request: Fetch multiple resources in one roundtrip instead of multiple REST endpoints
- Strongly Typed: Schema defines exact types, enabling validation and tooling
- Introspection: Self-documenting API with queryable schema
- Versioning Not Required: Add new fields without breaking existing queries
- Real-time Updates: Built-in subscription support for live data
- Nested Resources: Naturally handle complex relationships without N+1 queries
- Client-Driven: Clients control data shape, reducing backend changes
When to Stick with REST
- Simple CRUD operations with standard resources
- File uploads/downloads (GraphQL requires multipart handling)
- HTTP caching is critical (GraphQL typically uses POST)
- Team unfamiliar with GraphQL (learning curve)
- Existing REST infrastructure works well
Core Concepts
The GraphQL Type System
GraphQL's type system is its foundation. Every GraphQL API defines:
- Scalar Types: Basic data types (String, Int, Float, Boolean, ID)
- Object Types: Complex types with fields
- Query Type: Entry point for read operations
- Mutation Type: Entry point for write operations
- Subscription Type: Entry point for real-time updates
- Input Types: Complex inputs for mutations
- Enums: Fixed set of values
- Interfaces: Abstract types that objects implement
- Unions: Types that can be one of several types
- Non-Null Types: Types that cannot be null
- List Types: Arrays of types
Schema Definition
Two approaches for defining GraphQL schemas:
1. Schema Definition Language (SDL) - Declarative, readable:
type User { id: ID! name: String! email: String! posts: [Post!]! } type Post { id: ID! title: String! content: String author: User! } type Query { user(id: ID!): User posts: [Post!]! }
2. Programmatic API - Type-safe, programmatic:
const UserType = new GraphQLObjectType({ name: 'User', fields: { id: { type: new GraphQLNonNull(GraphQLID) }, name: { type: new GraphQLNonNull(GraphQLString) }, email: { type: new GraphQLNonNull(GraphQLString) }, posts: { type: new GraphQLList(new GraphQLNonNull(PostType)) } } });
Resolvers
Resolvers are functions that return data for schema fields. Every field can have a resolver:
const resolvers = { Query: { user: (parent, args, context, info) => { return context.db.findUserById(args.id); } }, User: { posts: (user, args, context) => { return context.db.findPostsByAuthorId(user.id); } } };
Resolver Function Signature:
: The result from the parent resolverparent
: Arguments passed to the fieldargs
: Shared context (database, auth, etc.)context
: Field-specific metadatainfo
Queries
Queries fetch data from your API:
query GetUser { user(id: "123") { id name email posts { title content } } }
Mutations
Mutations modify data:
mutation CreatePost { createPost(input: { title: "GraphQL is awesome" content: "Here's why..." authorId: "123" }) { id title author { name } } }
Subscriptions
Subscriptions enable real-time updates:
subscription OnPostCreated { postCreated { id title author { name } } }
Schema Design Patterns
Pattern 1: Input Types for Mutations
Always use input types for complex mutation arguments:
input CreateUserInput { name: String! email: String! age: Int bio: String } type Mutation { createUser(input: CreateUserInput!): User! }
Why: Easier to extend, better organization, reusable across mutations.
Pattern 2: Interfaces for Shared Fields
Use interfaces when multiple types share fields:
interface Node { id: ID! createdAt: String! updatedAt: String! } type User implements Node { id: ID! createdAt: String! updatedAt: String! name: String! email: String! } type Post implements Node { id: ID! createdAt: String! updatedAt: String! title: String! content: String }
Pattern 3: Unions for Polymorphic Returns
Use unions when a field can return different types:
union SearchResult = User | Post | Comment type Query { search(query: String!): [SearchResult!]! }
Pattern 4: Pagination Patterns
Offset-based pagination:
type Query { posts(offset: Int, limit: Int): PostConnection! } type PostConnection { items: [Post!]! total: Int! hasMore: Boolean! }
Cursor-based pagination (Relay-style):
type Query { posts(first: Int, after: String): PostConnection! } type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! } type PostEdge { node: Post! cursor: String! } type PageInfo { hasNextPage: Boolean! endCursor: String }
Pattern 5: Error Handling
Field-level errors:
type MutationPayload { success: Boolean! message: String user: User errors: [Error!] } type Error { field: String! message: String! }
Union-based error handling:
union CreateUserResult = User | ValidationError | DatabaseError type ValidationError { field: String! message: String! }
Pattern 6: Versioning with Directives
Deprecate fields instead of versioning:
type User { name: String! @deprecated(reason: "Use firstName and lastName") firstName: String! lastName: String! }
Query Optimization and Performance
The N+1 Problem
Problem: Fetching nested data causes multiple database queries:
// BAD: N+1 queries const UserType = new GraphQLObjectType({ name: 'User', fields: { posts: { type: new GraphQLList(PostType), resolve: (user) => { // This runs once PER user! return db.getPostsByUserId(user.id); } } } }); // Query for 100 users = 1 query for users + 100 queries for posts = 101 queries
DataLoader Solution
DataLoader batches and caches requests:
import DataLoader from 'dataloader'; // Create DataLoader const postLoader = new DataLoader(async (userIds) => { // Single query for all user IDs const posts = await db.getPostsByUserIds(userIds); // Group posts by userId const postsByUserId = {}; posts.forEach(post => { if (!postsByUserId[post.authorId]) { postsByUserId[post.authorId] = []; } postsByUserId[post.authorId].push(post); }); // Return in same order as userIds return userIds.map(id => postsByUserId[id] || []); }); // Use in resolver const UserType = new GraphQLObjectType({ name: 'User', fields: { posts: { type: new GraphQLList(PostType), resolve: (user, args, context) => { return context.loaders.postLoader.load(user.id); } } } }); // Add to context const context = { loaders: { postLoader: new DataLoader(batchLoadPosts) } };
Query Complexity Analysis
Limit expensive queries:
import { getComplexity, simpleEstimator } from 'graphql-query-complexity'; const complexity = getComplexity({ schema, query, estimators: [ simpleEstimator({ defaultComplexity: 1 }) ] }); if (complexity > 1000) { throw new Error('Query too complex'); }
Depth Limiting
Prevent deeply nested queries:
import depthLimit from 'graphql-depth-limit'; const server = new ApolloServer({ schema, validationRules: [depthLimit(5)] });
Mutations and Input Validation
Mutation Design Pattern
input CreatePostInput { title: String! content: String! authorId: ID! tags: [String!] } type CreatePostPayload { post: Post errors: [UserError!] success: Boolean! } type UserError { message: String! field: String } type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! }
Input Validation
const Mutation = new GraphQLObjectType({ name: 'Mutation', fields: { createPost: { type: CreatePostPayload, args: { input: { type: new GraphQLNonNull(CreatePostInput) } }, resolve: async (_, { input }, context) => { // Validate input const errors = []; if (input.title.length < 3) { errors.push({ field: 'title', message: 'Title must be at least 3 characters' }); } if (input.content.length < 10) { errors.push({ field: 'content', message: 'Content must be at least 10 characters' }); } if (errors.length > 0) { return { errors, success: false, post: null }; } // Create post const post = await context.db.createPost(input); return { post, errors: [], success: true }; } } } });
Subscriptions and Real-time Updates
Setting Up Subscriptions
import { GraphQLObjectType, GraphQLString } from 'graphql'; import { PubSub } from 'graphql-subscriptions'; const pubsub = new PubSub(); const Subscription = new GraphQLObjectType({ name: 'Subscription', fields: { postCreated: { type: PostType, subscribe: () => pubsub.asyncIterator(['POST_CREATED']) }, messageReceived: { type: MessageType, args: { channelId: { type: new GraphQLNonNull(GraphQLID) } }, subscribe: (_, { channelId }) => { return pubsub.asyncIterator([`MESSAGE_${channelId}`]); } } } });
Publishing Events
const Mutation = new GraphQLObjectType({ name: 'Mutation', fields: { createPost: { type: PostType, args: { input: { type: new GraphQLNonNull(CreatePostInput) } }, resolve: async (_, { input }, context) => { const post = await context.db.createPost(input); // Publish to subscribers pubsub.publish('POST_CREATED', { postCreated: post }); return post; } } } });
WebSocket Server Setup
import { createServer } from 'http'; import { WebSocketServer } from 'ws'; import { useServer } from 'graphql-ws/lib/use/ws'; import { execute, subscribe } from 'graphql'; import express from 'express'; const app = express(); const httpServer = createServer(app); // WebSocket server for subscriptions const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' }); useServer( { schema, execute, subscribe, context: (ctx) => { // Access connection params, headers return { userId: ctx.connectionParams?.userId, db: database }; } }, wsServer ); httpServer.listen(4000);
Authentication and Authorization
Context-Based Authentication
import jwt from 'jsonwebtoken'; // Middleware to extract user const authMiddleware = async (req) => { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) { return { user: null }; } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); const user = await db.findUserById(decoded.userId); return { user }; } catch (error) { return { user: null }; } }; // Add to GraphQL context app.all('/graphql', async (req, res) => { const auth = await authMiddleware(req); createHandler({ schema, context: { user: auth.user, db: database } })(req, res); });
Resolver-Level Authorization
const Query = new GraphQLObjectType({ name: 'Query', fields: { me: { type: UserType, resolve: (_, __, context) => { if (!context.user) { throw new Error('Authentication required'); } return context.user; } }, adminData: { type: GraphQLString, resolve: (_, __, context) => { if (!context.user) { throw new Error('Authentication required'); } if (context.user.role !== 'admin') { throw new Error('Admin access required'); } return 'Secret admin data'; } } } });
Field-Level Authorization
const PostType = new GraphQLObjectType({ name: 'Post', fields: { title: { type: GraphQLString }, content: { type: GraphQLString }, draft: { type: GraphQLBoolean, resolve: (post, args, context) => { // Only author can see draft status if (post.authorId !== context.user?.id) { return null; } return post.draft; } } } });
Directive-Based Authorization
directive @auth(requires: Role = USER) on FIELD_DEFINITION enum Role { USER ADMIN MODERATOR } type Query { publicData: String userData: String @auth(requires: USER) adminData: String @auth(requires: ADMIN) }
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils'; function authDirective(schema, directiveName) { return mapSchema(schema, { [MapperKind.OBJECT_FIELD]: (fieldConfig) => { const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0]; if (authDirective) { const { requires } = authDirective; const { resolve = defaultFieldResolver } = fieldConfig; fieldConfig.resolve = async (source, args, context, info) => { if (!context.user) { throw new Error('Authentication required'); } if (context.user.role !== requires) { throw new Error(`${requires} role required`); } return resolve(source, args, context, info); }; } return fieldConfig; } }); }
Caching Strategies
In-Memory Caching
import { LRUCache } from 'lru-cache'; const cache = new LRUCache({ max: 500, ttl: 1000 * 60 * 5 // 5 minutes }); const Query = new GraphQLObjectType({ name: 'Query', fields: { product: { type: ProductType, args: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolve: async (_, { id }, context) => { const cacheKey = `product:${id}`; const cached = cache.get(cacheKey); if (cached) { return cached; } const product = await context.db.findProductById(id); cache.set(cacheKey, product); return product; } } } });
Redis Caching
import Redis from 'ioredis'; const redis = new Redis({ host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }); const Query = new GraphQLObjectType({ name: 'Query', fields: { user: { type: UserType, args: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolve: async (_, { id }, context) => { const cacheKey = `user:${id}`; // Check cache const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // Fetch from database const user = await context.db.findUserById(id); // Cache for 10 minutes await redis.setex(cacheKey, 600, JSON.stringify(user)); return user; } } } });
Cache Invalidation
const Mutation = new GraphQLObjectType({ name: 'Mutation', fields: { updateUser: { type: UserType, args: { id: { type: new GraphQLNonNull(GraphQLID) }, input: { type: new GraphQLNonNull(UpdateUserInput) } }, resolve: async (_, { id, input }, context) => { const user = await context.db.updateUser(id, input); // Invalidate cache const cacheKey = `user:${id}`; await redis.del(cacheKey); // Also invalidate list caches await redis.del('users:all'); return user; } } } });
Error Handling
Custom Error Classes
class AuthenticationError extends Error { constructor(message) { super(message); this.name = 'AuthenticationError'; this.extensions = { code: 'UNAUTHENTICATED' }; } } class ForbiddenError extends Error { constructor(message) { super(message); this.name = 'ForbiddenError'; this.extensions = { code: 'FORBIDDEN' }; } } class ValidationError extends Error { constructor(message, fields) { super(message); this.name = 'ValidationError'; this.extensions = { code: 'BAD_USER_INPUT', fields }; } }
Error Formatting
import { formatError } from 'graphql'; const customFormatError = (error) => { // Log error for monitoring console.error('GraphQL Error:', { message: error.message, locations: error.locations, path: error.path, extensions: error.extensions }); // Don't expose internal errors to clients if (error.message.startsWith('Database')) { return { message: 'Internal server error', extensions: { code: 'INTERNAL_SERVER_ERROR' } }; } return formatError(error); }; const server = new ApolloServer({ schema, formatError: customFormatError });
Graceful Error Responses
const Query = new GraphQLObjectType({ name: 'Query', fields: { user: { type: UserType, args: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolve: async (_, { id }, context) => { try { const user = await context.db.findUserById(id); if (!user) { throw new Error(`User with ID ${id} not found`); } return user; } catch (error) { // Log error console.error('Error fetching user:', error); // Re-throw with user-friendly message if (error.code === 'ECONNREFUSED') { throw new Error('Unable to connect to database'); } throw error; } } } } });
Testing GraphQL APIs
Unit Testing Resolvers
import { describe, it, expect, jest } from '@jest/globals'; describe('User resolver', () => { it('returns user by ID', async () => { const mockDb = { findUserById: jest.fn().mockResolvedValue({ id: '1', name: 'Alice', email: 'alice@example.com' }) }; const context = { db: mockDb }; const result = await userResolver.resolve(null, { id: '1' }, context); expect(mockDb.findUserById).toHaveBeenCalledWith('1'); expect(result).toEqual({ id: '1', name: 'Alice', email: 'alice@example.com' }); }); it('throws error for non-existent user', async () => { const mockDb = { findUserById: jest.fn().mockResolvedValue(null) }; const context = { db: mockDb }; await expect( userResolver.resolve(null, { id: '999' }, context) ).rejects.toThrow('User with ID 999 not found'); }); });
Integration Testing
import { graphql } from 'graphql'; import { schema } from './schema'; describe('GraphQL Schema', () => { it('executes user query', async () => { const query = ` query { user(id: "1") { id name email } } `; const result = await graphql({ schema, source: query, contextValue: { db: mockDatabase, user: null } }); expect(result.errors).toBeUndefined(); expect(result.data?.user).toEqual({ id: '1', name: 'Alice', email: 'alice@example.com' }); }); it('handles authentication errors', async () => { const query = ` query { me { id name } } `; const result = await graphql({ schema, source: query, contextValue: { db: mockDatabase, user: null } }); expect(result.errors).toBeDefined(); expect(result.errors[0].message).toBe('Authentication required'); }); });
Testing with Apollo Server
import { ApolloServer } from '@apollo/server'; const testServer = new ApolloServer({ schema, }); describe('User queries', () => { it('fetches user successfully', async () => { const response = await testServer.executeOperation({ query: ` query GetUser($id: ID!) { user(id: $id) { id name } } `, variables: { id: '1' } }); expect(response.body.singleResult.errors).toBeUndefined(); expect(response.body.singleResult.data?.user).toMatchObject({ id: '1', name: expect.any(String) }); }); });
Production Best Practices
Schema Organization
src/ ├── schema/ │ ├── index.js # Combine all types │ ├── types/ │ │ ├── user.js # User type and resolvers │ │ ├── post.js # Post type and resolvers │ │ └── comment.js # Comment type and resolvers │ ├── queries/ │ │ ├── user.js # User queries │ │ └── post.js # Post queries │ ├── mutations/ │ │ ├── user.js # User mutations │ │ └── post.js # Post mutations │ └── subscriptions/ │ └── post.js # Post subscriptions ├── directives/ │ └── auth.js # Authorization directive ├── utils/ │ ├── loaders.js # DataLoader instances │ └── context.js # Context builder └── server.js # Server setup
Monitoring and Logging
import { ApolloServerPluginLandingPageGraphQLPlayground } from '@apollo/server-plugin-landing-page-graphql-playground'; const server = new ApolloServer({ schema, plugins: [ // Request logging { async requestDidStart(requestContext) { console.log('Request started:', requestContext.request.query); return { async didEncounterErrors(ctx) { console.error('Errors:', ctx.errors); }, async willSendResponse(ctx) { console.log('Response sent'); } }; } }, // Performance monitoring { async requestDidStart() { const start = Date.now(); return { async willSendResponse() { const duration = Date.now() - start; console.log(`Request duration: ${duration}ms`); } }; } } ] });
Rate Limiting
import rateLimit from 'express-rate-limit'; const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per window message: 'Too many requests, please try again later' }); app.use('/graphql', limiter);
Query Whitelisting
const allowedQueries = new Set([ 'query GetUser { user(id: $id) { id name email } }', 'mutation CreatePost { createPost(input: $input) { id title } }' ]); const validateQuery = (query) => { const normalized = query.replace(/\s+/g, ' ').trim(); if (!allowedQueries.has(normalized)) { throw new Error('Query not whitelisted'); } };
Security Headers
import helmet from 'helmet'; app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], } }, crossOriginEmbedderPolicy: false }));
Advanced Patterns
Federation (Microservices)
import { buildSubgraphSchema } from '@apollo/subgraph'; // Users service const userSchema = buildSubgraphSchema({ typeDefs: ` type User @key(fields: "id") { id: ID! name: String! email: String! } `, resolvers: { User: { __resolveReference(user) { return findUserById(user.id); } } } }); // Posts service const postSchema = buildSubgraphSchema({ typeDefs: ` type Post { id: ID! title: String! author: User! } extend type User @key(fields: "id") { id: ID! @external posts: [Post!]! } `, resolvers: { Post: { author(post) { return { __typename: 'User', id: post.authorId }; } }, User: { posts(user) { return findPostsByAuthorId(user.id); } } } });
Custom Scalars
import { GraphQLScalarType, Kind } from 'graphql'; const DateTimeScalar = new GraphQLScalarType({ name: 'DateTime', description: 'ISO-8601 DateTime string', serialize(value) { // Send to client return value instanceof Date ? value.toISOString() : null; }, parseValue(value) { // From variables return new Date(value); }, parseLiteral(ast) { // From query string if (ast.kind === Kind.STRING) { return new Date(ast.value); } return null; } }); // Use in schema const schema = new GraphQLSchema({ types: [DateTimeScalar], query: new GraphQLObjectType({ name: 'Query', fields: { now: { type: DateTimeScalar, resolve: () => new Date() } } }) });
Batch Operations
const Mutation = new GraphQLObjectType({ name: 'Mutation', fields: { batchCreateUsers: { type: new GraphQLList(UserType), args: { inputs: { type: new GraphQLNonNull( new GraphQLList(new GraphQLNonNull(CreateUserInput)) ) } }, resolve: async (_, { inputs }, context) => { const users = await Promise.all( inputs.map(input => context.db.createUser(input)) ); return users; } } } });
Common Patterns Summary
- Use Input Types: For all mutations with multiple arguments
- Implement DataLoader: Solve N+1 queries for nested data
- Add Pagination: For list fields that can grow unbounded
- Handle Errors Gracefully: Return user-friendly error messages
- Validate Inputs: At resolver level before database operations
- Use Context for Shared State: Database, authentication, loaders
- Implement Authorization: At resolver or directive level
- Cache Aggressively: Use Redis or in-memory for frequently accessed data
- Monitor Performance: Track query complexity and execution time
- Version with @deprecated: Never break existing queries
- Test Thoroughly: Unit test resolvers, integration test queries
- Document Schema: Use descriptions in SDL
- Use Non-Null Wisely: Only for truly required fields
- Organize Schema: Split into modules by domain
- Secure Production: Rate limiting, query whitelisting, depth limiting
Resources and Tools
Essential Libraries
- graphql-js: Core GraphQL implementation
- express: Web server framework
- graphql-http: HTTP handler for GraphQL
- dataloader: Batching and caching
- graphql-ws: WebSocket server for subscriptions
- graphql-scalars: Common custom scalars
- graphql-tools: Schema manipulation utilities
Development Tools
- GraphiQL: In-browser GraphQL IDE
- GraphQL Playground: Advanced GraphQL IDE
- Apollo Studio: Schema registry and monitoring
- GraphQL Code Generator: Generate TypeScript types
- eslint-plugin-graphql: Lint GraphQL queries
Learning Resources
- GraphQL Official Documentation: https://graphql.org
- GraphQL.js Repository: https://github.com/graphql/graphql-js
- How to GraphQL: https://howtographql.com
- Apollo GraphQL: https://apollographql.com
- GraphQL Weekly Newsletter: https://graphqlweekly.com
Skill Version: 1.0.0 Last Updated: October 2025 Skill Category: API Development, Backend, GraphQL, Web Development Compatible With: Node.js, Express, TypeScript, JavaScript