Claude Skill

api-graphql-apollo-server

GraphQL API server with Apollo Server — schema, resolvers, context, error handling, data sources, plugins

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-graphql-apollo-server_skills_api-graphql-apollo-server-3a51ef5.zip · 20 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-graphql-apollo-server/skills/api-graphql-apollo-server
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

GraphQL API with Apollo Server

Quick Guide: Use @apollo/server (v5) for schema-first GraphQL APIs. Define schemas with SDL (typeDefs), implement field population with resolvers, share per-request state via the context function, and handle errors with GraphQLError + extension codes. Use startStandaloneServer for quick setups or integrate with your HTTP framework for production. DataLoader solves the N+1 problem. Plugins hook into the request lifecycle for logging, auth, and performance.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST import from @apollo/server -- NOT the deprecated apollo-server or apollo-server-express packages)

(You MUST create new data source and DataLoader instances per request in the context function -- sharing across requests causes data leaks)

(You MUST throw GraphQLError (from graphql) with extension codes for client-facing errors -- generic Error exposes stack traces)

(You MUST use ApolloServerPluginDrainHttpServer when integrating with an HTTP framework -- without it the server doesn't shut down gracefully)

</critical_requirements>


Auto-detection: Apollo Server, @apollo/server, ApolloServer, startStandaloneServer, expressMiddleware, GraphQLError, typeDefs, resolvers, contextValue, DataLoader, RESTDataSource, @apollo/datasource-rest, ApolloServerPlugin, buildSubgraphSchema, @apollo/subgraph, graphql-ws, PubSub, formatError, gql tag

When to use:

  • Building a GraphQL API with schema-first (SDL) design
  • Defining typed resolvers with shared context (auth, data sources)
  • Wrapping REST APIs or databases behind a unified GraphQL layer
  • Implementing real-time features with subscriptions (via graphql-ws)
  • Building federated subgraphs with @apollo/subgraph
  • Adding lifecycle hooks with plugins (logging, auth, tracing)

When NOT to use:

  • Simple REST APIs without nested data relationships (a REST framework is simpler)
  • APIs consumed only by one client you control with no query flexibility needs
  • Performance-critical APIs where schema overhead matters (consider a code-first approach)

Key patterns covered:

  • Server setup with startStandaloneServer and framework middleware integration
  • Resolver structure, arguments (parent, args, contextValue, info), and chains
  • Context function for per-request state (auth tokens, data sources, DataLoaders)
  • Error handling with GraphQLError, built-in codes, and formatError
  • RESTDataSource for wrapping REST APIs with caching and deduplication
  • DataLoader for batching and deduplication (N+1 problem)
  • Custom plugins with server-level and request-level lifecycle hooks
  • Subscriptions with graphql-ws and WebSocket server
  • Federation subgraph setup with @apollo/subgraph

Detailed Resources:




<red_flags>

RED FLAGS

High Priority:

  • Importing from deprecated packages (apollo-server, apollo-server-express) -- use @apollo/server exclusively
  • Sharing data source or DataLoader instances across requests -- causes stale data and cross-user leaks; create new instances in the context function
  • Throwing generic Error from resolvers -- exposes stack traces to clients; use GraphQLError with extension codes
  • Missing ApolloServerPluginDrainHttpServer when using framework integration -- server won't shut down gracefully, leaving connections hanging
  • Calling expressMiddleware before server.start() -- throws an error; start() must complete first

Medium Priority:

  • Using c.req.params instead of resolver args -- bypasses GraphQL argument validation
  • No pagination limits on list resolvers -- returns entire datasets; always enforce max limits
  • In-memory PubSub in production -- only works for a single server instance; use a distributed system
  • Missing encodeURIComponent on dynamic URL segments in RESTDataSource -- path traversal vulnerability
  • Not using formatError to sanitize errors -- internal error messages and stack traces leak to clients in development mode

Gotchas & Edge Cases:

  • Default resolvers: Apollo Server auto-resolves fields matching property names on the parent object -- you don't need explicit resolvers for simple property access
  • contextValue is shared: Never destructively modify contextValue in resolvers -- other resolvers in the same operation share the object
  • Resolver return value of undefined: Triggers the default resolver to try accessing a property on the parent -- may cause unexpected behavior if the parent doesn't have that field
  • Introspection disabled in production: By default, introspection is off when NODE_ENV=production -- override with introspection: true if needed
  • Variable coercion errors: In v5, malformed variables return HTTP 400 by default (v4 returned 200) -- existing clients may need updates
  • Express integration in v5: Import expressMiddleware from @as-integrations/express4 or @as-integrations/express5 (not from @apollo/server/express4 which was removed)
  • Subscription resolvers: Must return an AsyncIterator from subscribe, not a direct value -- the resolve function (optional) transforms the event payload
  • Plugin lifecycle: All plugin methods are async except willResolveField and schemaDidLoadOrUpdate

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST import from @apollo/server -- NOT the deprecated apollo-server or apollo-server-express packages)

(You MUST create new data source and DataLoader instances per request in the context function -- sharing across requests causes data leaks)

(You MUST throw GraphQLError (from graphql) with extension codes for client-facing errors -- generic Error exposes stack traces)

(You MUST use ApolloServerPluginDrainHttpServer when integrating with an HTTP framework -- without it the server doesn't shut down gracefully)

Failure to follow these rules will cause data leaks between users, expose internal errors to clients, and prevent graceful shutdown.

</critical_reminders>

Files (skills)
  • examples
    • advanced.md 13.4 KB
      # Apollo Server - Advanced Patterns
      
      > Subscriptions, federation, and custom plugins. See [SKILL.md](../SKILL.md) for concepts and [core.md](core.md) for server setup.
      
      **Additional Examples:**
      
      - [core.md](core.md) - Server setup, resolvers, context, error handling
      - [data-sources.md](data-sources.md) - RESTDataSource, DataLoader, caching
      
      ---
      
      ## Pattern 1: Subscriptions with graphql-ws
      
      ### Good Example - WebSocket server alongside HTTP
      
      ```typescript
      import { ApolloServer } from "@apollo/server";
      import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
      import { expressMiddleware } from "@as-integrations/express4";
      import { makeExecutableSchema } from "@graphql-tools/schema";
      import { WebSocketServer } from "ws";
      import { useServer } from "graphql-ws/use/ws";
      import { PubSub } from "graphql-subscriptions";
      import express from "express";
      import http from "http";
      import cors from "cors";
      
      // PubSub instance -- in-memory, dev only
      // Production: use a distributed pub/sub system (Redis, Kafka, etc.)
      const pubsub = new PubSub();
      
      const POST_CREATED = "POST_CREATED";
      
      const typeDefs = `#graphql
        type Post {
          id: ID!
          title: String!
          content: String!
        }
      
        type Query {
          posts: [Post!]!
        }
      
        type Mutation {
          createPost(title: String!, content: String!): Post!
        }
      
        type Subscription {
          postCreated: Post!
        }
      `;
      
      const resolvers = {
        Query: {
          posts: async (
            _parent: undefined,
            _args: Record<string, never>,
            ctx: MyContext,
          ) => {
            return ctx.dataSources.postsAPI.getAll();
          },
        },
        Mutation: {
          createPost: async (
            _parent: undefined,
            args: { title: string; content: string },
            ctx: MyContext,
          ) => {
            const post = await ctx.dataSources.postsAPI.create(args);
      
            // Publish event to subscribers
            await pubsub.publish(POST_CREATED, { postCreated: post });
      
            return post;
          },
        },
        Subscription: {
          postCreated: {
            // subscribe returns an AsyncIterator
            subscribe: () => pubsub.asyncIterator([POST_CREATED]),
            // resolve is optional -- transforms the published payload before sending
          },
        },
      };
      
      // Build executable schema (required for graphql-ws)
      const schema = makeExecutableSchema({ typeDefs, resolvers });
      
      const app = express();
      const httpServer = http.createServer(app);
      
      // WebSocket server for subscriptions
      const WS_PATH = "/subscriptions";
      
      const wsServer = new WebSocketServer({
        server: httpServer,
        path: WS_PATH,
      });
      
      const serverCleanup = useServer({ schema }, wsServer);
      
      // Apollo Server with drain plugins for both HTTP and WS
      const server = new ApolloServer<MyContext>({
        schema,
        plugins: [
          // Drain HTTP connections on shutdown
          ApolloServerPluginDrainHttpServer({ httpServer }),
          // Drain WebSocket connections on shutdown
          {
            async serverWillStart() {
              return {
                async drainServer() {
                  await serverCleanup.dispose();
                },
              };
            },
          },
        ],
      });
      
      await server.start();
      
      app.use(
        "/graphql",
        cors<cors.CorsRequest>(),
        express.json(),
        expressMiddleware(server, {
          context: async ({ req }) => ({
            dataSources: { postsAPI: new PostsAPI({ cache: server.cache }) },
          }),
        }),
      );
      
      const DEFAULT_PORT = 4000;
      
      await new Promise<void>((resolve) => {
        httpServer.listen({ port: DEFAULT_PORT }, resolve);
      });
      ```
      
      **Why good:** separate drain plugins for HTTP and WebSocket, event name is a named constant, `makeExecutableSchema` creates schema usable by both Apollo Server and graphql-ws, subscription resolver returns `AsyncIterator`
      
      ### Bad Example - Missing drain and in-memory PubSub in production
      
      ```typescript
      // BAD: no drain plugin for WebSocket server
      const server = new ApolloServer({
        schema,
        plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
        // Missing WebSocket drain!
      });
      
      // BAD: startStandaloneServer does NOT support subscriptions
      const { url } = await startStandaloneServer(server);
      
      // BAD: in-memory PubSub doesn't work across multiple server instances
      const pubsub = new PubSub(); // Fine for dev, breaks in production
      ```
      
      **Why bad:** WebSocket connections hang on shutdown without drain plugin, `startStandaloneServer` has no WebSocket support, in-memory PubSub loses events across instances
      
      ---
      
      ## Pattern 2: Subscription with Filtering
      
      ### Good Example - Filtered subscriptions
      
      ```typescript
      import { withFilter } from "graphql-subscriptions";
      
      const COMMENT_ADDED = "COMMENT_ADDED";
      
      const resolvers = {
        Subscription: {
          // Filter: only send events for the post the client is watching
          commentAdded: {
            subscribe: withFilter(
              () => pubsub.asyncIterator([COMMENT_ADDED]),
              (payload, variables) => {
                // Only send to subscribers watching this specific post
                return payload.commentAdded.postId === variables.postId;
              },
            ),
          },
        },
      };
      
      // Schema: subscription field with argument
      // type Subscription {
      //   commentAdded(postId: ID!): Comment!
      // }
      ```
      
      **Why good:** `withFilter` prevents sending events to unrelated subscribers, filter function compares payload to subscription variables, reduces unnecessary WebSocket traffic
      
      ---
      
      ## Pattern 3: Custom Plugins
      
      ### Good Example - Request logging plugin
      
      ```typescript
      import type { ApolloServerPlugin } from "@apollo/server";
      
      function requestLoggingPlugin(): ApolloServerPlugin<MyContext> {
        return {
          async requestDidStart(requestContext) {
            const operationName = requestContext.request.operationName ?? "anonymous";
            const start = Date.now();
      
            return {
              // Log parsing errors
              async parsingDidStart() {
                return async (err) => {
                  if (err) {
                    console.error(`Parse error in ${operationName}:`, err);
                  }
                };
              },
      
              // Log validation errors
              async validationDidStart() {
                return async (errors) => {
                  if (errors) {
                    console.error(`Validation errors in ${operationName}:`, errors);
                  }
                };
              },
      
              // Log all encountered errors
              async didEncounterErrors(ctx) {
                for (const err of ctx.errors) {
                  console.error(
                    `Error in ${operationName}:`,
                    err.message,
                    err.extensions,
                  );
                }
              },
      
              // Log operation completion
              async willSendResponse() {
                const duration = Date.now() - start;
                console.log(`${operationName} completed in ${duration}ms`);
              },
            };
          },
        };
      }
      
      // Register plugin
      const server = new ApolloServer<MyContext>({
        typeDefs,
        resolvers,
        plugins: [requestLoggingPlugin()],
      });
      ```
      
      **Why good:** typed with `ApolloServerPlugin<MyContext>`, end hooks (returned functions) capture errors from parsing/validation, timing measured from request start to response, wrapped in factory function for reuse
      
      ### Good Example - Depth limiting plugin
      
      ```typescript
      import type { ApolloServerPlugin } from "@apollo/server";
      import { GraphQLError } from "graphql";
      
      const MAX_QUERY_DEPTH = 10;
      
      function depthLimitPlugin(
        maxDepth = MAX_QUERY_DEPTH,
      ): ApolloServerPlugin<MyContext> {
        return {
          async requestDidStart() {
            return {
              async didResolveOperation(requestContext) {
                const { document } = requestContext;
                const depth = calculateQueryDepth(document);
      
                if (depth > maxDepth) {
                  throw new GraphQLError(
                    `Query depth ${depth} exceeds maximum of ${maxDepth}`,
                    {
                      extensions: { code: "QUERY_TOO_DEEP", depth, maxDepth },
                    },
                  );
                }
              },
            };
          },
        };
      }
      ```
      
      **Why good:** named constant for max depth, plugin factory accepts configuration, throws `GraphQLError` with structured extensions, runs after operation is resolved but before execution
      
      ---
      
      ## Pattern 4: Server Lifecycle Plugin
      
      ### Good Example - Graceful startup and shutdown
      
      ```typescript
      import type { ApolloServerPlugin } from "@apollo/server";
      
      function lifecyclePlugin(): ApolloServerPlugin<MyContext> {
        return {
          // Runs when server starts
          async serverWillStart() {
            console.log("Apollo Server starting...");
      
            // Return object with shutdown hooks
            return {
              // Runs when server begins shutdown
              async drainServer() {
                console.log("Draining server connections...");
                // Close external connections (Redis, message queues, etc.)
              },
      
              // Runs after all connections are drained
              async serverWillStop() {
                console.log("Server stopped.");
              },
            };
          },
      
          // schemaDidLoadOrUpdate is synchronous (not async)
          schemaDidLoadOrUpdate(schemaContext) {
            console.log("Schema loaded/updated");
          },
        };
      }
      ```
      
      **Why good:** demonstrates the full server lifecycle, `drainServer` handles cleanup before shutdown, `schemaDidLoadOrUpdate` is correctly synchronous (only lifecycle hook that is)
      
      ---
      
      ## Pattern 5: Federation Subgraph
      
      ### Good Example - Subgraph with entity resolution
      
      ```typescript
      import { ApolloServer } from "@apollo/server";
      import { buildSubgraphSchema } from "@apollo/subgraph";
      import gql from "graphql-tag";
      
      // Federation 2 schema with @link directive
      const typeDefs = gql`
        extend schema
          @link(
            url: "https://specs.apollo.dev/federation/v2.0"
            import: ["@key", "@external", "@requires"]
          )
      
        type Product @key(fields: "id") {
          id: ID!
          name: String!
          price: Float!
          inStock: Boolean!
        }
      
        type Query {
          products: [Product!]!
          product(id: ID!): Product
        }
      `;
      
      const resolvers = {
        Query: {
          products: async (
            _parent: undefined,
            _args: Record<string, never>,
            ctx: MyContext,
          ) => {
            return ctx.dataSources.productsAPI.getAll();
          },
          product: async (
            _parent: undefined,
            args: { id: string },
            ctx: MyContext,
          ) => {
            return ctx.dataSources.productsAPI.getById(args.id);
          },
        },
        Product: {
          // Reference resolver: called by the gateway to resolve entities by @key fields
          __resolveReference: async (reference: { id: string }, ctx: MyContext) => {
            return ctx.dataSources.productsAPI.getById(reference.id);
          },
        },
      };
      
      const server = new ApolloServer({
        schema: buildSubgraphSchema({ typeDefs, resolvers }),
      });
      ```
      
      **Why good:** Federation 2 `@link` directive, `@key` designates entity identity, `__resolveReference` fetches entities by key for cross-subgraph resolution, `buildSubgraphSchema` adds federation metadata
      
      ### Good Example - Contributing to another subgraph's type
      
      ```typescript
      // Reviews subgraph extends Product from Products subgraph
      const typeDefs = gql`
        extend schema
          @link(
            url: "https://specs.apollo.dev/federation/v2.0"
            import: ["@key", "@external"]
          )
      
        type Review {
          id: ID!
          rating: Int!
          comment: String!
          product: Product!
        }
      
        # Extend Product from another subgraph
        type Product @key(fields: "id") {
          id: ID! @external
          reviews: [Review!]!
        }
      
        type Query {
          reviews: [Review!]!
        }
      `;
      
      const resolvers = {
        Product: {
          reviews: async (
            parent: { id: string },
            _args: Record<string, never>,
            ctx: MyContext,
          ) => {
            return ctx.dataSources.reviewsAPI.getByProductId(parent.id);
          },
        },
      };
      ```
      
      **Why good:** `@external` marks `id` as owned by another subgraph, `reviews` field is contributed by this subgraph, gateway composes both subgraphs into a unified API
      
      ---
      
      ## Pattern 6: Custom Directives
      
      ### Good Example - Schema directive for field-level authorization
      
      ```typescript
      import { mapSchema, getDirective, MapperKind } from "@graphql-tools/utils";
      import { GraphQLError } from "graphql";
      import type { GraphQLSchema } from "graphql";
      
      const AUTH_DIRECTIVE_NAME = "auth";
      
      // Schema directive definition
      // directive @auth(requires: Role = ADMIN) on FIELD_DEFINITION
      // enum Role { ADMIN USER }
      
      function authDirectiveTransformer(schema: GraphQLSchema): GraphQLSchema {
        return mapSchema(schema, {
          [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
            const authDirective = getDirective(
              schema,
              fieldConfig,
              AUTH_DIRECTIVE_NAME,
            )?.[0];
      
            if (authDirective) {
              const { requires: requiredRole } = authDirective;
              const { resolve: originalResolve } = fieldConfig;
      
              fieldConfig.resolve = async (source, args, contextValue, info) => {
                if (!contextValue.user) {
                  throw new GraphQLError("Authentication required", {
                    extensions: { code: "UNAUTHENTICATED" },
                  });
                }
                if (contextValue.user.role !== requiredRole) {
                  throw new GraphQLError(`Requires ${requiredRole} role`, {
                    extensions: { code: "FORBIDDEN", requiredRole },
                  });
                }
      
                return originalResolve
                  ? originalResolve(source, args, contextValue, info)
                  : source[info.fieldName];
              };
            }
      
            return fieldConfig;
          },
        });
      }
      
      // Apply directive to schema
      const schema = authDirectiveTransformer(
        makeExecutableSchema({ typeDefs, resolvers }),
      );
      
      const server = new ApolloServer({ schema });
      ```
      
      **Why good:** directive transformer uses `@graphql-tools/utils` (the standard approach), wraps original resolve to preserve behavior, falls back to default field resolution, auth checks before field execution
      
      ### Usage in schema
      
      ```graphql
      type Query {
        publicPosts: [Post!]!
        adminDashboard: Dashboard! @auth(requires: ADMIN)
        userProfile: Profile! @auth(requires: USER)
      }
      ```
      
    • core.md 13.3 KB
      # Apollo Server - Core Examples
      
      > Server setup, resolvers, context, and error handling. See [SKILL.md](../SKILL.md) for concepts and [reference.md](../reference.md) for decision frameworks.
      
      **Additional Examples:**
      
      - [data-sources.md](data-sources.md) - RESTDataSource, DataLoader, caching
      - [advanced.md](advanced.md) - Subscriptions, federation, custom plugins
      
      ---
      
      ## Pattern 1: Standalone Server Setup
      
      ### Good Example - Typed standalone server
      
      ```typescript
      import { ApolloServer } from "@apollo/server";
      import { startStandaloneServer } from "@apollo/server/standalone";
      
      // Schema definition using SDL
      const typeDefs = `#graphql
        type Book {
          id: ID!
          title: String!
          author: Author!
        }
      
        type Author {
          id: ID!
          name: String!
          books: [Book!]!
        }
      
        type Query {
          books: [Book!]!
          book(id: ID!): Book
          authors: [Author!]!
        }
      
        type Mutation {
          addBook(title: String!, authorId: ID!): Book!
        }
      `;
      
      // Context type shared across all resolvers
      interface MyContext {
        token: string | undefined;
        dataSources: {
          booksAPI: BooksAPI;
          authorsAPI: AuthorsAPI;
        };
      }
      
      // Pass context type as generic for type safety
      const server = new ApolloServer<MyContext>({ typeDefs, resolvers });
      
      const DEFAULT_PORT = 4000;
      
      const { url } = await startStandaloneServer(server, {
        context: async ({ req }) => {
          const token = req.headers.authorization;
          const { cache } = server;
          return {
            token,
            dataSources: {
              booksAPI: new BooksAPI({ cache, token }),
              authorsAPI: new AuthorsAPI({ cache }),
            },
          };
        },
        listen: { port: DEFAULT_PORT },
      });
      
      console.log(`Server ready at ${url}`);
      ```
      
      **Why good:** generic type parameter ensures context type safety across resolvers, new data source instances per request prevent data leaks, named constant for port
      
      ### Bad Example - Shared instances and no types
      
      ```typescript
      import { ApolloServer } from "@apollo/server";
      import { startStandaloneServer } from "@apollo/server/standalone";
      
      // BAD: data sources created once and shared across all requests
      const booksAPI = new BooksAPI();
      const authorsAPI = new AuthorsAPI();
      
      const server = new ApolloServer({ typeDefs, resolvers }); // No context type
      
      const { url } = await startStandaloneServer(server, {
        context: async () => ({
          dataSources: { booksAPI, authorsAPI }, // BAD: shared instances
        }),
        listen: { port: 4000 }, // BAD: magic number
      });
      ```
      
      **Why bad:** shared data source instances leak cached data between requests and users, missing generic type means resolver context is `any`, magic port number
      
      ---
      
      ## Pattern 2: Framework Middleware Integration
      
      ### Good Example - Express integration with graceful shutdown
      
      ```typescript
      import { ApolloServer } from "@apollo/server";
      import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
      // v5: import from integration package (not @apollo/server/express4)
      import { expressMiddleware } from "@as-integrations/express4";
      import express from "express";
      import http from "http";
      import cors from "cors";
      
      interface MyContext {
        token: string | undefined;
        dataSources: { booksAPI: BooksAPI };
      }
      
      const app = express();
      const httpServer = http.createServer(app);
      
      const server = new ApolloServer<MyContext>({
        typeDefs,
        resolvers,
        plugins: [
          // REQUIRED: enables graceful shutdown
          ApolloServerPluginDrainHttpServer({ httpServer }),
        ],
      });
      
      // MUST call start() before using expressMiddleware
      await server.start();
      
      const GRAPHQL_PATH = "/graphql";
      const ALLOWED_ORIGINS = ["https://app.example.com"];
      
      app.use(
        GRAPHQL_PATH,
        cors<cors.CorsRequest>({ origin: ALLOWED_ORIGINS }),
        express.json(),
        expressMiddleware(server, {
          context: async ({ req }) => {
            const token = req.headers.authorization;
            const { cache } = server;
            return {
              token,
              dataSources: {
                booksAPI: new BooksAPI({ cache, token }),
              },
            };
          },
        }),
      );
      
      const DEFAULT_PORT = 4000;
      
      await new Promise<void>((resolve) => {
        httpServer.listen({ port: DEFAULT_PORT }, resolve);
      });
      
      console.log(`Server ready at http://localhost:${DEFAULT_PORT}${GRAPHQL_PATH}`);
      ```
      
      **Why good:** drain plugin enables graceful shutdown, `server.start()` called before middleware, CORS configured with specific origins, new data source per request
      
      ### Bad Example - Missing drain plugin and calling start too late
      
      ```typescript
      // BAD: no drain plugin
      const server = new ApolloServer({ typeDefs, resolvers });
      
      // BAD: using removed v4 import path
      import { expressMiddleware } from "@apollo/server/express4";
      
      // BAD: missing server.start() before middleware
      app.use("/graphql", expressMiddleware(server)); // Throws!
      
      // BAD: wildcard CORS with no configuration
      app.use(cors()); // Allows all origins
      ```
      
      **Why bad:** no drain plugin means connections hang on shutdown, v4 import path removed in v5, missing `start()` throws at runtime, wildcard CORS is a security risk
      
      ---
      
      ## Pattern 3: Resolver Patterns
      
      ### Good Example - Typed resolvers with parent chaining
      
      ```typescript
      import { GraphQLError } from "graphql";
      
      const resolvers = {
        Query: {
          // Top-level resolver: parent is undefined
          books: async (
            _parent: undefined,
            _args: Record<string, never>,
            contextValue: MyContext,
          ) => {
            return contextValue.dataSources.booksAPI.getAllBooks();
          },
      
          // Args typed from schema
          book: async (
            _parent: undefined,
            args: { id: string },
            contextValue: MyContext,
          ) => {
            const book = await contextValue.dataSources.booksAPI.getBook(args.id);
            if (!book) {
              throw new GraphQLError(`Book with id "${args.id}" not found`, {
                extensions: { code: "NOT_FOUND" },
              });
            }
            return book;
          },
        },
      
        // Field resolver: parent is the Book returned by Query.book
        Book: {
          author: async (
            parent: Book,
            _args: Record<string, never>,
            contextValue: MyContext,
          ) => {
            return contextValue.dataSources.authorsAPI.getAuthor(parent.authorId);
          },
        },
      
        // Field resolver: parent is the Author
        Author: {
          books: async (
            parent: Author,
            _args: Record<string, never>,
            contextValue: MyContext,
          ) => {
            return contextValue.dataSources.booksAPI.getBooksByAuthor(parent.id);
          },
        },
      
        Mutation: {
          addBook: async (
            _parent: undefined,
            args: { title: string; authorId: string },
            contextValue: MyContext,
          ) => {
            if (!contextValue.token) {
              throw new GraphQLError("Authentication required", {
                extensions: { code: "UNAUTHENTICATED" },
              });
            }
            return contextValue.dataSources.booksAPI.addBook(
              args.title,
              args.authorId,
            );
          },
        },
      };
      ```
      
      **Why good:** parent type matches return type of parent resolver, data access delegated to data sources, auth check before mutation, `GraphQLError` with codes for client-facing errors
      
      ### Bad Example - Fat resolvers with direct data access
      
      ```typescript
      const resolvers = {
        Query: {
          books: async () => {
            // BAD: direct DB call in resolver -- not testable, not reusable
            const result = await db.query("SELECT * FROM books");
            return result.rows;
          },
          book: async (_parent, args) => {
            const result = await db.query("SELECT * FROM books WHERE id = $1", [
              args.id,
            ]);
            if (!result.rows[0]) {
              // BAD: generic Error exposes stack trace
              throw new Error("Not found");
            }
            return result.rows[0];
          },
        },
      };
      ```
      
      **Why bad:** direct database calls make resolvers untestable and non-reusable, generic `Error` exposes internals to client, no context usage means no per-request isolation
      
      ---
      
      ## Pattern 4: Error Handling
      
      ### Good Example - Custom error codes and formatError
      
      ```typescript
      import { GraphQLError } from "graphql";
      import { ApolloServerErrorCode } from "@apollo/server/errors";
      import { unwrapResolverError } from "@apollo/server/errors";
      
      // Custom error codes for your domain
      const ErrorCode = {
        NOT_FOUND: "NOT_FOUND",
        UNAUTHENTICATED: "UNAUTHENTICATED",
        FORBIDDEN: "FORBIDDEN",
        VALIDATION_ERROR: "VALIDATION_ERROR",
        RATE_LIMITED: "RATE_LIMITED",
      } as const;
      
      // Reusable error factory
      function notFoundError(resource: string, id: string): GraphQLError {
        return new GraphQLError(`${resource} with id "${id}" not found`, {
          extensions: {
            code: ErrorCode.NOT_FOUND,
            resource,
            id,
          },
        });
      }
      
      function authError(message: string): GraphQLError {
        return new GraphQLError(message, {
          extensions: { code: ErrorCode.UNAUTHENTICATED },
        });
      }
      
      // formatError to sanitize errors before sending to clients
      const server = new ApolloServer<MyContext>({
        typeDefs,
        resolvers,
        formatError: (formattedError, error) => {
          // Unwrap resolver errors to access the original error
          const originalError = unwrapResolverError(error);
      
          // Log internal errors for debugging
          if (
            formattedError.extensions?.code ===
            ApolloServerErrorCode.INTERNAL_SERVER_ERROR
          ) {
            console.error("Internal error:", originalError);
          }
      
          // Never send stack traces to clients in production
          // (Apollo Server already strips them when NODE_ENV=production)
          return {
            message: formattedError.message,
            extensions: {
              code: formattedError.extensions?.code,
            },
          };
        },
      });
      ```
      
      **Why good:** named error code constants, reusable error factories, `formatError` sanitizes internal details, `unwrapResolverError` accesses original error for logging
      
      ### Bad Example - Leaking internal details
      
      ```typescript
      const resolvers = {
        Query: {
          user: async (_parent, args, contextValue) => {
            try {
              return await contextValue.dataSources.usersAPI.getUser(args.id);
            } catch (error) {
              // BAD: leaks database error message to client
              throw new Error(`Database query failed: ${error.message}`);
            }
          },
        },
      };
      ```
      
      **Why bad:** database error messages leak to clients, no error code for client-side handling, no structured extensions
      
      ---
      
      ## Pattern 5: Schema Organization
      
      ### Good Example - Modular schema with type composition
      
      ```typescript
      // types/book.ts - Schema fragment for books
      export const bookTypeDefs = `#graphql
        type Book {
          id: ID!
          title: String!
          publishedYear: Int!
          author: Author!
        }
      
        input CreateBookInput {
          title: String!
          publishedYear: Int!
          authorId: ID!
        }
      
        extend type Query {
          books(limit: Int, offset: Int): [Book!]!
          book(id: ID!): Book
        }
      
        extend type Mutation {
          createBook(input: CreateBookInput!): Book!
          deleteBook(id: ID!): Boolean!
        }
      `;
      
      // types/author.ts - Schema fragment for authors
      export const authorTypeDefs = `#graphql
        type Author {
          id: ID!
          name: String!
          books: [Book!]!
        }
      
        extend type Query {
          authors: [Author!]!
          author(id: ID!): Author
        }
      `;
      
      // schema.ts - Root schema assembles fragments
      const rootTypeDefs = `#graphql
        type Query
        type Mutation
      `;
      
      const server = new ApolloServer<MyContext>({
        // typeDefs accepts an array -- fragments are merged
        typeDefs: [rootTypeDefs, bookTypeDefs, authorTypeDefs],
        resolvers: [bookResolvers, authorResolvers],
      });
      ```
      
      **Why good:** each domain owns its schema fragment, `extend type` adds fields without modifying root types, `typeDefs` array merges fragments automatically, resolvers also merge from arrays
      
      ### Bad Example - Monolithic schema
      
      ```typescript
      // BAD: 500+ line SDL in a single string
      const typeDefs = `#graphql
        type Book { ... }
        type Author { ... }
        type Publisher { ... }
        type Review { ... }
        # ... 50 more types
        type Query {
          # ... 30 query fields
        }
        type Mutation {
          # ... 20 mutation fields
        }
      `;
      ```
      
      **Why bad:** single-file schema becomes unmaintainable, can't tell which team/module owns which types, merge conflicts in version control
      
      ---
      
      ## Pattern 6: Authentication in Context
      
      ### Good Example - Auth verification in context function
      
      ```typescript
      interface MyContext {
        user: User | null;
        dataSources: { usersAPI: UsersAPI };
      }
      
      const server = new ApolloServer<MyContext>({ typeDefs, resolvers });
      
      const { url } = await startStandaloneServer(server, {
        context: async ({ req }) => {
          const token = req.headers.authorization?.replace("Bearer ", "");
      
          // Verify token and get user (null if invalid/missing)
          let user: User | null = null;
          if (token) {
            try {
              user = await verifyAndDecodeToken(token);
            } catch {
              // Invalid token -- user stays null, resolvers decide what to allow
            }
          }
      
          return {
            user,
            dataSources: {
              usersAPI: new UsersAPI({ cache: server.cache }),
            },
          };
        },
      });
      
      // In resolvers: check auth where needed
      const resolvers = {
        Mutation: {
          deleteBook: async (
            _parent: undefined,
            args: { id: string },
            contextValue: MyContext,
          ) => {
            if (!contextValue.user) {
              throw new GraphQLError("Authentication required", {
                extensions: { code: "UNAUTHENTICATED" },
              });
            }
            if (contextValue.user.role !== "admin") {
              throw new GraphQLError("Admin access required", {
                extensions: { code: "FORBIDDEN" },
              });
            }
            return contextValue.dataSources.booksAPI.deleteBook(args.id);
          },
        },
      };
      ```
      
      **Why good:** context function handles token verification once per request, resolvers check `contextValue.user` for auth, invalid tokens don't crash the context function, role-based authorization at the resolver level
      
    • data-sources.md 10 KB
      # Apollo Server - Data Sources
      
      > RESTDataSource, DataLoader, and caching patterns. See [SKILL.md](../SKILL.md) for concepts and [core.md](core.md) for server setup.
      
      **Additional Examples:**
      
      - [core.md](core.md) - Server setup, resolvers, context, error handling
      - [advanced.md](advanced.md) - Subscriptions, federation, custom plugins
      
      ---
      
      ## Pattern 1: RESTDataSource
      
      ### Good Example - Typed REST API wrapper with caching
      
      ```typescript
      import { RESTDataSource, type AugmentedRequest } from "@apollo/datasource-rest";
      import type { KeyValueCache } from "@apollo/utils.keyvaluecache";
      
      interface Movie {
        id: string;
        title: string;
        releaseYear: number;
      }
      
      interface MoviesResponse {
        results: Movie[];
        total: number;
      }
      
      const DEFAULT_PAGE_SIZE = 20;
      
      class MoviesAPI extends RESTDataSource {
        override baseURL = "https://movies-api.example.com/v1/";
      
        // Add auth headers to all outgoing requests
        override willSendRequest(_path: string, request: AugmentedRequest) {
          request.headers.authorization = this.token;
        }
      
        private token: string;
      
        constructor(options: { cache: KeyValueCache; token: string }) {
          super(options); // passes cache to parent
          this.token = options.token;
        }
      
        // GET with query params
        async getMovies(
          page = 1,
          pageSize = DEFAULT_PAGE_SIZE,
        ): Promise<MoviesResponse> {
          return this.get<MoviesResponse>("movies", {
            params: {
              page: page.toString(),
              page_size: pageSize.toString(),
            },
          });
        }
      
        // GET with dynamic path segment -- ALWAYS encode user input
        async getMovie(id: string): Promise<Movie> {
          return this.get<Movie>(`movies/${encodeURIComponent(id)}`);
        }
      
        // POST with JSON body
        async createMovie(input: {
          title: string;
          releaseYear: number;
        }): Promise<Movie> {
          return this.post<Movie>("movies", {
            body: input,
          });
        }
      
        // PUT replaces entire resource
        async updateMovie(id: string, input: Partial<Movie>): Promise<Movie> {
          return this.put<Movie>(`movies/${encodeURIComponent(id)}`, {
            body: input,
          });
        }
      
        // PATCH for partial updates
        async patchMovie(id: string, input: Partial<Movie>): Promise<Movie> {
          return this.patch<Movie>(`movies/${encodeURIComponent(id)}`, {
            body: input,
          });
        }
      
        // DELETE
        async deleteMovie(id: string): Promise<void> {
          await this.delete(`movies/${encodeURIComponent(id)}`);
        }
      }
      ```
      
      **Why good:** typed return values, `willSendRequest` adds auth to every request, `encodeURIComponent` prevents path traversal, cache passed through constructor, named constant for page size
      
      ### Bad Example - Missing security and no types
      
      ```typescript
      class MoviesAPI extends RESTDataSource {
        override baseURL = "https://movies-api.example.com/v1/";
      
        // BAD: no encodeURIComponent -- path traversal vulnerability
        async getMovie(id: string) {
          return this.get(`movies/${id}`);
        }
      
        // BAD: no type parameter -- return type is unknown
        async getMovies() {
          return this.get("movies");
        }
      }
      ```
      
      **Why bad:** unencoded user input enables path traversal attacks, untyped responses require unsafe casts downstream
      
      ---
      
      ## Pattern 2: RESTDataSource Caching Control
      
      ### Good Example - Custom cache TTL and deduplication
      
      ```typescript
      import { RESTDataSource } from "@apollo/datasource-rest";
      import type { RequestDeduplicationPolicy } from "@apollo/datasource-rest";
      
      const CACHE_TTL_SECONDS = 300; // 5 minutes
      
      class CatalogAPI extends RESTDataSource {
        override baseURL = "https://catalog-api.example.com/";
      
        // Override HTTP cache TTL (ignores response cache-control headers)
        override cacheOptionsFor() {
          return { ttl: CACHE_TTL_SECONDS };
        }
      
        // Override cache key (default is method + URL)
        override cacheKeyFor(url: URL, request: RequestInit) {
          // Include auth header in cache key so different users get different results
          const auth =
            (request.headers as Record<string, string>)?.authorization ?? "anonymous";
          return `${request.method}:${url.toString()}:${auth}`;
        }
      
        // Control request deduplication behavior
        override requestDeduplicationPolicyFor(
          url: URL,
          request: RequestInit,
        ): RequestDeduplicationPolicy {
          if (request.method === "GET") {
            // Default: deduplicate concurrent GET requests
            return {
              policy: "deduplicate-during-request-lifetime",
              deduplicationKey: url.toString(),
            };
          }
          // Don't deduplicate mutations
          return { policy: "do-not-deduplicate" };
        }
      
        async getProductById(id: string): Promise<Product> {
          return this.get<Product>(`products/${encodeURIComponent(id)}`);
        }
      
        async getCategories(): Promise<Category[]> {
          return this.get<Category[]>("categories");
        }
      }
      ```
      
      **Why good:** explicit cache TTL, cache key includes auth for user-specific data, deduplication policy documented, named constant for TTL
      
      ---
      
      ## Pattern 3: DataLoader for N+1 Prevention
      
      ### Good Example - Batched loading in context
      
      ```typescript
      import DataLoader from "dataloader";
      
      // Batch function: receives array of keys, returns array of results in same order
      async function batchUsers(ids: readonly string[]): Promise<(User | Error)[]> {
        // Single query fetches all requested users
        const users = await db.users.findMany({
          where: { id: { in: [...ids] } },
        });
      
        // Map results to match input order -- DataLoader requires 1:1 correspondence
        const userMap = new Map(users.map((u) => [u.id, u]));
        return ids.map((id) => userMap.get(id) ?? new Error(`User ${id} not found`));
      }
      
      // Context factory -- new DataLoader per request
      interface MyContext {
        loaders: {
          userLoader: DataLoader<string, User>;
          postLoader: DataLoader<string, Post[]>;
        };
      }
      
      const server = new ApolloServer<MyContext>({ typeDefs, resolvers });
      
      const { url } = await startStandaloneServer(server, {
        context: async ({ req }) => ({
          loaders: {
            // CRITICAL: new instances per request -- DataLoader caches for request lifetime
            userLoader: new DataLoader<string, User>(batchUsers),
            postLoader: new DataLoader<string, Post[]>(batchPostsByAuthor),
          },
        }),
      });
      
      // In resolvers: use loader instead of direct calls
      const resolvers = {
        Post: {
          author: async (
            parent: Post,
            _args: Record<string, never>,
            contextValue: MyContext,
          ) => {
            // If 50 posts reference 10 unique authors, this makes 1 batch call, not 50
            return contextValue.loaders.userLoader.load(parent.authorId);
          },
        },
        Author: {
          posts: async (
            parent: Author,
            _args: Record<string, never>,
            contextValue: MyContext,
          ) => {
            return contextValue.loaders.postLoader.load(parent.id);
          },
        },
      };
      ```
      
      **Why good:** batch function fetches all users in one query, result mapping preserves DataLoader's required order, new loaders per request prevent stale data, type parameters ensure key/value types match
      
      ### Bad Example - Direct calls causing N+1
      
      ```typescript
      const resolvers = {
        Post: {
          // BAD: called once per post in the list -- N+1 queries
          author: async (parent, _args, contextValue) => {
            return contextValue.dataSources.usersAPI.getUser(parent.authorId);
          },
        },
      };
      // If query returns 50 posts, this makes 50 separate getUser calls
      ```
      
      **Why bad:** each post triggers a separate database/API call, query with 50 posts makes 51 total calls (1 for posts + 50 for authors)
      
      ---
      
      ## Pattern 4: Combining RESTDataSource with DataLoader
      
      ### Good Example - DataLoader inside RESTDataSource
      
      ```typescript
      import { RESTDataSource } from "@apollo/datasource-rest";
      import DataLoader from "dataloader";
      
      class UsersAPI extends RESTDataSource {
        override baseURL = "https://users-api.example.com/";
      
        // Private DataLoader as implementation detail
        private batchLoader = new DataLoader<string, User>(async (ids) => {
          // Single batch request for multiple 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(`User ${id} not found`),
          );
        });
      
        // Public API uses DataLoader internally
        async getUser(id: string): Promise<User> {
          return this.batchLoader.load(id);
        }
      
        // Non-batched operations bypass DataLoader
        async searchUsers(query: string): Promise<User[]> {
          return this.get<User[]>("users/search", {
            params: { q: query },
          });
        }
      }
      ```
      
      **Why good:** DataLoader is an internal optimization detail, callers use simple `getUser(id)` API, batch requests combine multiple IDs into one HTTP call, RESTDataSource's deduplication layer still applies
      
      ---
      
      ## Pattern 5: Database Data Source Pattern
      
      ### Good Example - Typed database data source
      
      ```typescript
      // Custom data source class (no base class needed for databases)
      class BooksDataSource {
        private db: DatabaseConnection;
      
        constructor(db: DatabaseConnection) {
          this.db = db;
        }
      
        async getAll(limit: number, offset: number): Promise<Book[]> {
          return this.db.query<Book>(
            "SELECT * FROM books ORDER BY created_at DESC LIMIT $1 OFFSET $2",
            [limit, offset],
          );
        }
      
        async getById(id: string): Promise<Book | null> {
          const [book] = await this.db.query<Book>(
            "SELECT * FROM books WHERE id = $1",
            [id],
          );
          return book ?? null;
        }
      
        async getByAuthorId(authorId: string): Promise<Book[]> {
          return this.db.query<Book>("SELECT * FROM books WHERE author_id = $1", [
            authorId,
          ]);
        }
      
        async create(input: { title: string; authorId: string }): Promise<Book> {
          const [book] = await this.db.query<Book>(
            "INSERT INTO books (title, author_id) VALUES ($1, $2) RETURNING *",
            [input.title, input.authorId],
          );
          return book;
        }
      }
      
      // In context function
      const { url } = await startStandaloneServer(server, {
        context: async ({ req }) => {
          const db = await getDbConnection();
          return {
            dataSources: {
              books: new BooksDataSource(db),
            },
          };
        },
      });
      ```
      
      **Why good:** data source encapsulates all SQL, resolver only calls `dataSources.books.getById(id)`, new instance per request with dedicated connection, typed return values
      
  • reference.md 9.7 KB
    # Apollo Server Quick Reference
    
    > Decision frameworks, anti-patterns, and production checklist. Referenced from [SKILL.md](SKILL.md).
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Server Setup Decision
    
    ```
    Quick prototype or simple API?
    ├─ YES → startStandaloneServer (zero config)
    └─ NO → Need subscriptions, custom middleware, or CORS control?
        ├─ YES → Framework middleware (expressMiddleware, etc.)
        └─ NO → startStandaloneServer (simpler to maintain)
    ```
    
    ### Data Source Decision
    
    ```
    Wrapping a REST API?
    ├─ YES → RESTDataSource (built-in caching + deduplication)
    └─ NO → Direct database access?
        ├─ YES → Custom data source class + DataLoader for N+1
        └─ NO → Third-party service with SDK?
            └─ YES → Custom class wrapping SDK in context
    ```
    
    ### N+1 Problem Decision
    
    ```
    Is a field resolver called once per item in a list?
    ├─ YES → Does the underlying API support batch fetching?
    │   ├─ YES → DataLoader with batch function
    │   └─ NO → DataLoader for memoization (prevents duplicate single calls)
    └─ NO → No DataLoader needed
    ```
    
    ### Subscription Transport Decision
    
    ```
    Need real-time updates?
    ├─ YES → Use graphql-ws + ws package
    │   ├─ In-memory PubSub for development only
    │   └─ Distributed pub/sub (Redis, Kafka) for production
    └─ NO → Polling from client or webhooks may suffice
    ```
    
    ### Schema Organization Decision
    
    ```
    Schema < 200 lines?
    ├─ YES → Single typeDefs string is fine
    └─ NO → Split into domain-specific schema fragments
        ├─ Each domain file exports typeDefs using extend type
        └─ Merge via typeDefs array in ApolloServer constructor
    ```
    
    ### Federation Decision
    
    ```
    Single team, single service?
    ├─ YES → Monolithic Apollo Server
    └─ NO → Multiple teams or bounded contexts?
        ├─ YES → Federation with @apollo/subgraph
        │   ├─ Each team owns a subgraph
        │   └─ Gateway (Apollo Router) composes supergraph
        └─ NO → Schema stitching (simpler, less tooling)
    ```
    
    </decision_framework>
    
    ---
    
    <anti_patterns>
    
    ## Anti-Patterns
    
    ### Shared Data Source Instances
    
    ```typescript
    // ANTI-PATTERN: data sources created once, shared across all requests
    const usersAPI = new UsersAPI();
    const postsAPI = new PostsAPI();
    
    startStandaloneServer(server, {
      context: async () => ({
        dataSources: { usersAPI, postsAPI }, // Same instance for every request!
      }),
    });
    ```
    
    **Why wrong:** Cached data leaks between requests and users. One user's data served to another.
    
    **Fix:** Create new instances inside the context function for each operation.
    
    ---
    
    ### Generic Error Throws
    
    ```typescript
    // ANTI-PATTERN: generic Error in resolvers
    throw new Error("Something went wrong");
    
    // ANTI-PATTERN: leaking implementation details
    throw new Error(`PostgreSQL error: relation "users" does not exist`);
    ```
    
    **Why wrong:** Generic errors expose stack traces in development. Implementation details leak database structure and technology choices.
    
    **Fix:** Throw `GraphQLError` with structured extension codes. Use `formatError` to sanitize.
    
    ---
    
    ### Fat Resolvers
    
    ```typescript
    // ANTI-PATTERN: resolver does everything
    const resolvers = {
      Mutation: {
        createUser: async (_parent, args) => {
          // Validation
          if (!args.email.includes("@")) throw new Error("Invalid email");
          // Business logic
          const hashedPassword = await bcrypt.hash(args.password, 10);
          // Database access
          const user = await db.query("INSERT INTO users ...");
          // Side effects
          await sendWelcomeEmail(user.email);
          return user;
        },
      },
    };
    ```
    
    **Why wrong:** Untestable, non-reusable, mixes concerns. Resolver should orchestrate, not implement.
    
    **Fix:** Delegate to data sources and service functions. Resolver calls `dataSources.usersAPI.create(args)`.
    
    ---
    
    ### Missing Drain Plugin
    
    ```typescript
    // ANTI-PATTERN: framework integration without drain
    const httpServer = http.createServer(app);
    const server = new ApolloServer({ typeDefs, resolvers }); // No drain plugin!
    
    app.use("/graphql", expressMiddleware(server));
    httpServer.listen(4000);
    ```
    
    **Why wrong:** On SIGTERM, in-flight requests are terminated abruptly. Connections leak.
    
    **Fix:** Add `ApolloServerPluginDrainHttpServer({ httpServer })` to plugins.
    
    ---
    
    ### Deprecated Package Imports
    
    ```typescript
    // ANTI-PATTERN: legacy packages (pre-v4)
    import { ApolloServer } from "apollo-server"; // Wrong!
    import { ApolloServer } from "apollo-server-express"; // Wrong!
    import { expressMiddleware } from "@apollo/server/express4"; // Removed in v5!
    
    // CORRECT
    import { ApolloServer } from "@apollo/server";
    import { expressMiddleware } from "@as-integrations/express4"; // or express5
    ```
    
    **Why wrong:** `apollo-server` and `apollo-server-express` are unmaintained. The `@apollo/server/express4` path was removed in v5.
    
    ---
    
    ### Monolithic Resolver Map
    
    ```typescript
    // ANTI-PATTERN: all resolvers in one 1000+ line file
    const resolvers = {
      Query: {
        users: ...,    // 20 lines
        user: ...,     // 15 lines
        posts: ...,    // 25 lines
        post: ...,     // 15 lines
        comments: ..., // 20 lines
        // ... 30 more resolvers
      },
      Mutation: {
        // ... 20 more resolvers
      },
      User: { ... },
      Post: { ... },
      Comment: { ... },
    };
    ```
    
    **Why wrong:** Merge conflicts, hard to find resolvers, no clear domain ownership.
    
    **Fix:** Split into domain-specific resolver files, pass as array to `ApolloServer({ resolvers: [userResolvers, postResolvers] })`.
    
    </anti_patterns>
    
    ---
    
    ## Built-in Error Codes Reference
    
    | Code                            | Import from             | Use Case                               |
    | ------------------------------- | ----------------------- | -------------------------------------- |
    | `GRAPHQL_PARSE_FAILED`          | `@apollo/server/errors` | Syntax errors in operations            |
    | `GRAPHQL_VALIDATION_FAILED`     | `@apollo/server/errors` | Operations invalid against schema      |
    | `BAD_USER_INPUT`                | `@apollo/server/errors` | Invalid field argument values          |
    | `BAD_REQUEST`                   | `@apollo/server/errors` | Error before parsing attempted         |
    | `INTERNAL_SERVER_ERROR`         | `@apollo/server/errors` | Default for unspecified errors         |
    | `PERSISTED_QUERY_NOT_FOUND`     | `@apollo/server/errors` | APQ hash not in cache                  |
    | `PERSISTED_QUERY_NOT_SUPPORTED` | `@apollo/server/errors` | Server has APQ disabled                |
    | `OPERATION_RESOLUTION_FAILURE`  | `@apollo/server/errors` | Can't determine which operation to run |
    
    ## Plugin Lifecycle Events Reference
    
    ### Server Events
    
    | Event                   | Async  | Purpose                       |
    | ----------------------- | ------ | ----------------------------- |
    | `serverWillStart`       | Yes    | Server initialization         |
    | `schemaDidLoadOrUpdate` | **No** | Schema loaded or hot-reloaded |
    
    ### Request Events (returned from `requestDidStart`)
    
    | Event                  | Async  | Purpose                              |
    | ---------------------- | ------ | ------------------------------------ |
    | `didResolveSource`     | Yes    | After resolving operation source     |
    | `parsingDidStart`      | Yes    | Before parsing (returns end hook)    |
    | `validationDidStart`   | Yes    | Before validation (returns end hook) |
    | `didResolveOperation`  | Yes    | After operation identified           |
    | `responseForOperation` | Yes    | Override response for operation      |
    | `executionDidStart`    | Yes    | Before execution (returns end hook)  |
    | `willResolveField`     | **No** | Before each field resolves           |
    | `didEncounterErrors`   | Yes    | After errors encountered             |
    | `willSendResponse`     | Yes    | Before response sent                 |
    
    ### Shutdown Events (returned from `serverWillStart`)
    
    | Event            | Async | Purpose                       |
    | ---------------- | ----- | ----------------------------- |
    | `drainServer`    | Yes   | Drain connections before stop |
    | `serverWillStop` | Yes   | Final cleanup after drain     |
    
    ---
    
    ## Production Checklist
    
    ### Server Setup
    
    - [ ] Using `@apollo/server` package (not deprecated `apollo-server`)
    - [ ] Framework integration uses `ApolloServerPluginDrainHttpServer`
    - [ ] `server.start()` called before framework middleware
    - [ ] Context function creates new data source instances per request
    - [ ] `NODE_ENV=production` set in production (disables introspection, hides stack traces)
    
    ### Error Handling
    
    - [ ] All client-facing errors use `GraphQLError` with extension codes
    - [ ] `formatError` configured to sanitize internal error details
    - [ ] No database or infrastructure details in error messages
    - [ ] Internal errors logged for debugging (not just swallowed)
    
    ### Security
    
    - [ ] Introspection disabled in production (default behavior)
    - [ ] CSRF prevention enabled (default behavior)
    - [ ] Query depth limiting configured
    - [ ] CORS configured with specific origins (not wildcard)
    - [ ] `encodeURIComponent` used on all dynamic URL segments in RESTDataSource
    
    ### Performance
    
    - [ ] DataLoader used for N+1-prone field resolvers
    - [ ] RESTDataSource used for REST API wrapping (built-in caching)
    - [ ] Pagination with max limits on list resolvers
    - [ ] Response caching configured where appropriate
    
    ### Subscriptions (if used)
    
    - [ ] `graphql-ws` + `ws` packages installed
    - [ ] WebSocket drain plugin registered alongside HTTP drain
    - [ ] Production pub/sub system (not in-memory PubSub)
    - [ ] `startStandaloneServer` NOT used (doesn't support WebSocket)
    
    ### Federation (if used)
    
    - [ ] `@apollo/subgraph` installed
    - [ ] `buildSubgraphSchema` used instead of direct `ApolloServer({ typeDefs })`
    - [ ] `@key` directives on all entity types
    - [ ] `__resolveReference` implemented for all entities
    - [ ] Federation 2 `@link` directive in schema
    
  • SKILL.md 14.6 KB
    ---
    name: api-graphql-apollo-server
    description: GraphQL API server with Apollo Server — schema, resolvers, context, error handling, data sources, plugins
    ---
    
    # GraphQL API with Apollo Server
    
    > **Quick Guide:** Use `@apollo/server` (v5) for schema-first GraphQL APIs. Define schemas with SDL (`typeDefs`), implement field population with resolvers, share per-request state via the `context` function, and handle errors with `GraphQLError` + extension codes. Use `startStandaloneServer` for quick setups or integrate with your HTTP framework for production. DataLoader solves the N+1 problem. Plugins hook into the request lifecycle for logging, auth, and performance.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST import from `@apollo/server` -- NOT the deprecated `apollo-server` or `apollo-server-express` packages)**
    
    **(You MUST create new data source and DataLoader instances per request in the context function -- sharing across requests causes data leaks)**
    
    **(You MUST throw `GraphQLError` (from `graphql`) with extension codes for client-facing errors -- generic `Error` exposes stack traces)**
    
    **(You MUST use `ApolloServerPluginDrainHttpServer` when integrating with an HTTP framework -- without it the server doesn't shut down gracefully)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** Apollo Server, @apollo/server, ApolloServer, startStandaloneServer, expressMiddleware, GraphQLError, typeDefs, resolvers, contextValue, DataLoader, RESTDataSource, @apollo/datasource-rest, ApolloServerPlugin, buildSubgraphSchema, @apollo/subgraph, graphql-ws, PubSub, formatError, gql tag
    
    **When to use:**
    
    - Building a GraphQL API with schema-first (SDL) design
    - Defining typed resolvers with shared context (auth, data sources)
    - Wrapping REST APIs or databases behind a unified GraphQL layer
    - Implementing real-time features with subscriptions (via `graphql-ws`)
    - Building federated subgraphs with `@apollo/subgraph`
    - Adding lifecycle hooks with plugins (logging, auth, tracing)
    
    **When NOT to use:**
    
    - Simple REST APIs without nested data relationships (a REST framework is simpler)
    - APIs consumed only by one client you control with no query flexibility needs
    - Performance-critical APIs where schema overhead matters (consider a code-first approach)
    
    **Key patterns covered:**
    
    - Server setup with `startStandaloneServer` and framework middleware integration
    - Resolver structure, arguments (`parent`, `args`, `contextValue`, `info`), and chains
    - Context function for per-request state (auth tokens, data sources, DataLoaders)
    - Error handling with `GraphQLError`, built-in codes, and `formatError`
    - RESTDataSource for wrapping REST APIs with caching and deduplication
    - DataLoader for batching and deduplication (N+1 problem)
    - Custom plugins with server-level and request-level lifecycle hooks
    - Subscriptions with `graphql-ws` and WebSocket server
    - Federation subgraph setup with `@apollo/subgraph`
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Server setup, resolvers, context, error handling
    - [examples/data-sources.md](examples/data-sources.md) - RESTDataSource, DataLoader, caching
    - [examples/advanced.md](examples/advanced.md) - Subscriptions, federation, custom plugins
    - [reference.md](reference.md) - Decision frameworks, anti-patterns, production checklist
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    Apollo Server follows a **schema-first** approach: define your API contract in SDL, then implement resolvers to populate each field. The schema is the single source of truth for your API shape, documentation, and type system.
    
    **Core principles:**
    
    1. **Schema as contract** -- SDL defines what clients can query before implementation begins
    2. **Thin resolvers** -- Resolvers orchestrate data fetching but delegate to data sources and services
    3. **Per-request context** -- Each operation gets fresh data source instances and auth state via the context function
    4. **Graceful error handling** -- `GraphQLError` with extension codes communicates errors without leaking internals
    
    **Use Apollo Server when:**
    
    - You need a unified API layer over multiple data sources (REST, DB, services)
    - Clients benefit from querying exactly the data they need (mobile, varied frontends)
    - Schema documentation and introspection matter for developer experience
    - You want lifecycle plugins for observability, auth, and caching
    
    **Use simpler approaches when:**
    
    - A single REST endpoint suffices for your use case
    - You have no nested data relationships worth expressing in a graph
    - The overhead of schema definition and resolver wiring doesn't justify the flexibility
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Server Setup
    
    Two approaches: `startStandaloneServer` for quick/simple setups, or framework middleware integration for production.
    
    ```typescript
    import { ApolloServer } from "@apollo/server";
    import { startStandaloneServer } from "@apollo/server/standalone";
    
    const server = new ApolloServer({ typeDefs, resolvers });
    
    const DEFAULT_PORT = 4000;
    
    const { url } = await startStandaloneServer(server, {
      context: async ({ req }) => ({
        token: req.headers.authorization,
      }),
      listen: { port: DEFAULT_PORT },
    });
    ```
    
    **Why good:** minimal boilerplate for development, context function provides per-request auth state
    
    For production with an HTTP framework, use `expressMiddleware` (from `@as-integrations/express4` or `@as-integrations/express5`) with `ApolloServerPluginDrainHttpServer` for graceful shutdown.
    
    See [examples/core.md](examples/core.md) for both setup patterns with full TypeScript types.
    
    ---
    
    ### Pattern 2: Resolver Structure and Arguments
    
    Resolvers receive four arguments: `parent` (return value from parent resolver), `args` (field arguments), `contextValue` (shared per-request state), and `info` (operation metadata).
    
    ```typescript
    const resolvers = {
      Query: {
        user: async (_parent, args, contextValue) => {
          return contextValue.dataSources.usersAPI.getUser(args.id);
        },
      },
      User: {
        posts: async (parent, _args, contextValue) => {
          return contextValue.dataSources.postsAPI.getPostsByAuthor(parent.id);
        },
      },
    };
    ```
    
    **Why good:** parent chaining enables nested queries without client round-trips, data source access through context keeps resolvers thin
    
    See [examples/core.md](examples/core.md) for full resolver patterns with type safety.
    
    ---
    
    ### Pattern 3: Context Function
    
    The context function runs for every operation. Use it to create per-request data sources, extract auth, and set up DataLoaders.
    
    ```typescript
    interface MyContext {
      token: string | undefined;
      dataSources: {
        usersAPI: UsersAPI;
        postsAPI: PostsAPI;
      };
    }
    
    const server = new ApolloServer<MyContext>({ typeDefs, resolvers });
    ```
    
    **Critical:** Create new data source instances per request. Sharing instances across requests causes stale data and leaks between users.
    
    See [examples/core.md](examples/core.md) for complete context setup.
    
    ---
    
    ### Pattern 4: Error Handling with GraphQLError
    
    Throw `GraphQLError` (from the `graphql` package) with extension codes for structured client errors. Use `formatError` to sanitize errors before sending.
    
    ```typescript
    import { GraphQLError } from "graphql";
    
    throw new GraphQLError("User not found", {
      extensions: {
        code: "USER_NOT_FOUND",
        argumentName: "id",
      },
    });
    ```
    
    **Built-in codes** (from `ApolloServerErrorCode`): `GRAPHQL_PARSE_FAILED`, `GRAPHQL_VALIDATION_FAILED`, `BAD_USER_INPUT`, `INTERNAL_SERVER_ERROR`, `BAD_REQUEST`, `PERSISTED_QUERY_NOT_FOUND`.
    
    See [examples/core.md](examples/core.md) for `formatError`, custom codes, and auth error patterns.
    
    ---
    
    ### Pattern 5: RESTDataSource
    
    Wrap REST APIs with `@apollo/datasource-rest` for built-in caching, request deduplication, and HTTP method helpers.
    
    ```typescript
    import { RESTDataSource } from "@apollo/datasource-rest";
    
    class MoviesAPI extends RESTDataSource {
      override baseURL = "https://movies-api.example.com/";
    
      async getMovie(id: string): Promise<Movie> {
        return this.get<Movie>(`movies/${encodeURIComponent(id)}`);
      }
    }
    ```
    
    **Two-layer caching:** (1) request deduplication prevents duplicate GET calls within a single operation, (2) HTTP response caching honors `cache-control` headers.
    
    See [examples/data-sources.md](examples/data-sources.md) for RESTDataSource methods, custom caching, and `willSendRequest`.
    
    ---
    
    ### Pattern 6: DataLoader for N+1 Prevention
    
    Use DataLoader to batch and deduplicate database or API calls within a single GraphQL operation.
    
    ```typescript
    import DataLoader from "dataloader";
    
    const userLoader = new DataLoader<string, User>(async (ids) => {
      const users = await db.users.findMany({ where: { id: { in: [...ids] } } });
      return ids.map(
        (id) => users.find((u) => u.id === id) ?? new Error(`User ${id} not found`),
      );
    });
    ```
    
    **Critical:** Create new DataLoader instances per request (in the context function). DataLoader caches results for the request lifetime -- sharing across requests serves stale data.
    
    See [examples/data-sources.md](examples/data-sources.md) for DataLoader setup, batching patterns, and integration with context.
    
    ---
    
    ### Pattern 7: Custom Plugins
    
    Plugins hook into server and request lifecycle events. Use them for logging, auth checks, performance tracing, and error reporting.
    
    ```typescript
    import type { ApolloServerPlugin } from "@apollo/server";
    
    const loggingPlugin: ApolloServerPlugin<MyContext> = {
      async requestDidStart(requestContext) {
        const start = Date.now();
        return {
          async willSendResponse() {
            const duration = Date.now() - start;
            console.log(`Operation took ${duration}ms`);
          },
        };
      },
    };
    ```
    
    **Key lifecycle events:** `serverWillStart`, `requestDidStart`, `parsingDidStart`, `validationDidStart`, `executionDidStart`, `didEncounterErrors`, `willSendResponse`.
    
    See [examples/advanced.md](examples/advanced.md) for complete plugin patterns.
    
    ---
    
    ### Pattern 8: Subscriptions with graphql-ws
    
    Apollo Server does not include a built-in WebSocket transport. Use `graphql-ws` + `ws` for real-time subscriptions alongside your HTTP server.
    
    ```typescript
    import { WebSocketServer } from "ws";
    import { useServer } from "graphql-ws/use/ws";
    
    const wsServer = new WebSocketServer({
      server: httpServer,
      path: "/subscriptions",
    });
    const serverCleanup = useServer({ schema }, wsServer);
    ```
    
    **Note:** `startStandaloneServer` does not support subscriptions -- use framework middleware integration instead. The in-memory `PubSub` from `graphql-subscriptions` is for development only; use a distributed pub/sub system in production.
    
    See [examples/advanced.md](examples/advanced.md) for complete subscription setup with drain plugins.
    
    ---
    
    ### Pattern 9: Federation Subgraph
    
    Use `@apollo/subgraph` to build a subgraph that participates in a federated supergraph. Entities use `@key` directives and `__resolveReference` resolvers.
    
    ```typescript
    import { buildSubgraphSchema } from "@apollo/subgraph";
    
    const server = new ApolloServer({
      schema: buildSubgraphSchema({ typeDefs, resolvers }),
    });
    ```
    
    **Key concepts:** `@key` designates entity identity fields, `__resolveReference` fetches entities by their key fields, `extend type` contributes fields from other subgraphs.
    
    See [examples/advanced.md](examples/advanced.md) for federation schema and reference resolver patterns.
    
    </patterns>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority:**
    
    - **Importing from deprecated packages** (`apollo-server`, `apollo-server-express`) -- use `@apollo/server` exclusively
    - **Sharing data source or DataLoader instances across requests** -- causes stale data and cross-user leaks; create new instances in the context function
    - **Throwing generic `Error` from resolvers** -- exposes stack traces to clients; use `GraphQLError` with extension codes
    - **Missing `ApolloServerPluginDrainHttpServer`** when using framework integration -- server won't shut down gracefully, leaving connections hanging
    - **Calling `expressMiddleware` before `server.start()`** -- throws an error; `start()` must complete first
    
    **Medium Priority:**
    
    - **Using `c.req.params` instead of resolver `args`** -- bypasses GraphQL argument validation
    - **No pagination limits on list resolvers** -- returns entire datasets; always enforce max limits
    - **In-memory `PubSub` in production** -- only works for a single server instance; use a distributed system
    - **Missing `encodeURIComponent` on dynamic URL segments in RESTDataSource** -- path traversal vulnerability
    - **Not using `formatError` to sanitize errors** -- internal error messages and stack traces leak to clients in development mode
    
    **Gotchas & Edge Cases:**
    
    - **Default resolvers:** Apollo Server auto-resolves fields matching property names on the parent object -- you don't need explicit resolvers for simple property access
    - **`contextValue` is shared:** Never destructively modify `contextValue` in resolvers -- other resolvers in the same operation share the object
    - **Resolver return value of `undefined`:** Triggers the default resolver to try accessing a property on the parent -- may cause unexpected behavior if the parent doesn't have that field
    - **Introspection disabled in production:** By default, introspection is off when `NODE_ENV=production` -- override with `introspection: true` if needed
    - **Variable coercion errors:** In v5, malformed variables return HTTP 400 by default (v4 returned 200) -- existing clients may need updates
    - **Express integration in v5:** Import `expressMiddleware` from `@as-integrations/express4` or `@as-integrations/express5` (not from `@apollo/server/express4` which was removed)
    - **Subscription resolvers:** Must return an `AsyncIterator` from `subscribe`, not a direct value -- the `resolve` function (optional) transforms the event payload
    - **Plugin lifecycle:** All plugin methods are async except `willResolveField` and `schemaDidLoadOrUpdate`
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST import from `@apollo/server` -- NOT the deprecated `apollo-server` or `apollo-server-express` packages)**
    
    **(You MUST create new data source and DataLoader instances per request in the context function -- sharing across requests causes data leaks)**
    
    **(You MUST throw `GraphQLError` (from `graphql`) with extension codes for client-facing errors -- generic `Error` exposes stack traces)**
    
    **(You MUST use `ApolloServerPluginDrainHttpServer` when integrating with an HTTP framework -- without it the server doesn't shut down gracefully)**
    
    **Failure to follow these rules will cause data leaks between users, expose internal errors to clients, and prevent graceful shutdown.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related