graphql-api-development

安装量: 37
排名: #18837

安装

npx skills add https://github.com/manutej/luxor-claude-marketplace --skill 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 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

返回排行榜