api-graphql-apollo-server
GraphQL API server with Apollo Server — schema, resolvers, context, error handling, data sources, plugins
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-graphql-apollo-server/skills/api-graphql-apollo-server
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
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 thecontextfunction, and handle errors withGraphQLError+ extension codes. UsestartStandaloneServerfor 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
startStandaloneServerand 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, andformatError - 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-wsand WebSocket server - Federation subgraph setup with
@apollo/subgraph
Detailed Resources:
- examples/core.md - Server setup, resolvers, context, error handling
- examples/data-sources.md - RESTDataSource, DataLoader, caching
- examples/advanced.md - Subscriptions, federation, custom plugins
- reference.md - Decision frameworks, anti-patterns, production checklist
<red_flags>
RED FLAGS
High Priority:
- Importing from deprecated packages (
apollo-server,apollo-server-express) -- use@apollo/serverexclusively - Sharing data source or DataLoader instances across requests -- causes stale data and cross-user leaks; create new instances in the context function
- Throwing generic
Errorfrom resolvers -- exposes stack traces to clients; useGraphQLErrorwith extension codes - Missing
ApolloServerPluginDrainHttpServerwhen using framework integration -- server won't shut down gracefully, leaving connections hanging - Calling
expressMiddlewarebeforeserver.start()-- throws an error;start()must complete first
Medium Priority:
- Using
c.req.paramsinstead of resolverargs-- bypasses GraphQL argument validation - No pagination limits on list resolvers -- returns entire datasets; always enforce max limits
- In-memory
PubSubin production -- only works for a single server instance; use a distributed system - Missing
encodeURIComponenton dynamic URL segments in RESTDataSource -- path traversal vulnerability - Not using
formatErrorto 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
contextValueis shared: Never destructively modifycontextValuein 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 withintrospection: trueif 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
expressMiddlewarefrom@as-integrations/express4or@as-integrations/express5(not from@apollo/server/express4which was removed) - Subscription resolvers: Must return an
AsyncIteratorfromsubscribe, not a direct value -- theresolvefunction (optional) transforms the event payload - Plugin lifecycle: All plugin methods are async except
willResolveFieldandschemaDidLoadOrUpdate
</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.
Reviews (0)
No reviews yet.
No comments yet.