| 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:
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:
- Schema as contract -- SDL defines what clients can query before implementation begins
- Thin resolvers -- Resolvers orchestrate data fetching but delegate to data sources and services
- Per-request context -- Each operation gets fresh data source instances and auth state via the context function
- 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
Core Patterns
Pattern 1: Server Setup
Two approaches: startStandaloneServer for quick/simple setups, or framework middleware integration for production.
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 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).
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 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.
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 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.
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 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.
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 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.
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 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.
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 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.
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 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.
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 for federation schema and reference resolver 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>