# GRAPHQL API Design

> Use when designing GraphQL APIs with schema-first or code-first approaches, implementing resolvers, DataLoader patterns, subscriptions, Apollo Federation, error handling, authentication, authorization, pagination, and production best practices. Includes SDL patterns, Relay spec, and performance optimization.

- Skill: `tomevault-io/graphql-api-design-3` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/graphql-api-design-3`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/graphql-api-design-3/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/graphql-api-design-3

---


# GraphQL API Design Skill — GraphQL Best Practices

## Overview

This skill provides comprehensive guidance for **designing and implementing GraphQL APIs**, including schema design with SDL, resolver implementation, DataLoader patterns for N+1 prevention, subscriptions, Apollo Federation, error handling, authentication, authorization, pagination, and production best practices. Based on GraphQL specification, Apollo Server documentation, and production best practices.

## When to Use

**Use this skill when:**

- Designing GraphQL schemas with SDL (Schema Definition Language)
- Implementing GraphQL type definitions (Object, Input, Interface, Union, Enum, Scalar)
- Creating GraphQL queries and mutations
- Setting up GraphQL subscriptions with WebSocket or Server-Sent Events
- Implementing resolvers (object, field, class-based)
- Preventing N+1 queries with DataLoader pattern
- Handling GraphQL errors with GraphQLError and formatted errors
- Validating input with custom directives and schema directives
- Authenticating requests with JWT and context
- Authorizing access with directives and middleware
- Versioning GraphQL schemas (URL, header, schema evolution)
- Implementing pagination with Relay spec and cursor-based approaches
- Adding filtering with input types and arguments
- Implementing sorting with enum and input types
- Building search with full-text and fuzzy matching
- Batching requests with Apollo Link Batch and schema directives
- Caching with HTTP cache and Redis integration
- Optimizing performance with persisted queries and response caching
- Analyzing query complexity with graphql-query-complexity
- Limiting query depth with graphql-depth-limit
- Configuring introspection for production environments
- Choosing between SDL-first and code-first approaches
- Implementing Apollo Federation for microservices
- Using Apollo Schema Stitching for legacy API integration
- Configuring Apollo Server, Express GraphQL, or GraphQL Yoga
- Setting up GraphQL Mesh for REST/GraphQL federation
- Establishing production best practices and monitoring

**Do NOT use this skill when:**

- Designing RESTful APIs (use api-design skill for REST endpoint patterns)
- Building database schemas (use database-design skill for table structures)
- Implementing frontend GraphQL clients (use frontend framework skills)
- Setting up CI/CD pipelines (use devops-pipeline skill)
- Containerizing GraphQL servers (use docker-containerization skill)
- Deploying to cloud infrastructure (use terraform-iac or kubernetes-orchestration skill)
- Writing application business logic (use domain-driven skill)
- Performing security audits (use security-auditor skill)

**Why avoid:** GraphQL is specifically for API query language and runtime. Use REST for simple CRUD, databases for data storage, and the right tools for infrastructure and deployment.

## Core Concepts

### Schema Design Approaches

| Approach               | Best For                               | Tools                               |
| ---------------------- | -------------------------------------- | ----------------------------------- |
| **Schema-First (SDL)** | Team collaboration, API contracts      | GraphQL Code Generator, graphql-tag |
| **Code-First**         | Rapid prototyping, TypeScript projects | type-graphql, Nexus, GraphQL Tools  |
| **Federation**         | Microservices, distributed teams       | Apollo Gateway, Apollo Router       |
| **Schema Stitching**   | Legacy integration, gradual migration  | Apollo Schema Stitching             |

### Type System Overview

| Type          | Purpose                     | Example                                   |
| ------------- | --------------------------- | ----------------------------------------- |
| **Object**    | Complex data structures     | `type User { id: ID! name: String! }`     |
| **Input**     | Mutation/Query input shapes | `input CreateUserInput { name: String! }` |
| **Interface** | Shared fields across types  | `interface Node { id: ID! }`              |
| **Union**     | One of multiple types       | `union SearchResult = User \| Post`       |
| **Enum**      | Fixed set of values         | `enum Role { ADMIN USER }`                |
| **Scalar**    | Custom primitive types      | `scalar DateTime`, `scalar JSON`          |

### Server Framework Comparison

| Framework           | Language | Key Features                                     |
| ------------------- | -------- | ------------------------------------------------ |
| **Apollo Server**   | Node.js  | Federation, caching, tracing, production-ready   |
| **GraphQL Yoga**    | Node.js  | Built-in subscriptions, PWA support, lightweight |
| **Express GraphQL** | Node.js  | Minimal, Express middleware integration          |
| **GraphQL Mesh**    | Node.js  | REST/GraphQL federation, multi-source            |
| **Strawberry**      | Python   | Python-native, async support, type inference     |
| **Graphene**        | Python   | Django/Flask integration, schema-first           |

## Schema Design with SDL

### Complete Schema Example

```graphql
# ============================================
# Scalar Types
# ============================================
scalar DateTime
scalar JSON
scalar UUID
scalar URL
scalar Email

# ============================================
# Enum Types
# ============================================
enum Role {
  ADMIN
  MODERATOR
  USER
  GUEST
}

enum Status {
  ACTIVE
  INACTIVE
  PENDING
  SUSPENDED
}

enum SortOrder {
  ASC
  DESC
}

# ============================================
# Interface Types
# ============================================
interface Node {
  id: ID!
}

interface Timestamps {
  createdAt: DateTime!
  updatedAt: DateTime!
}

# ============================================
# Union Types
# ============================================
union SearchResult = User | Post | Comment

union AuthenticationResult = LoginSuccess | LoginError

# ============================================
# Input Types
# ============================================
input CreateUserInput {
  name: String!
  email: Email!
  password: String!
  role: Role = USER
}

input UpdateUserInput {
  name: String
  email: Email
  role: Role
}

input UserFilterInput {
  status: Status
  role: Role
  search: String
  createdAtFrom: DateTime
  createdAtTo: DateTime
}

input UserSortInput {
  field: UserSortField = createdAt
  order: SortOrder = DESC
}

enum UserSortField {
  name
  email
  createdAt
  updatedAt
}

# ============================================
# Object Types
# ============================================
type User implements Node & Timestamps {
  id: ID!
  name: String!
  email: Email!
  role: Role!
  status: Status!
  posts: [Post!]!
  comments: [Comment!]!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Post implements Node & Timestamps {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
  status: Status!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Comment implements Node & Timestamps {
  id: ID!
  text: String!
  author: User!
  post: Post!
  createdAt: DateTime!
  updatedAt: DateTime!
}

# ============================================
# Pagination Types (Relay Spec)
# ============================================
type UserEdge {
  node: User!
  cursor: String!
}

type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

# ============================================
# Authentication Types
# ============================================
type LoginSuccess {
  token: String!
  user: User!
}

type LoginError {
  message: String!
  code: String!
}

input LoginInput {
  email: Email!
  password: String!
}

# ============================================
# Query Type
# ============================================
type Query {
  user(id: ID!): User
  post(id: ID!): Post

  users(
    first: Int = 20
    after: String
    filter: UserFilterInput
    sort: UserSortInput
  ): UserConnection!

  search(query: String!): [SearchResult!]!
  health: HealthStatus!
}

type HealthStatus {
  status: String!
  version: String!
  timestamp: DateTime!
}

# ============================================
# Mutation Type
# ============================================
type Mutation {
  login(input: LoginInput!): AuthenticationResult!
  logout: Boolean!

  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!

  createPost(input: CreatePostInput!): Post!
  updatePost(id: ID!, input: UpdatePostInput!): Post!
  deletePost(id: ID!): Boolean!
}

# ============================================
# Subscription Type
# ============================================
type Subscription {
  userCreated: User!
  userUpdated(id: ID!): User!
  postCreated(categoryId: ID): Post!
  commentAdded(postId: ID!): Comment!
}
```

## Resolver Implementation

### Object Resolvers

```typescript
import { GraphQLResolveInfo, GraphQLScalarType } from 'graphql';
import { GraphQLError } from 'graphql/error';
import { DateTimeResolver } from 'graphql-scalars';
import { User, Post, Comment } from '../types';
import { UserRole, UserStatus } from '../enums';
import { prisma } from '../db';
import { requireAuth, requireRole } from '../middleware';

// Custom Scalar Resolvers
const DateTime: GraphQLScalarType = new DateTimeResolver();

// Object Type Resolvers
const UserResolvers = {
  posts: (user: User) => prisma.post.findMany({ where: { authorId: user.id } }),
  comments: (user: User) =>
    prisma.comment.findMany({ where: { authorId: user.id } }),
  fullName: (user: User) => `${user.firstName} ${user.lastName}`,
  ssn: (user: User, args, context) => {
    requireAuth(context);
    requireRole(context, UserRole.ADMIN);
    return user.ssn;
  },
};

const PostResolvers = {
  author: (post: Post) =>
    prisma.user.findUnique({ where: { id: post.authorId } }),
  comments: (post: Post) =>
    prisma.comment.findMany({ where: { postId: post.id } }),
  wordCount: (post: Post) => post.content.split(/\s+/).length,
  excerpt: (post: Post) => post.content.substring(0, 200) + '...',
};

// Root Resolvers (Query, Mutation, Subscription)
const resolvers = {
  Query: {
    user: (_, { id }, context) => {
      requireAuth(context);
      return prisma.user.findUnique({ where: { id } });
    },

    users: (_, { first = 20, after, filter, sort }, context) => {
      requireAuth(context);

      const where: any = {};
      if (filter?.status) where.status = filter.status;
      if (filter?.role) where.role = filter.role;
      if (filter?.search) {
        where.OR = [
          { name: { contains: filter.search } },
          { email: { contains: filter.search } },
        ];
      }

      const orderBy: any = {};
      const sortField = sort?.field || 'createdAt';
      const sortOrder = sort?.order || 'DESC';
      orderBy[sortField] = sortOrder;

      const cursor = after ? { id: after } : undefined;
      const take = first + 1;

      const users = await prisma.user.findMany({
        where,
        orderBy,
        cursor,
        take,
        skip: after ? 1 : 0,
      });

      const hasNextPage = users.length > first;
      const data = hasNextPage ? users.slice(0, -1) : users;

      return {
        edges: data.map((user) => ({ node: user, cursor: user.id })),
        pageInfo: {
          hasNextPage,
          hasPreviousPage: !!after,
          startCursor: data.length > 0 ? data[0].id : null,
          endCursor: data.length > 0 ? data[data.length - 1].id : null,
        },
        totalCount: await prisma.user.count({ where }),
      };
    },

    search: async (_, { query }, context) => {
      requireAuth(context);

      const [users, posts, comments] = await Promise.all([
        prisma.user.findMany({
          where: {
            OR: [{ name: { contains: query } }, { email: { contains: query } }],
          },
          take: 10,
        }),
        prisma.post.findMany({
          where: {
            OR: [
              { title: { contains: query } },
              { content: { contains: query } },
            ],
          },
          take: 10,
        }),
        prisma.comment.findMany({
          where: { text: { contains: query } },
          take: 10,
        }),
      ]);

      return [...users, ...posts, ...comments];
    },

    health: () => ({
      status: 'ok',
      version: process.env.APP_VERSION || '1.0.0',
      timestamp: new Date().toISOString(),
    }),
  },

  Mutation: {
    login: async (_, { input }) => {
      const user = await prisma.user.findUnique({
        where: { email: input.email },
      });

      if (!user || !(await bcrypt.compare(input.password, user.password))) {
        return {
          __typename: 'LoginError',
          message: 'Invalid credentials',
          code: 'AUTH_FAILED',
        };
      }

      const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
      return { __typename: 'LoginSuccess', token, user };
    },

    createUser: (_, { input }, context) => {
      requireAuth(context);
      requireRole(context, UserRole.ADMIN);
      const hashedPassword = await bcrypt.hash(input.password, 10);
      return prisma.user.create({
        data: { ...input, password: hashedPassword },
      });
    },

    updateUser: async (_, { id, input }, context) => {
      requireAuth(context);
      if (context.userId !== id && context.role !== UserRole.ADMIN) {
        throw new GraphQLError('Unauthorized', {
          extensions: { code: 'FORBIDDEN' },
        });
      }
      return prisma.user.update({ where: { id }, data: input });
    },

    deleteUser: async (_, { id }, context) => {
      requireAuth(context);
      requireRole(context, UserRole.ADMIN);
      await prisma.user.delete({ where: { id } });
      return true;
    },
  },

  Subscription: {
    userCreated: {
      subscribe: (_, __, context) =>
        context.pubsub.asyncIterator(['USER_CREATED']),
    },
    userUpdated: {
      subscribe: (_, { id }, context) =>
        context.pubsub.asyncIterator([`USER_UPDATED:${id}`]),
    },
    postCreated: {
      subscribe: (_, { categoryId }, context) => {
        const events = categoryId
          ? [`POST_CREATED:${categoryId}`]
          : ['POST_CREATED'];
        return context.pubsub.asyncIterator(events);
      },
    },
    commentAdded: {
      subscribe: (_, { postId }, context) =>
        context.pubsub.asyncIterator([`COMMENT_ADDED:${postId}`]),
    },
  },

  User: UserResolvers,
  Post: PostResolvers,

  AuthenticationResult: {
    __resolveType(obj) {
      if (obj.token) return 'LoginSuccess';
      return 'LoginError';
    },
  },

  SearchResult: {
    __resolveType(obj) {
      if (obj.email) return 'User';
      if (obj.title) return 'Post';
      return 'Comment';
    },
  },

  DateTime,
};

export default resolvers;
```

## DataLoader Pattern (N+1 Prevention)

### DataLoader Implementation

```typescript
import DataLoader from 'dataloader';
import { prisma } from '../db';

// User DataLoader
const createUserLoader = () =>
  new DataLoader(async (userIds: readonly string[]) => {
    const users = await prisma.user.findMany({
      where: { id: { in: Array.from(userIds) } },
    });
    return userIds.map((id) => users.find((u) => u.id === id) || null);
  });

// Post DataLoader
const createPostLoader = () =>
  new DataLoader(async (postIds: readonly string[]) => {
    const posts = await prisma.post.findMany({
      where: { id: { in: Array.from(postIds) } },
    });
    return postIds.map((id) => posts.find((p) => p.id === id) || null);
  });

// Comment count DataLoader (with aggregation)
const createCommentLoader = () =>
  new DataLoader(async (postIds: readonly string[]) => {
    const comments = await prisma.comment.groupBy({
      by: ['postId'],
      _count: { _all: true },
      where: { postId: { in: Array.from(postIds) } },
    });
    return postIds.map(
      (postId) => comments.find((c) => c.postId === postId)?._count._all || 0
    );
  });

// Context setup (new DataLoader instances per request)
const createContext = (req: Request) => ({
  prisma,
  pubsub,
  userId: getUserIdFromToken(req),
  role: getRoleFromToken(req),
  userLoader: createUserLoader(),
  postLoader: createPostLoader(),
  commentLoader: createCommentLoader(),
});

// Resolver using DataLoader
const resolvers = {
  Post: {
    // ✅ GOOD: Uses DataLoader (1 query for N posts)
    author: (post, __, context) => context.userLoader.load(post.authorId),
    commentCount: (post, __, context) => context.commentLoader.load(post.id),

    // ❌ BAD: Causes N+1 queries (1 query per post)
    // author: (post) => prisma.user.findUnique({ where: { id: post.authorId } })
  },

  Comment: {
    author: (comment, __, context) => context.userLoader.load(comment.authorId),
    post: (comment, __, context) => context.postLoader.load(comment.postId),
  },
};
```

### DataLoader Best Practices

| Practice                    | Description                                                       |
| --------------------------- | ----------------------------------------------------------------- |
| **Per-request instances**   | Create new DataLoader per request to avoid caching stale data     |
| **Batch factory functions** | Use factory functions to create DataLoaders with fresh state      |
| **Cache key consistency**   | Ensure batch function returns results in same order as input keys |
| **Null handling**           | Return null for missing keys, not undefined                       |
| **Error handling**          | Use DataLoader's built-in error handling or maxBatchSize          |

## Error Handling

### Custom Error Classes

```typescript
import { GraphQLError, GraphQLFormattedError } from 'graphql';

class UserInputError extends GraphQLError {
  constructor(message: string, properties?: Record<string, unknown>) {
    super(message, {
      extensions: {
        code: 'BAD_USER_INPUT',
        timestamp: new Date().toISOString(),
        ...properties,
      },
    });
  }
}

class AuthenticationError extends GraphQLError {
  constructor(message: string = 'Authentication required') {
    super(message, {
      extensions: {
        code: 'UNAUTHENTICATED',
        timestamp: new Date().toISOString(),
      },
    });
  }
}

class ForbiddenError extends GraphQLError {
  constructor(message: string = 'Insufficient permissions') {
    super(message, {
      extensions: { code: 'FORBIDDEN', timestamp: new Date().toISOString() },
    });
  }
}

class NotFoundError extends GraphQLError {
  constructor(message: string, properties?: Record<string, unknown>) {
    super(message, {
      extensions: {
        code: 'NOT_FOUND',
        timestamp: new Date().toISOString(),
        ...properties,
      },
    });
  }
}

// Format errors for production
const formatError = (error: GraphQLFormattedError): GraphQLFormattedError => {
  const { code, timestamp, ...extensions } = error.extensions || {};

  if (
    process.env.NODE_ENV === 'production' &&
    code === 'INTERNAL_SERVER_ERROR'
  ) {
    return {
      ...error,
      message: 'Internal server error',
      extensions: { code, timestamp },
    };
  }

  return { ...error, extensions: { code, timestamp, ...extensions } };
};

// Usage in resolvers
const resolvers = {
  Query: {
    user: (_, { id }, context) => {
      if (!context.userId) throw new AuthenticationError();

      const user = await prisma.user.findUnique({ where: { id } });
      if (!user)
        throw new NotFoundError(`User with id ${id} not found`, { id });

      return user;
    },

    updateUser: async (_, { id, input }, context) => {
      if (!context.userId) throw new AuthenticationError();

      if (input.email && !isValidEmail(input.email)) {
        throw new UserInputError('Invalid email format', { field: 'email' });
      }

      if (context.userId !== id && context.role !== UserRole.ADMIN) {
        throw new ForbiddenError('Cannot update other users');
      }

      return prisma.user.update({ where: { id }, data: input });
    },
  },
};
```

## Authentication and Authorization

### JWT Authentication with Context

```typescript
import jwt from 'jsonwebtoken';

interface Context {
  userId?: string;
  user?: User;
  role?: UserRole;
  prisma: PrismaClient;
  pubsub: PubSub;
  userLoader: DataLoader<string, User>;
  postLoader: DataLoader<string, Post>;
}

// JWT Verification
const verifyToken = (token: string): { userId: string; role: UserRole } => {
  try {
    return jwt.verify(token, process.env.JWT_SECRET) as {
      userId: string;
      role: UserRole;
    };
  } catch {
    throw new AuthenticationError('Invalid or expired token');
  }
};

// Context Creator
const createContext = ({ req }: CreateContextOptions): Context => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  let userId: string | undefined;
  let role: UserRole | undefined;

  if (token) {
    const decoded = verifyToken(token);
    userId = decoded.userId;
    role = decoded.role;
  }

  return {
    userId,
    role,
    prisma,
    pubsub,
    userLoader: createUserLoader(),
    postLoader: createPostLoader(),
  };
};

// Middleware Functions
const requireAuth = (context: Context): void => {
  if (!context.userId) throw new AuthenticationError();
};

const requireRole = (context: Context, ...roles: UserRole[]): void => {
  requireAuth(context);
  if (!roles.includes(context.role!))
    throw new ForbiddenError('Insufficient permissions');
};

const requireOwner = (context: Context, resourceOwnerId: string): void => {
  requireAuth(context);
  if (context.role !== UserRole.ADMIN && context.userId !== resourceOwnerId) {
    throw new ForbiddenError('You can only modify your own resources');
  }
};
```

### Schema Directives for Authorization

```typescript
const authDirective = {
  typeDefinition: `directive @auth(requires: Role = USER) on FIELD_DEFINITION`,

  resolverDefinition: ({
    requires = UserRole.USER,
  }: {
    requires?: UserRole;
  }) => {
    return async (next, parent, args, context, info) => {
      requireAuth(context);
      requireRole(context, requires);
      return next();
    };
  },
};

// Usage in schema
// type Query {
//   adminDashboard: DashboardStats! @auth(requires: ADMIN)
//   user: User! @auth(requires: USER)
// }
```

## Pagination Patterns

### Relay Cursor-Based Pagination

```typescript
// Schema
type Query {
  users(
    first: Int = 20
    after: String
    last: Int
    before: String
    filter: UserFilterInput
    sort: UserSortInput
  ): UserConnection!
}

type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  node: User!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

// Resolver
const resolvers = {
  Query: {
    users: async (_, { first = 20, after, last, before, filter, sort }, context) => {
      const where = buildWhere(filter);
      const orderBy = buildOrderBy(sort);

      // Forward pagination
      if (first !== undefined) {
        const cursor = after ? { id: after } : undefined;
        const take = first + 1;

        const users = await prisma.user.findMany({
          where, orderBy, cursor, take, skip: after ? 1 : 0
        });

        const hasNextPage = users.length > first;
        const data = hasNextPage ? users.slice(0, -1) : users;

        return {
          edges: data.map(user => ({ node: user, cursor: encodeCursor(user.id) })),
          pageInfo: {
            hasNextPage,
            hasPreviousPage: !!after,
            startCursor: data.length > 0 ? encodeCursor(data[0].id) : null,
            endCursor: data.length > 0 ? encodeCursor(data[data.length - 1].id) : null
          },
          totalCount: await prisma.user.count({ where })
        };
      }

      // Backward pagination
      if (last !== undefined) {
        const cursor = before ? { id: before } : undefined;
        const take = last + 1;

        const users = await prisma.user.findMany({
          where, orderBy: reverseOrder(orderBy), cursor, take, skip: before ? 1 : 0
        });

        const hasPreviousPage = users.length > last;
        const data = hasPreviousPage ? users.slice(0, -1) : users;
        data.reverse();

        return {
          edges: data.map(user => ({ node: user, cursor: encodeCursor(user.id) })),
          pageInfo: {
            hasNextPage: !!before,
            hasPreviousPage,
            startCursor: data.length > 0 ? encodeCursor(data[0].id) : null,
            endCursor: data.length > 0 ? encodeCursor(data[data.length - 1].id) : null
          },
          totalCount: await prisma.user.count({ where })
        };
      }
    }
  }
};

const encodeCursor = (id: string): string => Buffer.from(`cursor:${id}`).toString('base64');
const decodeCursor = (cursor: string): string => Buffer.from(cursor, 'base64').toString('utf-8').replace('cursor:', '');
```

### Offset-Based Pagination (Alternative)

```typescript
type Query {
  users(
    page: Int = 1
    limit: Int = 20
    filter: UserFilterInput
    sort: UserSortInput
  ): UserPagination!
}

type UserPagination {
  data: [User!]!
  total: Int!
  page: Int!
  limit: Int!
  totalPages: Int!
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
}

// Resolver
const resolvers = {
  Query: {
    users: async (_, { page = 1, limit = 20, filter, sort }, context) => {
      const where = buildWhere(filter);
      const orderBy = buildOrderBy(sort);
      const skip = (page - 1) * limit;

      const [data, total] = await Promise.all([
        prisma.user.findMany({ where, orderBy, skip, take: limit }),
        prisma.user.count({ where })
      ]);

      const totalPages = Math.ceil(total / limit);

      return {
        data, total, page, limit, totalPages,
        hasNextPage: page < totalPages,
        hasPreviousPage: page > 1
      };
    }
  }
};
```

## Apollo Federation

### Subgraph Setup

```typescript
// subgraph-user/src/index.ts
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';

const server = new ApolloServer({
  schema: buildSubgraphSchema({ typeDefs, resolvers }),
});

const typeDefs = `#graphql
  extend type Query {
    _service: _Service!
  }
  
  type User implements @key(fields: "id") {
    id: ID!
    name: String!
    email: String!
    role: Role!
  }
  
  enum Role { ADMIN USER }
`;

const resolvers = {
  Query: { user: (_, { id }) => findUserById(id) },
  User: { __resolveReference: ({ id }) => findUserById(id) },
};
```

### Apollo Gateway Setup

```typescript
// gateway/src/index.ts
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloGateway } from '@apollo/gateway';
import express from 'express';
import { json } from 'body-parser';

const gateway = new ApolloGateway({
  supergraphs: [
    {
      url: 'http://localhost:4001',
      sdl: fs.readFileSync('./user-subgraph.schema', 'utf8'),
    },
    {
      url: 'http://localhost:4002',
      sdl: fs.readFileSync('./post-subgraph.schema', 'utf8'),
    },
  ],
});

const server = new ApolloServer();
const app = express();

async function start() {
  const { schema } = await server.start();

  app.use(
    '/graphql',
    json(),
    expressMiddleware(server, {
      context: async ({ req }) => ({ userId: getUserIdFromToken(req) }),
    })
  );

  await server.listen({ port: 4000 });
  console.log('Gateway ready at http://localhost:4000/graphql');
}

start();
```

## Performance Optimization

### Query Complexity Analysis

```typescript
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const validationRules = [
  createComplexityLimitRule(1000, {
    scalarCost: 1,
    objectCost: 0,
    listFactor: 10,
  }),
];

const server = new ApolloServer({
  schema,
  validationRules,
  plugins: [
    persistedQueries.plugin({ cache: new InMemoryLRUCache() }),
    responseCachePlugin({
      ttl: {
        default: 60,
        Query: { users: 30, health: 300 },
      },
    }),
  ],
});
```

### Query Depth Limiting

```typescript
import depthLimit from 'graphql-depth-limit';

const validationRules = [
  depthLimit(10), // Maximum query depth of 10
  createComplexityLimitRule(1000),
];

const server = new ApolloServer({ schema, validationRules, formatError });
```

## Server Configuration

### Apollo Server Setup

```typescript
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { startStandaloneServer } from '@apollo/server/standalone';
import { json } from 'body-parser';
import cors from 'cors';
import { schema } from './schema';
import { createContext } from './context';
import { formatError } from './errors';
import { validationRules } from './validation';

const server = new ApolloServer({
  schema,
  formatError,
  validationRules,
  introspection: process.env.NODE_ENV !== 'production',
  plugins: [
    {
      async serverWillStart() {
        return {
          async drainServer() {
            await prisma.$disconnect();
          },
        };
      },
    },
  ],
});

// Standalone mode
const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  context: async (requestContext) => createContext(requestContext),
});

console.log(`Server ready at ${url}`);
```

### GraphQL Yoga Setup

```typescript
import { createYoga } from 'graphql-yoga';
import { schema } from './schema';

const yoga = createYoga({
  schema,
  context: async ({ request }) => ({
    userId: getUserIdFromToken(request),
    userLoader: createUserLoader(),
  }),
  graphqlEndpoint: '/graphql',
  ide: process.env.NODE_ENV !== 'production' ? 'graphiql' : 'apollo',
  cors: {
    origin: ['https://yourdomain.com'],
    credentials: true,
  },
});

server.listen(4000).then(({ url }) => {
  console.log(`Server ready at ${url}`);
});
```

## Production Checklist

Before deploying to production:

- [ ] Schema design follows GraphQL best practices
- [ ] DataLoader implemented for N+1 prevention
- [ ] Query complexity analysis configured
- [ ] Query depth limiting enabled
- [ ] Error handling with custom error classes
- [ ] Authentication with JWT and context
- [ ] Authorization with directives and middleware
- [ ] Pagination with Relay spec or cursor-based
- [ ] Subscriptions with WebSocket or SSE
- [ ] Introspection disabled in production
- [ ] Rate limiting configured
- [ ] Response caching enabled
- [ ] Persisted queries configured
- [ ] Health check endpoint implemented
- [ ] Logging and monitoring setup
- [ ] GraphQL Playground disabled in production
- [ ] CORS configured correctly
- [ ] Schema validation in CI/CD pipeline

## Real-World Impact

**Before this skill:**

- N+1 query problems causing slow responses
- No authentication or authorization
- Missing error handling
- No pagination or filtering
- Deep queries causing server overload
- No performance monitoring

**After this skill:**

- Efficient data loading with DataLoader
- Secure authentication and authorization
- Comprehensive error handling
- Relay-compliant pagination
- Query complexity and depth limiting
- Production-ready monitoring

## Cross-References

- **`api-design`** - For RESTful API design patterns
- **`database-design`** - For database schema design
- **`redis-caching`** - For Redis caching strategies
- **`docker-containerization`** - For containerizing GraphQL servers
- **`kubernetes-orchestration`** - For deploying to Kubernetes
- **`devops-pipeline`** - For CI/CD with GraphQL
- **`security-auditor`** - For security reviews
- **`microservices-architecture`** - For Apollo Federation patterns

## References

- [GraphQL Specification](https://spec.graphql.org/)
- [Apollo Server Documentation](https://www.apollographql.com/docs/apollo-server/)
- [GraphQL Yoga Documentation](https://the-guild.dev/graphql/yoga-server/docs)
- [Apollo Federation](https://www.apollographql.com/docs/federation/)
- [Relay Specification](https://relay.dev/graphql/)
- [GraphQL Best Practices](https://www.apollographql.com/docs/apollo-server/performance/best-practices/)
- [DataLoader Documentation](https://github.com/graphql/dataloader)
- [GraphQL Depth Limit](https://github.com/stems/graphql-depth-limit)
- [GraphQL Query Complexity](https://github.com/slicknode/graphql-query-complexity)

---
> Source: [Omar-Obando/qwen-orchestrator](https://github.com/Omar-Obando/qwen-orchestrator) — distributed by [TomeVault](https://tomevault.io).
<!-- tomevault:4.0:skill_md:2026-06-15 -->

