Back to skills
SkillHub ClubWrite Technical DocsFull StackBackendData / AI

apollo-server

Guide for building GraphQL servers with Apollo Server 5.x. Use this skill when: (1) setting up a new Apollo Server project, (2) writing resolvers or defining GraphQL schemas, (3) implementing authentication or authorization, (4) creating plugins or custom data sources, (5) troubleshooting Apollo Server errors or performance issues.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
36
Hot score
90
Updated
March 20, 2026
Overall rating
C2.1
Composite score
2.1
Best-practice grade
C64.8

Install command

npx @skill-hub/cli install apollographql-skills-apollo-server

Repository

apollographql/skills

Skill path: skills/apollo-server

Guide for building GraphQL servers with Apollo Server 5.x. Use this skill when: (1) setting up a new Apollo Server project, (2) writing resolvers or defining GraphQL schemas, (3) implementing authentication or authorization, (4) creating plugins or custom data sources, (5) troubleshooting Apollo Server errors or performance issues.

Open repository

Best for

Primary workflow: Write Technical Docs.

Technical facets: Full Stack, Backend, Data / AI, Tech Writer.

Target audience: everyone.

License: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: apollographql.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install apollo-server into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/apollographql/skills before adding apollo-server to shared team environments
  • Use apollo-server for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: apollo-server
description: >
  Guide for building GraphQL servers with Apollo Server 5.x. Use this skill when:
  (1) setting up a new Apollo Server project,
  (2) writing resolvers or defining GraphQL schemas,
  (3) implementing authentication or authorization,
  (4) creating plugins or custom data sources,
  (5) troubleshooting Apollo Server errors or performance issues.
license: MIT
compatibility: Node.js v20+, TypeScript 4.7+. Works with Express v4/v5, standalone, Fastify, and serverless.
metadata:
  author: apollographql
  version: "1.0.0"
allowed-tools: Bash(npm:*) Bash(npx:*) Bash(node:*) Read Write Edit Glob Grep
---

# Apollo Server 5.x Guide

Apollo Server is an open-source GraphQL server that works with any GraphQL schema. Apollo Server 5 is framework-agnostic and runs standalone or integrates with Express, Fastify, and serverless environments.

## Quick Start

### Step 1: Install

```bash
npm install @apollo/server graphql
```

For Express integration:

```bash
npm install @apollo/server @as-integrations/express5 express graphql cors
```

### Step 2: Define Schema

```typescript
const typeDefs = `#graphql
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;
```

### Step 3: Write Resolvers

```typescript
const resolvers = {
  Query: {
    books: () => [
      { title: "The Great Gatsby", author: "F. Scott Fitzgerald" },
      { title: "1984", author: "George Orwell" },
    ],
  },
};
```

### Step 4: Start Server

**Standalone (Recommended for prototyping):**

The standalone server is great for prototyping, but for production services, we recommend integrating Apollo Server with a more fully-featured web framework such as Express, Koa, or Fastify. Swapping from the standalone server to a web framework later is straightforward.

```typescript
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

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

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

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

**Express:**

```typescript
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@as-integrations/express5";
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
import express from "express";
import http from "http";
import cors from "cors";

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

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});

await server.start();

app.use(
  "/graphql",
  cors(),
  express.json(),
  expressMiddleware(server, {
    context: async ({ req }) => ({ token: req.headers.authorization }),
  }),
);

await new Promise<void>((resolve) => httpServer.listen({ port: 4000 }, resolve));
console.log("Server ready at http://localhost:4000/graphql");
```

## Schema Definition

### Scalar Types

- `Int` - 32-bit integer
- `Float` - Double-precision floating-point
- `String` - UTF-8 string
- `Boolean` - true/false
- `ID` - Unique identifier (serialized as String)

### Type Definitions

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

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

input CreatePostInput {
  title: String!
  content: String
}

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

type Mutation {
  createPost(input: CreatePostInput!): Post!
}
```

### Enums and Interfaces

```graphql
enum Status {
  DRAFT
  PUBLISHED
  ARCHIVED
}

interface Node {
  id: ID!
}

type Article implements Node {
  id: ID!
  title: String!
}
```

## Resolvers Overview

Resolvers follow the signature: `(parent, args, contextValue, info)`

- **parent**: Result from parent resolver (root resolvers receive undefined)
- **args**: Arguments passed to the field
- **contextValue**: Shared context object (auth, dataSources, etc.)
- **info**: Field-specific info and schema details (rarely used)

```typescript
const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      return dataSources.usersAPI.getUser(id);
    },
  },
  User: {
    posts: async (parent, _, { dataSources }) => {
      return dataSources.postsAPI.getPostsByAuthor(parent.id);
    },
  },
  Mutation: {
    createPost: async (_, { input }, { dataSources, user }) => {
      if (!user) throw new GraphQLError("Not authenticated");
      return dataSources.postsAPI.create({ ...input, authorId: user.id });
    },
  },
};
```

## Context Setup

Context is created per-request and passed to all resolvers.

```typescript
interface MyContext {
  token?: string;
  user?: User;
  dataSources: {
    usersAPI: UsersDataSource;
    postsAPI: PostsDataSource;
  };
}

const server = new ApolloServer<MyContext>({
  typeDefs,
  resolvers,
});

// Standalone
const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => ({
    token: req.headers.authorization || "",
    user: await getUser(req.headers.authorization || ""),
    dataSources: {
      usersAPI: new UsersDataSource(),
      postsAPI: new PostsDataSource(),
    },
  }),
});

// Express middleware
expressMiddleware(server, {
  context: async ({ req, res }) => ({
    token: req.headers.authorization,
    user: await getUser(req.headers.authorization),
    dataSources: {
      usersAPI: new UsersDataSource(),
      postsAPI: new PostsDataSource(),
    },
  }),
});
```

## Reference Files

Detailed documentation for specific topics:

- [Resolvers](references/resolvers.md) - Resolver patterns and best practices
- [Context and Auth](references/context-and-auth.md) - Authentication and authorization
- [Plugins](references/plugins.md) - Server and request lifecycle hooks
- [Data Sources](references/data-sources.md) - RESTDataSource and DataLoader
- [Error Handling](references/error-handling.md) - GraphQLError and error formatting
- [Troubleshooting](references/troubleshooting.md) - Common issues and solutions

## Key Rules

### Schema Design

- Use **!** (non-null) for fields that always have values
- Prefer input types for mutations over inline arguments
- Use interfaces for polymorphic types
- Keep schema descriptions for documentation

### Resolver Best Practices

- Keep resolvers thin - delegate to services/data sources
- Always handle errors explicitly
- Use DataLoader for batching related queries
- Return partial data when possible (GraphQL's strength)

### Performance

- Use `@defer` and `@stream` for large responses
- Implement DataLoader to solve N+1 queries
- Consider persisted queries for production
- Use caching headers and CDN where appropriate

## Ground Rules

- ALWAYS use Apollo Server 5.x patterns (not v4 or earlier)
- ALWAYS type your context with TypeScript generics
- ALWAYS use `GraphQLError` from `graphql` package for errors
- NEVER expose stack traces in production errors
- PREFER `startStandaloneServer` for prototyping only
- USE an integration with a server framework like Express, Koa, Fastify, Next, etc. for production apps
- IMPLEMENT authentication in context, authorization in resolvers


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/resolvers.md

```markdown
# Resolvers Reference

## Table of Contents

- [Resolver Signature](#resolver-signature)
- [Resolver Map Structure](#resolver-map-structure)
- [Async Resolvers](#async-resolvers)
- [Field Resolvers](#field-resolvers)
- [Default Resolvers](#default-resolvers)
- [N+1 Problem](#n1-problem)
- [Best Practices](#best-practices)

## Resolver Signature

Every resolver receives four positional arguments:

```typescript
(parent, args, contextValue, info) => result;
```

### Arguments

| Argument       | Description                                                                               |
| -------------- | ----------------------------------------------------------------------------------------- |
| `parent`       | Return value of the parent resolver. For root types (Query, Mutation), this is undefined. |
| `args`         | Object containing all arguments passed to the field.                                      |
| `contextValue` | Shared object for all resolvers in a request. Contains auth, dataSources, etc.            |
| `info`         | Contains field name, path, schema, and AST information. Rarely needed.                    |

### TypeScript Typing

```typescript
import { GraphQLResolveInfo } from "graphql";

type Resolver<TParent, TArgs, TContext, TResult> = (
  parent: TParent,
  args: TArgs,
  context: TContext,
  info: GraphQLResolveInfo,
) => TResult | Promise<TResult>;

// Example
const userResolver: Resolver<undefined, { id: string }, MyContext, User | null> = async (
  _,
  { id },
  { dataSources },
) => {
  return dataSources.usersAPI.getUser(id);
};
```

## Resolver Map Structure

Resolvers are organized by type and field name:

```typescript
const resolvers = {
  Query: {
    users: (_, __, { dataSources }) => dataSources.usersAPI.getAll(),
    user: (_, { id }, { dataSources }) => dataSources.usersAPI.getById(id),
  },

  Mutation: {
    createUser: (_, { input }, { dataSources }) => dataSources.usersAPI.create(input),
    updateUser: (_, { id, input }, { dataSources }) => dataSources.usersAPI.update(id, input),
    deleteUser: (_, { id }, { dataSources }) => dataSources.usersAPI.delete(id),
  },

  Subscription: {
    userCreated: {
      subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(["USER_CREATED"]),
    },
  },

  User: {
    posts: (parent, _, { dataSources }) => dataSources.postsAPI.getByAuthor(parent.id),
    fullName: (parent) => `${parent.firstName} ${parent.lastName}`,
  },

  // Custom scalar
  DateTime: new GraphQLScalarType({
    name: "DateTime",
    serialize: (value) => value.toISOString(),
    parseValue: (value) => new Date(value),
  }),

  // Enum mapping (when values differ from schema)
  Status: {
    DRAFT: 0,
    PUBLISHED: 1,
    ARCHIVED: 2,
  },
};
```

## Async Resolvers

Resolvers can return Promises. Apollo Server awaits them automatically.

```typescript
const resolvers = {
  Query: {
    // Async/await pattern (recommended)
    user: async (_, { id }, { dataSources }) => {
      const user = await dataSources.usersAPI.getById(id);
      if (!user) {
        throw new GraphQLError("User not found", {
          extensions: { code: "NOT_FOUND" },
        });
      }
      return user;
    },

    // Promise pattern
    users: (_, __, { dataSources }) => {
      return dataSources.usersAPI.getAll();
    },

    // Parallel fetching
    dashboard: async (_, __, { dataSources }) => {
      const [users, posts, stats] = await Promise.all([
        dataSources.usersAPI.getAll(),
        dataSources.postsAPI.getRecent(),
        dataSources.analyticsAPI.getStats(),
      ]);
      return { users, posts, stats };
    },
  },
};
```

## Field Resolvers

Field resolvers compute derived values or fetch related data:

```typescript
const resolvers = {
  User: {
    // Computed field
    fullName: (parent) => `${parent.firstName} ${parent.lastName}`,

    // Related data (one-to-many)
    posts: async (parent, _, { dataSources }) => {
      return dataSources.postsAPI.getByAuthorId(parent.id);
    },

    // With arguments
    posts: async (parent, { limit, offset }, { dataSources }) => {
      return dataSources.postsAPI.getByAuthorId(parent.id, { limit, offset });
    },

    // Conditional fetching
    privateData: async (parent, _, { user }) => {
      if (user?.id !== parent.id) {
        return null;
      }
      return parent.privateData;
    },
  },

  Post: {
    // Related data (many-to-one)
    author: async (parent, _, { dataSources }) => {
      return dataSources.usersAPI.getById(parent.authorId);
    },
  },
};
```

## Default Resolvers

Apollo Server provides default resolvers that return `parent[fieldName]`:

```typescript
// Schema
type User {
  id: ID!
  name: String!
  email: String
}

// Resolvers - you don't need to write these
const resolvers = {
  User: {
    id: (parent) => parent.id,      // Default resolver handles this
    name: (parent) => parent.name,  // Default resolver handles this
    email: (parent) => parent.email, // Default resolver handles this
  },
};

// Only define resolvers when:
// 1. Field name differs from data property
// 2. Data needs transformation
// 3. Field requires fetching related data
const resolvers = {
  User: {
    fullName: (parent) => `${parent.first_name} ${parent.last_name}`,
  },
};
```

## N+1 Problem

The N+1 problem occurs when fetching related data triggers separate queries for each item.

### Problem Example

```typescript
// Query: { users { posts { title } } }
// This causes:
// 1 query for users
// N queries for posts (one per user)

const resolvers = {
  Query: {
    users: () => db.query("SELECT * FROM users"), // 1 query
  },
  User: {
    posts: (parent) => db.query("SELECT * FROM posts WHERE author_id = ?", [parent.id]), // N queries
  },
};
```

### Solution: DataLoader

```typescript
import DataLoader from "dataloader";

// Create loader per request (in context)
const context = async () => ({
  loaders: {
    postsByAuthor: new DataLoader(async (authorIds) => {
      const posts = await db.query("SELECT * FROM posts WHERE author_id IN (?)", [authorIds]);
      // Return posts grouped by author_id in same order as input
      return authorIds.map((id) => posts.filter((p) => p.author_id === id));
    }),
  },
});

// Use loader in resolver
const resolvers = {
  User: {
    posts: (parent, _, { loaders }) => loaders.postsByAuthor.load(parent.id),
  },
};
```

## Best Practices

### Keep Resolvers Thin

```typescript
// Bad - business logic in resolver
const resolvers = {
  Mutation: {
    createOrder: async (_, { input }, { dataSources, user }) => {
      const items = await dataSources.inventory.checkStock(input.items);
      const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
      const discount = user.isPremium ? total * 0.1 : 0;
      // ... more logic
    },
  },
};

// Good - delegate to service
const resolvers = {
  Mutation: {
    createOrder: async (_, { input }, { services, user }) => {
      return services.orders.create(input, user);
    },
  },
};
```

### Handle Null Appropriately

```typescript
const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      // Return null for not found (matches nullable schema)
      return dataSources.usersAPI.getById(id);
    },

    userRequired: async (_, { id }, { dataSources }) => {
      const user = await dataSources.usersAPI.getById(id);
      if (!user) {
        throw new GraphQLError("User not found");
      }
      return user;
    },
  },
};
```

### Avoid Over-fetching in Parent

```typescript
// Bad - fetching all data upfront
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      const user = await db.user.findById(id);
      const posts = await db.posts.findByAuthor(id);
      const followers = await db.followers.findByUser(id);
      return { ...user, posts, followers };
    },
  },
};

// Good - fetch only when requested
const resolvers = {
  Query: {
    user: (_, { id }) => db.user.findById(id),
  },
  User: {
    posts: (parent) => db.posts.findByAuthor(parent.id),
    followers: (parent) => db.followers.findByUser(parent.id),
  },
};
```

```

### references/context-and-auth.md

```markdown
# Context and Authentication Reference

## Table of Contents

- [Context Function](#context-function)
- [TypeScript Context Typing](#typescript-context-typing)
- [Authentication Patterns](#authentication-patterns)
- [Authorization Patterns](#authorization-patterns)
- [Data Sources in Context](#data-sources-in-context)
- [Security Best Practices](#security-best-practices)

## Context Function

The context function runs for every request and returns an object shared across all resolvers.

### Standalone Server

```typescript
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

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

const { url } = await startStandaloneServer(server, {
  context: async ({ req, res }) => {
    // req: IncomingMessage
    // res: ServerResponse
    return {
      token: req.headers.authorization,
    };
  },
});
```

### Express Middleware

```typescript
import { expressMiddleware } from "@as-integrations/express5";

app.use(
  "/graphql",
  cors(),
  express.json(),
  expressMiddleware(server, {
    context: async ({ req, res }) => {
      // req: express.Request
      // res: express.Response
      return {
        token: req.headers.authorization,
        ip: req.ip,
      };
    },
  }),
);
```

### Context Initialization Order

```typescript
const context = async ({ req }) => {
  // 1. Extract credentials
  const token = req.headers.authorization?.replace("Bearer ", "");

  // 2. Validate and decode (fail fast)
  let user = null;
  if (token) {
    try {
      user = await verifyToken(token);
    } catch (e) {
      // Don't throw - let resolvers handle auth
      console.warn("Invalid token:", e.message);
    }
  }

  // 3. Initialize data sources
  const dataSources = {
    usersAPI: new UsersDataSource(),
    postsAPI: new PostsDataSource(),
  };

  // 4. Return context object
  return { token, user, dataSources };
};
```

## TypeScript Context Typing

Define and use a typed context for type safety:

```typescript
import { ApolloServer } from "@apollo/server";

// Define context type
interface MyContext {
  token?: string;
  user?: {
    id: string;
    email: string;
    roles: string[];
  };
  dataSources: {
    usersAPI: UsersDataSource;
    postsAPI: PostsDataSource;
  };
}

// Pass to ApolloServer
const server = new ApolloServer<MyContext>({
  typeDefs,
  resolvers,
});

// Resolvers get typed context
const resolvers = {
  Query: {
    me: async (_, __, context: MyContext) => {
      if (!context.user) {
        throw new GraphQLError("Not authenticated");
      }
      return context.dataSources.usersAPI.getById(context.user.id);
    },
  },
};
```

## Authentication Patterns

### JWT Authentication

```typescript
import jwt from "jsonwebtoken";

interface JwtPayload {
  userId: string;
  email: string;
  roles: string[];
}

const context = async ({ req }) => {
  const token = req.headers.authorization?.replace("Bearer ", "");

  let user = null;
  if (token) {
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
      user = {
        id: decoded.userId,
        email: decoded.email,
        roles: decoded.roles,
      };
    } catch (e) {
      // Token invalid or expired - user remains null
    }
  }

  return { user };
};
```

### Session Authentication

```typescript
import session from "express-session";

// Express setup
app.use(
  session({
    secret: process.env.SESSION_SECRET!,
    resave: false,
    saveUninitialized: false,
  }),
);

app.use(
  "/graphql",
  cors(),
  express.json(),
  expressMiddleware(server, {
    context: async ({ req }) => ({
      user: req.session.user,
      session: req.session,
    }),
  }),
);

// Login mutation
const resolvers = {
  Mutation: {
    login: async (_, { email, password }, { session, dataSources }) => {
      const user = await dataSources.usersAPI.authenticate(email, password);
      if (!user) {
        throw new GraphQLError("Invalid credentials");
      }
      session.user = user;
      return user;
    },

    logout: async (_, __, { session }) => {
      return new Promise((resolve, reject) => {
        session.destroy((err) => {
          if (err) reject(err);
          else resolve(true);
        });
      });
    },
  },
};
```

### API Key Authentication

```typescript
const context = async ({ req }) => {
  const apiKey = req.headers["x-api-key"];

  let client = null;
  if (apiKey) {
    client = await db.apiKeys.findOne({ key: apiKey, active: true });
  }

  return {
    client,
    isAuthenticated: !!client,
  };
};
```

## Authorization Patterns

### Field-Level Authorization

```typescript
import { GraphQLError } from "graphql";

const resolvers = {
  User: {
    email: (parent, _, { user }) => {
      // Only return email to the user themselves or admins
      if (user?.id === parent.id || user?.roles.includes("admin")) {
        return parent.email;
      }
      return null;
    },

    privateData: (parent, _, { user }) => {
      if (!user) {
        throw new GraphQLError("Not authenticated", {
          extensions: { code: "UNAUTHENTICATED" },
        });
      }
      if (user.id !== parent.id) {
        throw new GraphQLError("Not authorized", {
          extensions: { code: "FORBIDDEN" },
        });
      }
      return parent.privateData;
    },
  },
};
```

### Role-Based Authorization

```typescript
// Helper function
function requireRole(user: User | null, roles: string[]): void {
  if (!user) {
    throw new GraphQLError("Not authenticated", {
      extensions: { code: "UNAUTHENTICATED" },
    });
  }

  const hasRole = roles.some((role) => user.roles.includes(role));
  if (!hasRole) {
    throw new GraphQLError(`Requires one of: ${roles.join(", ")}`, {
      extensions: { code: "FORBIDDEN" },
    });
  }
}

const resolvers = {
  Mutation: {
    deleteUser: async (_, { id }, { user, dataSources }) => {
      requireRole(user, ["admin"]);
      return dataSources.usersAPI.delete(id);
    },

    updatePost: async (_, { id, input }, { user, dataSources }) => {
      requireRole(user, ["admin", "editor"]);
      return dataSources.postsAPI.update(id, input);
    },
  },
};
```

### Directive-Based Authorization

```typescript
import { mapSchema, getDirective, MapperKind } from "@graphql-tools/utils";
import { defaultFieldResolver } from "graphql";

// Schema directive
const typeDefs = `#graphql
  directive @auth(requires: Role = USER) on FIELD_DEFINITION

  enum Role {
    ADMIN
    USER
    GUEST
  }

  type Query {
    users: [User!]! @auth(requires: ADMIN)
    me: User @auth
  }
`;

// Transform schema
function authDirectiveTransformer(schema) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
      const authDirective = getDirective(schema, fieldConfig, "auth")?.[0];

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

        fieldConfig.resolve = async (source, args, context, info) => {
          if (!context.user) {
            throw new GraphQLError("Not authenticated");
          }

          if (requires && !context.user.roles.includes(requires)) {
            throw new GraphQLError(`Requires ${requires} role`);
          }

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

      return fieldConfig;
    },
  });
}
```

## Data Sources in Context

### Creating Data Sources

```typescript
interface MyContext {
  dataSources: {
    usersAPI: UsersDataSource;
    postsAPI: PostsDataSource;
  };
}

const context = async ({ req }): Promise<MyContext> => {
  return {
    dataSources: {
      usersAPI: new UsersDataSource(),
      postsAPI: new PostsDataSource(),
    },
  };
};
```

### Passing User to Data Sources

```typescript
class AuthenticatedDataSource extends RESTDataSource {
  private user?: User;

  setUser(user: User) {
    this.user = user;
  }

  override willSendRequest(path: string, request: AugmentedRequest) {
    if (this.user) {
      request.headers["x-user-id"] = this.user.id;
    }
  }
}

const context = async ({ req }) => {
  const user = await getUser(req.headers.authorization);

  const usersAPI = new UsersDataSource();
  if (user) {
    usersAPI.setUser(user);
  }

  return { user, dataSources: { usersAPI } };
};
```

## Security Best Practices

### Never Trust Client Input

```typescript
const context = async ({ req }) => {
  // Bad - trusting client header
  const userId = req.headers["x-user-id"];

  // Good - verify token server-side
  const token = req.headers.authorization?.replace("Bearer ", "");
  const user = token ? await verifyToken(token) : null;

  return { user };
};
```

### Don't Expose Internal Errors

```typescript
const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (formattedError, error) => {
    // Log full error internally
    console.error(error);

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

    return formattedError;
  },
});
```

### Rate Limiting

```typescript
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
});

app.use("/graphql", limiter);
```

### Depth Limiting

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

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(10)],
});
```

### Query Complexity

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

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    createComplexityLimitRule(1000, {
      onCost: (cost) => console.log("Query cost:", cost),
    }),
  ],
});
```

```

### references/plugins.md

```markdown
# Plugins Reference

## Table of Contents

- [Plugin Structure](#plugin-structure)
- [Server Lifecycle Hooks](#server-lifecycle-hooks)
- [Request Lifecycle Hooks](#request-lifecycle-hooks)
- [Common Plugin Patterns](#common-plugin-patterns)
- [Built-in Plugins](#built-in-plugins)

## Plugin Structure

Plugins are objects that implement lifecycle hooks:

```typescript
import { ApolloServerPlugin } from "@apollo/server";

const myPlugin: ApolloServerPlugin<MyContext> = {
  // Server lifecycle
  async serverWillStart(service) {
    console.log("Server starting");
    return {
      async drainServer() {
        console.log("Server draining");
      },
      async serverWillStop() {
        console.log("Server stopping");
      },
    };
  },

  // Request lifecycle
  async requestDidStart(requestContext) {
    console.log("Request started");
    return {
      async parsingDidStart() {
        return async (err) => {
          if (err) console.log("Parsing error:", err);
        };
      },
      async validationDidStart() {
        return async (errs) => {
          if (errs) console.log("Validation errors:", errs);
        };
      },
      async didResolveOperation(requestContext) {
        console.log("Operation:", requestContext.operationName);
      },
      async executionDidStart() {
        return {
          willResolveField({ info }) {
            const start = Date.now();
            return () => {
              console.log(`${info.fieldName}: ${Date.now() - start}ms`);
            };
          },
        };
      },
      async willSendResponse(requestContext) {
        console.log("Sending response");
      },
    };
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [myPlugin],
});
```

## Server Lifecycle Hooks

### serverWillStart

Called when `server.start()` is invoked, before the server begins accepting requests.

```typescript
const plugin: ApolloServerPlugin = {
  async serverWillStart(service) {
    // service.schema - the GraphQL schema
    // service.apollo - Apollo config (if using Apollo Studio)

    console.log("Schema types:", Object.keys(service.schema.getTypeMap()));

    // Return object with cleanup hooks
    return {
      async drainServer() {
        // Called when server.stop() begins
        // Use to stop accepting new requests
        await closeConnections();
      },

      async serverWillStop() {
        // Called after drainServer, before server fully stops
        // Use for final cleanup
        await db.disconnect();
      },

      schemaDidLoadOrUpdate(schemaContext) {
        // Called when schema changes (e.g., with gateway)
        console.log("Schema updated");
      },
    };
  },
};
```

### drainServer Pattern

```typescript
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
import http from "http";

const httpServer = http.createServer(app);

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    ApolloServerPluginDrainHttpServer({ httpServer }),
    // Custom drain plugin
    {
      async serverWillStart() {
        return {
          async drainServer() {
            // Stop background jobs
            await scheduler.stop();
            // Close database connections
            await db.end();
          },
        };
      },
    },
  ],
});
```

## Request Lifecycle Hooks

Request lifecycle flows in this order:

```
requestDidStart
  └─> didResolveSource
  └─> parsingDidStart / parsingDidEnd
  └─> validationDidStart / validationDidEnd
  └─> didResolveOperation
  └─> responseForOperation (can short-circuit)
  └─> executionDidStart
        └─> willResolveField (per field)
  └─> didEncounterErrors (if errors)
  └─> willSendResponse
```

### Full Request Lifecycle

```typescript
const plugin: ApolloServerPlugin<MyContext> = {
  async requestDidStart(requestContext) {
    const start = Date.now();
    const { request, contextValue } = requestContext;

    console.log("Query:", request.query);
    console.log("Variables:", request.variables);

    return {
      async didResolveSource(requestContext) {
        // Source (query string) has been resolved
      },

      async parsingDidStart(requestContext) {
        // Return end hook for when parsing completes
        return async (err) => {
          if (err) {
            console.error("Parse error:", err);
          }
        };
      },

      async validationDidStart(requestContext) {
        return async (errs) => {
          if (errs?.length) {
            console.error("Validation errors:", errs);
          }
        };
      },

      async didResolveOperation(requestContext) {
        // Operation name and type are now available
        console.log("Operation:", requestContext.operationName);
        console.log("Type:", requestContext.operation?.operation);
      },

      async responseForOperation(requestContext) {
        // Return response to skip execution (e.g., cached response)
        // Return null to continue normal execution
        return null;
      },

      async executionDidStart(requestContext) {
        return {
          willResolveField({ source, args, contextValue, info }) {
            const fieldStart = Date.now();
            return (error, result) => {
              const duration = Date.now() - fieldStart;
              if (duration > 100) {
                console.log(`Slow field ${info.fieldName}: ${duration}ms`);
              }
            };
          },
        };
      },

      async didEncounterErrors(requestContext) {
        for (const error of requestContext.errors) {
          console.error("GraphQL Error:", error);
        }
      },

      async willSendResponse(requestContext) {
        console.log(`Request completed in ${Date.now() - start}ms`);
      },
    };
  },
};
```

## Common Plugin Patterns

### Logging Plugin

```typescript
const loggingPlugin: ApolloServerPlugin<MyContext> = {
  async requestDidStart({ request, contextValue }) {
    const start = Date.now();
    const requestId = crypto.randomUUID();

    console.log(
      JSON.stringify({
        type: "request_start",
        requestId,
        operationName: request.operationName,
        userId: contextValue.user?.id,
      }),
    );

    return {
      async willSendResponse({ response }) {
        console.log(
          JSON.stringify({
            type: "request_end",
            requestId,
            duration: Date.now() - start,
            errors: response.body.kind === "single" ? (response.body.singleResult.errors?.length ?? 0) : 0,
          }),
        );
      },
    };
  },
};
```

### Timing Plugin

```typescript
const timingPlugin: ApolloServerPlugin = {
  async requestDidStart() {
    const fieldTimes: Map<string, number[]> = new Map();

    return {
      async executionDidStart() {
        return {
          willResolveField({ info }) {
            const start = process.hrtime.bigint();
            return () => {
              const duration = Number(process.hrtime.bigint() - start) / 1e6;
              const key = `${info.parentType.name}.${info.fieldName}`;
              const times = fieldTimes.get(key) ?? [];
              times.push(duration);
              fieldTimes.set(key, times);
            };
          },
        };
      },

      async willSendResponse({ response }) {
        if (response.body.kind === "single") {
          response.body.singleResult.extensions = {
            ...response.body.singleResult.extensions,
            timing: Object.fromEntries(fieldTimes),
          };
        }
      },
    };
  },
};
```

### Error Tracking Plugin

```typescript
const errorTrackingPlugin: ApolloServerPlugin<MyContext> = {
  async requestDidStart({ request, contextValue }) {
    return {
      async didEncounterErrors({ errors, request }) {
        for (const error of errors) {
          // Skip client errors
          if (error.extensions?.code === "BAD_USER_INPUT") continue;

          // Report to error tracking service
          await errorTracker.captureException(error.originalError ?? error, {
            extra: {
              operationName: request.operationName,
              query: request.query,
              variables: request.variables,
              userId: contextValue.user?.id,
            },
          });
        }
      },
    };
  },
};
```

### Caching Plugin

```typescript
const cachingPlugin: ApolloServerPlugin = {
  async requestDidStart({ request }) {
    const cacheKey = createHash("sha256")
      .update(request.query ?? "")
      .update(JSON.stringify(request.variables ?? {}))
      .digest("hex");

    return {
      async responseForOperation() {
        const cached = await cache.get(cacheKey);
        if (cached) {
          return JSON.parse(cached);
        }
        return null;
      },

      async willSendResponse({ response }) {
        if (response.body.kind === "single" && !response.body.singleResult.errors) {
          await cache.set(cacheKey, JSON.stringify(response.body.singleResult), {
            ttl: 300,
          });
        }
      },
    };
  },
};
```

## Built-in Plugins

### ApolloServerPluginDrainHttpServer

Gracefully shuts down HTTP server during `server.stop()`:

```typescript
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
```

### ApolloServerPluginLandingPageLocalDefault

Shows Apollo Sandbox for local development:

```typescript
import { ApolloServerPluginLandingPageLocalDefault } from "@apollo/server/plugin/landingPage/default";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
});
```

### ApolloServerPluginLandingPageProductionDefault

Shows production landing page:

```typescript
import { ApolloServerPluginLandingPageProductionDefault } from "@apollo/server/plugin/landingPage/default";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    process.env.NODE_ENV === "production"
      ? ApolloServerPluginLandingPageProductionDefault()
      : ApolloServerPluginLandingPageLocalDefault({ embed: true }),
  ],
});
```

### ApolloServerPluginUsageReporting

Reports metrics to Apollo Studio:

```typescript
import { ApolloServerPluginUsageReporting } from "@apollo/server/plugin/usageReporting";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    ApolloServerPluginUsageReporting({
      sendVariableValues: { all: true },
      sendHeaders: { all: true },
    }),
  ],
});
```

### ApolloServerPluginInlineTrace

Includes trace data in responses (for federated graphs):

```typescript
import { ApolloServerPluginInlineTrace } from "@apollo/server/plugin/inlineTrace";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [ApolloServerPluginInlineTrace()],
});
```

```

### references/data-sources.md

```markdown
# Data Sources Reference

## Table of Contents

- [RESTDataSource](#restdatasource)
- [DataLoader](#dataloader)
- [Custom Data Sources](#custom-data-sources)
- [Best Practices](#best-practices)

## RESTDataSource

`@apollo/datasource-rest` provides a class for fetching data from REST APIs with built-in caching and request deduplication.

### Installation

```bash
npm install @apollo/datasource-rest
```

### Basic Usage

```typescript
import { RESTDataSource } from "@apollo/datasource-rest";

class UsersAPI extends RESTDataSource {
  override baseURL = "https://api.example.com/";

  async getUser(id: string): Promise<User> {
    return this.get<User>(`users/${id}`);
  }

  async getUsers(): Promise<User[]> {
    return this.get<User[]>("users");
  }

  async createUser(input: CreateUserInput): Promise<User> {
    return this.post<User>("users", { body: input });
  }

  async updateUser(id: string, input: UpdateUserInput): Promise<User> {
    return this.put<User>(`users/${id}`, { body: input });
  }

  async deleteUser(id: string): Promise<void> {
    return this.delete(`users/${id}`);
  }
}
```

### Context Integration

```typescript
interface MyContext {
  dataSources: {
    usersAPI: UsersAPI;
    postsAPI: PostsAPI;
  };
}

const server = new ApolloServer<MyContext>({ typeDefs, resolvers });

const { url } = await startStandaloneServer(server, {
  context: async () => ({
    dataSources: {
      usersAPI: new UsersAPI(),
      postsAPI: new PostsAPI(),
    },
  }),
});
```

### Request Customization

```typescript
class AuthenticatedAPI extends RESTDataSource {
  override baseURL = "https://api.example.com/";

  private token: string;

  constructor(token: string) {
    super();
    this.token = token;
  }

  // Add headers to every request
  override willSendRequest(path: string, request: AugmentedRequest) {
    request.headers["authorization"] = `Bearer ${this.token}`;
    request.headers["x-request-id"] = crypto.randomUUID();
  }

  // Transform response data
  override async didReceiveResponse<T>(response: Response, request: Request): Promise<T> {
    const body = await super.didReceiveResponse<T>(response, request);
    // Add metadata or transform
    return body;
  }
}
```

### Caching

RESTDataSource supports caching based on HTTP cache headers:

```typescript
class CachedAPI extends RESTDataSource {
  override baseURL = "https://api.example.com/";

  // Override cache options per request
  async getUser(id: string): Promise<User> {
    return this.get<User>(`users/${id}`, {
      cacheOptions: { ttl: 300 }, // 5 minutes
    });
  }

  // Disable caching for specific requests
  async getUserFresh(id: string): Promise<User> {
    return this.get<User>(`users/${id}`, {
      cacheOptions: { ttl: 0 },
    });
  }
}
```

### Error Handling

```typescript
import { GraphQLError } from "graphql";

class UsersAPI extends RESTDataSource {
  override baseURL = "https://api.example.com/";

  async getUser(id: string): Promise<User> {
    try {
      return await this.get<User>(`users/${id}`);
    } catch (error) {
      if (error.extensions?.response?.status === 404) {
        throw new GraphQLError("User not found", {
          extensions: { code: "NOT_FOUND" },
        });
      }
      throw error;
    }
  }
}
```

## DataLoader

DataLoader batches and caches individual loads within a single request, solving the N+1 problem.

### Installation

```bash
npm install dataloader
```

### Basic Usage

```typescript
import DataLoader from "dataloader";

// Batch function receives array of keys, returns array of results in same order
const userLoader = new DataLoader<string, User>(async (ids) => {
  const users = await db.query("SELECT * FROM users WHERE id IN (?)", [ids]);

  // IMPORTANT: Return results in same order as input ids
  const userMap = new Map(users.map((u) => [u.id, u]));
  return ids.map((id) => userMap.get(id) ?? new Error(`User ${id} not found`));
});

// Usage
const user1 = await userLoader.load("1");
const user2 = await userLoader.load("2");

// Both loads are batched into single query
```

### Context Integration

Create new DataLoader instances per request to prevent caching across requests:

```typescript
import DataLoader from "dataloader";

interface MyContext {
  loaders: {
    userLoader: DataLoader<string, User>;
    postsByAuthorLoader: DataLoader<string, Post[]>;
  };
}

const context = async (): Promise<MyContext> => ({
  loaders: {
    userLoader: new DataLoader(async (ids) => {
      const users = await db.users.findByIds(ids);
      const userMap = new Map(users.map((u) => [u.id, u]));
      return ids.map((id) => userMap.get(id) ?? null);
    }),

    postsByAuthorLoader: new DataLoader(async (authorIds) => {
      const posts = await db.posts.findByAuthorIds(authorIds);
      return authorIds.map((id) => posts.filter((p) => p.authorId === id));
    }),
  },
});

// Resolvers
const resolvers = {
  Post: {
    author: (post, _, { loaders }) => loaders.userLoader.load(post.authorId),
  },
  User: {
    posts: (user, _, { loaders }) => loaders.postsByAuthorLoader.load(user.id),
  },
};
```

### Options

```typescript
const loader = new DataLoader(batchFn, {
  // Maximum batch size (default: Infinity)
  maxBatchSize: 100,

  // Use object keys (default: false)
  cacheKeyFn: (key) => JSON.stringify(key),

  // Disable caching (default: true)
  cache: false,

  // Custom cache implementation
  cacheMap: new Map(),

  // Batch scheduling function
  batchScheduleFn: (callback) => setTimeout(callback, 10),
});
```

### Priming the Cache

```typescript
// Pre-populate cache after batch fetch
const users = await db.users.findAll();
users.forEach((user) => userLoader.prime(user.id, user));

// Clear specific key
userLoader.clear("1");

// Clear all
userLoader.clearAll();
```

## Custom Data Sources

Create custom data sources for databases, queues, or other services:

```typescript
class DatabaseSource {
  private db: Database;

  constructor(db: Database) {
    this.db = db;
  }

  async getUser(id: string): Promise<User | null> {
    return this.db.users.findUnique({ where: { id } });
  }

  async createUser(input: CreateUserInput): Promise<User> {
    return this.db.users.create({ data: input });
  }

  async getUsersWithPosts(ids: string[]): Promise<UserWithPosts[]> {
    return this.db.users.findMany({
      where: { id: { in: ids } },
      include: { posts: true },
    });
  }
}

// Context
const context = async () => ({
  dataSources: {
    db: new DatabaseSource(prisma),
  },
});
```

### With Connection Pooling

```typescript
class PooledDataSource {
  private pool: Pool;

  constructor(pool: Pool) {
    this.pool = pool;
  }

  async query<T>(sql: string, params: unknown[]): Promise<T[]> {
    const client = await this.pool.connect();
    try {
      const result = await client.query(sql, params);
      return result.rows;
    } finally {
      client.release();
    }
  }

  async getUser(id: string): Promise<User | null> {
    const rows = await this.query<User>("SELECT * FROM users WHERE id = $1", [id]);
    return rows[0] ?? null;
  }
}
```

## Best Practices

### Create Data Sources Per Request

```typescript
// Good - new instance per request
context: async () => ({
  dataSources: {
    usersAPI: new UsersAPI(),
  },
});

// Bad - shared instance across requests
const usersAPI = new UsersAPI();
context: async () => ({
  dataSources: { usersAPI },
});
```

### Combine DataLoader with RESTDataSource

```typescript
import DataLoader from "dataloader";

class UsersAPI extends RESTDataSource {
  override baseURL = "https://api.example.com/";

  private batchLoader = new DataLoader<string, User>(async (ids) => {
    const users = await this.get<User[]>("users", {
      params: { ids: ids.join(",") },
    });
    const userMap = new Map(users.map((u) => [u.id, u]));
    return ids.map((id) => userMap.get(id) ?? new Error(`Not found: ${id}`));
  });

  async getUser(id: string): Promise<User> {
    return this.batchLoader.load(id);
  }

  async getUsers(ids: string[]): Promise<User[]> {
    return this.batchLoader.loadMany(ids);
  }
}
```

### Handle Partial Failures

```typescript
const userLoader = new DataLoader<string, User>(async (ids) => {
  const users = await fetchUsers(ids);
  const userMap = new Map(users.map((u) => [u.id, u]));

  // Return Error for missing items, not null
  return ids.map((id) => userMap.get(id) ?? new Error(`User ${id} not found`));
});

// In resolver, handle the error
const resolvers = {
  Post: {
    author: async (post, _, { loaders }) => {
      try {
        return await loaders.userLoader.load(post.authorId);
      } catch (e) {
        // Return null for nullable field
        return null;
      }
    },
  },
};
```

### Avoid Over-fetching

```typescript
// Bad - always fetches all relations
async getUser(id: string): Promise<UserWithRelations> {
  return this.get(`users/${id}?include=posts,followers,following`);
}

// Good - separate methods for different needs
async getUser(id: string): Promise<User> {
  return this.get(`users/${id}`);
}

async getUserWithPosts(id: string): Promise<UserWithPosts> {
  return this.get(`users/${id}?include=posts`);
}
```

```

### references/error-handling.md

```markdown
# Error Handling Reference

## Table of Contents

- [GraphQLError](#graphqlerror)
- [Error Codes](#error-codes)
- [formatError Option](#formaterror-option)
- [Production Error Handling](#production-error-handling)

## GraphQLError

Apollo Server 4 uses `GraphQLError` from the `graphql` package. Always import from `graphql`, not from Apollo Server.

### Basic Usage

```typescript
import { GraphQLError } from "graphql";

const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      const user = await dataSources.usersAPI.getById(id);

      if (!user) {
        throw new GraphQLError("User not found", {
          extensions: {
            code: "NOT_FOUND",
            argumentName: "id",
          },
        });
      }

      return user;
    },
  },
};
```

### Error Response Format

```json
{
  "errors": [
    {
      "message": "User not found",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["user"],
      "extensions": {
        "code": "NOT_FOUND",
        "argumentName": "id"
      }
    }
  ],
  "data": {
    "user": null
  }
}
```

### GraphQLError Options

```typescript
new GraphQLError(message, {
  // Custom error code
  extensions: {
    code: "CUSTOM_CODE",
    // Any additional metadata
    http: { status: 400 },
  },

  // Original error (for stack traces)
  originalError: caughtError,

  // AST node(s) associated with the error
  nodes: fieldNode,

  // Source location
  positions: [15],

  // Path to the field that caused the error
  path: ["user", "email"],
});
```

## Error Codes

### Built-in Codes

| Code                            | Description                       | HTTP Status |
| ------------------------------- | --------------------------------- | ----------- |
| `GRAPHQL_PARSE_FAILED`          | Syntax error in query             | 400         |
| `GRAPHQL_VALIDATION_FAILED`     | Query doesn't match schema        | 400         |
| `BAD_USER_INPUT`                | Invalid argument value            | 400         |
| `UNAUTHENTICATED`               | Missing or invalid authentication | 401         |
| `FORBIDDEN`                     | Not authorized for this operation | 403         |
| `PERSISTED_QUERY_NOT_FOUND`     | APQ hash not found                | 404         |
| `PERSISTED_QUERY_NOT_SUPPORTED` | Server doesn't support APQ        | 400         |
| `OPERATION_RESOLUTION_FAILURE`  | Operation couldn't be determined  | 400         |
| `BAD_REQUEST`                   | Invalid request format            | 400         |
| `INTERNAL_SERVER_ERROR`         | Unexpected server error           | 500         |

### Custom Error Classes

```typescript
import { GraphQLError } from "graphql";

export class NotFoundError extends GraphQLError {
  constructor(resource: string, id: string) {
    super(`${resource} with id '${id}' not found`, {
      extensions: {
        code: "NOT_FOUND",
        resource,
        id,
      },
    });
  }
}

export class AuthenticationError extends GraphQLError {
  constructor(message = "Not authenticated") {
    super(message, {
      extensions: {
        code: "UNAUTHENTICATED",
        http: { status: 401 },
      },
    });
  }
}

export class ForbiddenError extends GraphQLError {
  constructor(message = "Not authorized") {
    super(message, {
      extensions: {
        code: "FORBIDDEN",
        http: { status: 403 },
      },
    });
  }
}

export class ValidationError extends GraphQLError {
  constructor(message: string, field: string) {
    super(message, {
      extensions: {
        code: "BAD_USER_INPUT",
        field,
      },
    });
  }
}

// Usage
throw new NotFoundError("User", id);
throw new AuthenticationError();
throw new ForbiddenError("Admin access required");
throw new ValidationError("Email is invalid", "email");
```

### Setting HTTP Status

```typescript
throw new GraphQLError("Not authenticated", {
  extensions: {
    code: "UNAUTHENTICATED",
    http: {
      status: 401,
      headers: new Map([["WWW-Authenticate", "Bearer"]]),
    },
  },
});
```

## formatError Option

Use `formatError` to transform errors before sending to clients.

### Basic Formatting

```typescript
const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (formattedError, error) => {
    // formattedError: already formatted GraphQL error
    // error: original error (may be wrapped)

    console.error("GraphQL Error:", error);

    return formattedError;
  },
});
```

### Masking Internal Errors

```typescript
const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (formattedError, error) => {
    // Log full error for debugging
    console.error(error);

    // Don't expose internal server errors
    if (formattedError.extensions?.code === "INTERNAL_SERVER_ERROR") {
      return {
        message: "An internal error occurred",
        extensions: {
          code: "INTERNAL_SERVER_ERROR",
        },
      };
    }

    // Remove stacktrace in production
    if (process.env.NODE_ENV === "production") {
      delete formattedError.extensions?.stacktrace;
    }

    return formattedError;
  },
});
```

### Adding Request Context

```typescript
const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (formattedError, error) => {
    // Add request ID for support
    return {
      ...formattedError,
      extensions: {
        ...formattedError.extensions,
        requestId: crypto.randomUUID(),
        timestamp: new Date().toISOString(),
      },
    };
  },
});
```

### Error Classification

```typescript
const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (formattedError, error) => {
    const code = formattedError.extensions?.code;

    // Client errors - return as-is
    const clientCodes = ["BAD_USER_INPUT", "UNAUTHENTICATED", "FORBIDDEN", "NOT_FOUND"];

    if (clientCodes.includes(code as string)) {
      return formattedError;
    }

    // Server errors - mask details
    return {
      message: "Something went wrong",
      extensions: {
        code: "INTERNAL_SERVER_ERROR",
      },
    };
  },
});
```

## Production Error Handling

### Logging Strategy

```typescript
import { ApolloServerPlugin } from "@apollo/server";

const errorLoggingPlugin: ApolloServerPlugin = {
  async requestDidStart() {
    return {
      async didEncounterErrors({ errors, request, contextValue }) {
        for (const error of errors) {
          const code = error.extensions?.code;

          // Log all errors with context
          const logEntry = {
            message: error.message,
            code,
            path: error.path,
            operationName: request.operationName,
            userId: contextValue.user?.id,
            timestamp: new Date().toISOString(),
          };

          // Different log levels based on error type
          if (code === "INTERNAL_SERVER_ERROR") {
            console.error("Server Error:", logEntry, error.originalError);
          } else if (code === "UNAUTHENTICATED" || code === "FORBIDDEN") {
            console.warn("Auth Error:", logEntry);
          } else {
            console.info("Client Error:", logEntry);
          }
        }
      },
    };
  },
};
```

### Error Reporting Service

```typescript
import * as Sentry from "@sentry/node";

const sentryPlugin: ApolloServerPlugin<MyContext> = {
  async requestDidStart({ request, contextValue }) {
    return {
      async didEncounterErrors({ errors }) {
        for (const error of errors) {
          // Only report server errors
          if (error.extensions?.code === "INTERNAL_SERVER_ERROR") {
            Sentry.withScope((scope) => {
              scope.setTag("kind", "graphql");
              scope.setExtra("query", request.query);
              scope.setExtra("variables", request.variables);
              scope.setUser({ id: contextValue.user?.id });

              Sentry.captureException(error.originalError ?? error);
            });
          }
        }
      },
    };
  },
};
```

### Partial Data with Errors

GraphQL can return partial data alongside errors:

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

// Query
query {
  user(id: "1") { name }
  posts { title }
}

// If user resolver throws but posts succeeds:
{
  "errors": [
    {
      "message": "User not found",
      "path": ["user"]
    }
  ],
  "data": {
    "user": null,
    "posts": [
      { "title": "Post 1" },
      { "title": "Post 2" }
    ]
  }
}
```

### Error Recovery Patterns

```typescript
const resolvers = {
  Query: {
    // Return null for optional fields instead of throwing
    user: async (_, { id }, { dataSources }) => {
      try {
        return await dataSources.usersAPI.getById(id);
      } catch (e) {
        console.error("Failed to fetch user:", e);
        return null;
      }
    },

    // Return empty array for list fields
    users: async (_, __, { dataSources }) => {
      try {
        return await dataSources.usersAPI.getAll();
      } catch (e) {
        console.error("Failed to fetch users:", e);
        return [];
      }
    },
  },

  User: {
    // Handle failures in field resolvers gracefully
    posts: async (parent, _, { dataSources }) => {
      try {
        return await dataSources.postsAPI.getByAuthor(parent.id);
      } catch (e) {
        console.error("Failed to fetch posts:", e);
        return [];
      }
    },
  },
};
```

### Validation Errors

```typescript
import { z } from "zod";

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
  age: z.number().int().positive().optional(),
});

const resolvers = {
  Mutation: {
    createUser: async (_, { input }, { dataSources }) => {
      const result = CreateUserSchema.safeParse(input);

      if (!result.success) {
        const errors = result.error.issues.map((issue) => ({
          field: issue.path.join("."),
          message: issue.message,
        }));

        throw new GraphQLError("Validation failed", {
          extensions: {
            code: "BAD_USER_INPUT",
            validationErrors: errors,
          },
        });
      }

      return dataSources.usersAPI.create(result.data);
    },
  },
};
```

```

### references/troubleshooting.md

```markdown
# Troubleshooting Reference

## Table of Contents

- [Setup Issues](#setup-issues)
- [Schema Issues](#schema-issues)
- [Runtime Errors](#runtime-errors)
- [Performance Issues](#performance-issues)
- [Integration Issues](#integration-issues)
- [Debugging Tips](#debugging-tips)

## Setup Issues

### Module Not Found Errors

**Error:** `Cannot find module '@apollo/server'`

```bash
# Ensure correct packages are installed
npm install @apollo/server graphql

# For Express integration
npm install @apollo/server express graphql cors

# Clear node_modules and reinstall if issues persist
rm -rf node_modules package-lock.json
npm install
```

**Error:** `Cannot find module '@apollo/server/standalone'`

This is a subpath export. Ensure:

- Node.js v18+ (for native ESM subpath exports)
- TypeScript `moduleResolution` is `bundler`, `node16`, or `nodenext`

### TypeScript Configuration

**Error:** `Cannot use import statement outside a module`

```json
// package.json
{
  "type": "module"
}

// tsconfig.json
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "target": "ES2022"
  }
}
```

**Error:** `Property 'xxx' does not exist on type 'BaseContext'`

```typescript
// Define and use typed context
interface MyContext {
  user?: User;
  dataSources: DataSources;
}

const server = new ApolloServer<MyContext>({ typeDefs, resolvers });
```

### CommonJS Compatibility

Apollo Server 4 is ESM-first. For CommonJS projects:

```typescript
// Use dynamic import
const { ApolloServer } = await import('@apollo/server');

// Or configure tsconfig for interop
{
  "compilerOptions": {
    "module": "CommonJS",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  }
}
```

## Schema Issues

### Unknown Type Error

**Error:** `Unknown type "User". Did you mean...`

```typescript
// Ensure all types are defined in typeDefs
const typeDefs = `#graphql
  type Query {
    user(id: ID!): User  # User must be defined
  }

  type User {  # Define the type
    id: ID!
    name: String!
  }
`;
```

### Missing Resolver

**Error:** `Cannot return null for non-nullable field Query.user`

```typescript
// Schema declares non-null
type Query {
  user(id: ID!): User!  # ! means non-null
}

// Resolver must return a value
const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      const user = await dataSources.usersAPI.getById(id);
      if (!user) {
        throw new GraphQLError('User not found');  // Throw, don't return null
      }
      return user;
    },
  },
};
```

### Enum Mismatch

**Error:** `Enum "Status" cannot represent value: "draft"`

```typescript
// Schema defines uppercase
enum Status {
  DRAFT
  PUBLISHED
}

// But database returns lowercase
// Solution: Map enum values
const resolvers = {
  Status: {
    DRAFT: 'draft',
    PUBLISHED: 'published',
  },
  // Or transform in resolver
  Post: {
    status: (parent) => parent.status.toUpperCase(),
  },
};
```

### Input Type Errors

**Error:** `Variable "$input" got invalid value`

```graphql
# Schema
input CreateUserInput {
  email: String!
  name: String!
}

# Query - ensure variable matches input type exactly
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) { id }
}

# Variables - must match schema structure
{
  "input": {
    "email": "[email protected]",
    "name": "Test User"
  }
}
```

## Runtime Errors

### Context Undefined

**Error:** `Cannot read properties of undefined (reading 'user')`

```typescript
// Ensure context function returns complete object
const context = async ({ req }) => {
  // Always return all expected properties
  return {
    user: (await getUser(req.headers.authorization)) ?? null,
    dataSources: {
      usersAPI: new UsersAPI(),
    },
  };
};
```

### Async/Await Issues

**Error:** `[object Promise]` returned instead of actual data

```typescript
// Bad - missing await
const resolvers = {
  Query: {
    user: (_, { id }, { dataSources }) => {
      dataSources.usersAPI.getById(id); // Missing return/await
    },
  },
};

// Good - return promise or use async/await
const resolvers = {
  Query: {
    user: (_, { id }, { dataSources }) => {
      return dataSources.usersAPI.getById(id); // Return promise
    },
    // OR
    user: async (_, { id }, { dataSources }) => {
      return await dataSources.usersAPI.getById(id); // Async/await
    },
  },
};
```

### Circular Reference

**Error:** `Converting circular structure to JSON`

```typescript
// Avoid returning raw ORM objects with circular refs
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      const user = await prisma.user.findUnique({
        where: { id },
        include: { posts: { include: { author: true } } }, // Circular!
      });

      // Transform to plain object
      return {
        id: user.id,
        name: user.name,
        posts: user.posts.map((p) => ({ id: p.id, title: p.title })),
      };
    },
  },
};
```

## Performance Issues

### N+1 Queries

**Symptom:** Slow queries, many database calls

```typescript
// Problem: Each user triggers separate posts query
const resolvers = {
  User: {
    posts: (parent) => db.posts.findByAuthor(parent.id), // N queries
  },
};

// Solution: Use DataLoader
import DataLoader from "dataloader";

const context = async () => ({
  loaders: {
    postsByAuthor: new DataLoader(async (authorIds) => {
      const posts = await db.posts.findByAuthorIds(authorIds); // 1 query
      return authorIds.map((id) => posts.filter((p) => p.authorId === id));
    }),
  },
});

const resolvers = {
  User: {
    posts: (parent, _, { loaders }) => loaders.postsByAuthor.load(parent.id),
  },
};
```

### Memory Leaks

**Symptom:** Memory usage grows over time

```typescript
// Problem: Shared data source instances
const sharedAPI = new UsersAPI(); // Wrong!
const context = async () => ({ dataSources: { usersAPI: sharedAPI } });

// Solution: Create per-request instances
const context = async () => ({
  dataSources: {
    usersAPI: new UsersAPI(), // New instance per request
  },
});

// Problem: DataLoader created once
const loader = new DataLoader(batchFn); // Wrong - caches forever!

// Solution: Create per-request DataLoaders
const context = async () => ({
  loaders: {
    userLoader: new DataLoader(batchFn), // New instance per request
  },
});
```

### Large Response Handling

**Symptom:** Timeout or memory errors on large queries

```typescript
// Add pagination
type Query {
  users(limit: Int = 10, offset: Int = 0): [User!]!
}

// Limit query depth
import depthLimit from 'graphql-depth-limit';

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

// Add query complexity limit
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [createComplexityLimitRule(1000)],
});
```

## Integration Issues

### CORS Errors

**Error:** `Access-Control-Allow-Origin` header missing

```typescript
import cors from "cors";

// Express integration - add cors before middleware
app.use(
  "/graphql",
  cors({
    origin: ["http://localhost:3000", "https://myapp.com"],
    credentials: true,
  }),
  express.json(),
  expressMiddleware(server),
);

// Standalone - configure cors option
const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  context: async ({ req }) => ({
    /* ... */
  }),
  // Standalone has basic CORS enabled by default
});
```

### Body Parser Issues

**Error:** `req.body` is undefined or empty

```typescript
// Express - ensure json middleware is before Apollo
app.use(express.json()); // Must come before expressMiddleware

app.use(
  "/graphql",
  cors(),
  express.json(), // JSON parser is required
  expressMiddleware(server),
);
```

### WebSocket / Subscriptions

**Error:** Subscriptions not working

```typescript
// Apollo Server 4 doesn't include subscription support by default
// Use graphql-ws package
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { makeExecutableSchema } from "@graphql-tools/schema";

const schema = makeExecutableSchema({ typeDefs, resolvers });

const wsServer = new WebSocketServer({
  server: httpServer,
  path: "/graphql",
});

useServer({ schema }, wsServer);
```

## Debugging Tips

### Enable Debug Logging

```typescript
const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    {
      async requestDidStart({ request }) {
        console.log("Query:", request.query);
        console.log("Variables:", JSON.stringify(request.variables, null, 2));

        return {
          async willSendResponse({ response }) {
            console.log("Response:", JSON.stringify(response.body, null, 2));
          },
        };
      },
    },
  ],
});
```

### Inspect Context

```typescript
const resolvers = {
  Query: {
    debug: (_, __, context) => {
      console.log("Context keys:", Object.keys(context));
      console.log("User:", context.user);
      return "Check server logs";
    },
  },
};
```

### Test Resolvers Directly

```typescript
// Unit test resolvers without server
import { resolvers } from "./resolvers";

describe("Query.user", () => {
  it("returns user by id", async () => {
    const mockDataSources = {
      usersAPI: {
        getById: jest.fn().mockResolvedValue({ id: "1", name: "Test" }),
      },
    };

    const result = await resolvers.Query.user(undefined, { id: "1" }, { dataSources: mockDataSources });

    expect(result).toEqual({ id: "1", name: "Test" });
  });
});
```

### Check Schema

```typescript
import { printSchema } from "graphql";
import { makeExecutableSchema } from "@graphql-tools/schema";

const schema = makeExecutableSchema({ typeDefs, resolvers });
console.log(printSchema(schema));
```

### Apollo Sandbox

Enable Apollo Sandbox for interactive debugging:

```typescript
import { ApolloServerPluginLandingPageLocalDefault } from "@apollo/server/plugin/landingPage/default";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
});
```

Open `http://localhost:4000/graphql` in browser to access Sandbox.

```

apollo-server | SkillHub