graphql-api-development

GraphQL API Development

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "graphql-api-development" with this command: npx skills add manutej/luxor-claude-marketplace/manutej-luxor-claude-marketplace-graphql-api-development

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

  • Precise Data Fetching: Clients request exactly what they need, no over/under-fetching

  • Single Request: Fetch multiple resources in one roundtrip instead of multiple REST endpoints

  • Strongly Typed: Schema defines exact types, enabling validation and tooling

  • Introspection: Self-documenting API with queryable schema

  • Versioning Not Required: Add new fields without breaking existing queries

  • Real-time Updates: Built-in subscription support for live data

  • Nested Resources: Naturally handle complex relationships without N+1 queries

  • Client-Driven: Clients control data shape, reducing backend changes

When to Stick with REST

  • Simple CRUD operations with standard resources

  • File uploads/downloads (GraphQL requires multipart handling)

  • HTTP caching is critical (GraphQL typically uses POST)

  • Team unfamiliar with GraphQL (learning curve)

  • Existing REST infrastructure works well

Core Concepts

The GraphQL Type System

GraphQL's type system is its foundation. Every GraphQL API defines:

  • Scalar Types: Basic data types (String, Int, Float, Boolean, ID)

  • Object Types: Complex types with fields

  • Query Type: Entry point for read operations

  • Mutation Type: Entry point for write operations

  • Subscription Type: Entry point for real-time updates

  • Input Types: Complex inputs for mutations

  • Enums: Fixed set of values

  • Interfaces: Abstract types that objects implement

  • Unions: Types that can be one of several types

  • Non-Null Types: Types that cannot be null

  • List Types: Arrays of types

Schema Definition

Two approaches for defining GraphQL schemas:

  1. Schema Definition Language (SDL) - Declarative, readable:

type User { id: ID! name: String! email: String! posts: [Post!]! }

type Post { id: ID! title: String! content: String author: User! }

type Query { user(id: ID!): User posts: [Post!]! }

  1. 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 resolver

  • args : Arguments passed to the field

  • context : 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 queries

DataLoader Solution

DataLoader batches and caches requests:

import DataLoader from 'dataloader';

// Create DataLoader const postLoader = new DataLoader(async (userIds) => { // Single query for all user IDs const posts = await db.getPostsByUserIds(userIds);

// Group posts by userId const postsByUserId = {}; posts.forEach(post => { if (!postsByUserId[post.authorId]) { postsByUserId[post.authorId] = []; } postsByUserId[post.authorId].push(post); });

// Return in same order as userIds return userIds.map(id => postsByUserId[id] || []); });

// Use in resolver const UserType = new GraphQLObjectType({ name: 'User', fields: { posts: { type: new GraphQLList(PostType), resolve: (user, args, context) => { return context.loaders.postLoader.load(user.id); } } } });

// Add to context const context = { loaders: { postLoader: new DataLoader(batchLoadPosts) } };

Query Complexity Analysis

Limit expensive queries:

import { getComplexity, simpleEstimator } from 'graphql-query-complexity';

const complexity = getComplexity({ schema, query, estimators: [ simpleEstimator({ defaultComplexity: 1 }) ] });

if (complexity > 1000) { throw new Error('Query too complex'); }

Depth Limiting

Prevent deeply nested queries:

import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({ schema, validationRules: [depthLimit(5)] });

Mutations and Input Validation

Mutation Design Pattern

input CreatePostInput { title: String! content: String! authorId: ID! tags: [String!] }

type CreatePostPayload { post: Post errors: [UserError!] success: Boolean! }

type UserError { message: String! field: String }

type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! }

Input Validation

const Mutation = new GraphQLObjectType({ name: 'Mutation', fields: { createPost: { type: CreatePostPayload, args: { input: { type: new GraphQLNonNull(CreatePostInput) } }, resolve: async (_, { input }, context) => { // Validate input const errors = [];

    if (input.title.length < 3) {
      errors.push({
        field: 'title',
        message: 'Title must be at least 3 characters'
      });
    }

    if (input.content.length < 10) {
      errors.push({
        field: 'content',
        message: 'Content must be at least 10 characters'
      });
    }

    if (errors.length > 0) {
      return { errors, success: false, post: null };
    }

    // Create post
    const post = await context.db.createPost(input);
    return { post, errors: [], success: true };
  }
}

} });

Subscriptions and Real-time Updates

Setting Up Subscriptions

import { GraphQLObjectType, GraphQLString } from 'graphql'; import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

const Subscription = new GraphQLObjectType({ name: 'Subscription', fields: { postCreated: { type: PostType, subscribe: () => pubsub.asyncIterator(['POST_CREATED']) }, messageReceived: { type: MessageType, args: { channelId: { type: new GraphQLNonNull(GraphQLID) } }, subscribe: (_, { channelId }) => { return pubsub.asyncIterator([MESSAGE_${channelId}]); } } } });

Publishing Events

const Mutation = new GraphQLObjectType({ name: 'Mutation', fields: { createPost: { type: PostType, args: { input: { type: new GraphQLNonNull(CreatePostInput) } }, resolve: async (_, { input }, context) => { const post = await context.db.createPost(input);

    // Publish to subscribers
    pubsub.publish('POST_CREATED', { postCreated: post });

    return post;
  }
}

} });

WebSocket Server Setup

import { createServer } from 'http'; import { WebSocketServer } from 'ws'; import { useServer } from 'graphql-ws/lib/use/ws'; import { execute, subscribe } from 'graphql'; import express from 'express';

const app = express(); const httpServer = createServer(app);

// WebSocket server for subscriptions const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' });

useServer( { schema, execute, subscribe, context: (ctx) => { // Access connection params, headers return { userId: ctx.connectionParams?.userId, db: database }; } }, wsServer );

httpServer.listen(4000);

Authentication and Authorization

Context-Based Authentication

import jwt from 'jsonwebtoken';

// Middleware to extract user const authMiddleware = async (req) => { const token = req.headers.authorization?.replace('Bearer ', '');

if (!token) { return { user: null }; }

try { const decoded = jwt.verify(token, process.env.JWT_SECRET); const user = await db.findUserById(decoded.userId); return { user }; } catch (error) { return { user: null }; } };

// Add to GraphQL context app.all('/graphql', async (req, res) => { const auth = await authMiddleware(req);

createHandler({ schema, context: { user: auth.user, db: database } })(req, res); });

Resolver-Level Authorization

const Query = new GraphQLObjectType({ name: 'Query', fields: { me: { type: UserType, resolve: (, __, context) => { if (!context.user) { throw new Error('Authentication required'); } return context.user; } }, adminData: { type: GraphQLString, resolve: (, __, context) => { if (!context.user) { throw new Error('Authentication required'); }

    if (context.user.role !== 'admin') {
      throw new Error('Admin access required');
    }

    return 'Secret admin data';
  }
}

} });

Field-Level Authorization

const PostType = new GraphQLObjectType({ name: 'Post', fields: { title: { type: GraphQLString }, content: { type: GraphQLString }, draft: { type: GraphQLBoolean, resolve: (post, args, context) => { // Only author can see draft status if (post.authorId !== context.user?.id) { return null; } return post.draft; } } } });

Directive-Based Authorization

directive @auth(requires: Role = USER) on FIELD_DEFINITION

enum Role { USER ADMIN MODERATOR }

type Query { publicData: String userData: String @auth(requires: USER) adminData: String @auth(requires: ADMIN) }

import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';

function authDirective(schema, directiveName) { return mapSchema(schema, { [MapperKind.OBJECT_FIELD]: (fieldConfig) => { const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];

  if (authDirective) {
    const { requires } = authDirective;
    const { resolve = defaultFieldResolver } = fieldConfig;

    fieldConfig.resolve = async (source, args, context, info) => {
      if (!context.user) {
        throw new Error('Authentication required');
      }

      if (context.user.role !== requires) {
        throw new Error(`${requires} role required`);
      }

      return resolve(source, args, context, info);
    };
  }

  return fieldConfig;
}

}); }

Caching Strategies

In-Memory Caching

import { LRUCache } from 'lru-cache';

const cache = new LRUCache({ max: 500, ttl: 1000 * 60 * 5 // 5 minutes });

const Query = new GraphQLObjectType({ name: 'Query', fields: { product: { type: ProductType, args: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolve: async (_, { id }, context) => { const cacheKey = product:${id}; const cached = cache.get(cacheKey);

    if (cached) {
      return cached;
    }

    const product = await context.db.findProductById(id);
    cache.set(cacheKey, product);
    return product;
  }
}

} });

Redis Caching

import Redis from 'ioredis';

const redis = new Redis({ host: process.env.REDIS_HOST, port: process.env.REDIS_PORT });

const Query = new GraphQLObjectType({ name: 'Query', fields: { user: { type: UserType, args: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolve: async (_, { id }, context) => { const cacheKey = user:${id};

    // Check cache
    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    // Fetch from database
    const user = await context.db.findUserById(id);

    // Cache for 10 minutes
    await redis.setex(cacheKey, 600, JSON.stringify(user));

    return user;
  }
}

} });

Cache Invalidation

const Mutation = new GraphQLObjectType({ name: 'Mutation', fields: { updateUser: { type: UserType, args: { id: { type: new GraphQLNonNull(GraphQLID) }, input: { type: new GraphQLNonNull(UpdateUserInput) } }, resolve: async (_, { id, input }, context) => { const user = await context.db.updateUser(id, input);

    // Invalidate cache
    const cacheKey = `user:${id}`;
    await redis.del(cacheKey);

    // Also invalidate list caches
    await redis.del('users:all');

    return user;
  }
}

} });

Error Handling

Custom Error Classes

class AuthenticationError extends Error { constructor(message) { super(message); this.name = 'AuthenticationError'; this.extensions = { code: 'UNAUTHENTICATED' }; } }

class ForbiddenError extends Error { constructor(message) { super(message); this.name = 'ForbiddenError'; this.extensions = { code: 'FORBIDDEN' }; } }

class ValidationError extends Error { constructor(message, fields) { super(message); this.name = 'ValidationError'; this.extensions = { code: 'BAD_USER_INPUT', fields }; } }

Error Formatting

import { formatError } from 'graphql';

const customFormatError = (error) => { // Log error for monitoring console.error('GraphQL Error:', { message: error.message, locations: error.locations, path: error.path, extensions: error.extensions });

// Don't expose internal errors to clients if (error.message.startsWith('Database')) { return { message: 'Internal server error', extensions: { code: 'INTERNAL_SERVER_ERROR' } }; }

return formatError(error); };

const server = new ApolloServer({ schema, formatError: customFormatError });

Graceful Error Responses

const Query = new GraphQLObjectType({ name: 'Query', fields: { user: { type: UserType, args: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolve: async (_, { id }, context) => { try { const user = await context.db.findUserById(id);

      if (!user) {
        throw new Error(`User with ID ${id} not found`);
      }

      return user;
    } catch (error) {
      // Log error
      console.error('Error fetching user:', error);

      // Re-throw with user-friendly message
      if (error.code === 'ECONNREFUSED') {
        throw new Error('Unable to connect to database');
      }

      throw error;
    }
  }
}

} });

Testing GraphQL APIs

Unit Testing Resolvers

import { describe, it, expect, jest } from '@jest/globals';

describe('User resolver', () => { it('returns user by ID', async () => { const mockDb = { findUserById: jest.fn().mockResolvedValue({ id: '1', name: 'Alice', email: 'alice@example.com' }) };

const context = { db: mockDb };
const result = await userResolver.resolve(null, { id: '1' }, context);

expect(mockDb.findUserById).toHaveBeenCalledWith('1');
expect(result).toEqual({
  id: '1',
  name: 'Alice',
  email: 'alice@example.com'
});

});

it('throws error for non-existent user', async () => { const mockDb = { findUserById: jest.fn().mockResolvedValue(null) };

const context = { db: mockDb };

await expect(
  userResolver.resolve(null, { id: '999' }, context)
).rejects.toThrow('User with ID 999 not found');

}); });

Integration Testing

import { graphql } from 'graphql'; import { schema } from './schema';

describe('GraphQL Schema', () => { it('executes user query', async () => { const query = query { user(id: "1") { id name email } } ;

const result = await graphql({
  schema,
  source: query,
  contextValue: {
    db: mockDatabase,
    user: null
  }
});

expect(result.errors).toBeUndefined();
expect(result.data?.user).toEqual({
  id: '1',
  name: 'Alice',
  email: 'alice@example.com'
});

});

it('handles authentication errors', async () => { const query = query { me { id name } } ;

const result = await graphql({
  schema,
  source: query,
  contextValue: {
    db: mockDatabase,
    user: null
  }
});

expect(result.errors).toBeDefined();
expect(result.errors[0].message).toBe('Authentication required');

}); });

Testing with Apollo Server

import { ApolloServer } from '@apollo/server';

const testServer = new ApolloServer({ schema, });

describe('User queries', () => { it('fetches user successfully', async () => { const response = await testServer.executeOperation({ query: query GetUser($id: ID!) { user(id: $id) { id name } } , variables: { id: '1' } });

expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data?.user).toMatchObject({
  id: '1',
  name: expect.any(String)
});

}); });

Production Best Practices

Schema Organization

src/ ├── schema/ │ ├── index.js # Combine all types │ ├── types/ │ │ ├── user.js # User type and resolvers │ │ ├── post.js # Post type and resolvers │ │ └── comment.js # Comment type and resolvers │ ├── queries/ │ │ ├── user.js # User queries │ │ └── post.js # Post queries │ ├── mutations/ │ │ ├── user.js # User mutations │ │ └── post.js # Post mutations │ └── subscriptions/ │ └── post.js # Post subscriptions ├── directives/ │ └── auth.js # Authorization directive ├── utils/ │ ├── loaders.js # DataLoader instances │ └── context.js # Context builder └── server.js # Server setup

Monitoring and Logging

import { ApolloServerPluginLandingPageGraphQLPlayground } from '@apollo/server-plugin-landing-page-graphql-playground';

const server = new ApolloServer({ schema, plugins: [ // Request logging { async requestDidStart(requestContext) { console.log('Request started:', requestContext.request.query);

    return {
      async didEncounterErrors(ctx) {
        console.error('Errors:', ctx.errors);
      },
      async willSendResponse(ctx) {
        console.log('Response sent');
      }
    };
  }
},

// Performance monitoring
{
  async requestDidStart() {
    const start = Date.now();

    return {
      async willSendResponse() {
        const duration = Date.now() - start;
        console.log(`Request duration: ${duration}ms`);
      }
    };
  }
}

] });

Rate Limiting

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per window message: 'Too many requests, please try again later' });

app.use('/graphql', limiter);

Query Whitelisting

const allowedQueries = new Set([ 'query GetUser { user(id: $id) { id name email } }', 'mutation CreatePost { createPost(input: $input) { id title } }' ]);

const validateQuery = (query) => { const normalized = query.replace(/\s+/g, ' ').trim(); if (!allowedQueries.has(normalized)) { throw new Error('Query not whitelisted'); } };

Security Headers

import helmet from 'helmet';

app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], } }, crossOriginEmbedderPolicy: false }));

Advanced Patterns

Federation (Microservices)

import { buildSubgraphSchema } from '@apollo/subgraph';

// Users service const userSchema = buildSubgraphSchema({ typeDefs: type User @key(fields: "id") { id: ID! name: String! email: String! } , resolvers: { User: { __resolveReference(user) { return findUserById(user.id); } } } });

// Posts service const postSchema = buildSubgraphSchema({ typeDefs: ` type Post { id: ID! title: String! author: User! }

extend type User @key(fields: "id") {
  id: ID! @external
  posts: [Post!]!
}

`, resolvers: { Post: { author(post) { return { __typename: 'User', id: post.authorId }; } }, User: { posts(user) { return findPostsByAuthorId(user.id); } } } });

Custom Scalars

import { GraphQLScalarType, Kind } from 'graphql';

const DateTimeScalar = new GraphQLScalarType({ name: 'DateTime', description: 'ISO-8601 DateTime string',

serialize(value) { // Send to client return value instanceof Date ? value.toISOString() : null; },

parseValue(value) { // From variables return new Date(value); },

parseLiteral(ast) { // From query string if (ast.kind === Kind.STRING) { return new Date(ast.value); } return null; } });

// Use in schema const schema = new GraphQLSchema({ types: [DateTimeScalar], query: new GraphQLObjectType({ name: 'Query', fields: { now: { type: DateTimeScalar, resolve: () => new Date() } } }) });

Batch Operations

const Mutation = new GraphQLObjectType({ name: 'Mutation', fields: { batchCreateUsers: { type: new GraphQLList(UserType), args: { inputs: { type: new GraphQLNonNull( new GraphQLList(new GraphQLNonNull(CreateUserInput)) ) } }, resolve: async (_, { inputs }, context) => { const users = await Promise.all( inputs.map(input => context.db.createUser(input)) ); return users; } } } });

Common Patterns Summary

  • Use Input Types: For all mutations with multiple arguments

  • Implement DataLoader: Solve N+1 queries for nested data

  • Add Pagination: For list fields that can grow unbounded

  • Handle Errors Gracefully: Return user-friendly error messages

  • Validate Inputs: At resolver level before database operations

  • Use Context for Shared State: Database, authentication, loaders

  • Implement Authorization: At resolver or directive level

  • Cache Aggressively: Use Redis or in-memory for frequently accessed data

  • Monitor Performance: Track query complexity and execution time

  • Version with @deprecated: Never break existing queries

  • Test Thoroughly: Unit test resolvers, integration test queries

  • Document Schema: Use descriptions in SDL

  • Use Non-Null Wisely: Only for truly required fields

  • Organize Schema: Split into modules by domain

  • Secure Production: Rate limiting, query whitelisting, depth limiting

Resources and Tools

Essential Libraries

  • graphql-js: Core GraphQL implementation

  • express: Web server framework

  • graphql-http: HTTP handler for GraphQL

  • dataloader: Batching and caching

  • graphql-ws: WebSocket server for subscriptions

  • graphql-scalars: Common custom scalars

  • graphql-tools: Schema manipulation utilities

Development Tools

  • GraphiQL: In-browser GraphQL IDE

  • GraphQL Playground: Advanced GraphQL IDE

  • Apollo Studio: Schema registry and monitoring

  • GraphQL Code Generator: Generate TypeScript types

  • eslint-plugin-graphql: Lint GraphQL queries

Learning Resources

Skill Version: 1.0.0 Last Updated: October 2025 Skill Category: API Development, Backend, GraphQL, Web Development Compatible With: Node.js, Express, TypeScript, JavaScript

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

golang-backend-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

fastapi-microservices-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

nodejs-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

fastapi-development

No summary provided by upstream source.

Repository SourceNeeds Review