Docs
Launch GraphOS Studio

Migrating to Apollo Server 4


📣 Apollo Server 4 is generally available!

4 focuses on improving Apollo Server's extensibility and making it simpler to use, maintain, and . To learn more about the inspiration behind this release, see the Apollo Server Roadmap.

4 provides the following features:

  • A well-defined API with a stable HTTP abstraction, enabling contributors to easily build and maintain integrations in their preferred frameworks.
  • A new @apollo/server package, combining numerous smaller packages and including the startStandaloneServer and expressMiddleware functions.
  • Packages that can be used as either ECMAScript or CJS modules.
  • Experimental support for incremental delivery when combined with a pre-release of graphql-js.

For a list of all breaking changes, see the changelog.

🚚 This guide helps you migrate from Apollo Server 3 to Apollo Server 4. If you are using Apollo Server 2, you must first migrate to Apollo Server 3 before following this guide.

We recommend that all users of upgrade to Apollo Server 4 as soon as possible. Apollo Server 2 and Apollo Server 3 are deprecated, with an end-of-life date of October 22, 2023 and October 22, 2024 respectively.

The new @apollo/server package

3 is distributed as a fixed set of packages for integrating with different web frameworks and environments. The main "batteries-included" apollo-server package reduces setup time by providing a minimally customizable .

In 3, the apollo-server-core package defines an ApolloServer "base" class, which each integration package (apollo-server-express,apollo-server-lambda, etc.) subclasses with a slightly different API. This packaging structure means that new integration package releases are lockstep versioned to itself, making it challenging to support major versions of frameworks and add integration-specific changes. Additionally, Apollo Server 3 doesn't provide a way to add new integrations for additional frameworks.

4 takes a different approach to integrations by providing a stable web framework integration API, which includes explicit support for framework life cycles.

The new @apollo/server package contains:

  • The ApolloServer class
  • An Express 4 integration (similar to 3's apollo-server-express package)
  • A standalone server (similar to 3's apollo-server package)
  • A set of core plugins (similar to 3's apollo-server-core package)

There are no integration-specific subclasses in 4. Instead, there's a single ApolloServer class with a single API that all integrations use.

In 3, the Apollo Server core team was responsible for maintaining every integration package. With Apollo Server 4, the AS core team no longer directly maintains most integration packages. Instead, we work with the broader open source community to maintain Apollo Server integrations, enabling those who regularly use different web frameworks to make the best choices for their framework's integration.

For those migrating from 3 to Apollo Server 4, use the below flowchart to see your migration path:

N
Yes
N
Yes
Am I using the apollo-server package?
Am I using the apollo-server-express package?
Use the startStandaloneServer function
See if a community-supported integration exists
Use the expressMiddleware function

  • If you're currently using the apollo-server package, you should use the startStandaloneServer function.
  • If you're currently using the apollo-server-express package, you should use the expressMiddleware function.

The @apollo/server package exports these functions alongside the ApolloServer class.

If you are using another 3 framework integration package (such as apollo-server-koa or apollo-server-lambda), check out our list of integrations to see if a community-maintained integration package exists for your framework of choice.

If there is no integration for your favorite framework yet, help the broader community by building a new integration! You can also join the discussions about maintaining our existing integrations.

Below are a few high-level changes for using framework integrations:

  • You can pass your context initialization function directly to your framework's integration function (e.g., expressMiddleware or startStandaloneServer) instead of the ApolloServer constructor.
  • You are responsible for setting up HTTP body parsing and CORS using your framework integration's standard functionality.
  • If you want your server to listen on a specific URL path, pass that path directly to your framework's instead of using the path option. If you did not specify a URL path, the default in 3 was /graphql, so to preserve existing behavior, you should specify that path explicitly.

The following sections show how servers using apollo-server or apollo-server-express can update to 4.

NOTE

In the examples below, we use top-level await calls to start our server asynchronously. If you'd like to see how we set this up, check out the Getting Started guide for details.

Migrate from apollo-server

In 3, the apollo-server package is a "batteries-included" package that wraps apollo-server-express, providing an HTTP server with minimal HTTP-level customization.

If you used the "batteries included" apollo-server package in 3, use the startStandaloneServer function in 4.

This 3 code:

apollo-server-3-standalone.ts
// npm install apollo-server graphql
import { ApolloServer } from 'apollo-server';
import { typeDefs, resolvers } from './schema';
interface MyContext {
token?: String;
}
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => ({ token: req.headers.token }),
});
const { url } = await server.listen(4000);
console.log(`🚀 Server ready at ${url}`);
apollo-server-3-standalone.js
// npm install apollo-server graphql
import { ApolloServer } from 'apollo-server';
import { typeDefs, resolvers } from './schema';
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => ({ token: req.headers.token }),
});
const { url } = await server.listen(4000);
console.log(`🚀 Server ready at ${url}`);

looks like this in 4:

apollo-server-4-standalone.ts
// npm install @apollo/server graphql
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { typeDefs, resolvers } from './schema';
interface MyContext {
token?: String;
}
const server = new ApolloServer<MyContext>({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({ token: req.headers.token }),
listen: { port: 4000 },
});
console.log(`🚀 Server ready at ${url}`);
apollo-server-4-standalone.js
// npm install @apollo/server graphql
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { typeDefs, resolvers } from './schema';
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({ token: req.headers.token }),
listen: { port: 4000 },
});
console.log(`🚀 Server ready at ${url}`);

The startStandaloneServer function accepts two ; the first is the instance of ApolloServer that should begin listening for incoming requests. The second is an object for configuring your server's options, which most notably accepts the following properties:

Name /
Type
Description
context

Function

An optional context initialization function. The context function receives req and res options (see below for more details.).

In 3, you pass the context function to the constructor. In 4, you pass the context function to startStandaloneServer.

listen

Object

An optional listen configuration option. The listen option accepts an object with the same properties as the net.Server.listen options object.

For example, in 3, if you used server.listen(4321), you'll now pass listen: { port: 4321 } to the startStandaloneServer function in 4. If you didn't pass any to Apollo Server 3's server.listen() method; you don't need to specify this listen option.

The startStandaloneServer function doesn't enable you to configure your server's CORS behavior. If you previously used the cors constructor option to customize your CORS settings in 3, use the expressMiddleware function in 4.

Similarly, if you used the stopGracePeriodMillis constructor option in 3, use the expressMiddleware function and specify stopGracePeriodMillis to the ApolloServerPluginDrainHttpServer plugin.

Migrate from apollo-server-express

If you used the apollo-server-express package in 3, use the expressMiddleware function in 4 (i.e., instead of using server.applyMiddleware or server.getMiddleware).

To migrate from 3's apollo-server-express package to using the expressMiddleware function, do the following:

  1. Install the @apollo/server and cors packages.
  2. Import symbols from @apollo/server (i.e., instead of from apollo-server-express and apollo-server-core).
  3. Add cors to your server setup.
  4. Remove the 3 apollo-server-express and apollo-server-core packages.
  5. If you are using apollo-server-express's default /graphql URL path (i.e., not specifying another URL with the path option), you can mount expressMiddleware at /graphql to maintain behavior. To use another URL path, mount your server (with app.use) at the specified path.

This 3 code:

apollo-server-3.ts
// npm install apollo-server-express apollo-server-core express graphql
import { ApolloServer } from 'apollo-server-express';
import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import express from 'express';
import http from 'http';
import { typeDefs, resolvers } from './schema';
interface MyContext {
token?: String;
}
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => ({ token: req.headers.token }),
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();
server.applyMiddleware({ app });
await new Promise<void>((resolve) => httpServer.listen({ port: 4000 }, resolve));
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
apollo-server-3.js
// npm install apollo-server-express apollo-server-core express graphql
import { ApolloServer } from 'apollo-server-express';
import { ApolloServerPluginDrainHttpServer } from 'apollo-server-core';
import express from 'express';
import http from 'http';
import { typeDefs, resolvers } from './schema';
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => ({ token: req.headers.token }),
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();
server.applyMiddleware({ app });
await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);

looks like this in 4:

apollo-server-4.ts
// npm install @apollo/server express graphql cors
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';
import { typeDefs, resolvers } from './schema';
interface MyContext {
token?: String;
}
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer<MyContext>({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();
app.use(
'/graphql',
cors<cors.CorsRequest>(),
express.json(),
expressMiddleware(server, {
context: async ({ req }) => ({ token: req.headers.token }),
}),
);
await new Promise<void>((resolve) => httpServer.listen({ port: 4000 }, resolve));
console.log(`🚀 Server ready at http://localhost:4000/graphql`);
apollo-server-4.js
// npm install @apollo/server express graphql cors
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';
import { typeDefs, resolvers } from './schema';
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.token }),
}),
);
await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));
console.log(`🚀 Server ready at http://localhost:4000/graphql`);

Removed integrations

4 takes a fundamentally different approach to web framework integrations. By offering a well-defined API with a stable HTTP abstraction, Apollo Server 4 enables contributors to build and maintain integrations for the first time.

To that end, the core team no longer maintains the following integration packages in Apollo Server 4:

Instead, we now work with the broader community to maintain the following open-source integrations for :

If an integration doesn't exist for your framework, you can always build your own!

In 3, the apollo-server-express package supported both Express and its older predecessor Connect. In 4, expressMiddleware no longer supports Connect. An interested developer could build a Connect-specific middleware, and a PR to this migration guide is welcome if someone does this!

Packages merged into @apollo/server

As shown above, 4 combines the functionality of the apollo-server, apollo-server-express, and apollo-server-core packages into a new @apollo/server package.

But wait: there's more! The @apollo/server package also combines the following packages:

Plugins are in deep imports

In 3, the apollo-server-core package exports built-in plugins, like ApolloServerUsageReporting, at the top level. To use these plugins, you must install both the apollo-server-core package and the package you use to import ApolloServer (e.g., apollo-server or apollo-server-express).

In 4, these built-in plugins are part of the main @apollo/server package, which also imports the ApolloServer class. The @apollo/server package exports these built-in plugins with deep exports. This means you use deep imports for each built-in plugin, enabling you to evaluate only the plugin you use in your app and making it easier for bundlers to eliminate unused code.

There's one exception: the ApolloServerPluginLandingPageGraphQLPlayground plugin is now in its own package @apollo/server-plugin-landing-page-graphql-playground, which you can install separately.

This plugin installs the unmaintained Playground project as a landing page and is provided for compatibility with 2. This package will not be supported after 4 is released. We strongly recommend you switch to Apollo Server's 4's default landing page, which installs the actively maintained .

exports the following plugins:

PluginImport path
ApolloServerPluginCacheControl@apollo/server/plugin/cacheControl
ApolloServerPluginCacheControlDisabled@apollo/server/plugin/disabled
ApolloServerPluginDrainHttpServer@apollo/server/plugin/drainHttpServer
ApolloServerPluginInlineTrace@apollo/server/plugin/inlineTrace
ApolloServerPluginInlineTraceDisabled@apollo/server/plugin/disabled
ApolloServerPluginLandingPageDisabled@apollo/server/plugin/disabled
ApolloServerPluginLandingPageLocalDefault@apollo/server/plugin/landingPage/default
ApolloServerPluginLandingPageProductionDefault@apollo/server/plugin/landingPage/default
ApolloServerPluginSchemaReporting@apollo/server/plugin/schemaReporting
ApolloServerPluginUsageReporting@apollo/server/plugin/usageReporting
ApolloServerPluginUsageReportingDisabled@apollo/server/plugin/disabled

For example, replace this 3 code:

import { ApolloServerPluginUsageReporting } from 'apollo-server-core';

with this 4 code:

import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';

You can also import each plugin's associated TypeScript types (e.g., ApolloServerPluginUsageReportingOptions) from the same deep import as that plugin.

Once you've updated your imports, you can remove your project's dependency on apollo-server-core.

Known regressions

Appropriate 400 status codes

v4 responds to an invalid variables object with a 200 status code, whereas v3 responds appropriately with a 400 status code. This regression was introduced in PR #6502 and brought to our attention in Issue #7462.

Specifically, this regression affects cases where input variable coercion fails. of an incorrect type (i.e. String instead of Int) or unexpectedly null are examples that fail coercion. Additionally, missing or incorrect on input objects as well as custom that throw during validation will also fail variable coercion. For additional specifics on variable coercion, see the "Input Coercion" sections in the GraphQL spec.

We recommend mitigating this regression unless you've already modified your application to work around it. To do so, add the status400ForVariableCoercionErrors: true option to your ApolloServer constructor:

new ApolloServer({
// ...
status400ForVariableCoercionErrors: true,
});
new ApolloServer({
// ...
status400ForVariableCoercionErrors: true,
});

This option will no longer be needed (and will be ignored) in v5.

Bumped dependencies

Node.js

4 supports Node.js 14.16.0 and later. (Apollo Server 3 supports Node.js 12.) This includes all LTS and Current major versions at the time of release.

If you're using Node.js 12, upgrade your runtime before upgrading to 4.

( 4 specifically requires v14.16.0 instead of merely v14.0.0, because that is the minimum version of Node.js 14 supported by our minimum supported version of graphql, as described in the next section.)

graphql

has a peer dependency on graphql (the core JS implementation). 4 supports graphql v16.6.0 and later, but we strongly recommend using at least v16.7.0 due to a bug in graphql which can crash your server. ( 3 supports graphql v15.3.0 through v16.)

If you're using an older version of graphql, upgrade it to a supported version before upgrading to 4.

Note that upgrading graphql may require you to upgrade other libraries that are installed in your project. For example, if you use with Apollo Gateway, you should upgrade Apollo Gateway to at least v0.50.1 or any v2.x version for full graphql 16 support before upgrading to 4.

TypeScript

If you use with TypeScript, you must use TypeScript v4.7.0 or newer.

For background, uses features introduced in v4.7. We want to put out "downleveled" versions of @apollo/server's type definitions for older versions of TypeScript, but have found TypeScript's typesVersions feature challenging to use.

If supporting older versions of TypeScript is important to you and you'd like to help us get typesVersions working, we'd appreciate PRs!

Removed constructor options

The following ApolloServer constructor options have been removed in favor of other features or configuration methods.

dataSources

📣 See our new Fetching Data article for more information on how the concept of a has changed in Apollo Sever 4.

In 3, the top-level dataSources constructor option essentially adds a post-processing step to your app's context function, creating DataSource subclasses and adding them to a dataSources on your context object. This means the TypeScript type the context function returns is different from the context type your and plugins receive. Additionally, this design obfuscates that DataSource objects are created once per request (i.e., like the rest of the context object).

4 removes the dataSources constructor option. You can now treat DataSources like any other part of your context object.

In 3, immediately after constructing each DataSource subclass, invokes the dataSource.initialize({ cache, context }) function on each new DataSource. If you need to replicate this behavior in 4, you can pass the cache and context to each DataSource constructor. In 4, you can find cache as a new readonly on ApolloServer.

For example, below, we use the RESTDataSource class to create a DataSource with 3:

Apollo Server 3
import { RESTDataSource, RequestOptions } from 'apollo-datasource-rest';
import { ApolloServer } from 'apollo-server';
class MoviesAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
override willSendRequest(request: RequestOptions) {
request.headers.set('Authorization', this.context.token);
}
async getMovie(id: string): Promise<Movie> {
return this.get<Movie>(`movies/${encodeURIComponent(id)}`);
}
async updateMovie(movie: Movie): Promise<Movie> {
return this.patch(
'movies',
// Syntax for passing a request body
{ id: movie.id, movie },
);
}
}
interface ContextValue {
token: string;
dataSources: {
moviesAPI: MoviesAPI;
};
}
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req: ExpressRequest }): Omit<ContextValue, 'dataSources'> => {
return {
token: getTokenFromRequest(req),
};
},
dataSources: (): ContextValue['dataSources'] => {
return {
moviesAPI: new MoviesAPI(),
};
},
});
await server.listen();
Apollo Server 3
import { RESTDataSource } from 'apollo-datasource-rest';
import { ApolloServer } from 'apollo-server';
class MoviesAPI extends RESTDataSource {
baseURL = 'https://movies-api.example.com/';
willSendRequest(request) {
request.headers.set('Authorization', this.context.token);
}
async getMovie(id) {
return this.get(`movies/${encodeURIComponent(id)}`);
}
async updateMovie(movie) {
return this.patch(
'movies',
// Syntax for passing a request body
{ id: movie.id, movie },
);
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req: ExpressRequest }) => {
return {
token: getTokenFromRequest(req),
};
},
dataSources: () => {
return {
moviesAPI: new MoviesAPI(),
};
},
});
await server.listen();

Below is how you write the same code in 4.

Apollo Server 4
import { RESTDataSource, AugmentedRequest } from '@apollo/datasource-rest';
// KeyValueCache is the type of Apollo server's default cache
import type { KeyValueCache } from '@apollo/utils.keyvaluecache';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
class MoviesAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
private token: string;
constructor(options: { token: string; cache: KeyValueCache }) {
super(options); // this sends our server's `cache` through
this.token = options.token;
}
override willSendRequest(path: string, request: AugmentedRequest) {
request.headers.authorization = this.token;
}
async getMovie(id: string): Promise<Movie> {
return this.get<Movie>(`movies/${encodeURIComponent(id)}`);
}
async updateMovie(movie: Movie): Promise<Movie> {
return this.patch(
'movies',
// Note the way we pass request bodies has also changed!
{ body: { id: movie.id, movie } },
);
}
}
interface ContextValue {
token: string;
dataSources: {
moviesAPI: MoviesAPI;
};
}
const server = new ApolloServer<ContextValue>({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = getTokenFromRequest(req);
const { cache } = server;
return {
token,
dataSources: {
moviesAPI: new MoviesAPI({ cache, token }),
},
};
},
});
console.log(`🚀 Server ready at ${url}`);
Apollo Server 4
import { RESTDataSource } from '@apollo/datasource-rest';
// KeyValueCache is the type of Apollo server's default cache
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
class MoviesAPI extends RESTDataSource {
baseURL = 'https://movies-api.example.com/';
constructor(options) {
super(options); // this sends our server's `cache` through
this.token = options.token;
}
willSendRequest(path, request) {
request.headers.authorization = this.token;
}
async getMovie(id) {
return this.get(`movies/${encodeURIComponent(id)}`);
}
async updateMovie(movie) {
return this.patch(
'movies',
// Note the way we pass request bodies has also changed!
{ body: { id: movie.id, movie } },
);
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = getTokenFromRequest(req);
const { cache } = server;
return {
token,
dataSources: {
moviesAPI: new MoviesAPI({ cache, token }),
},
};
},
});
console.log(`🚀 Server ready at ${url}`);

In 4, we've moved apollo-datasource-rest to the new @apollo/datasource-rest package. Most of the functionality between the two packages is the same. However, some small syntax differences exist in how we pass a request's headers, params, cacheOptions, and body. See Fetching from REST for more details.

If you want to access your entire context's value within your DataSource, you can do so by making your context value a class (enabling it to refer to itself via this in its constructor):

import { RESTDataSource, WillSendRequestOptions } from '@apollo/datasource-rest';
import { KeyValueCache } from '@apollo/utils.keyvaluecache';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { IncomingMessage } from 'http';
class MoviesAPI extends RESTDataSource {
override baseURL = 'https://movies-api.example.com/';
private contextValue: ContextValue;
constructor(options: { contextValue: ContextValue; cache: KeyValueCache }) {
super(options); // this should send `cache` through
this.contextValue = options.contextValue;
}
override willSendRequest(path: string, request: WillSendRequestOptions) {
request.headers['authorization'] = this.contextValue.token;
}
async getMovie(id): Promise<Movie> {
return this.get<Movie>(`movies/${encodeURIComponent(id)}`);
}
}
class ContextValue {
public token: string;
public dataSources: {
moviesAPI: MoviesAPI;
};
constructor({ req, server }: { req: IncomingMessage; server: ApolloServer<ContextValue> }) {
this.token = getTokenFromRequest(req);
const { cache } = server;
this.dataSources = {
moviesAPI: new MoviesAPI({ cache, contextValue: this }),
};
}
}
const server = new ApolloServer<ContextValue>({
typeDefs,
resolvers,
});
await startStandaloneServer(server, {
context: async ({ req }) => new ContextValue({ req, server }),
});
import { RESTDataSource } from '@apollo/datasource-rest';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
class MoviesAPI extends RESTDataSource {
baseURL = 'https://movies-api.example.com/';
constructor(options) {
super(options); // this should send `cache` through
this.contextValue = options.contextValue;
}
willSendRequest(path, request) {
request.headers['authorization'] = this.contextValue.token;
}
async getMovie(id) {
return this.get(`movies/${encodeURIComponent(id)}`);
}
}
class ContextValue {
constructor({ req, server }) {
this.token = getTokenFromRequest(req);
const { cache } = server;
this.dataSources = {
moviesAPI: new MoviesAPI({ cache, contextValue: this }),
};
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
});
await startStandaloneServer(server, {
context: async ({ req }) => new ContextValue({ req, server }),
});

If you want to migrate quickly to 4 without altering your , the snippet below replicates 3's dataSources behavior with a custom plugin.

We include this as a short-term fix and encourage you to create custom data source classes best suited for each source.

modules

In 3, there are several ways to provide your ApolloServer instance with a schema. One of the most common ways is to provide typeDefs and resolvers options (each of which can optionally be an array). Another way is using the modules option with an array of objects, each object containing typeDefs and resolvers keys. Under the hood, these two options use entirely different logic to do the same thing.

To simplify its API, 4 removes the modules constructor option. You can replace any previous usage of modules with the following syntax:

new ApolloServer({
typeDefs: modules.map({ typeDefs } => typeDefs),
resolvers: modules.map({ resolvers } => resolvers),
})

Additionally, the corresponding GraphQLSchemaModule TypeScript type is no longer exported.

mocks and mockEntireSchema

In 3, the mocks and mockEntireSchema constructor options enable to return simulated data for based on your server's schema. Under the hood, Apollo Server 3's mocking functionality is provided via an outdated version of the @graphql-tools/mocks library.

4 removes both the mocks and mockEntireSchema constructor options. You can instead directly incorporate the @graphql-tools/mock package into your app, enabling you to get the most up-to-date mocking features. For more details on configuring mocks, see the @graphql-tools/mocks docs.

The following examples compare the mocks and mockEntireSchema constructor options in 3 on the left and a replacement using @graphql-tools/mock on the right. You can also incrementally apply these changes in 3 without affecting behavior.

Apollo Server 3
new ApolloServer({
mocks: true,
});
Apollo Server 4
import { addMocksToSchema } from '@graphql-tools/mock';
import { makeExecutableSchema } from '@graphql-tools/schema';
new ApolloServer({
schema: addMocksToSchema({
schema: makeExecutableSchema({ typeDefs, resolvers }),
}),
});
Apollo Server 3
const mocks = {
Int: () => 6,
};
new ApolloServer({
mocks,
});
Apollo Server 4
import { addMocksToSchema } from '@graphql-tools/mock';
import { makeExecutableSchema } from '@graphql-tools/schema';
const mocks = {
Int: () => 6,
};
new ApolloServer({
schema: addMocksToSchema({
schema: makeExecutableSchema({ typeDefs, resolvers }),
mocks,
}),
});
Apollo Server 3
const mocks = {
Int: () => 6,
};
new ApolloServer({
mocks,
mockEntireSchema: false,
});
Apollo Server 4
import { addMocksToSchema } from '@graphql-tools/mock';
import { makeExecutableSchema } from '@graphql-tools/schema';
const mocks = {
Int: () => 6,
};
new ApolloServer({
schema: addMocksToSchema({
schema: makeExecutableSchema({ typeDefs, resolvers }),
mocks,
preserveResolvers: true,
}),
});

debug

In 3, the debug constructor option (which defaults to true unless the NODE_ENV environment variable is either production or test) controls several unrelated aspects of :

  • If debug is true, responses with errors include stack traces.
  • If debug is true and ApolloServer uses the default logger, prints all DEBUG log-level messages.
    • 3 rarely sends messages at the DEBUG level, so this primarily affects plugins that use the provided logger to send DEBUG messages.
  • The debug flag is also available to plugins on GraphQLRequestContext to use as they wish.

4 removes the debug constructor option. In its place is a new includeStacktraceInErrorResponses option which controls its namesake feature. Like debug, this option defaults to true unless the NODE_ENV environment variable is either production or test.

If you use debug in 3, you can use includeStacktraceInErrorResponses with the same value in 4:

const apolloServerInstance = new ApolloServer<MyContext>({
typeDefs,
resolvers,
includeStacktraceInErrorResponses: true,
});

Additionally, if your app or a plugin uses DEBUG-level log messages and your server doesn't use a custom logger, you are responsible for setting the default log level. For example, you can use the same Logger implementation that uses by default:

import loglevel from 'loglevel';
const logger = loglevel.getLogger('apollo-server');
logger.setLevel(shouldShowDebugMessages ? loglevel.levels.DEBUG : loglevel.levels.INFO);
const server = new ApolloServer({
logger,
// ...
});

(Note that the stack traces themselves have moved from extensions.exception.stacktrace to extensions.stacktrace.)

formatResponse hook

3 provides the formatResponse hook as a top-level constructor . The formatResponse hook is called after an successfully gets to the "execution" stage, enabling you to transform the structure of response objects before sending them to a client.

The formatResponse hook receives a successful 's response and requestContext (containing an unset response ). If the formatResponse hook returns a non-null GraphQLResponse, it uses that response instead of the initially received response .

4 removes the formatResponse hook. We instead recommend using the willSendResponse plugin hook, which enables you to do everything you previously did with formatResponse. The willSendResponse plugin hook receives an 's requestContext, which has a response containing a GraphQLResponse object. Note that the willSendResponse hook is allowed to mutate the requestContext.response .

4 changes the structure of GraphQLResponse, see below for more details.

calls the willSendResponse plugin hook for all requests that get far enough along to invoke requestDidStart (i.e., requests with a parsable JSON body, etc. ). This means that calls the willSendResponse hook in more contexts than the previous formatResponse hook.

To only use willSendResponse after an 's "execution" stage (i.e., like the previous formatResponse hook), you can make a filter checking for the existence of a data in the result. If an has a data in the result, it has made it to the execution phase. Note, there are some edge cases: for example, an error in the coercion of values calls formatResponse but doesn't have data in the result. If differentiating these edge cases is important to you, please open an issue, and we'll help.

For example, if your 3 code used formatResponse like this:

new ApolloServer({
typeDefs,
resolvers,
formatResponse({ response, requestContext }) {
return {
...response,
extensions: {
...(response.extensions),
hello: 'world',
},
},
},
}),

your 4 code can use willSendResponse, like so:

new ApolloServer<MyContext>({
typeDefs,
resolvers,
plugins: [
{
async requestDidStart() {
return {
async willSendResponse(requestContext) {
const { response } = requestContext;
// Augment response with an extension, as long as the operation
// actually executed. (The `kind` check allows you to handle
// incremental delivery responses specially.)
if (response.body.kind === 'single' && 'data' in response.body.singleResult) {
response.body.singleResult.extensions = {
...response.body.singleResult.extensions,
hello: 'world',
};
}
},
};
},
},
],
});

executor

In 3, there are two different ways to specify a replacement for graphql-js's execution functionality. Both of them involve defining a function of the type GraphQLExecutor. One way is to specify that function directly as the executor constructor option. The other way involves using the gateway option.

In 4, this redundancy has been removed: there is no longer an executor constructor option. (Additionally, the TypeScript GraphQLExecutor type has been renamed GatewayExecutor and moved to the @apollo/server-gateway-interface package.)

If your 3 code defined an executor function and used it like this:

new ApolloServer({
executor,
// ...
});
new ApolloServer({
executor,
// ...
});

your code can use gateway, like so:

new ApolloServer({
gateway: {
async load() {
return { executor };
},
onSchemaLoadOrUpdate() {
return () => {};
},
async stop() {},
},
});
new ApolloServer({
gateway: {
async load() {
return { executor };
},
onSchemaLoadOrUpdate() {
return () => {};
},
async stop() {},
},
});

Removed features

Several small features have been removed from 4.

Health checks

In 3, the health check feature supports a simple HTTP-level health check that always returns a 200 status code.

4 no longer supports built-in health checks. We found that running a trivial was a better way of checking the status of your server, because a query ensures your server successfully serves traffic and performs .

Every supports a trivial that requests the __typename of the top-level Query type. This means every can respond to a GET request to a URL, such as:

https://your.server/?query=%7B__typename%7D

You should also send an apollo-require-preflight: true header alongside your health check, so it isn't blocked by the CSRF prevention feature.

If you want a health check for your HTTP server unrelated to the health of the execution engine (i.e., like 3's health check feature), you can add a GET handler that always succeeds to your web framework.

To prevent inaccurate positive health checks, ensure that the Accept header is either unset or explicitly set to application/json.

NOTE

Using a trivial with 3's health check feature always produces a response. Apollo Server 4 returns an HTML page unless the Accept header's value is unset or explicitly set to application/json.

Path parsing

In 3, many framework integrations enable you to use the path option to configure the URL path where processes requests. By default, the path option uses the /graphql URL path.

In 4, you should use your framework's routing feature to mount your integration at the URL path where you want Apollo Server to process requests. For example, if you are using apollo-server-express in 3 and would like to continue using the default /graphql path, you should now mount the expressMiddleware function at the /graphql path.

3's batteries-included apollo-server package, replaced by startStandaloneServer in 4, serves all URLs (i.e., rather than only listening on /graphql).

HTTP body parsing and CORS

In 3, framework integrations automatically set up HTTP body parsing and CORS response headers. You can customize your integration's CORS or body parsing functionality using the Apollo Server API; these configuration options vary by integration.

In 4, it's your responsibility to set up HTTP body parsing and CORS headers for your web framework. Specifically, when using expressMiddleware, you can use the native express.json() function (available in Express v4.16.0 onwards) for body parsing and the cors npm package for CORS headers. You can install use the cors package in your Express app, just like with any other JSON-based API server. If you passed a cors option to applyMiddleware or getMiddleware, pass the same value to the cors function. If you passed a bodyParserConfig option to applyMiddleware or getMiddleware, pass the same value to the express.json() function.

Note that startStandaloneServer sets up body parsing and CORS functionality for you, but you can't configure this behavior. In 3, you could configure the batteries-included apollo-server's CORS behavior via the cors constructor option. In 4, if you need to configure CORS behavior, use expressMiddleware rather than startStandaloneServer.

gql GraphQL tag

3 depends on the graphql-tag npm package and re-exports its gql template literal tag. The gql tag is essentially a caching wrapper around graphql-js's parser, and most IDEs know to treat the contents of gql strings as .

4 does not depend on the graphql-tag library, nor does it export the gql tag. If you want to continue using the gql tag, you can directly install graphql-tag into your app, then update your import, replacing this line:

import { gql } from 'apollo-server';

with this line:

import gql from 'graphql-tag';

The apollo-server package exports gql as a named export, whereas the gql tag is the default export for graphql-tag.

ApolloError

4 removes both ApolloError and toApolloError in favor of using GraphQLError.

The graphql package exports GraphQLError, and you can use it like so:

import { GraphQLError } from 'graphql';
// ...
throw new GraphQLError(message, {
extensions: { code: 'YOUR_ERROR_CODE' },
});

If you used the optional code with ApolloError:

throw new ApolloError(message, 'YOUR_ERROR_CODE');

you should now pass your error code to GraphQLError's extensions option; see the above code snippet for an example.

Built-in error classes

3 exports several error classes. Apollo Server uses some of these error classes in specific situations (e.g., SyntaxError, ValidationError, and UserInputError), while other classes (ForbiddenError and AuthenticationError) are for users to use in their apps. All of these error classes are subclasses of the main ApolloError class.

In 4, ApolloError no longer exists, so doesn't export specific error classes. Instead, you can create your own error codes using graphql's GraphQLError class. Additionally, now provides an enum of error codes (ApolloServerErrorCode) that you can check against to see if a given error is one of the types recognized by .

In 3, you can throw a new ForbiddenError, like so:

import { ForbiddenError } from 'apollo-server';
throw new ForbiddenError("my message", { myExtension: "foo" })

In 4, you should define your own error using GraphQLError, like so:

import { GraphQLError } from 'graphql';
throw new GraphQLError("my message", {
extensions: {
code: 'FORBIDDEN',
myExtension: "foo",
},
});

For an AuthenticationError, use the code 'UNAUTHENTICATED'.

In 3, you can check the type of an error, like so:

if (error instanceof SyntaxError)

In 4, you can use the ApolloServerErrorCode enum to check if an error is one of the types recognized by , like so:

import { ApolloServerErrorCode } from '@apollo/server/errors';
if (error.extensions?.code === ApolloServerErrorCode.GRAPHQL_PARSE_FAILED)

For ValidationError, use ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED. For UserInputError, use ApolloServerErrorCode.BAD_USER_INPUT.

__resolveObject

4 removes the dependency on @apollographql/apollo-tooling, additionally removing the __resolveObject pseudo-. The __resolveObject function was an un predecessor to the __resolveReference method. While we believe __resolveObject is a useful feature, it would work better if implemented directly in graphql-js rather than in .

requestAgent option to ApolloServerPluginUsageReporting

's usage reporting plugin (i.e., ApolloServerPluginUsageReporting) lets you replace its HTTP client using the fetcher option. In 3, you can use an older requestAgent option, passed to the fetcher function via the non-standard agent option.

4 removes the requestAgent option from ApolloServerPluginUsageReporting. Now, all of the options you pass to ApolloServerPluginUsageReporting's fetcher are part of the Fetch API spec.

If you are using requestAgent in 3, you can use the node-fetch npm package to override fetcher.

So, where you previously wrote:

ApolloServerPluginUsageReporting({ requestAgent })

You can now write:

import fetch from 'node-fetch';
ApolloServerPluginUsageReporting({
fetcher: (url, options) => fetch(url, {
...options,
agent: requestAgent,
}),
});

rewriteError plugin option

In 3, you can specify a function to rewrite errors before sending them to Apollo's server via the rewriteError option to ApolloServerPluginUsageReporting (for monoliths) and ApolloServerPluginInlineTrace (for ).

In 4, you specify the same function as the transform option on the sendErrors option to ApolloServerPluginUsageReporting and the includeErrors option to ApolloServerPluginInlineTrace.

(Additionally, the default behavior has changed to mask errors.)

So, where you previously wrote:

// monoliths
new ApolloServer({
plugins: [ApolloServerPluginUsageReporting({ rewriteError })],
// ...
})
// subgraphs
new ApolloServer({
plugins: [ApolloServerPluginInlineTrace({ rewriteError })],
// ...
})

you can now write:

// monoliths
new ApolloServer({
plugins: [ApolloServerPluginUsageReporting({
sendErrors: { transform: rewriteError },
})],
// ...
})
// subgraphs
new ApolloServer({
plugins: [ApolloServerPluginInlineTrace({
includeErrors: { transform: rewriteError },
})],
// ...
})

Doubly-escaped variables and extensions in requests

3 and 4 both accept POST requests with a JSON body.

3 supports an edge case where the variables and extensions inside a POST request's JSON-encoded body can be JSON-encoded strings.

4 requires that within a POST request's JSON-encoded body, the variables and extensions must be objects (not doubly-encoded).

For example, below is a valid :

{
"query": "{ __typename }", "extensions": { "foo": 1 }
}

Whereas this would be invalid:

{
"query": "{ __typename }", "extensions": "{ \"foo\": 1 }"
}

(Moreover, 4 responds with a 400 status code if variables and extensions are provided in a POST body with any type other than object, such as array, boolean, or null. Similarly, it responds with a 400 status code if operationName is provided in a POST body with any type other than string.)

If you'd like to restore the previous behavior, you can JSON.parse the variables and extensions after your framework has parsed the request body. In Express that might look like:

app.use(express.json());
app.use((req, res, next) => {
if (typeof req.body?.variables === 'string') {
try {
req.body.variables = JSON.parse(req.body.variables);
} catch (e) {
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#json-parsing-failure
res.status(400).send(e instanceof Error ? e.message : e);
}
}
next();
});
app.use(expressMiddleware(server));

Changed features

New approach to serverless frameworks

In 3, our approach to creating frameworks involves subclassing ApolloServer and overriding the serverlessFramework() method.

In 4, integrations differentiate themselves by using the startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests method. The length of this function's name discourages its use when building non- apps.

Users of integrations shouldn't call any start-related functions before passing in an ApolloServer instance:

const server = new ApolloServer({
typeDefs,
resolvers,
});
exports.handler = startServerAndCreateLambdaHandler(server);

In the above example, the startServerAndCreateLambdaHandler integration function should call the server.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests() method.

4's other non- framework integrations expect the developer to await the server.start() method, before validating that the server has started by calling server.assertStarted().

context initialization function

In 3, you could provide an initial context to your by adding a context initialization function to the ApolloServer constructor:

// Apollo Server 3 Constructor
const server = new ApolloServer({
typeDefs,
resolvers,
csrfPrevention: true,
context: ({ req }) => ({
authScope: getScope(req.headers.authorization)
})
});

In 4, the context function is a named passed into your web integration function (e.g., expressMiddleware or startStandaloneServer). ApolloServer itself now has a generic type parameter specifying the type of your context value. The context function should return an object, which is then accessible to your server's and plugins (via the contextValue field).

Below is an example of providing a context initialization function to the startStandaloneServer function:

interface MyContext {
token: String;
}
const server = new ApolloServer<MyContext>({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
// A named context function is required if you are not
// using ApolloServer<BaseContext>
context: async ({ req, res }) => ({
token: await getTokenForRequest(req),
}),
listen: { port: 4000 },
});
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
// A named context function is required if you are not
// using ApolloServer<BaseContext>
context: async ({ req, res }) => ({
token: await getTokenForRequest(req),
}),
listen: { port: 4000 },
});

The context function's syntax is similar for the expressMiddleware function:

interface MyContext {
token: String;
}
const server = new ApolloServer<MyContext>({
typeDefs,
resolvers,
});
await server.start();
const app = express();
app.use(
// A named context function is required if you are not
// using ApolloServer<BaseContext>
expressMiddleware(server, {
context: async ({ req, res }) => ({
token: await getTokenForRequest(req),
}),
}),
);
const server = new ApolloServer({
typeDefs,
resolvers,
});
await server.start();
const app = express();
app.use(
// A named context function is required if you are not
// using ApolloServer<BaseContext>
expressMiddleware(server, {
context: async ({ req, res }) => ({
token: await getTokenForRequest(req),
}),
}),
);

In the expressMiddleware function, the req and res objects passed to the context function are express.Request and express.Response types. In the startStandaloneServer function, the req and res objects are http.IncomingMessage and http.ServerResponse types. If you need to use Express-specific properties in your context function, use expressMiddleware.

executeOperation accepts context value

The server.executeOperation method enables you to execute by specifying an operation's text directly instead of doing so via an HTTP request. You can use executeOperation to test your server.

In 3, you can indirectly specify an 's context value by passing a second optional to executeOperation; ApolloServer then passes this to its context function. For example, if you're using apollo-server-express, you can create an Express request and response then pass them to executeOperation as a { req, res } object.

In 4, the executeOperation method optionally receives a context value directly, bypassing your context function. If you want to test the behavior of your context function, we recommend running actual HTTP requests against your server.

Additionally, the structure of the returned GraphQLResponse has changed, as described below.

So a test for 3 that looks like this:

const server = new ApolloServer({
typeDefs: 'type Query { hello: String!}',
resolvers: {
Query: {
hello: (_, __, context) => `Hello ${context.name}!`,
},
},
context: async ({ req }) => ({ name: req.headers.name }),
});
const result = await server.executeOperation(
{
query: 'query helloContext { hello }',
},
{
// A half-hearted attempt at making something vaguely like an express.Request,
// and not bothering to make the express.Response at all.
req: { headers: { name: 'world' } },
},
);
expect(result.data?.hello).toBe('Hello world!'); // -> true
const server = new ApolloServer({
typeDefs: 'type Query { hello: String!}',
resolvers: {
Query: {
hello: (_, __, context) => `Hello ${context.name}!`,
},
},
context: async ({ req }) => ({ name: req.headers.name }),
});
const result = await server.executeOperation(
{
query: 'query helloContext { hello }',
},
{
// A half-hearted attempt at making something vaguely like an express.Request,
// and not bothering to make the express.Response at all.
req: { headers: { name: 'world' } },
},
);
expect(result.data?.hello).toBe('Hello world!'); // -> true

looks like this in 4:

interface MyContext {
name: string;
}
const server = new ApolloServer<MyContext>({
typeDefs: 'type Query { hello: String!}',
resolvers: {
Query: {
hello: (_, __, context) => `Hello ${context.name}!`,
},
},
});
const { body } = await server.executeOperation(
{
query: 'query helloContext { hello }',
},
{
contextValue: {
name: 'world',
},
},
);
// Note the use of Node's assert rather than Jest's expect; if using
// TypeScript, `assert` will appropriately narrow the type of `body`
// and `expect` will not.
assert(body.kind === 'single');
expect(body.singleResult.data?.hello).toBe('Hello world!'); // -> true
const server = new ApolloServer({
typeDefs: 'type Query { hello: String!}',
resolvers: {
Query: {
hello: (_, __, context) => `Hello ${context.name}!`,
},
},
});
const { body } = await server.executeOperation(
{
query: 'query helloContext { hello }',
},
{
contextValue: {
name: 'world',
},
},
);
// Note the use of Node's assert rather than Jest's expect; if using
// TypeScript, `assert` will appropriately narrow the type of `body`
// and `expect` will not.
assert(body.kind === 'single');
expect(body.singleResult.data?.hello).toBe('Hello world!'); // -> true

Error formatting changes

formatError improvements

3 supports the formatError hook, which has the following signature:

(error: GraphQLError) => GraphQLFormattedError

This hook receives an error already altered by 3, and differs from the initially thrown error.

In 4, this becomes:

(formattedError: GraphQLFormattedError, error: unknown) => GraphQLFormattedError

Above, formattedError is the default JSON object sent in the response according to the GraphQL specification, and error is the originally thrown error. If you need a from the original error that isn't in GraphQLFormattedError, you can access that value from the error .

One caveat: if the error was thrown inside a , error is not the error your code threw; it is a GraphQLError wrapping it and adding helpful context such as the path in the to the 's . If you want the exact error you threw, 4 provides the unwrapResolverError function in @apollo/server/errors, which removes this outer layer if you pass it a error (and returns its otherwise).

So, you can format errors like this:

import { unwrapResolverError } from '@apollo/server/errors';
new ApolloServer({
formatError: (formattedError, error) => {
// Don't give the specific errors to the client.
if (unwrapResolverError(error) instanceof CustomDBError) {
return { message: 'Internal server error' };
}
// Strip `Validation: ` prefix and use `extensions.code` instead
if (formattedError.message.startsWith('Validation:')) {
return {
...formattedError,
message: formattedError.message.replace(/^Validation: /, ''),
extensions: { ...formattedError?.extensions, code: 'VALIDATION' },
};
}
// Otherwise, return the original error. The error can also
// be manipulated in other ways, as long as it's returned.
return formattedError;
},
// ...
});

error.extensions.exception is removed

When 3 formats an error, it may add an extension called exception. This extension is an object with a for every enumerable property of the originally thrown error. (This does not apply if the originally thrown error was already a GraphQLError.) In addition, when in dev/debug mode, exception contains an array of strings called stacktrace.

For example, if this code runs in a :

const e = new Error("hello");
e.extraProperty = "bye";
throw e;

then (in debug mode) 3 will format the error like this:

{
"errors": [
{
"message": "hello",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["x"],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"extraProperty": "bye",
"stacktrace": [
"Error: hello",
" at Object.x (file:///private/tmp/as3-t/server.mjs:8:27)",
" at field.resolve (/private/tmp/as3-t/node_modules/apollo-server-core/dist/utils/schemaInstrumentation.js:56:26)",
// more lines elided
]
}
}
}
]
}

It was often hard to predict exactly which properties of which errors would be publicly exposed in this manner, which could lead to surprising information leaks.

In 4, there is no exception extension. The stacktrace is provided directly on extensions. If you'd like to copy some or all properties from the original error onto the formatted error, you can do that with the formatError hook.

If you'd like your errors to be formatted like they are in 3 (with the stack trace and the enumerable properties of the original error on the exception extension), you can provide this formatError implementation:

function formatError(formattedError: GraphQLFormattedError, error: unknown) {
const originalError = unwrapResolverError(error);
const exception: Record<string, unknown> = {
...(typeof originalError === 'object' ? originalError : null),
};
delete exception.extensions;
if (formattedError.extensions?.stacktrace) {
exception.stacktrace = formattedError.extensions.stacktrace;
}
const extensions: Record<string, unknown> = {
...formattedError.extensions,
exception,
};
delete extensions.stacktrace;
return {
...formattedError,
extensions,
};
}
function formatError(formattedError, error) {
const originalError = unwrapResolverError(error);
const exception = {
...(typeof originalError === 'object' ? originalError : null),
};
delete exception.extensions;
if (formattedError.extensions?.stacktrace) {
exception.stacktrace = formattedError.extensions.stacktrace;
}
const extensions = {
...formattedError.extensions,
exception,
};
delete extensions.stacktrace;
return {
...formattedError,
extensions,
};
}

3.5.0 and newer included a TypeScript declare module declaration that teaches TypeScript that error.extensions.exception.stacktrace is an array of strings on all GraphQLError objects. 4 does not provide a replacement for this: that is, we do not tell TypeScript the type of error.extensions.code or error.extensions.stacktrace. (The 3 declare module declaration also incorrectly teaches TypeScript that error.extensions.exception.code is a string, which should have been error.extensions.code instead.)

Improvements to error handling outside of resolvers

3 returns some errors relating to over HTTP/JSON as text/plain error messages.

4 now returns all non-landing-page-related responses as application/json JSON responses. This means all single-error responses render like any other error:

{
"errors":[{"message": "..."}]
}

Additionally, the formatError hook receives and can format all of these error instances.

4 also introduces new plugin hooks startupDidFail, contextCreationDidFail, invalidRequestWasReceived, and unexpectedErrorProcessingRequest, enabling plugins to observe errors in new settings.

In 3, if your context function throws, then the string "Context creation failed: " is always prepended to its message, and the error is rendered with HTTP status code 500 (if the error is a Error with extensions.code equal to INTERNAL_SERVER_ERROR) or 400. You cannot select a different HTTP status code or control HTTP response headers.

In 4, if your context function throws, the string "Context creation failed: " is only prepended to the message if the thrown error was not a GraphQLError. There is no special-casing of extensions.code; instead, you can use extensions.http to set the HTTP status code or headers. If this extension is not provided, the status code defaults to 500 (not 400).

In 4, if the execute function throws an error, that error is rendered with the HTTP status code 500 (rather than 400). Note that the execute function commonly returns a non-empty list of errors rather than throwing an explicit error.

Warning for servers without draining

Versions of 3.2 and above add a "draining" phase to server shutdown, enabling a server to complete in-progress before continuing to shut down. Without this draining phase, operations can reach your server even after it has stopped, causing those operations to fail.

In 4, if your server hasn't set up draining and it receives an as the server is shutting down, the server logs a warning before failing that operation.

If you are using the startStandaloneServer function, your server drains automatically. If you are using expressMiddleware or another http.Server-based web server, you can add draining using the ApolloServerPluginDrainHttpServer plugin.

Cache control plugin sets cache-control header for uncached requests

The cache control plugin is installed by default. It does two things: it calculates requestContext.overallCachePolicy based on static and dynamic hints, and it sets the Cache-Control response HTTP header.

In 3, the cache control plugin only sets the Cache-Control header when the response is cacheable.

In 4, the cache control plugin also sets the Cache-Control header (to the value no-store) when the response is not cacheable.

To restore the behavior from 3, you can install the cache control plugin and set calculateHttpHeaders: 'if-cacheable':

import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';
new ApolloServer({
// ...
plugins: [ApolloServerPluginCacheControl({ calculateHttpHeaders: 'if-cacheable' })],
});
import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';
new ApolloServer({
// ...
plugins: [ApolloServerPluginCacheControl({ calculateHttpHeaders: 'if-cacheable' })],
});

CacheScope type

In 4, CacheScope is now a union of strings (PUBLIC or PRIVATE) rather than an enum:

export type CacheScope = 'PUBLIC' | 'PRIVATE';

You can no longer type CacheScope.Public or CacheScope.Private. Instead, just use the string 'PUBLIC' or 'PRIVATE'. Values defined as CacheScope will only acce