
Graphql Api Development
- 12 installs
- 1 repo stars
- Updated November 29, 2025
- manutej/crush-mcp-server
Helps with backend & apis tasks.
About
graphql-api-development is a Claude Code skill for backend & apis. It helps solo builders move faster with AI-assisted development.
- graphql-api-development
- Backend & APIs
- AI-coding skill
Graphql Api Development by the numbers
- 12 all-time installs (skills.sh)
- Ranked #3,541 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/manutej/crush-mcp-server --skill graphql-api-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 1 |
| Last updated | November 29, 2025 |
| Repository | manutej/crush-mcp-server ↗ |
What it does
Helps with backend & apis tasks.
Files
GraphQL 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
1. Precise Data Fetching: Clients request exactly what they need, no over/under-fetching 2. Single Request: Fetch multiple resources in one roundtrip instead of multiple REST endpoints 3. Strongly Typed: Schema defines exact types, enabling validation and tooling 4. Introspection: Self-documenting API with queryable schema 5. Versioning Not Required: Add new fields without breaking existing queries 6. Real-time Updates: Built-in subscription support for live data 7. Nested Resources: Naturally handle complex relationships without N+1 queries 8. 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:
1. Scalar Types: Basic data types (String, Int, Float, Boolean, ID) 2. Object Types: Complex types with fields 3. Query Type: Entry point for read operations 4. Mutation Type: Entry point for write operations 5. Subscription Type: Entry point for real-time updates 6. Input Types: Complex inputs for mutations 7. Enums: Fixed set of values 8. Interfaces: Abstract types that objects implement 9. Unions: Types that can be one of several types 10. Non-Null Types: Types that cannot be null 11. 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:
parent: The result from the parent resolverargs: Arguments passed to the fieldcontext: Shared context (database, auth, etc.)info: Field-specific metadata
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 queriesDataLoader 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 setupMonitoring 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
1. Use Input Types: For all mutations with multiple arguments 2. Implement DataLoader: Solve N+1 queries for nested data 3. Add Pagination: For list fields that can grow unbounded 4. Handle Errors Gracefully: Return user-friendly error messages 5. Validate Inputs: At resolver level before database operations 6. Use Context for Shared State: Database, authentication, loaders 7. Implement Authorization: At resolver or directive level 8. Cache Aggressively: Use Redis or in-memory for frequently accessed data 9. Monitor Performance: Track query complexity and execution time 10. Version with @deprecated: Never break existing queries 11. Test Thoroughly: Unit test resolvers, integration test queries 12. Document Schema: Use descriptions in SDL 13. Use Non-Null Wisely: Only for truly required fields 14. Organize Schema: Split into modules by domain 15. 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
GraphQL API Development - Practical Examples
This file contains 20+ detailed, production-ready examples demonstrating GraphQL API patterns, best practices, and real-world scenarios using graphql-js.
Table of Contents
1. Basic Schema and Server Setup 2. User Management API 3. Blog API with Nested Data 4. E-Commerce Product Catalog 5. Input Types and Mutations 6. DataLoader for N+1 Prevention 7. Authentication with JWT 8. Field-Level Authorization 9. Real-Time Subscriptions 10. Pagination with Cursors 11. Error Handling Patterns 12. Custom Scalar Types 13. Interfaces and Polymorphism 14. Union Types for Search 15. Caching with Redis 16. File Upload Handling 17. Batch Mutations 18. Query Complexity Limiting 19. Testing GraphQL Resolvers 20. Production Server with Monitoring 21. Social Media Feed 22. Multi-Tenant API
---
Example 1: Basic Schema and Server Setup
A minimal GraphQL server demonstrating the fundamentals.
import express from 'express';
import { createHandler } from 'graphql-http/lib/use/express';
import { buildSchema } from 'graphql';
// Define schema using SDL
const schema = buildSchema(`
type Query {
hello: String
random: Float!
rollDice(numDice: Int!, numSides: Int): [Int]
}
`);
// Implement resolvers
const root = {
hello: () => {
return 'Hello world!';
},
random: () => {
return Math.random();
},
rollDice: ({ numDice, numSides }) => {
const sides = numSides || 6;
const output = [];
for (let i = 0; i < numDice; i++) {
output.push(1 + Math.floor(Math.random() * sides));
}
return output;
}
};
// Create Express server
const app = express();
app.all('/graphql', createHandler({
schema: schema,
rootValue: root
}));
app.listen(4000);
console.log('Running a GraphQL API server at localhost:4000/graphql');Query Examples:
# Simple query
{
hello
random
}
# Query with arguments
{
rollDice(numDice: 3, numSides: 6)
}
# Using variables
query RollDice($dice: Int!, $sides: Int) {
rollDice(numDice: $dice, numSides: $sides)
}---
Example 2: User Management API
Complete user CRUD operations with proper typing.
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLList, GraphQLNonNull, GraphQLSchema } from 'graphql';
import { randomBytes } from 'crypto';
// In-memory database
const users = new Map();
// User Type
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) },
bio: { type: GraphQLString }
}
});
// Query Type
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) }
},
resolve: (_, { id }) => {
const user = users.get(id);
if (!user) {
throw new Error(`User with ID ${id} not found`);
}
return user;
}
},
users: {
type: new GraphQLList(new GraphQLNonNull(UserType)),
resolve: () => {
return Array.from(users.values());
}
}
}
});
// Mutation Type
const MutationType = new GraphQLObjectType({
name: 'Mutation',
fields: {
createUser: {
type: UserType,
args: {
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) },
bio: { type: GraphQLString }
},
resolve: (_, { name, email, bio }) => {
const id = randomBytes(10).toString('hex');
const user = { id, name, email, bio };
users.set(id, user);
return user;
}
},
updateUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: GraphQLString },
email: { type: GraphQLString },
bio: { type: GraphQLString }
},
resolve: (_, { id, ...updates }) => {
const user = users.get(id);
if (!user) {
throw new Error(`User with ID ${id} not found`);
}
Object.assign(user, updates);
return user;
}
},
deleteUser: {
type: GraphQLID,
args: {
id: { type: new GraphQLNonNull(GraphQLID) }
},
resolve: (_, { id }) => {
if (!users.has(id)) {
throw new Error(`User with ID ${id} not found`);
}
users.delete(id);
return id;
}
}
}
});
// Create schema
const schema = new GraphQLSchema({
query: QueryType,
mutation: MutationType
});Usage:
# Create user
mutation {
createUser(name: "Alice", email: "alice@example.com", bio: "Developer") {
id
name
email
}
}
# Get all users
query {
users {
id
name
email
}
}
# Update user
mutation {
updateUser(id: "abc123", bio: "Senior Developer") {
id
name
bio
}
}---
Example 3: Blog API with Nested Data
Demonstrates relationships between types and nested resolvers.
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLList, GraphQLNonNull, GraphQLSchema } from 'graphql';
// Mock database
const users = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' }
];
const posts = [
{ id: '1', title: 'First Post', content: 'Hello World', authorId: '1' },
{ id: '2', title: 'GraphQL Tutorial', content: 'Learn GraphQL', authorId: '1' },
{ id: '3', title: 'Node.js Tips', content: 'Best practices', authorId: '2' }
];
const comments = [
{ id: '1', text: 'Great post!', postId: '1', authorId: '2' },
{ id: '2', text: 'Very helpful', postId: '2', authorId: '2' },
{ id: '3', text: 'Thanks!', postId: '1', authorId: '1' }
];
// Type Definitions
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)),
resolve: (user) => {
return posts.filter(post => post.authorId === user.id);
}
}
})
});
const PostType = new GraphQLObjectType({
name: 'Post',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLID) },
title: { type: new GraphQLNonNull(GraphQLString) },
content: { type: GraphQLString },
author: {
type: new GraphQLNonNull(UserType),
resolve: (post) => {
return users.find(user => user.id === post.authorId);
}
},
comments: {
type: new GraphQLList(new GraphQLNonNull(CommentType)),
resolve: (post) => {
return comments.filter(comment => comment.postId === post.id);
}
}
})
});
const CommentType = new GraphQLObjectType({
name: 'Comment',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLID) },
text: { type: new GraphQLNonNull(GraphQLString) },
author: {
type: new GraphQLNonNull(UserType),
resolve: (comment) => {
return users.find(user => user.id === comment.authorId);
}
},
post: {
type: new GraphQLNonNull(PostType),
resolve: (comment) => {
return posts.find(post => post.id === comment.postId);
}
}
})
});
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: (_, { id }) => users.find(u => u.id === id)
},
post: {
type: PostType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: (_, { id }) => posts.find(p => p.id === id)
},
posts: {
type: new GraphQLList(new GraphQLNonNull(PostType)),
resolve: () => posts
}
}
});
const schema = new GraphQLSchema({
query: QueryType
});Query Examples:
# Nested query
{
post(id: "1") {
title
content
author {
name
email
}
comments {
text
author {
name
}
}
}
}
# Get user with all posts
{
user(id: "1") {
name
posts {
title
comments {
text
}
}
}
}---
Example 4: E-Commerce Product Catalog
Product catalog with categories, pricing, and inventory.
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLFloat, GraphQLInt, GraphQLBoolean, GraphQLList, GraphQLNonNull, GraphQLSchema, GraphQLEnumType } from 'graphql';
// Enums
const CategoryEnum = new GraphQLEnumType({
name: 'Category',
values: {
ELECTRONICS: { value: 'electronics' },
CLOTHING: { value: 'clothing' },
BOOKS: { value: 'books' },
HOME: { value: 'home' }
}
});
// Types
const ProductType = new GraphQLObjectType({
name: 'Product',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
description: { type: GraphQLString },
price: { type: new GraphQLNonNull(GraphQLFloat) },
category: { type: new GraphQLNonNull(CategoryEnum) },
inStock: { type: new GraphQLNonNull(GraphQLBoolean) },
stockQuantity: { type: new GraphQLNonNull(GraphQLInt) },
images: { type: new GraphQLList(new GraphQLNonNull(GraphQLString)) },
rating: { type: GraphQLFloat },
reviews: {
type: new GraphQLList(new GraphQLNonNull(ReviewType)),
resolve: (product) => {
return reviews.filter(r => r.productId === product.id);
}
}
}
});
const ReviewType = new GraphQLObjectType({
name: 'Review',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
rating: { type: new GraphQLNonNull(GraphQLInt) },
comment: { type: GraphQLString },
authorName: { type: new GraphQLNonNull(GraphQLString) },
product: {
type: new GraphQLNonNull(ProductType),
resolve: (review) => {
return products.find(p => p.id === review.productId);
}
}
}
});
// Mock data
const products = [
{
id: '1',
name: 'Laptop',
description: 'High-performance laptop',
price: 999.99,
category: 'electronics',
inStock: true,
stockQuantity: 15,
images: ['laptop1.jpg', 'laptop2.jpg'],
rating: 4.5
},
{
id: '2',
name: 'T-Shirt',
description: 'Cotton t-shirt',
price: 19.99,
category: 'clothing',
inStock: true,
stockQuantity: 50,
images: ['tshirt1.jpg'],
rating: 4.0
}
];
const reviews = [
{ id: '1', productId: '1', rating: 5, comment: 'Excellent!', authorName: 'Alice' },
{ id: '2', productId: '1', rating: 4, comment: 'Good value', authorName: 'Bob' }
];
// Query
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
product: {
type: ProductType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: (_, { id }) => products.find(p => p.id === id)
},
products: {
type: new GraphQLList(new GraphQLNonNull(ProductType)),
args: {
category: { type: CategoryEnum },
inStock: { type: GraphQLBoolean }
},
resolve: (_, { category, inStock }) => {
let filtered = products;
if (category) {
filtered = filtered.filter(p => p.category === category);
}
if (inStock !== undefined) {
filtered = filtered.filter(p => p.inStock === inStock);
}
return filtered;
}
}
}
});
const schema = new GraphQLSchema({
query: QueryType
});Queries:
# Get product with reviews
{
product(id: "1") {
name
price
inStock
rating
reviews {
rating
comment
authorName
}
}
}
# Filter by category
{
products(category: ELECTRONICS, inStock: true) {
name
price
stockQuantity
}
}---
Example 5: Input Types and Mutations
Proper mutation design with input types and validation.
import { GraphQLObjectType, GraphQLInputObjectType, GraphQLString, GraphQLID, GraphQLNonNull, GraphQLList, GraphQLBoolean, GraphQLSchema } from 'graphql';
import { randomBytes } from 'crypto';
// Input Types
const MessageInput = new GraphQLInputObjectType({
name: 'MessageInput',
fields: {
content: { type: new GraphQLNonNull(GraphQLString) },
author: { type: new GraphQLNonNull(GraphQLString) }
}
});
const UpdateMessageInput = new GraphQLInputObjectType({
name: 'UpdateMessageInput',
fields: {
content: { type: GraphQLString },
author: { type: GraphQLString }
}
});
// Error Type
const ValidationError = new GraphQLObjectType({
name: 'ValidationError',
fields: {
field: { type: GraphQLString },
message: { type: new GraphQLNonNull(GraphQLString) }
}
});
// Output Types
const Message = new GraphQLObjectType({
name: 'Message',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
content: { type: new GraphQLNonNull(GraphQLString) },
author: { type: new GraphQLNonNull(GraphQLString) },
createdAt: { type: new GraphQLNonNull(GraphQLString) }
}
});
const CreateMessagePayload = new GraphQLObjectType({
name: 'CreateMessagePayload',
fields: {
message: { type: Message },
errors: { type: new GraphQLList(new GraphQLNonNull(ValidationError)) },
success: { type: new GraphQLNonNull(GraphQLBoolean) }
}
});
// Database
const fakeDatabase = {};
// Helper function for validation
const validateMessage = (input) => {
const errors = [];
if (!input.content || input.content.trim().length === 0) {
errors.push({ field: 'content', message: 'Content is required' });
} else if (input.content.length < 3) {
errors.push({ field: 'content', message: 'Content must be at least 3 characters' });
} else if (input.content.length > 500) {
errors.push({ field: 'content', message: 'Content must not exceed 500 characters' });
}
if (!input.author || input.author.trim().length === 0) {
errors.push({ field: 'author', message: 'Author is required' });
}
return errors;
};
// Mutations
const MutationType = new GraphQLObjectType({
name: 'Mutation',
fields: {
createMessage: {
type: CreateMessagePayload,
args: {
input: { type: new GraphQLNonNull(MessageInput) }
},
resolve: (_, { input }) => {
// Validate
const errors = validateMessage(input);
if (errors.length > 0) {
return { message: null, errors, success: false };
}
// Create message
const id = randomBytes(10).toString('hex');
const message = {
id,
content: input.content,
author: input.author,
createdAt: new Date().toISOString()
};
fakeDatabase[id] = message;
return { message, errors: [], success: true };
}
},
updateMessage: {
type: CreateMessagePayload,
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
input: { type: new GraphQLNonNull(UpdateMessageInput) }
},
resolve: (_, { id, input }) => {
const message = fakeDatabase[id];
if (!message) {
return {
message: null,
errors: [{ field: 'id', message: `Message with ID ${id} not found` }],
success: false
};
}
// Validate updates
const updatedMessage = { ...message, ...input };
const errors = validateMessage(updatedMessage);
if (errors.length > 0) {
return { message: null, errors, success: false };
}
// Update
Object.assign(fakeDatabase[id], input);
return { message: fakeDatabase[id], errors: [], success: true };
}
}
}
});
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
getMessage: {
type: Message,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: (_, { id }) => {
return fakeDatabase[id] || null;
}
},
messages: {
type: new GraphQLList(new GraphQLNonNull(Message)),
resolve: () => Object.values(fakeDatabase)
}
}
});
const schema = new GraphQLSchema({
query: QueryType,
mutation: MutationType
});Usage:
# Create message with validation
mutation {
createMessage(input: {
content: "Hello GraphQL"
author: "Alice"
}) {
success
message {
id
content
author
createdAt
}
errors {
field
message
}
}
}
# Invalid input (too short)
mutation {
createMessage(input: {
content: "Hi"
author: "Bob"
}) {
success
errors {
field
message
}
}
}---
Example 6: DataLoader for N+1 Prevention
Solving the N+1 query problem with DataLoader.
import DataLoader from 'dataloader';
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLList, GraphQLNonNull, GraphQLSchema } from 'graphql';
// Mock database queries
const getUsersByIds = async (userIds) => {
console.log(`Fetching users: ${userIds.join(', ')}`);
// Simulate database query
await new Promise(resolve => setTimeout(resolve, 100));
return userIds.map(id => ({
id,
name: `User ${id}`,
email: `user${id}@example.com`
}));
};
const getPostsByUserIds = async (userIds) => {
console.log(`Fetching posts for users: ${userIds.join(', ')}`);
await new Promise(resolve => setTimeout(resolve, 100));
const allPosts = {
'1': [
{ id: '1', title: 'Post 1', authorId: '1' },
{ id: '2', title: 'Post 2', authorId: '1' }
],
'2': [
{ id: '3', title: 'Post 3', authorId: '2' }
],
'3': []
};
return userIds.map(userId => allPosts[userId] || []);
};
// Create DataLoaders
const createLoaders = () => ({
userLoader: new DataLoader(getUsersByIds),
postLoader: new DataLoader(getPostsByUserIds)
});
// Types with DataLoader
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)),
resolve: (user, args, context) => {
// Uses DataLoader - batches requests
return context.loaders.postLoader.load(user.id);
}
}
})
});
const PostType = new GraphQLObjectType({
name: 'Post',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLID) },
title: { type: new GraphQLNonNull(GraphQLString) },
author: {
type: new GraphQLNonNull(UserType),
resolve: (post, args, context) => {
// Uses DataLoader - batches and caches
return context.loaders.userLoader.load(post.authorId);
}
}
})
});
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
users: {
type: new GraphQLList(new GraphQLNonNull(UserType)),
resolve: (_, __, context) => {
return context.loaders.userLoader.loadMany(['1', '2', '3']);
}
}
}
});
const schema = new GraphQLSchema({
query: QueryType
});
// Express setup with DataLoader in context
import express from 'express';
import { createHandler } from 'graphql-http/lib/use/express';
const app = express();
app.all('/graphql', (req, res) => {
// Create new loaders per request
const loaders = createLoaders();
createHandler({
schema,
context: { loaders }
})(req, res);
});
app.listen(4000);Query:
# Without DataLoader: 1 query for users + 3 queries for posts = 4 queries
# With DataLoader: 1 query for users + 1 batched query for all posts = 2 queries
{
users {
id
name
posts {
title
}
}
}---
Example 7: Authentication with JWT
JWT-based authentication in GraphQL context.
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLNonNull, GraphQLSchema } from 'graphql';
const JWT_SECRET = 'your-secret-key';
// Mock user database
const users = new Map([
['1', {
id: '1',
email: 'alice@example.com',
password: '$2b$10$abcdefghijklmnopqrstuv', // bcrypt hash
name: 'Alice'
}]
]);
// Types
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
email: { type: new GraphQLNonNull(GraphQLString) },
name: { type: new GraphQLNonNull(GraphQLString) }
}
});
const AuthPayload = new GraphQLObjectType({
name: 'AuthPayload',
fields: {
token: { type: GraphQLString },
user: { type: UserType },
error: { type: GraphQLString }
}
});
// Queries
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
me: {
type: UserType,
resolve: (_, __, context) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return context.user;
}
}
}
});
// Mutations
const MutationType = new GraphQLObjectType({
name: 'Mutation',
fields: {
signup: {
type: AuthPayload,
args: {
email: { type: new GraphQLNonNull(GraphQLString) },
password: { type: new GraphQLNonNull(GraphQLString) },
name: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: async (_, { email, password, name }) => {
// Check if user exists
for (const user of users.values()) {
if (user.email === email) {
return { error: 'User already exists', token: null, user: null };
}
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const id = String(users.size + 1);
const user = { id, email, password: hashedPassword, name };
users.set(id, user);
// Generate token
const token = jwt.sign({ userId: id }, JWT_SECRET, { expiresIn: '7d' });
return {
token,
user: { id, email, name },
error: null
};
}
},
login: {
type: AuthPayload,
args: {
email: { type: new GraphQLNonNull(GraphQLString) },
password: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: async (_, { email, password }) => {
// Find user
let foundUser = null;
for (const user of users.values()) {
if (user.email === email) {
foundUser = user;
break;
}
}
if (!foundUser) {
return { error: 'Invalid credentials', token: null, user: null };
}
// Verify password
const valid = await bcrypt.compare(password, foundUser.password);
if (!valid) {
return { error: 'Invalid credentials', token: null, user: null };
}
// Generate token
const token = jwt.sign({ userId: foundUser.id }, JWT_SECRET, { expiresIn: '7d' });
return {
token,
user: { id: foundUser.id, email: foundUser.email, name: foundUser.name },
error: null
};
}
}
}
});
const schema = new GraphQLSchema({
query: QueryType,
mutation: MutationType
});
// Express with auth middleware
import express from 'express';
import { createHandler } from 'graphql-http/lib/use/express';
const app = express();
app.all('/graphql', async (req, res) => {
// Extract token from header
const token = req.headers.authorization?.replace('Bearer ', '');
let user = null;
if (token) {
try {
const decoded = jwt.verify(token, JWT_SECRET);
user = users.get(decoded.userId);
// Remove password from context
if (user) {
user = { id: user.id, email: user.email, name: user.name };
}
} catch (error) {
console.error('Invalid token:', error.message);
}
}
createHandler({
schema,
context: { user }
})(req, res);
});
app.listen(4000);Usage:
# Sign up
mutation {
signup(email: "bob@example.com", password: "secret123", name: "Bob") {
token
user {
id
email
name
}
error
}
}
# Login
mutation {
login(email: "bob@example.com", password: "secret123") {
token
user {
id
name
}
error
}
}
# Get current user (requires Authorization header)
query {
me {
id
email
name
}
}---
Example 8: Field-Level Authorization
Granular permission control at the field level.
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLBoolean, GraphQLNonNull, GraphQLList, GraphQLSchema } from 'graphql';
// Mock data
const users = [
{ id: '1', email: 'alice@example.com', name: 'Alice', role: 'user', salary: 50000 },
{ id: '2', email: 'bob@example.com', name: 'Bob', role: 'admin', salary: 80000 }
];
const documents = [
{ id: '1', title: 'Public Doc', content: 'Anyone can see this', isPublic: true, ownerId: '1' },
{ id: '2', title: 'Private Doc', content: 'Only owner can see', isPublic: false, ownerId: '1' }
];
// User Type with field-level auth
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
email: {
type: GraphQLString,
resolve: (user, args, context) => {
// Only return email if viewing own profile or admin
if (context.user?.id === user.id || context.user?.role === 'admin') {
return user.email;
}
return null;
}
},
role: {
type: GraphQLString,
resolve: (user, args, context) => {
// Only admins can see roles
if (context.user?.role === 'admin') {
return user.role;
}
return null;
}
},
salary: {
type: GraphQLString,
resolve: (user, args, context) => {
// Only return salary if viewing own profile
if (context.user?.id === user.id) {
return user.salary;
}
return null;
}
}
}
});
const DocumentType = new GraphQLObjectType({
name: 'Document',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
title: { type: new GraphQLNonNull(GraphQLString) },
content: {
type: GraphQLString,
resolve: (doc, args, context) => {
// Public docs or owner can see content
if (doc.isPublic || context.user?.id === doc.ownerId) {
return doc.content;
}
return null;
}
},
isPublic: { type: new GraphQLNonNull(GraphQLBoolean) },
owner: {
type: new GraphQLNonNull(UserType),
resolve: (doc) => {
return users.find(u => u.id === doc.ownerId);
}
}
}
});
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: (_, { id }) => {
return users.find(u => u.id === id);
}
},
document: {
type: DocumentType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: (_, { id }, context) => {
const doc = documents.find(d => d.id === id);
if (!doc) {
throw new Error('Document not found');
}
// Check if user can access
if (!doc.isPublic && context.user?.id !== doc.ownerId) {
throw new Error('Not authorized to view this document');
}
return doc;
}
},
adminData: {
type: GraphQLString,
resolve: (_, __, context) => {
if (context.user?.role !== 'admin') {
throw new Error('Admin access required');
}
return 'Secret admin data';
}
}
}
});
const schema = new GraphQLSchema({
query: QueryType
});Queries:
# As regular user viewing own profile
query {
user(id: "1") {
name
email # visible
salary # visible
role # null (not admin)
}
}
# As regular user viewing another profile
query {
user(id: "2") {
name
email # null (not own profile)
salary # null (not own profile)
}
}
# As admin viewing any profile
query {
user(id: "1") {
name
email # visible
role # visible (is admin)
salary # null (not own profile)
}
}---
Example 9: Real-Time Subscriptions
WebSocket-based subscriptions for real-time updates.
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLNonNull, GraphQLList, GraphQLSchema } from 'graphql';
import { PubSub } from 'graphql-subscriptions';
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';
import { createHandler } from 'graphql-http/lib/use/express';
const pubsub = new PubSub();
// Events
const MESSAGE_SENT = 'MESSAGE_SENT';
const USER_JOINED = 'USER_JOINED';
const TYPING_STARTED = 'TYPING_STARTED';
// Types
const MessageType = new GraphQLObjectType({
name: 'Message',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
text: { type: new GraphQLNonNull(GraphQLString) },
author: { type: new GraphQLNonNull(GraphQLString) },
channelId: { type: new GraphQLNonNull(GraphQLID) },
timestamp: { type: new GraphQLNonNull(GraphQLString) }
}
});
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) }
}
});
const TypingEventType = new GraphQLObjectType({
name: 'TypingEvent',
fields: {
userId: { type: new GraphQLNonNull(GraphQLID) },
userName: { type: new GraphQLNonNull(GraphQLString) },
channelId: { type: new GraphQLNonNull(GraphQLID) }
}
});
// Storage
const messages = [];
let messageIdCounter = 1;
// Query
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
messages: {
type: new GraphQLList(new GraphQLNonNull(MessageType)),
args: {
channelId: { type: new GraphQLNonNull(GraphQLID) }
},
resolve: (_, { channelId }) => {
return messages.filter(m => m.channelId === channelId);
}
}
}
});
// Mutation
const MutationType = new GraphQLObjectType({
name: 'Mutation',
fields: {
sendMessage: {
type: MessageType,
args: {
text: { type: new GraphQLNonNull(GraphQLString) },
author: { type: new GraphQLNonNull(GraphQLString) },
channelId: { type: new GraphQLNonNull(GraphQLID) }
},
resolve: (_, { text, author, channelId }) => {
const message = {
id: String(messageIdCounter++),
text,
author,
channelId,
timestamp: new Date().toISOString()
};
messages.push(message);
// Publish to subscribers
pubsub.publish(MESSAGE_SENT, { messageSent: message });
return message;
}
},
startTyping: {
type: TypingEventType,
args: {
userId: { type: new GraphQLNonNull(GraphQLID) },
userName: { type: new GraphQLNonNull(GraphQLString) },
channelId: { type: new GraphQLNonNull(GraphQLID) }
},
resolve: (_, args) => {
const event = {
userId: args.userId,
userName: args.userName,
channelId: args.channelId
};
pubsub.publish(TYPING_STARTED, { typingStarted: event });
return event;
}
}
}
});
// Subscription
const SubscriptionType = new GraphQLObjectType({
name: 'Subscription',
fields: {
messageSent: {
type: MessageType,
args: {
channelId: { type: GraphQLID }
},
subscribe: (_, { channelId }) => {
if (channelId) {
// Filter by channel
return pubsub.asyncIterator([MESSAGE_SENT]);
}
return pubsub.asyncIterator([MESSAGE_SENT]);
},
resolve: (payload, args) => {
// Filter in resolve if channelId provided
if (args.channelId && payload.messageSent.channelId !== args.channelId) {
return null;
}
return payload.messageSent;
}
},
typingStarted: {
type: TypingEventType,
args: {
channelId: { type: new GraphQLNonNull(GraphQLID) }
},
subscribe: (_, { channelId }) => {
return pubsub.asyncIterator([TYPING_STARTED]);
},
resolve: (payload, { channelId }) => {
if (payload.typingStarted.channelId !== channelId) {
return null;
}
return payload.typingStarted;
}
}
}
});
const schema = new GraphQLSchema({
query: QueryType,
mutation: MutationType,
subscription: SubscriptionType
});
// Server setup
const app = express();
app.all('/graphql', createHandler({ schema }));
const httpServer = createServer(app);
// WebSocket server
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
useServer(
{
schema,
execute,
subscribe,
context: (ctx) => {
return {
connectionParams: ctx.connectionParams
};
}
},
wsServer
);
httpServer.listen(4000, () => {
console.log('Server running on http://localhost:4000/graphql');
console.log('WebSocket on ws://localhost:4000/graphql');
});Client Usage:
# Subscribe to messages
subscription {
messageSent(channelId: "1") {
id
text
author
timestamp
}
}
# Send message (triggers subscription)
mutation {
sendMessage(
text: "Hello everyone!"
author: "Alice"
channelId: "1"
) {
id
text
}
}
# Subscribe to typing events
subscription {
typingStarted(channelId: "1") {
userName
}
}---
Example 10: Pagination with Cursors
Relay-style cursor-based pagination for scalable lists.
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLInt, GraphQLBoolean, GraphQLNonNull, GraphQLList, GraphQLSchema } from 'graphql';
// Mock data
const allPosts = Array.from({ length: 100 }, (_, i) => ({
id: String(i + 1),
title: `Post ${i + 1}`,
content: `Content for post ${i + 1}`,
createdAt: new Date(Date.now() - i * 3600000).toISOString()
}));
// Helper to encode/decode cursors
const encodeCursor = (id) => Buffer.from(id).toString('base64');
const decodeCursor = (cursor) => Buffer.from(cursor, 'base64').toString('utf-8');
// Types
const PostType = new GraphQLObjectType({
name: 'Post',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
title: { type: new GraphQLNonNull(GraphQLString) },
content: { type: GraphQLString },
createdAt: { type: new GraphQLNonNull(GraphQLString) }
}
});
const PageInfoType = new GraphQLObjectType({
name: 'PageInfo',
fields: {
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
hasPreviousPage: { type: new GraphQLNonNull(GraphQLBoolean) },
startCursor: { type: GraphQLString },
endCursor: { type: GraphQLString }
}
});
const PostEdgeType = new GraphQLObjectType({
name: 'PostEdge',
fields: {
node: { type: new GraphQLNonNull(PostType) },
cursor: { type: new GraphQLNonNull(GraphQLString) }
}
});
const PostConnectionType = new GraphQLObjectType({
name: 'PostConnection',
fields: {
edges: { type: new GraphQLList(new GraphQLNonNull(PostEdgeType)) },
pageInfo: { type: new GraphQLNonNull(PageInfoType) },
totalCount: { type: new GraphQLNonNull(GraphQLInt) }
}
});
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
posts: {
type: PostConnectionType,
args: {
first: { type: GraphQLInt },
after: { type: GraphQLString },
last: { type: GraphQLInt },
before: { type: GraphQLString }
},
resolve: (_, { first, after, last, before }) => {
let posts = [...allPosts];
// Handle cursor-based filtering
if (after) {
const afterId = decodeCursor(after);
const afterIndex = posts.findIndex(p => p.id === afterId);
posts = posts.slice(afterIndex + 1);
}
if (before) {
const beforeId = decodeCursor(before);
const beforeIndex = posts.findIndex(p => p.id === beforeId);
posts = posts.slice(0, beforeIndex);
}
// Handle first/last
let hasNextPage = false;
let hasPreviousPage = false;
if (first) {
hasNextPage = posts.length > first;
posts = posts.slice(0, first);
}
if (last) {
hasPreviousPage = posts.length > last;
posts = posts.slice(-last);
}
// Create edges
const edges = posts.map(post => ({
node: post,
cursor: encodeCursor(post.id)
}));
// Create page info
const pageInfo = {
hasNextPage,
hasPreviousPage,
startCursor: edges.length > 0 ? edges[0].cursor : null,
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null
};
return {
edges,
pageInfo,
totalCount: allPosts.length
};
}
}
}
});
const schema = new GraphQLSchema({
query: QueryType
});Usage:
# Get first 10 posts
query {
posts(first: 10) {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
# Get next 10 posts using cursor
query {
posts(first: 10, after: "MTA=") {
edges {
node {
id
title
}
}
pageInfo {
hasNextPage
hasPreviousPage
endCursor
}
}
}---
Example 11: Error Handling Patterns
Comprehensive error handling with custom error types.
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLNonNull, GraphQLList, GraphQLBoolean, GraphQLUnionType, GraphQLSchema } from 'graphql';
// Custom Error Classes
class AuthenticationError extends Error {
constructor(message = 'Authentication required') {
super(message);
this.name = 'AuthenticationError';
this.extensions = { code: 'UNAUTHENTICATED' };
}
}
class ForbiddenError extends Error {
constructor(message = 'Access denied') {
super(message);
this.name = 'ForbiddenError';
this.extensions = { code: 'FORBIDDEN' };
}
}
class NotFoundError extends Error {
constructor(resource, id) {
super(`${resource} with ID ${id} not found`);
this.name = 'NotFoundError';
this.extensions = { code: 'NOT_FOUND', resource, id };
}
}
// Error Types
const ValidationErrorType = new GraphQLObjectType({
name: 'ValidationError',
fields: {
field: { type: GraphQLString },
message: { type: new GraphQLNonNull(GraphQLString) },
code: { type: GraphQLString }
}
});
const NotFoundErrorType = new GraphQLObjectType({
name: 'NotFoundError',
fields: {
message: { type: new GraphQLNonNull(GraphQLString) },
resource: { type: GraphQLString },
id: { type: GraphQLString }
}
});
const UnauthorizedErrorType = new GraphQLObjectType({
name: 'UnauthorizedError',
fields: {
message: { type: new GraphQLNonNull(GraphQLString) }
}
});
// Success Types
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) }
}
});
// Union for mutation results
const CreateUserResult = new GraphQLUnionType({
name: 'CreateUserResult',
types: [UserType, ValidationErrorType, UnauthorizedErrorType],
resolveType(obj) {
if (obj.id) return 'User';
if (obj.field) return 'ValidationError';
if (obj.message) return 'UnauthorizedError';
return null;
}
});
const GetUserResult = new GraphQLUnionType({
name: 'GetUserResult',
types: [UserType, NotFoundErrorType],
resolveType(obj) {
if (obj.id) return 'User';
return 'NotFoundError';
}
});
// Traditional payload pattern
const CreateUserPayload = new GraphQLObjectType({
name: 'CreateUserPayload',
fields: {
user: { type: UserType },
errors: { type: new GraphQLList(new GraphQLNonNull(ValidationErrorType)) },
success: { type: new GraphQLNonNull(GraphQLBoolean) }
}
});
// Mock database
const users = new Map();
// Mutations
const MutationType = new GraphQLObjectType({
name: 'Mutation',
fields: {
// Union-based error handling
createUserUnion: {
type: CreateUserResult,
args: {
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: (_, { name, email }, context) => {
// Check auth
if (!context.user) {
return { message: 'Authentication required' };
}
// Validate
if (name.length < 2) {
return {
field: 'name',
message: 'Name must be at least 2 characters',
code: 'NAME_TOO_SHORT'
};
}
if (!email.includes('@')) {
return {
field: 'email',
message: 'Invalid email format',
code: 'INVALID_EMAIL'
};
}
// Create user
const id = String(users.size + 1);
const user = { id, name, email };
users.set(id, user);
return user;
}
},
// Payload-based error handling
createUserPayload: {
type: CreateUserPayload,
args: {
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: (_, { name, email }) => {
const errors = [];
// Validate
if (name.length < 2) {
errors.push({
field: 'name',
message: 'Name must be at least 2 characters',
code: 'NAME_TOO_SHORT'
});
}
if (!email.includes('@')) {
errors.push({
field: 'email',
message: 'Invalid email format',
code: 'INVALID_EMAIL'
});
}
if (errors.length > 0) {
return { user: null, errors, success: false };
}
// Create user
const id = String(users.size + 1);
const user = { id, name, email };
users.set(id, user);
return { user, errors: [], success: true };
}
}
}
});
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
// Union-based not found
getUserUnion: {
type: GetUserResult,
args: {
id: { type: new GraphQLNonNull(GraphQLID) }
},
resolve: (_, { id }) => {
const user = users.get(id);
if (!user) {
return {
message: `User with ID ${id} not found`,
resource: 'User',
id
};
}
return user;
}
},
// Throw error approach
getUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) }
},
resolve: (_, { id }) => {
const user = users.get(id);
if (!user) {
throw new NotFoundError('User', id);
}
return user;
}
}
}
});
const schema = new GraphQLSchema({
query: QueryType,
mutation: MutationType
});Usage:
# Union-based error handling
mutation {
createUserUnion(name: "A", email: "invalid") {
... on User {
id
name
}
... on ValidationError {
field
message
code
}
... on UnauthorizedError {
message
}
}
}
# Payload-based error handling
mutation {
createUserPayload(name: "Alice", email: "alice@example.com") {
success
user {
id
name
}
errors {
field
message
}
}
}
# Not found with union
query {
getUserUnion(id: "999") {
... on User {
id
name
}
... on NotFoundError {
message
resource
}
}
}---
Example 12: Custom Scalar Types
Implementing custom scalars for dates, JSON, and more.
import { GraphQLScalarType, GraphQLObjectType, GraphQLString, GraphQLID, GraphQLNonNull, GraphQLSchema, Kind } from 'graphql';
// DateTime Scalar
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO-8601 DateTime string',
serialize(value) {
// Sent to client
if (value instanceof Date) {
return value.toISOString();
}
if (typeof value === 'string') {
return new Date(value).toISOString();
}
throw new Error('DateTime must be a Date object or ISO string');
},
parseValue(value) {
// From variables
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new Error('Invalid DateTime value');
}
return date;
},
parseLiteral(ast) {
// From query string
if (ast.kind === Kind.STRING) {
const date = new Date(ast.value);
if (isNaN(date.getTime())) {
throw new Error('Invalid DateTime literal');
}
return date;
}
throw new Error('DateTime must be a string');
}
});
// JSON Scalar
const JSONScalar = new GraphQLScalarType({
name: 'JSON',
description: 'Arbitrary JSON value',
serialize(value) {
return value;
},
parseValue(value) {
return value;
},
parseLiteral(ast) {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return ast.value;
case Kind.INT:
case Kind.FLOAT:
return parseFloat(ast.value);
case Kind.OBJECT:
return parseObject(ast);
case Kind.LIST:
return ast.values.map(n => JSONScalar.parseLiteral(n));
default:
return null;
}
}
});
function parseObject(ast) {
const value = Object.create(null);
ast.fields.forEach(field => {
value[field.name.value] = JSONScalar.parseLiteral(field.value);
});
return value;
}
// Email Scalar with validation
const EmailScalar = new GraphQLScalarType({
name: 'Email',
description: 'Valid email address',
serialize(value) {
if (typeof value !== 'string') {
throw new Error('Email must be a string');
}
if (!isValidEmail(value)) {
throw new Error('Invalid email format');
}
return value;
},
parseValue(value) {
if (!isValidEmail(value)) {
throw new Error('Invalid email format');
}
return value;
},
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new Error('Email must be a string');
}
if (!isValidEmail(ast.value)) {
throw new Error('Invalid email format');
}
return ast.value;
}
});
function isValidEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
// Use in schema
const EventType = new GraphQLObjectType({
name: 'Event',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
startTime: { type: new GraphQLNonNull(DateTimeScalar) },
endTime: { type: DateTimeScalar },
metadata: { type: JSONScalar },
organizerEmail: { type: new GraphQLNonNull(EmailScalar) }
}
});
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
event: {
type: EventType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) }
},
resolve: () => ({
id: '1',
name: 'GraphQL Workshop',
startTime: new Date('2025-01-15T10:00:00Z'),
endTime: new Date('2025-01-15T12:00:00Z'),
metadata: {
location: 'Room 101',
capacity: 50,
tags: ['workshop', 'graphql']
},
organizerEmail: 'organizer@example.com'
})
},
now: {
type: DateTimeScalar,
resolve: () => new Date()
}
}
});
const schema = new GraphQLSchema({
query: QueryType,
types: [DateTimeScalar, JSONScalar, EmailScalar]
});Usage:
query {
event(id: "1") {
name
startTime
endTime
metadata
organizerEmail
}
now
}
# Result:
{
"event": {
"name": "GraphQL Workshop",
"startTime": "2025-01-15T10:00:00.000Z",
"endTime": "2025-01-15T12:00:00.000Z",
"metadata": {
"location": "Room 101",
"capacity": 50,
"tags": ["workshop", "graphql"]
},
"organizerEmail": "organizer@example.com"
},
"now": "2025-10-18T12:30:45.123Z"
}---
Example 13: Interfaces and Polymorphism
Using interfaces for shared fields across types.
import { GraphQLInterfaceType, GraphQLObjectType, GraphQLString, GraphQLID, GraphQLList, GraphQLNonNull, GraphQLSchema } from 'graphql';
// Interface
const NodeInterface = new GraphQLInterfaceType({
name: 'Node',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
createdAt: { type: new GraphQLNonNull(GraphQLString) },
updatedAt: { type: new GraphQLNonNull(GraphQLString) }
},
resolveType(obj) {
if (obj.email) return 'User';
if (obj.title) return 'Post';
if (obj.text) return 'Comment';
return null;
}
});
// Implementing types
const UserType = new GraphQLObjectType({
name: 'User',
interfaces: [NodeInterface],
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
createdAt: { type: new GraphQLNonNull(GraphQLString) },
updatedAt: { type: new GraphQLNonNull(GraphQLString) },
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) }
}
});
const PostType = new GraphQLObjectType({
name: 'Post',
interfaces: [NodeInterface],
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
createdAt: { type: new GraphQLNonNull(GraphQLString) },
updatedAt: { type: new GraphQLNonNull(GraphQLString) },
title: { type: new GraphQLNonNull(GraphQLString) },
content: { type: GraphQLString },
author: {
type: new GraphQLNonNull(UserType),
resolve: (post) => users.find(u => u.id === post.authorId)
}
}
});
const CommentType = new GraphQLObjectType({
name: 'Comment',
interfaces: [NodeInterface],
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
createdAt: { type: new GraphQLNonNull(GraphQLString) },
updatedAt: { type: new GraphQLNonNull(GraphQLString) },
text: { type: new GraphQLNonNull(GraphQLString) },
author: {
type: new GraphQLNonNull(UserType),
resolve: (comment) => users.find(u => u.id === comment.authorId)
}
}
});
// Mock data
const users = [
{ id: '1', name: 'Alice', email: 'alice@example.com', createdAt: '2025-01-01T00:00:00Z', updatedAt: '2025-01-01T00:00:00Z' }
];
const posts = [
{ id: '2', title: 'First Post', content: 'Hello', authorId: '1', createdAt: '2025-01-02T00:00:00Z', updatedAt: '2025-01-02T00:00:00Z' }
];
const comments = [
{ id: '3', text: 'Great!', authorId: '1', createdAt: '2025-01-03T00:00:00Z', updatedAt: '2025-01-03T00:00:00Z' }
];
// Query
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
node: {
type: NodeInterface,
args: {
id: { type: new GraphQLNonNull(GraphQLID) }
},
resolve: (_, { id }) => {
return users.find(u => u.id === id) ||
posts.find(p => p.id === id) ||
comments.find(c => c.id === id);
}
},
nodes: {
type: new GraphQLList(NodeInterface),
resolve: () => {
return [...users, ...posts, ...comments];
}
}
}
});
const schema = new GraphQLSchema({
query: QueryType,
types: [UserType, PostType, CommentType]
});Usage:
# Query interface
{
node(id: "1") {
id
createdAt
... on User {
name
email
}
... on Post {
title
content
}
... on Comment {
text
}
}
}
# Query all nodes
{
nodes {
__typename
id
createdAt
... on User {
name
}
... on Post {
title
}
}
}---
Example 14: Union Types for Search
Polymorphic search results using unions.
import { GraphQLUnionType, GraphQLObjectType, GraphQLString, GraphQLID, GraphQLList, GraphQLNonNull, GraphQLInt, GraphQLSchema } from 'graphql';
// Types
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
bio: { type: GraphQLString }
}
});
const PostType = new GraphQLObjectType({
name: 'Post',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
title: { type: new GraphQLNonNull(GraphQLString) },
content: { type: GraphQLString }
}
});
const ProductType = new GraphQLObjectType({
name: 'Product',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
price: { type: new GraphQLNonNull(GraphQLInt) }
}
});
// Union
const SearchResult = new GraphQLUnionType({
name: 'SearchResult',
types: [UserType, PostType, ProductType],
resolveType(obj) {
if (obj.bio !== undefined) return 'User';
if (obj.price !== undefined) return 'Product';
if (obj.content !== undefined) return 'Post';
return null;
}
});
// Mock data
const users = [
{ id: '1', name: 'Alice Developer', bio: 'GraphQL enthusiast' },
{ id: '2', name: 'Bob Designer', bio: 'UI/UX expert' }
];
const posts = [
{ id: '1', title: 'GraphQL Best Practices', content: 'Learn GraphQL...' },
{ id: '2', title: 'API Design', content: 'Building APIs...' }
];
const products = [
{ id: '1', name: 'GraphQL Book', price: 29 },
{ id: '2', name: 'API Course', price: 99 }
];
// Query
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
search: {
type: new GraphQLList(SearchResult),
args: {
query: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: (_, { query }) => {
const q = query.toLowerCase();
const results = [];
// Search users
users.forEach(user => {
if (user.name.toLowerCase().includes(q) || user.bio.toLowerCase().includes(q)) {
results.push(user);
}
});
// Search posts
posts.forEach(post => {
if (post.title.toLowerCase().includes(q) || post.content.toLowerCase().includes(q)) {
results.push(post);
}
});
// Search products
products.forEach(product => {
if (product.name.toLowerCase().includes(q)) {
results.push(product);
}
});
return results;
}
}
}
});
const schema = new GraphQLSchema({
query: QueryType
});Usage:
{
search(query: "graphql") {
__typename
... on User {
id
name
bio
}
... on Post {
id
title
}
... on Product {
id
name
price
}
}
}
# Result:
{
"search": [
{
"__typename": "User",
"id": "1",
"name": "Alice Developer",
"bio": "GraphQL enthusiast"
},
{
"__typename": "Post",
"id": "1",
"title": "GraphQL Best Practices"
},
{
"__typename": "Product",
"id": "1",
"name": "GraphQL Book",
"price": 29
}
]
}---
Example 15: Caching with Redis
Redis-based caching for improved performance.
import Redis from 'ioredis';
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLInt, GraphQLNonNull, GraphQLList, GraphQLSchema } from 'graphql';
// Redis client
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
});
// Types
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) },
postCount: {
type: GraphQLInt,
resolve: async (user) => {
const cacheKey = `user:${user.id}:postCount`;
// Check cache
const cached = await redis.get(cacheKey);
if (cached !== null) {
console.log('Cache HIT for postCount');
return parseInt(cached, 10);
}
// Simulate expensive query
console.log('Cache MISS for postCount');
const count = await fetchPostCount(user.id);
// Cache for 5 minutes
await redis.setex(cacheKey, 300, count);
return count;
}
}
}
});
// Mock database
async function fetchUserById(id) {
console.log(`DB query: fetching user ${id}`);
await new Promise(resolve => setTimeout(resolve, 100));
return {
id,
name: `User ${id}`,
email: `user${id}@example.com`
};
}
async function fetchPostCount(userId) {
console.log(`DB query: counting posts for user ${userId}`);
await new Promise(resolve => setTimeout(resolve, 100));
return Math.floor(Math.random() * 20);
}
// Query
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) }
},
resolve: async (_, { id }) => {
const cacheKey = `user:${id}`;
// Check cache
const cached = await redis.get(cacheKey);
if (cached) {
console.log('Cache HIT for user');
return JSON.parse(cached);
}
// Fetch from database
console.log('Cache MISS for user');
const user = await fetchUserById(id);
// Cache for 10 minutes
await redis.setex(cacheKey, 600, JSON.stringify(user));
return user;
}
}
}
});
// Mutation with cache invalidation
const MutationType = new GraphQLObjectType({
name: 'Mutation',
fields: {
updateUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: GraphQLString },
email: { type: GraphQLString }
},
resolve: async (_, { id, name, email }) => {
// Update in database
console.log(`DB query: updating user ${id}`);
const user = {
id,
name: name || `User ${id}`,
email: email || `user${id}@example.com`
};
// Invalidate cache
await redis.del(`user:${id}`);
await redis.del(`user:${id}:postCount`);
console.log('Cache invalidated');
return user;
}
}
}
});
const schema = new GraphQLSchema({
query: QueryType,
mutation: MutationType
});
// Cleanup on exit
process.on('SIGINT', async () => {
await redis.quit();
process.exit(0);
});Usage:
# First query - cache miss
{
user(id: "1") {
name
email
postCount
}
}
# Second query - cache hit
{
user(id: "1") {
name
email
postCount
}
}
# Update invalidates cache
mutation {
updateUser(id: "1", name: "Alice Updated") {
name
}
}---
Continued in Part 2...
This file demonstrates 15 comprehensive examples. The remaining 7+ examples include:
- File Upload Handling
- Batch Mutations
- Query Complexity Limiting
- Testing GraphQL Resolvers
- Production Server with Monitoring
- Social Media Feed
- Multi-Tenant API
Each example provides production-ready patterns with Context7 integration and best practices from the GraphQL.js documentation.
---
Total Examples: 20+ Lines of Code: 2000+ Coverage: Schema design, mutations, subscriptions, authentication, authorization, caching, testing, production patterns Context7 Integration: All examples use patterns from /graphql/graphql-js documentation
GraphQL API Development
Build production-ready GraphQL APIs with Node.js, Express, and graphql-js. This comprehensive skill covers everything from basic schema design to advanced patterns like federation, subscriptions, and real-time updates.
Overview
GraphQL is a query language and runtime for APIs that gives clients precise control over the data they request. Unlike REST APIs with fixed endpoints, GraphQL allows clients to specify exactly what data they need in a single request, reducing over-fetching and under-fetching.
Key Features
Schema-First Development
- Strongly Typed: Every field has a defined type, enabling validation and tooling
- Self-Documenting: Schema serves as live documentation via introspection
- Contract-Based: Schema defines the contract between client and server
- IDE Support: Rich autocomplete and validation in development tools
Flexible Data Fetching
- Precise Queries: Clients request exactly what they need
- Single Request: Fetch multiple resources in one roundtrip
- Nested Data: Navigate relationships naturally
- No Versioning: Add new fields without breaking existing clients
Real-Time Capabilities
- Subscriptions: WebSocket-based real-time updates
- Live Queries: Automatically update when data changes
- Event-Driven: Publish-subscribe pattern for notifications
- Bi-directional: Server can push updates to clients
Developer Experience
- GraphiQL/Playground: Interactive API exploration
- Type Generation: Auto-generate TypeScript types from schema
- Error Handling: Rich error information with field-level precision
- Introspection: Query the schema itself for documentation
When to Use GraphQL
Ideal Use Cases
1. Complex Data Requirements
- Apps with many views requiring different data shapes
- Mobile apps with bandwidth constraints
- Multiple client types (web, mobile, desktop)
2. Rapid Iteration
- Frontend teams need flexibility without backend changes
- Frequent UI changes requiring different data
- A/B testing different features
3. Microservices Architecture
- Multiple data sources to aggregate
- Need unified API across services
- Service composition and federation
4. Real-Time Features
- Chat applications
- Live dashboards
- Collaborative editing
- Notifications and updates
5. Developer Productivity
- Type safety end-to-end
- Self-documenting API
- Strong tooling ecosystem
When REST Might Be Better
- Simple CRUD operations
- File uploads/downloads (requires multipart handling in GraphQL)
- HTTP caching is critical (GraphQL typically uses POST)
- Team unfamiliar with GraphQL (learning curve)
- Existing REST infrastructure works well
Core Concepts
The Type System
GraphQL's type system is the foundation of every GraphQL API:
# Scalar types
String # Text data
Int # 32-bit integer
Float # Floating point number
Boolean # true or false
ID # Unique identifier
# Object types
type User {
id: ID! # ! means non-null (required)
name: String!
email: String!
age: Int
posts: [Post!]! # List of posts (non-null list, non-null items)
}
# Input types (for mutations)
input CreateUserInput {
name: String!
email: String!
age: Int
}
# Enums
enum Role {
USER
ADMIN
MODERATOR
}
# Interfaces (shared fields)
interface Node {
id: ID!
createdAt: String!
}
# Unions (multiple possible types)
union SearchResult = User | Post | CommentSchema Structure
Every GraphQL schema has entry points:
# Read operations
type Query {
user(id: ID!): User
posts: [Post!]!
search(query: String!): [SearchResult!]!
}
# Write operations
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
# Real-time updates
type Subscription {
userCreated: User!
postPublished: Post!
messageReceived(channelId: ID!): Message!
}Resolvers
Resolvers are functions that fetch the data for each field:
const resolvers = {
Query: {
user: (parent, args, context) => {
return context.db.findUserById(args.id);
},
posts: (parent, args, context) => {
return context.db.getAllPosts();
}
},
User: {
posts: (user, args, context) => {
// Fetch posts for this specific user
return context.db.findPostsByAuthorId(user.id);
}
},
Mutation: {
createUser: (parent, { input }, context) => {
return context.db.createUser(input);
}
}
};Resolver signature: (parent, args, context, info) => result
- parent: Result from parent resolver
- args: Field arguments
- context: Shared state (database, user, etc.)
- info: Query metadata
Quick Start
1. Install Dependencies
npm install graphql express graphql-http2. Define Schema
import { buildSchema } from 'graphql';
const schema = buildSchema(`
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
}
`);3. Implement Resolvers
const users = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' }
];
const root = {
user: ({ id }) => users.find(u => u.id === id),
users: () => users,
createUser: ({ name, email }) => {
const user = {
id: String(users.length + 1),
name,
email
};
users.push(user);
return user;
}
};4. Create Server
import express from 'express';
import { createHandler } from 'graphql-http/lib/use/express';
const app = express();
app.all('/graphql', createHandler({
schema,
rootValue: root
}));
app.listen(4000, () => {
console.log('GraphQL server running at http://localhost:4000/graphql');
});5. Query Your API
# Fetch a user
query {
user(id: "1") {
id
name
email
}
}
# Create a user
mutation {
createUser(name: "Charlie", email: "charlie@example.com") {
id
name
}
}
# Fetch all users
query {
users {
id
name
}
}Schema Design Principles
1. Design from the Client Perspective
Think about what clients need, not what your database looks like:
# Good: Client-focused
type Product {
displayPrice: String! # "$19.99"
isAvailable: Boolean!
canShipToday: Boolean!
}
# Avoid: Database-focused
type Product {
price_cents: Int!
stock_quantity: Int!
warehouse_id: ID!
}2. Use Nullable Fields Carefully
Only mark fields as non-null (!) if they'll ALWAYS have a value:
type User {
id: ID! # ✓ Always has ID
name: String! # ✓ Required field
email: String! # ✓ Required for login
bio: String # ✗ Optional, might be null
avatar: String # ✗ Optional
}3. Embrace Relationships
GraphQL naturally handles relationships:
type User {
id: ID!
name: String!
posts: [Post!]! # User has many posts
followers: [User!]! # User has many followers
}
type Post {
id: ID!
title: String!
author: User! # Post belongs to user
comments: [Comment!]!
}4. Use Input Types for Mutations
Always use input types for complex mutations:
# Good
input CreatePostInput {
title: String!
content: String!
tags: [String!]
draft: Boolean
}
type Mutation {
createPost(input: CreatePostInput!): Post!
}
# Avoid
type Mutation {
createPost(
title: String!,
content: String!,
tags: [String!],
draft: Boolean
): Post!
}5. Plan for Pagination
Lists that can grow should be paginated:
type Query {
# Simple offset pagination
posts(offset: Int, limit: Int): PostConnection!
# Cursor-based (better for real-time data)
feed(first: Int, after: String): FeedConnection!
}
type PostConnection {
items: [Post!]!
total: Int!
hasMore: Boolean!
}6. Handle Errors Gracefully
Design mutations to return detailed error information:
type CreateUserPayload {
user: User
errors: [UserError!]
success: Boolean!
}
type UserError {
field: String
message: String!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}Performance Optimization
The N+1 Problem
Most common GraphQL performance issue:
// BAD: Causes N+1 queries
type User {
posts: [Post!]!
}
// Resolver runs once per user!
User: {
posts: (user) => db.query('SELECT * FROM posts WHERE author_id = ?', user.id)
}
// Querying 100 users = 1 query for users + 100 queries for posts = 101 total!Solution: DataLoader
import DataLoader from 'dataloader';
const postLoader = new DataLoader(async (userIds) => {
// One query for ALL users
const posts = await db.query(
'SELECT * FROM posts WHERE author_id IN (?)',
userIds
);
// Group by user ID
const postsByUser = {};
posts.forEach(post => {
if (!postsByUser[post.author_id]) {
postsByUser[post.author_id] = [];
}
postsByUser[post.author_id].push(post);
});
// Return in same order as input
return userIds.map(id => postsByUser[id] || []);
});
// Use in resolver
User: {
posts: (user, args, context) => context.loaders.post.load(user.id)
}
// 100 users = 1 query for users + 1 query for ALL posts = 2 total!Authentication and Authorization
Context-Based Auth
// Extract user from JWT token
const context = async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return { user: null };
}
const user = await verifyToken(token);
return { user };
};
// Use in resolver
Query: {
me: (parent, args, context) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return context.user;
}
}Field-Level Authorization
Post: {
draft: (post, args, context) => {
// Only author can see draft status
if (post.authorId !== context.user?.id) {
return null;
}
return post.draft;
}
}Real-Time with Subscriptions
Setup
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers = {
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
}
},
Mutation: {
createPost: async (parent, { input }) => {
const post = await db.createPost(input);
// Notify subscribers
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
}
}
};Client Usage
subscription {
postCreated {
id
title
author {
name
}
}
}Testing
Unit Test Resolvers
import { expect, test } from '@jest/globals';
test('user resolver returns user', async () => {
const mockDb = {
findUserById: () => Promise.resolve({
id: '1',
name: 'Alice'
})
};
const result = await resolvers.Query.user(
null,
{ id: '1' },
{ db: mockDb }
);
expect(result.name).toBe('Alice');
});Integration Test Queries
import { graphql } from 'graphql';
test('executes user query', async () => {
const query = `
query {
user(id: "1") {
id
name
}
}
`;
const result = await graphql({
schema,
source: query,
contextValue: { db: mockDb }
});
expect(result.errors).toBeUndefined();
expect(result.data.user.name).toBe('Alice');
});Best Practices Checklist
- [ ] Use input types for all mutations
- [ ] Implement DataLoader for nested data
- [ ] Add pagination for unbounded lists
- [ ] Handle errors with detailed messages
- [ ] Validate inputs before database operations
- [ ] Use context for shared state (db, auth, loaders)
- [ ] Implement proper authentication/authorization
- [ ] Cache frequently accessed data
- [ ] Monitor query complexity and depth
- [ ] Version with @deprecated, never break existing queries
- [ ] Test resolvers and integration
- [ ] Document schema with descriptions
- [ ] Use non-null only for truly required fields
- [ ] Organize schema into logical modules
- [ ] Secure production with rate limiting
Example Use Cases
E-Commerce API
type Product {
id: ID!
name: String!
price: Float!
description: String
images: [String!]!
inStock: Boolean!
reviews: [Review!]!
}
type Cart {
id: ID!
items: [CartItem!]!
total: Float!
}
type Mutation {
addToCart(productId: ID!, quantity: Int!): Cart!
checkout(cartId: ID!, paymentMethod: String!): Order!
}Social Media API
type User {
id: ID!
username: String!
posts: [Post!]!
followers: [User!]!
following: [User!]!
}
type Post {
id: ID!
content: String!
author: User!
likes: Int!
comments: [Comment!]!
createdAt: String!
}
type Subscription {
newPost: Post!
newFollower: User!
}Blog API
type Post {
id: ID!
title: String!
content: String!
author: Author!
tags: [String!]!
publishedAt: String
draft: Boolean!
}
type Query {
post(id: ID!): Post
posts(tag: String, published: Boolean): [Post!]!
search(query: String!): [Post!]!
}Common Patterns
Relay Connection Pattern
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}Mutation Payload Pattern
type CreatePostPayload {
post: Post
errors: [Error!]
userErrors: [UserError!]
success: Boolean!
}Abstract Types Pattern
interface Node {
id: ID!
}
union SearchResult = User | Post | Comment
type Query {
node(id: ID!): Node
search(query: String!): [SearchResult!]!
}Next Steps
1. Read SKILL.md for comprehensive documentation 2. Check EXAMPLES.md for detailed code examples 3. Build a simple GraphQL API following the quick start 4. Implement DataLoader for performance 5. Add authentication and authorization 6. Set up subscriptions for real-time features 7. Write tests for your resolvers 8. Deploy to production with monitoring
Resources
- Official Docs: https://graphql.org/learn/
- GraphQL.js: https://github.com/graphql/graphql-js
- How to GraphQL: https://howtographql.com
- Apollo Docs: https://apollographql.com/docs/
- GraphQL Tools: https://graphql-tools.com
---
Version: 1.0.0 Author: Claude Code Skill License: MIT
GraphQL API Development
Build production-ready GraphQL APIs with Node.js, Express, and graphql-js. This comprehensive skill covers everything from basic schema design to advanced patterns like federation, subscriptions, and real-time updates.
Overview
GraphQL is a query language and runtime for APIs that gives clients precise control over the data they request. Unlike REST APIs with fixed endpoints, GraphQL allows clients to specify exactly what data they need in a single request, reducing over-fetching and under-fetching.
Key Features
Schema-First Development
- Strongly Typed: Every field has a defined type, enabling validation and tooling
- Self-Documenting: Schema serves as live documentation via introspection
- Contract-Based: Schema defines the contract between client and server
- IDE Support: Rich autocomplete and validation in development tools
Flexible Data Fetching
- Precise Queries: Clients request exactly what they need
- Single Request: Fetch multiple resources in one roundtrip
- Nested Data: Navigate relationships naturally
- No Versioning: Add new fields without breaking existing clients
Real-Time Capabilities
- Subscriptions: WebSocket-based real-time updates
- Live Queries: Automatically update when data changes
- Event-Driven: Publish-subscribe pattern for notifications
- Bi-directional: Server can push updates to clients
Developer Experience
- GraphiQL/Playground: Interactive API exploration
- Type Generation: Auto-generate TypeScript types from schema
- Error Handling: Rich error information with field-level precision
- Introspection: Query the schema itself for documentation
When to Use GraphQL
Ideal Use Cases
1. Complex Data Requirements
- Apps with many views requiring different data shapes
- Mobile apps with bandwidth constraints
- Multiple client types (web, mobile, desktop)
2. Rapid Iteration
- Frontend teams need flexibility without backend changes
- Frequent UI changes requiring different data
- A/B testing different features
3. Microservices Architecture
- Multiple data sources to aggregate
- Need unified API across services
- Service composition and federation
4. Real-Time Features
- Chat applications
- Live dashboards
- Collaborative editing
- Notifications and updates
5. Developer Productivity
- Type safety end-to-end
- Self-documenting API
- Strong tooling ecosystem
When REST Might Be Better
- Simple CRUD operations
- File uploads/downloads (requires multipart handling in GraphQL)
- HTTP caching is critical (GraphQL typically uses POST)
- Team unfamiliar with GraphQL (learning curve)
- Existing REST infrastructure works well
Core Concepts
The Type System
GraphQL's type system is the foundation of every GraphQL API:
# Scalar types
String # Text data
Int # 32-bit integer
Float # Floating point number
Boolean # true or false
ID # Unique identifier
# Object types
type User {
id: ID! # ! means non-null (required)
name: String!
email: String!
age: Int
posts: [Post!]! # List of posts (non-null list, non-null items)
}
# Input types (for mutations)
input CreateUserInput {
name: String!
email: String!
age: Int
}
# Enums
enum Role {
USER
ADMIN
MODERATOR
}
# Interfaces (shared fields)
interface Node {
id: ID!
createdAt: String!
}
# Unions (multiple possible types)
union SearchResult = User | Post | CommentSchema Structure
Every GraphQL schema has entry points:
# Read operations
type Query {
user(id: ID!): User
posts: [Post!]!
search(query: String!): [SearchResult!]!
}
# Write operations
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
# Real-time updates
type Subscription {
userCreated: User!
postPublished: Post!
messageReceived(channelId: ID!): Message!
}Resolvers
Resolvers are functions that fetch the data for each field:
const resolvers = {
Query: {
user: (parent, args, context) => {
return context.db.findUserById(args.id);
},
posts: (parent, args, context) => {
return context.db.getAllPosts();
}
},
User: {
posts: (user, args, context) => {
// Fetch posts for this specific user
return context.db.findPostsByAuthorId(user.id);
}
},
Mutation: {
createUser: (parent, { input }, context) => {
return context.db.createUser(input);
}
}
};Resolver signature: (parent, args, context, info) => result
- parent: Result from parent resolver
- args: Field arguments
- context: Shared state (database, user, etc.)
- info: Query metadata
Quick Start
1. Install Dependencies
npm install graphql express graphql-http2. Define Schema
import { buildSchema } from 'graphql';
const schema = buildSchema(`
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
}
`);3. Implement Resolvers
const users = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' }
];
const root = {
user: ({ id }) => users.find(u => u.id === id),
users: () => users,
createUser: ({ name, email }) => {
const user = {
id: String(users.length + 1),
name,
email
};
users.push(user);
return user;
}
};4. Create Server
import express from 'express';
import { createHandler } from 'graphql-http/lib/use/express';
const app = express();
app.all('/graphql', createHandler({
schema,
rootValue: root
}));
app.listen(4000, () => {
console.log('GraphQL server running at http://localhost:4000/graphql');
});5. Query Your API
# Fetch a user
query {
user(id: "1") {
id
name
email
}
}
# Create a user
mutation {
createUser(name: "Charlie", email: "charlie@example.com") {
id
name
}
}
# Fetch all users
query {
users {
id
name
}
}Schema Design Principles
1. Design from the Client Perspective
Think about what clients need, not what your database looks like:
# Good: Client-focused
type Product {
displayPrice: String! # "$19.99"
isAvailable: Boolean!
canShipToday: Boolean!
}
# Avoid: Database-focused
type Product {
price_cents: Int!
stock_quantity: Int!
warehouse_id: ID!
}2. Use Nullable Fields Carefully
Only mark fields as non-null (!) if they'll ALWAYS have a value:
type User {
id: ID! # ✓ Always has ID
name: String! # ✓ Required field
email: String! # ✓ Required for login
bio: String # ✗ Optional, might be null
avatar: String # ✗ Optional
}3. Embrace Relationships
GraphQL naturally handles relationships:
type User {
id: ID!
name: String!
posts: [Post!]! # User has many posts
followers: [User!]! # User has many followers
}
type Post {
id: ID!
title: String!
author: User! # Post belongs to user
comments: [Comment!]!
}4. Use Input Types for Mutations
Always use input types for complex mutations:
# Good
input CreatePostInput {
title: String!
content: String!
tags: [String!]
draft: Boolean
}
type Mutation {
createPost(input: CreatePostInput!): Post!
}
# Avoid
type Mutation {
createPost(
title: String!,
content: String!,
tags: [String!],
draft: Boolean
): Post!
}5. Plan for Pagination
Lists that can grow should be paginated:
type Query {
# Simple offset pagination
posts(offset: Int, limit: Int): PostConnection!
# Cursor-based (better for real-time data)
feed(first: Int, after: String): FeedConnection!
}
type PostConnection {
items: [Post!]!
total: Int!
hasMore: Boolean!
}6. Handle Errors Gracefully
Design mutations to return detailed error information:
type CreateUserPayload {
user: User
errors: [UserError!]
success: Boolean!
}
type UserError {
field: String
message: String!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}Performance Optimization
The N+1 Problem
Most common GraphQL performance issue:
// BAD: Causes N+1 queries
type User {
posts: [Post!]!
}
// Resolver runs once per user!
User: {
posts: (user) => db.query('SELECT * FROM posts WHERE author_id = ?', user.id)
}
// Querying 100 users = 1 query for users + 100 queries for posts = 101 total!Solution: DataLoader
import DataLoader from 'dataloader';
const postLoader = new DataLoader(async (userIds) => {
// One query for ALL users
const posts = await db.query(
'SELECT * FROM posts WHERE author_id IN (?)',
userIds
);
// Group by user ID
const postsByUser = {};
posts.forEach(post => {
if (!postsByUser[post.author_id]) {
postsByUser[post.author_id] = [];
}
postsByUser[post.author_id].push(post);
});
// Return in same order as input
return userIds.map(id => postsByUser[id] || []);
});
// Use in resolver
User: {
posts: (user, args, context) => context.loaders.post.load(user.id)
}
// 100 users = 1 query for users + 1 query for ALL posts = 2 total!Authentication and Authorization
Context-Based Auth
// Extract user from JWT token
const context = async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return { user: null };
}
const user = await verifyToken(token);
return { user };
};
// Use in resolver
Query: {
me: (parent, args, context) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return context.user;
}
}Field-Level Authorization
Post: {
draft: (post, args, context) => {
// Only author can see draft status
if (post.authorId !== context.user?.id) {
return null;
}
return post.draft;
}
}Real-Time with Subscriptions
Setup
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers = {
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
}
},
Mutation: {
createPost: async (parent, { input }) => {
const post = await db.createPost(input);
// Notify subscribers
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
}
}
};Client Usage
subscription {
postCreated {
id
title
author {
name
}
}
}Testing
Unit Test Resolvers
import { expect, test } from '@jest/globals';
test('user resolver returns user', async () => {
const mockDb = {
findUserById: () => Promise.resolve({
id: '1',
name: 'Alice'
})
};
const result = await resolvers.Query.user(
null,
{ id: '1' },
{ db: mockDb }
);
expect(result.name).toBe('Alice');
});Integration Test Queries
import { graphql } from 'graphql';
test('executes user query', async () => {
const query = `
query {
user(id: "1") {
id
name
}
}
`;
const result = await graphql({
schema,
source: query,
contextValue: { db: mockDb }
});
expect(result.errors).toBeUndefined();
expect(result.data.user.name).toBe('Alice');
});Best Practices Checklist
- [ ] Use input types for all mutations
- [ ] Implement DataLoader for nested data
- [ ] Add pagination for unbounded lists
- [ ] Handle errors with detailed messages
- [ ] Validate inputs before database operations
- [ ] Use context for shared state (db, auth, loaders)
- [ ] Implement proper authentication/authorization
- [ ] Cache frequently accessed data
- [ ] Monitor query complexity and depth
- [ ] Version with @deprecated, never break existing queries
- [ ] Test resolvers and integration
- [ ] Document schema with descriptions
- [ ] Use non-null only for truly required fields
- [ ] Organize schema into logical modules
- [ ] Secure production with rate limiting
Example Use Cases
E-Commerce API
type Product {
id: ID!
name: String!
price: Float!
description: String
images: [String!]!
inStock: Boolean!
reviews: [Review!]!
}
type Cart {
id: ID!
items: [CartItem!]!
total: Float!
}
type Mutation {
addToCart(productId: ID!, quantity: Int!): Cart!
checkout(cartId: ID!, paymentMethod: String!): Order!
}Social Media API
type User {
id: ID!
username: String!
posts: [Post!]!
followers: [User!]!
following: [User!]!
}
type Post {
id: ID!
content: String!
author: User!
likes: Int!
comments: [Comment!]!
createdAt: String!
}
type Subscription {
newPost: Post!
newFollower: User!
}Blog API
type Post {
id: ID!
title: String!
content: String!
author: Author!
tags: [String!]!
publishedAt: String
draft: Boolean!
}
type Query {
post(id: ID!): Post
posts(tag: String, published: Boolean): [Post!]!
search(query: String!): [Post!]!
}Common Patterns
Relay Connection Pattern
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}Mutation Payload Pattern
type CreatePostPayload {
post: Post
errors: [Error!]
userErrors: [UserError!]
success: Boolean!
}Abstract Types Pattern
interface Node {
id: ID!
}
union SearchResult = User | Post | Comment
type Query {
node(id: ID!): Node
search(query: String!): [SearchResult!]!
}Next Steps
1. Read SKILL.md for comprehensive documentation 2. Check EXAMPLES.md for detailed code examples 3. Build a simple GraphQL API following the quick start 4. Implement DataLoader for performance 5. Add authentication and authorization 6. Set up subscriptions for real-time features 7. Write tests for your resolvers 8. Deploy to production with monitoring
Resources
- Official Docs: https://graphql.org/learn/
- GraphQL.js: https://github.com/graphql/graphql-js
- How to GraphQL: https://howtographql.com
- Apollo Docs: https://apollographql.com/docs/
- GraphQL Tools: https://graphql-tools.com
---
Version: 1.0.0 Author: Claude Code Skill License: MIT