Apollo Server
Apollo Server setup, configuration, plugins, and production deployment patterns
You are an expert in Apollo Server v4, helping developers set up, configure, and deploy production-grade GraphQL APIs using the Apollo Server ecosystem. ## Key Points 1. **Always use `ApolloServerPluginDrainHttpServer`** — this ensures in-flight requests complete before the server shuts down, critical for graceful deployments. 2. **Separate schema, resolvers, and data sources** — organize by domain module, not by GraphQL concept. Each module exports its typeDefs and resolvers. 3. **Disable introspection in production** — unless you have a specific reason to expose it, disable introspection to reduce your attack surface. 4. **Use persisted queries** — Apollo's Automatic Persisted Queries (APQ) reduce bandwidth and enable query whitelisting for security. 5. **Set request timeouts** — configure HTTP-level timeouts and use the `didResolveOperation` plugin hook to enforce operation-level budgets. 6. **Enable Apollo Studio in production** — usage reporting provides essential visibility into field-level performance and error rates. - **Not starting the server before applying middleware** — `await server.start()` must be called before `expressMiddleware(server)` or the middleware will throw. - **Creating expensive resources in the context function** — database connections should be pooled, not created per request. The context function should reference a shared pool. - **Forgetting CORS configuration** — Apollo Server v4 does not configure CORS automatically; you must apply `cors()` middleware yourself. - **Logging full errors in production** — stack traces and internal details in error responses leak implementation details. Always sanitize with `formatError`. - **Ignoring Apollo Server v3 to v4 migration** — v4 removed built-in Express integration; it must be added explicitly via `@apollo/server/express4`.
skilldb get graphql-skills/Apollo ServerFull skill: 305 linesApollo Server — GraphQL
You are an expert in Apollo Server v4, helping developers set up, configure, and deploy production-grade GraphQL APIs using the Apollo Server ecosystem.
Overview
Apollo Server is the most widely adopted GraphQL server for Node.js. Version 4 is framework-agnostic, integrating with Express, Fastify, Next.js, and serverless platforms. It provides schema execution, plugin-based lifecycle hooks, and seamless integration with Apollo Studio for observability.
Core Concepts
Basic Server Setup
A minimal Apollo Server with Express:
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
import express from "express";
import http from "http";
import cors from "cors";
const typeDefs = `#graphql
type Query {
hello: String!
}
`;
const resolvers = {
Query: {
hello: () => "Hello, world!",
},
};
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();
app.use(
"/graphql",
cors(),
express.json(),
expressMiddleware(server, {
context: async ({ req }) => ({
token: req.headers.authorization,
}),
})
);
await new Promise<void>((resolve) => httpServer.listen({ port: 4000 }, resolve));
console.log("Server ready at http://localhost:4000/graphql");
Context Function
The context function runs on every request and builds the shared state available to all resolvers:
interface Context {
currentUser: User | null;
dataSources: DataSources;
loaders: ReturnType<typeof createLoaders>;
}
expressMiddleware(server, {
context: async ({ req }): Promise<Context> => {
const token = req.headers.authorization?.replace("Bearer ", "");
const currentUser = token ? await verifyAndDecodeToken(token) : null;
const loaders = createLoaders(db);
const dataSources = {
users: new UsersDataSource(db, loaders),
posts: new PostsDataSource(db, loaders),
};
return { currentUser, dataSources, loaders };
},
});
Schema Construction
Apollo Server supports both SDL-first and code-first approaches:
// SDL-first with multiple type definition files
import { readFileSync } from "fs";
import { mergeTypeDefs, mergeResolvers } from "@graphql-tools/merge";
const typeDefs = mergeTypeDefs([
readFileSync("./schema/base.graphql", "utf-8"),
readFileSync("./schema/user.graphql", "utf-8"),
readFileSync("./schema/post.graphql", "utf-8"),
]);
const resolvers = mergeResolvers([userResolvers, postResolvers]);
Implementation Patterns
Custom Plugins
Apollo Server's plugin API hooks into the request lifecycle:
import type { ApolloServerPlugin } from "@apollo/server";
const loggingPlugin: ApolloServerPlugin<Context> = {
async requestDidStart({ request, contextValue }) {
const start = Date.now();
const operationName = request.operationName ?? "anonymous";
return {
async didResolveOperation({ operation }) {
console.log(`Operation: ${operation.operation} ${operationName}`);
},
async didEncounterErrors({ errors }) {
for (const error of errors) {
console.error(`GraphQL error in ${operationName}:`, error.message);
}
},
async willSendResponse() {
const duration = Date.now() - start;
console.log(`${operationName} completed in ${duration}ms`);
},
};
},
};
Error Formatting
Customize error responses for production:
import { GraphQLError } from "graphql";
import { unwrapResolverError } from "@apollo/server/errors";
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (formattedError, error) => {
const originalError = unwrapResolverError(error);
// Never expose internal errors to clients
if (originalError instanceof DatabaseError) {
return {
message: "Internal server error",
extensions: { code: "INTERNAL_SERVER_ERROR" },
};
}
// Pass through known GraphQL errors
return formattedError;
},
});
// Throwing typed errors in resolvers
throw new GraphQLError("Post not found", {
extensions: {
code: "NOT_FOUND",
argumentName: "id",
},
});
Query Complexity and Depth Limiting
Protect against abusive queries:
import depthLimit from "graphql-depth-limit";
import { createComplexityLimitRule } from "graphql-validation-complexity";
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(10),
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 10,
listFactor: 20,
}),
],
});
Apollo Federation (Subgraph)
Set up a service as a federated subgraph:
import { ApolloServer } from "@apollo/server";
import { buildSubgraphSchema } from "@apollo/subgraph";
import { parse } from "graphql";
const typeDefs = parse(`#graphql
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
email: String!
displayName: String!
}
type Query {
me: User
}
`);
const resolvers = {
Query: {
me: (_, __, { currentUser }) => currentUser,
},
User: {
__resolveReference: (ref, { dataSources }) =>
dataSources.users.getById(ref.id),
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});
Health Checks and Graceful Shutdown
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
// Close database connections, flush caches, etc.
await db.end();
},
};
},
},
],
});
// Health check endpoint (outside GraphQL)
app.get("/health", (_, res) => {
res.status(200).json({ status: "ok" });
});
Core Philosophy
Apollo Server is most effective when treated as a thin execution layer that delegates all real work to domain-specific data sources and services. The server's job is to parse the incoming operation, run it through the schema's resolver tree, and return the result — nothing more. Business logic, data fetching, authorization, and caching belong in dedicated modules that resolvers call into, not in the resolvers themselves. This separation makes the GraphQL layer testable, replaceable, and easy to reason about.
Production readiness in Apollo Server comes from the plugin system and operational discipline, not from the schema alone. Plugins hook into every stage of the request lifecycle — from parsing through execution to response formatting — enabling cross-cutting concerns like logging, tracing, error sanitization, and performance budgets without polluting resolver code. Teams that invest in a small set of well-tested plugins get observability and safety guarantees across their entire API surface.
The context function is the gateway between the HTTP transport and the GraphQL execution engine. It should be fast, deterministic, and focused: verify the token, build DataLoaders, reference the connection pool, and nothing else. Expensive operations in the context function run on every request, so deferring work to resolvers (which may not even be called depending on the query) is always the right instinct.
Anti-Patterns
-
Fat resolvers with inline business logic — embedding validation, authorization, data transformation, and persistence logic directly in resolvers creates an untestable monolith. Resolvers should be one-liners that call into service or data source methods.
-
Creating database connections per request in the context function — instantiating a new connection (rather than referencing a shared pool) inside the context function exhausts the database under load and adds latency to every request.
-
Exposing raw internal errors to clients — returning unformatted error messages leaks stack traces, database details, and internal structure to callers. Always sanitize errors with
formatErrorand return only safe, categorized error codes. -
Skipping depth and complexity limits — a schema without query depth or complexity restrictions is an open invitation for resource exhaustion attacks. Malicious or accidental deeply-nested queries can bring down the server.
-
Relying on introspection as documentation — leaving introspection enabled in production without access controls exposes your full schema to anyone. Disable it or restrict it behind authentication, and maintain external documentation separately.
Best Practices
- Always use
ApolloServerPluginDrainHttpServer— this ensures in-flight requests complete before the server shuts down, critical for graceful deployments. - Separate schema, resolvers, and data sources — organize by domain module, not by GraphQL concept. Each module exports its typeDefs and resolvers.
- Disable introspection in production — unless you have a specific reason to expose it, disable introspection to reduce your attack surface.
- Use persisted queries — Apollo's Automatic Persisted Queries (APQ) reduce bandwidth and enable query whitelisting for security.
- Set request timeouts — configure HTTP-level timeouts and use the
didResolveOperationplugin hook to enforce operation-level budgets. - Enable Apollo Studio in production — usage reporting provides essential visibility into field-level performance and error rates.
Common Pitfalls
- Not starting the server before applying middleware —
await server.start()must be called beforeexpressMiddleware(server)or the middleware will throw. - Creating expensive resources in the context function — database connections should be pooled, not created per request. The context function should reference a shared pool.
- Forgetting CORS configuration — Apollo Server v4 does not configure CORS automatically; you must apply
cors()middleware yourself. - Logging full errors in production — stack traces and internal details in error responses leak implementation details. Always sanitize with
formatError. - Ignoring Apollo Server v3 to v4 migration — v4 removed built-in Express integration; it must be added explicitly via
@apollo/server/express4.
Install this skill directly: skilldb add graphql-skills
Related Skills
Apollo Client
Apollo Client with React for querying, mutating, and caching GraphQL data
Authentication
Authentication and authorization patterns for securing GraphQL APIs
Code Generation
Type-safe GraphQL development with graphql-codegen for TypeScript
Pagination
Cursor-based pagination following the Relay connection specification
Resolvers
Resolver patterns, data loading strategies, and the N+1 problem in GraphQL
Schema Design
Schema design principles for building maintainable, intuitive GraphQL APIs