Launch GraphOS Studio
Apollo Server 3 is officially deprecated, with end-of-life scheduled for 22 October 2024. Learn more about upgrading to a supported Apollo Server version.

Migrating to Apollo Server 3

⚠️ As of 15 November 2022, Apollo Server 3 is officially deprecated, with end-of-life scheduled for 22 October 2024.

Learn more about deprecation and end-of-life.

3 is deprecated, and we strongly recommend upgrading to Apollo Server 4. If you are currently using 2, we recommend first upgrading to Apollo Server 3 using this guide. Afterward, you can continue upgrading from Apollo Server 3 to 4.

The focus of this major-version release is to provide a lighter, nimbler core library as a foundation for future features and improved extensibility.

Many Apollo Server 2 users don't need to make any code changes to upgrade to Apollo Server 3! This is especially likely if you use the "batteries-included" apollo-server library (as opposed to a middleware-specific library).

This explains which features do require code changes and how to make them. If you encounter issues while migrating, please create an issue.

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

Bumped dependencies


Apollo Server 3 supports Node.js 12 and later. ( 2 supports back to Node.js 6.) This includes all LTS and Current versions at the time of release.

If you're using an older version of Node.js, upgrade your runtime before upgrading to 3.


has a peer dependency on graphql (the core JS implementation), which means you are responsible for choosing the version installed in your app.

Apollo Server 3 supports graphql v15.3.0 and later. ( 2 supported graphql v0.12 through v15.)

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

Removed integrations

2 provides built-in support for and file uploads via the subscriptions-transport-ws and graphql-upload packages, respectively. It also serves Playground from its base URL by default.

3 removes these built-in integrations, in favor of enabling users to provide their own mechanisms for these features.

You can reenable all of these integrations as they exist in 2.


2 provides limited, built-in support for WebSocket-based via the subscriptions-transport-ws package. This integration doesn't work with 's plugin system or Apollo Studio usage reporting.

3 no longer contains this built-in integration. However, you can still use subscriptions-transport-ws for if you depend on this implementation. Note that as with 2, this integration won't work with the plugin system or Studio usage reporting.

Additionally, 3 no longer re-exports all of the exports from the graphql-subscriptions package such as PubSub.

We hope to add more fully-integrated support to in a future version.

Note that the subscriptions-transport-ws library is no longer maintained. A newer, actively-maintained graphql-ws package exists. These libraries implement different protocols for over WebSocket, so you need to adjust your client to support graphql-ws. This migration guide shows how to reenable subscriptions-transport-ws to make your transition from 2 to Apollo Server 3 as easy as possible, but we recommend that you then migrate from subscriptions-transport-ws to graphql-ws once you've finished the upgrade. The subscriptions documentation shows how to use the newer graphql-ws rather than the unmaintained subscriptions-transport-ws.

Reenabling subscriptions

Currently, these instructions are only for 's Express integration. It is likely possible to integrate subscriptions-transport-ws with other integrations. PRs to this migration guide are certainly welcome!

In 3, you can't use subscriptions-transport-ws with the "batteries-included" apollo-server package. If your project uses apollo-server with , first swap to apollo-server-express.

Then, complete the following:

  1. Install subscriptions-transport-ws and @graphql-tools/schema.

    npm install subscriptions-transport-ws @graphql-tools/schema
  2. Add the following imports in the module where your is currently instantiated. We'll use these in the subsequent steps.

    import { createServer } from 'http';
    import { execute, subscribe } from 'graphql';
    import { SubscriptionServer } from 'subscriptions-transport-ws';
    import { makeExecutableSchema } from '@graphql-tools/schema';
  3. Create an http.Server instance with your Express app.

    In order to set up both the HTTP and WebSocket servers, we'll need to create an http.Server. Do this by passing your Express app to the createServer function which we imported from the Node.js http module.

    // This `app` is the returned value from `express()`.
    const httpServer = createServer(app);
  4. Create an instance of GraphQLSchema (if one doesn't already exist).

    Your server may already pass a schema to the ApolloServer constructor. If it does, this step can be skipped. You'll use the existing schema instance in a later step.

    The SubscriptionServer (which we'll instantiate next) doesn't accept typeDefs and resolvers directly, but rather an executable GraphQLSchema. We can pass this schema object to both the SubscriptionServer and ApolloServer. This way, it's clear that the same schema is being used in both places.

    const schema = makeExecutableSchema({ typeDefs, resolvers });
    // ...
    const server = new ApolloServer({
  5. Create the SubscriptionServer.

    const subscriptionServer = SubscriptionServer.create({
    // This is the `schema` we just created.
    // These are imported from `graphql`.
    // This `server` is the instance returned from `new ApolloServer`.
    // Ensures the same graphql validation rules are applied to both the Subscription Server and the ApolloServer
    validationRules: server.requestOptions.validationRules
    // Providing `onConnect` is the `SubscriptionServer` equivalent to the
    // `context` function in `ApolloServer`. Please [see the docs](
    // for more information on this hook.
    async onConnect(
    connectionParams: Object,
    webSocket: WebSocket,
    context: ConnectionContext
    ) {
    // If an object is returned here, it will be passed as the `context`
    // argument to your subscription resolvers.
    }, {
    // This is the `httpServer` we created in a previous step.
    server: httpServer,
    // This `server` is the instance returned from `new ApolloServer`.
    path: server.graphqlPath,
  6. Add a plugin to your ApolloServer constructor to close the SubscriptionServer.

    const server = new ApolloServer({
    plugins: [{
    async serverWillStart() {
    return {
    async drainServer() {
  7. Finally, adjust the existing listen.

    Most applications will be calling app.listen(...) on their Express app. This should be changed to httpServer.listen(...) using the same . This will begin listening on the HTTP and WebSocket transports simultaneously.

A completed example of migrating is shown below:

File uploads

2 provides built-in support for file uploads via an outdated version of the graphql-upload library. Using an updated version of graphql-upload requires you to disable this built-in support due to backward incompatible changes.

This built-in support is removed in 3. To use graphql-upload, you can choose an appropriate version and integrate it yourself. Note that graphql-upload does not support federation or every Node.js framework supported by .

To use graphql-upload with 3, see the documentation on enabling file uploads in Apollo Server. Note that if your project currently uses uploads with the "batteries-included" apollo-server package, you must first swap to apollo-server-express.

Warning: using graphql-upload in your server without proper mitigation increases your server's vulnerability to Cross-Site Request Forgery (CSRF) attacks. This affected the default configuration of 2 and will affect you if you manually integrate with graphql-upload. We do not typically recommend the use of graphql-upload and think that file uploads generally work best out of band from your API; however, if you do want to use graphql-upload then you must take actions to protect your users from CSRF attacks. The easiest way to do this is to ensure you are using at least 3.7 and enable our CSRF prevention feature by passing csrfPrevention: true to new ApolloServer(). Note that you will have to configure your upload client (like apollo-upload-client) to pass a non-empty Apollo-Require-Preflight header to your server once this feature is enabled.

Additionally, 3 no longer re-exports the GraphQLUpload symbol from the graphql-upload package.

GraphQL Playground

By default, 2 serves the (now-retired) GraphQL Playground IDE from its base URL in non-production environments. In production, it serves no landing page. 2 also accepts a playground constructor option to override this default behavior (for example, to enable Playground even in production).

3 removes Playground (and its associated constructor option) in favor of a landing page that links to in non-production environments. In production, it serves a simplified landing page instead.

You can customize your 3 landing page, as described in Build and run queries.

Reenabling GraphQL Playground

You can continue to use Playground by installing its associated plugin. You customize its behavior by passing options to the plugin instead of via the playground constructor option.

If your code did not specify the playground constructor option and you'd like to keep the previous behavior instead of trying the new splash page, you can do that as follows:

import { ApolloServerPluginLandingPageGraphQLPlayground,
ApolloServerPluginLandingPageDisabled } from 'apollo-server-core';
new ApolloServer({
plugins: [
process.env.NODE_ENV === 'production'
? ApolloServerPluginLandingPageDisabled()
: ApolloServerPluginLandingPageGraphQLPlayground(),

If your code passed new ApolloServer({playground: true}), you can keep the previous behavior with:

import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core';
new ApolloServer({
plugins: [

If your code passed new ApolloServer({playground: false}) you can keep the previous behavior with:

import { ApolloServerPluginLandingPageDisabled } from 'apollo-server-core';
new ApolloServer({
plugins: [

If your code passed an options object like new ApolloServer({playground: playgroundOptions}), you can keep the previous behavior with:

import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core';
new ApolloServer({
plugins: [

Additional changes to GraphQL Playground

Specifying an endpoint

In 2, the default value of Playground's endpoint option is determined in different ways by different Node.js framework integrations. In many cases, it's necessary to manually specify playground: {endpoint}.

In 3, the default endpoint used by Playground is the browser's current URL. In many cases, this means that you don't have to specify endpoint anymore. If your 2 app specified playground: {endpoint} (and you wish to continue using Playground), try removing endpoint from the options passed to ApolloServerPluginLandingPageGraphQLPlayground and see if it works for you.

Specifying settings

In 2, the behavior of the settings Playground option can be surprising. If you don't explicitly pass {playground: {settings: {...}}} then Playground always uses settings that are built into its React application (some of which can be adjusted by the user in their browser). However, if you pass any object as playground: {settings: {...}}, several default value overrides take effect.

This surprising behavior is removed in 3. All settings use default values from the Playground React app if they aren't specified in the settings option to ApolloServerPluginLandingPageGraphQLPlayground.

If your app does pass in playground: {settings: {...}} and you want to make sure the settings used in your Playground do not change, you should copy any relevant settings from the Apollo Server 2 code into your app.

For example, you could replace:

new ApolloServer({playground: {settings: {'some.setting': true}}})


import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core';
new ApolloServer({
plugins: [
settings: {
'some.setting': true,
'general.betaUpdates': false,
'editor.theme': 'dark',
'editor.cursorShape': 'line',
'editor.reuseHeaders': true,
'tracing.hideTracingResponse': true,
'queryPlan.hideQueryPlanResponse': true,
'editor.fontSize': 14,
'editor.fontFamily': `'Source Code Pro', 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace`,
'request.credentials': 'omit',

Removed constructor options

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


3 removes support for the graphql-extensions API, which was used to extend 's functionality. This API has numerous limitations relative to the plugins API introduced in v2.2.0.

Unlike graphql-extensions, the plugins API enables cross-request state, and its hooks virtually all interact with the same GraphQLRequestContext object.

If you've written your own (passed to new ApolloServer({extensions: ...})), you should rewrite them as plugins before upgrading to 3.


"Engine" is a previous name of Apollo Studio. Prior to v2.18.0, you passed the engine constructor option to configure how communicates with Studio. Additionally, you could specifying your Apollo API key with the ENGINE_API_KEY environment variable, and you could specify a with the ENGINE_SCHEMA_TAG environment variable.

In later versions of (including Apollo Server 3), you instead provide this configuration via a combination of the apollo constructor option, plugins, and APOLLO_-prefixed environment variables. 3 does not support the engine constructor option or the ENGINE_-prefixed environment variables.

If your project still uses the engine option or the ENGINE_-prefixed environment variables, see Migrating from the engine option before upgrading to 3.

Note that if you are using Studio, make sure to set your graph ref or graph ID.


In 2, you can pass schemaDirectives to new ApolloServer alongside typeDefs and resolvers. These are all passed through to the makeExecutableSchema function from the graphql-tools package.

The graphql-tools project deprecated the schemaDirectives feature and removed it in v8 of @graphql-tools/schema.

In 3, the ApolloServer constructor now only passes typeDefs, resolvers, and parseOptions through to makeExecutableSchema.

If you would like to still use schemaDirectives, you can install an older version of @graphql-tools/schema yourself with npm install @graphql-tools/schema@7 and call makeExecutableSchema yourself and pass its returned schema as the schema constructor option.

That is, you can replace:

new ApolloServer({


// Make sure you are using v7, not anything newer!
import { makeExecutableSchema } from '@graphql-tools/schema';
new ApolloServer({
schema: makeExecutableSchema({

This can help you with the initial migration to 3. As schemaDirectives is no longer part of the actively maintained version of @graphql-tools/schema, we recommend that once you've successfully migrated to 3, you then port your schema to the newer schema directives API which uses the mapSchema function in @graphql-tools/utils.

In 2, there are subtle differences between providing a schema with schema versus providing it with typeDefs and resolvers. For example, the automatic definition of the @cacheControl is added only in the latter case. These differences are removed in 3 (for example, the definition of the @cacheControl is never automatically added).


In 2, the tracing constructor option enables a trace mechanism implemented in the apollo-tracing package. This package uses a comparatively inefficient JSON format for execution traces returned via the tracing response extension. The format is consumed only by the deprecated engineproxy and Playground. It is not the tracing format used for Apollo Studio usage reporting or federated inline traces.

The tracing constructor option is removed in 3. The apollo-tracing package has been deprecated and is no longer being published.

If you rely on this deprecated trace format, you might be able to use the old version of apollo-server-tracing directly:

new ApolloServer({
plugins: [

This workaround has not been tested! If you need this to work and it doesn't, please file an issue and we will investigate a fix to enable support in 3.


In 2, cache policy support is configured via the cacheControl constructor option. There are several improvements to the semantics of cache policies in 3, as well as changes to how caching is configured.

The cacheControl constructor option is removed in 3. To customize cache control, you instead manually install the cache control plugin and provide custom options to it.

For example, if you currently provide defaultMaxAge and/or calculateHttpHeaders to cacheControl like so:

new ApolloServer({
cacheControl: {

You now provide them like so:

import { ApolloServerPluginCacheControl } from 'apollo-server-core';
new ApolloServer({
plugins: [

If you currently pass cacheControl: false like so:

new ApolloServer({
cacheControl: false,

You now install the disabling plugin like so:

import { ApolloServerPluginCacheControlDisabled } from 'apollo-server-core';
new ApolloServer({
plugins: [

In 2, cacheControl: true was a shorthand for setting cacheControl: {stripFormattedExtensions: false, calculateHttpHeaders: false}. If you either passed cacheControl: true or explicitly passed stripFormattedExtensions: false, 2 would include a cacheControl response extension inside your response. This was used by the deprecated engineproxy server. Support for writing this response extension has been removed from 2. This allows for a more memory-efficient cache control plugin implementation.

In 2, definitions of the @cacheControl (and the CacheControlScope enum that it uses) were sometimes automatically inserted into your schema. (Specifically, they were added if you defined your schema with the typeDefs and resolvers options, but not if you used the modules or schema options or if you were a federated gateway. Passing cacheControl: false did not stop the definitions from being inserted!) In 3, these definitions are never automatically inserted.

So if you use the @cacheControl directive in your schema, you should add these definitions to your schema:

enum CacheControlScope {
directive @cacheControl(
maxAge: Int
scope: CacheControlScope
inheritMaxAge: Boolean

(You may add them to your schema in 2 before upgrading if you'd like.)

In 2, plugins that want to change the 's overall cache policy can overwrite the requestContext.overallCachePolicy. In 3, that is considered read-only, but it does have new methods to mutate its state. So you should replace:

requestContext.overallCachePolicy = { maxAge: 100 };


requestContext.overallCachePolicy.replace({ maxAge: 100 });

(You may also want to consider using restrict instead of replace; this method only allows maxAge to be reduced and only allows scope to change from PUBLIC to PRIVATE.)

In 2, returning a union type are treated similarly to fields returning a type: @cacheControl on the type itself is ignored, and maxAge if unspecified is inherited from its parent in the (unless it is a root ) instead of defaulting to defaultMaxAge (which itself defaults to 0). In 3, returning a union type are treated similarly to fields returning an interface or : @cacheControl on the type itself is honored, and maxAge if unspecified defaults to defaultMaxAge. If you were relying on the inheritance behavior, you can specify @cacheControl(maxAge: ...) explicitly on your union types or union-returning , or you can use the new @cacheControl(inheritMaxAge: true) feature on the union-returning to restore the 2 behavior. If your schema contained union SomeUnion @cacheControl(...), that will start having an effect when you upgrade to 3.

In 2, the @cacheControl is honored on type definitions but not on type . That is, if you write type SomeType @cacheControl(maxAge: 123) it takes effect but if you write extend type SomeType @cacheControl(maxAge: 123) it does not take effect. In 3, @cacheControl is honored on object, interface, and union . If your schema accidentally contained @cacheControl on an extend, that will start having an effect when you upgrade to 3.

In 2, most of the logic related to cache control lives in the apollo-cache-control package. This package exports a function called plugin as well as TypeScript types CacheControlFormat, CacheHint, CacheScope, and CacheControlExtensionOptions. In 3, this logic lives in apollo-server-core and the apollo-cache-control package is no longer published. apollo-server-core exports ApolloServerPluginCacheControl and ApolloServerPluginCacheControlOptions (which are similar to plugin and CacheControlExtensionOptions). apollo-server-types exports CacheHint and CacheScope. There is no equivalent export to CacheControlFormat because as described above, the code to write the cacheControl response extension is not part of 3.


See GraphQL Playground.

Removed exports

In 2, apollo-server and framework integration packages such as apollo-server-express import many symbols from third-party packages and re-export them. This effectively ties the API of to a specific version of those third-party packages and makes it challenging to upgrade to a newer version or for you to upgrade those packages yourself.

In 3, most of these "re-exports" are removed. If you want to use these exports, you should import them directly from their originating package.

Exports from graphql-tools

2 exports every symbol exported by graphql-tools v4. If you're importing any of the following symbols from an package, you should instead run npm install graphql-tools@4.x and import the symbol from graphql-tools.

Alternatively, read the GraphQL Tools docs and find out which @graphql-tools/subpackage the symbol is exported from in more modern versions of Tools.

  • AddArgumentsAsVariables
  • AddTypenameToAbstract
  • CheckResultAndHandleErrors
  • DirectiveResolverFn
  • ExpandAbstractTypes
  • ExtractField
  • FilterRootFields
  • FilterToSchema
  • FilterTypes
  • GraphQLParseOptions
  • IAddResolveFunctionsToSchemaOptions
  • IConnectorCls
  • IConnectorFn
  • IConnector
  • IConnectors
  • IDelegateToSchemaOptions
  • IDirectiveResolvers
  • IEnumResolver
  • IExecutableSchemaDefinition
  • IFieldIteratorFn
  • IFieldResolver
  • IGraphQLToolsResolveInfo
  • ILogger
  • IMockFn
  • IMockOptions
  • IMockServer
  • IMockTypeFn
  • IMocks
  • IResolverObject
  • IResolverOptions
  • IResolverValidationOptions
  • IResolversParameter
  • IResolvers
  • ITypeDefinitions
  • ITypedef
  • MergeInfo
  • MergeTypeCandidate
  • MockList
  • NextResolverFn
  • Operation
  • RenameRootFields
  • RenameTypes
  • ReplaceFieldWithFragment
  • Request
  • ResolveType
  • Result
  • SchemaDirectiveVisitor
  • SchemaError
  • TransformRootFields
  • Transform
  • TypeWithResolvers
  • UnitOrList
  • VisitTypeResult
  • VisitType
  • WrapQuery
  • addCatchUndefinedToSchema
  • addErrorLoggingToSchema
  • addMockFunctionsToSchema
  • addResolveFunctionsToSchema
  • addSchemaLevelResolveFunction
  • assertResolveFunctionsPresent
  • attachConnectorsToContext
  • attachDirectiveResolvers
  • buildSchemaFromTypeDefinitions
  • chainResolvers
  • checkForResolveTypeResolver
  • concatenateTypeDefs
  • decorateWithLogger
  • defaultCreateRemoteResolver
  • defaultMergedResolver
  • delegateToSchema
  • extendResolversFromInterfaces
  • extractExtensionDefinitions
  • forEachField
  • introspectSchema
  • makeExecutableSchema
  • makeRemoteExecutableSchema
  • mergeSchemas
  • mockServer
  • transformSchema

Exports from graphql-subscriptions

2 exports every symbol exported by graphql-subscriptions. If you are importing any of the following symbols from an package, you should instead run npm install graphql-subscriptions and import the symbol from graphql-subscriptions instead.

  • FilterFn
  • PubSub
  • PubSubEngine
  • PubSubOptions
  • ResolverFn
  • withFilter

Exports from graphql-upload

2 exports the GraphQLUpload symbol from (our fork of) graphql-upload. 3 no longer has built-in graphql-upload integration. See the documentation on how to enable file uploads in Apollo Server 3.

2 exports a defaultPlaygroundOptions object, along with PlaygroundConfig and PlaygroundRenderPageOptions types to support the playground top-level constructor .

In 3, Playground is one of several landing pages implemented via plugins, and there are no default options for it. The ApolloServerPluginLandingPageGraphQLPlaygroundOptions type exported from apollo-server-core plays a similar role to PlaygroundConfig and PlaygroundRenderPageOptions. See the section on playground above for more details on configuring Playground in 3.

Removed features

Several small features have been removed from 3.

Guessing Apollo Studio graph ID from API key

In 2, if you specify an Apollo API key (e.g., with the APOLLO_KEY environment variable) that starts with service:your-graph-id:, automatically guesses that your Studio ID (used for usage reporting, schema reporting, , and so on) is your-graph-id.

In 3, you should instead specify your Studio ID explicitly when using features that connect to Studio. You can specify the graph ID by itself in the APOLLO_GRAPH_ID environment variable (or via new ApolloServer({apollo: {graphId}})), or alongside the in a string like your-graph-id@your-graph-variant in the APOLLO_GRAPH_REF environment variable (or via new ApolloServer({apollo: {graphRef}})). See the apollo constructor option for more details.

If you set your API key but do not set your or ID:

  • If you explicitly set up features like the usage reporting plugin or , 3 throws an error on startup.
  • If you don't explicitly set up or disable the usage reporting plugin, 3 logs a warning suggesting that you either set your or disable the usage reporting plugin.

ApolloServer.schema field

2 has a deprecated ApolloServer.schema (which doesn't work when the server is a federated gateway). 3 does not contain this . To access your server's schema, you have a few options:

  • Construct the schema yourself (e.g., with const schema = makeExecutableSchema({typeDefs, resolvers}) from @graphql-tools/schema), pass it in as new ApolloServer({schema}), and refer to this same schema value elsewhere. 3 won't modify the schema you pass in.
  • Write a plugin that uses the serverWillStart event to obtain the schema. Note that if your server is a gateway, this receives the first schema that's loaded on startup but doesn't receive any subsequent schemas if it updates dynamically.
  • If your server is a gateway, register a callback with gateway.onSchemaChange. Note that this API has some inconsistent behavior. To resolve this, we are considering adding an Apollo Server plugin event that receives all schema updates.


2 contains a package apollo-server-testing. This package is a thin wrapper around the server.executeOperation method. As of 2.25.0, we no longer this package and instead document using executeOperation directly.

In 3, we no longer publish this package. It's possible that the Apollo Server 2 version of this package might work with Apollo Server 3 servers. If you do use apollo-server-testing, we suggest that you migrate to using executeOperation directly (with at least 2.25.0 of ) before upgrading to Apollo Server 3.

If you used apollo-server-testing like so:

const { createTestClient } = require('apollo-server-testing');
const { query, mutate } = createTestClient(server);
await query({ query: QUERY });
await mutate({ mutation: MUTATION });

then you can use executeOperation like so:

await server.executeOperation({ query: QUERY });
await server.executeOperation({ query: MUTATION });

Note that prior to 2.25.0, apollo-server-testing functions allowed you to pass your as a string or a DocumentNode as returned from gql, but executeOperation only supported strings. As of v2.25.0, executeOperation takes string or DocumentNode, which makes the change shown above straightforward in all cases.

apollo-datasource-rest: baseURL override change

When you create a RESTDataSource subclass, you need to provide its baseURL. This can be done via this.baseURL = ... in the constructor or resolveURL, or via baseURL = ... at the class level.

In 2 you can also provide baseURL via a getter like get baseURL() { ... }. 3 is compiled with TypeScript 4, which no longer supports overriding a property with an accessor, so this is no longer allowed.

If you used a getter in order to provide a dynamic URL, like this:

class MyDataSource extends RESTDataSource {
get baseURL() {
return someDynamicallyCalculatedURL();

you can instead override resolveURL:

class MyDataSource extends RESTDataSource {
async resolveURL(request: RequestOptions) {
if (!this.baseURL) {
this.baseURL = someDynamicallyCalculatedURL();
return super.resolveURL(request);

apollo-server-env's global type definitions

Global TypeScript definitions have been removed from apollo-server-env since they conflicted with similar global types provided by @types/supertest, which we use in 's test suite. These removed types include types which resemble those of the Fetch API including, e.g., fetch, RequestInfo, Headers, Request, Response, ResponseInit, and more. See the full list prior to removal here.

We don't expect this to affect many users and we have not publicly suggested using these types in the past, but it's possible that implementations may be using them inadvertently (e.g., from auto-import on apollo-server-env). While other type definition sets provide similar hand-curated types, for the time being, we have chosen to rely the same-named types from TypeScript's lib.dom.d.ts — e.g., its RequestInfo type definition. Even if those types are more appropriate for browsers, they're a reliable and well-maintained source. For more details, including our plan for adjusting this again in the future, see PR #5165.

Changed features

Plugin API

Almost all plugin events are now async

In 2, some plugin events are synchronous (their return value is not a Promise), and some are "maybe-asynchronous" (they could return a Promise if they wanted, but didn't have to). This means that you can't do asynchronous work in the former events, and the typings for the latter events are somewhat complex.

In 3, almost all plugin methods are always asynchronous: they always return a Promise type. This includes end hooks as well. The exceptions are willResolveField and its end hook and schemaDidLoadOrUpdate, which are always synchronous.

In practice, this means that all of your plugin events should use async functions or methods. If you are using TypeScript, you need to do this for your code to compile.

In practice, generally uses await or Promise.all on values returned by plugin methods instead of an explicit .then. These constructs accept both Promises and normal values. If you aren't using TypeScript, you might be able to get away with synchronous plugin methods for the time being.

willSendResponse is called more consistently

In 2, some errors related to persisted queries invoke the requestDidStart and didEncounterError plugin events without invoking the willSendResponse event afterwards.

In 3, any request that makes it far enough to invoke requestDidStart also invokes willSendResponse. See this PR for details.

executionDidStart can no longer return a function

In 2, the executionDidStart plugin event could return nothing, an object, or a function. If a function was provided, it would be called when execution finished (one might imagine an executionDidEnd).

In 3, executionDidStart must return either nothing or an object. If you previously returned a function, you can now return an object with an executionDidEnd like so:

const server = new ApolloServer({
plugins: [{
async requestDidStart() {
return {
async executionDidStart() {
return {
async executionDidEnd() {
// your code here

For more information, you can check out the related plugin documentation on end hooks.

Gateway interface renamed and simplified

In 2, the TypeScript type used for the gateway constructor option is called GraphQLService. In 3, it's called GatewayInterface. (For now, an identical interface named GraphQLService continues to be exported.)

This interface now requires the following:

  • The stop method must be present.
  • The executor method must async
  • The apollo option must always be passed to the load method.

All recent versions of @apollo/gateway satisfy these stronger requirements.

Bad request errors more consistently return 4xx

In 2, certain poorly formatted requests receive HTTP responses with a 5xx status (indicating a server error) instead of 4xx (indicating a client error). For example, this occurs in some integrations for a missing POST body or a JSON parse error.

In 3, these errors are handled in a more consistent manner across integrations and consistently return HTTP responses with a 4xx status.

Extensions (custom details) on ApolloError

Initializing an error

In 2, error extensions can be passed to the ApolloError constructor either as the third , or as an extensions option on the third .

In other words, these two lines are equivalent with 2:

new ApolloError(message, code, {key: 'value'})
new ApolloError(message, code, {extensions: {key: 'value'}})

In 3, only the first line above is supported. If you try to use the second line, the constructor throws an error. Before upgrading, replace any code using the second form with the first form.

Reading error extensions

In 2, an error extension (foo) is present in two locations on an ApolloError object (error): and When that object is serialized for a JSON response, the extension is also present in two locations in the JSON: and

In 3, an error extension is present in one ApolloError location: When the error is serialized to JSON, the extension is present only in

See this PR for more details.


In 2, the mocks and mockEntireSchema constructor options are essentially implemented as follows:

import { addMockFunctionsToSchema } from 'graphql-tools'; // v4.x
const { mocks, mockEntireSchema } = constructorOptions;
const schemaWithMocks = addMockFunctionsToSchema({
typeof mocks === 'boolean' || typeof mocks === 'undefined'
? {} : mocks,
typeof mockEntireSchema === 'undefined' ? false : !mockEntireSchema,

In 3, we've upgraded from graphql-tools v4 to @graphql-tools/mock v8. The details of how mocking works in Tools has changed, and the name of the function that implements it has changed too. The new implementation is as follows:

import { addMocksToSchema } from '@graphql-tools/mock'; // v8.x
const { mocks, mockEntireSchema } = constructorOptions;
const schemaWithMocks = addMocksToSchema({
mocks: mocks === true || typeof mocks === 'undefined' ? {} : mocks,
typeof mockEntireSchema === 'undefined' ? false : !mockEntireSchema,

So the name of the function has changed, and additionally, the semantics of the function have changed. You can read the GraphQL Tools migration docs to see if you use any mocking features that have changed between v4 and v8, and adjust the value of your mocks if so.

To use features of addMocksToSchema that require passing more options to it than the three that ApolloServer passes through, you can use the library directly. To do so, run npm install @graphql-tools/mock @graphql-tools/schema in your app, and replace

new ApolloServer({ typeDefs, resolvers, mocks });


import { addMocksToSchema } from '@graphql-tools/mock';
import { makeExecutableSchema } from '@graphql-tools/schema';
new ApolloServer({
schema: addMocksToSchema({
schema: makeExecutableSchema({ typeDefs, resolvers }),
mocks: // ...
// ...

Alternatively, to keep the current behavior without changing your mock functions at all, you can continue to use graphql-tools v4. To do this, run npm install graphql-tools@v4.x in your app, and replace

new ApolloServer({typeDefs, resolvers, mocks, preserveResolvers});


import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
new ApolloServer({
schema: addMockFunctionsToSchema({
schema: makeExecutableSchema({typeDefs, resolvers}),
typeof mockEntireSchema === 'undefined' ? false : !mockEntireSchema,

apollo-server-caching test suite helpers

In 2, the apollo-server-caching package exports functions like testKeyValueCache, testKeyValueCache_Basic, and testKeyValueCache_Expiration, which define Jest test suites. The package also exports a TestableKeyValueCache type that's required for these test suites to be able to flush and close the cache.

You can use this to define Jest test suites for your own implementation of the KeyValueCache interface, although this requires intricate Jest and TypeScript config to set up.

In 3, these functions and type are removed. Instead, a runKeyValueCacheTests function is exported which can be run in any test suite (without any Jest-specific behavior). See the apollo-server-caching README for more details.

Changes to framework integrations

start() now mandatory for non-serverless framework integrations

v2.22 introduced the server.start() method for non- framework integrations (Express, Fastify, Hapi, Koa, Micro, and Cloudflare). Users of these integrations were encouraged to await a call to it immediately after creating an ApolloServer object:

async function startServer() {
const server = new ApolloServer({...});
await server.start();
server.applyMiddleware({ app });

If any plugin serverWillStart events throw, or if the server fails to load its schema properly (for example, if the server is a gateway and cannot load its schema), then await server.start() will throw. This allows you to ensure that has successfully loaded its configuration before you start listening for HTTP requests.

In 2, calling this method was optional. If you didn't call it, you could still call methods like applyMiddleware and start listening. If a startup failure occurred, all requests would fail.

In 3, you must await server.start() immediately after new ApolloServer before calling applyMiddleware (or getMiddleware or getHandler, depending on the integration). Note that you don't have to call it before calling the testing method executeOperation. Otherwise, that method will throw.

This does not apply to the batteries-included apollo-server package (the server is started as part of the async method server.listen()) or to framework integrations.

Peer deps instead of direct deps

In 2, the apollo-server-express, apollo-server-koa, and apollo-server-micro packages have direct dependencies on express, koa, and micro respectively, and apollo-server-fastify and apollo-server-hapi have no dependency on fastify, hapi, or @hapi/hapi.

In 3, these packages have peer dependencies on their corresponding framework packages. This means that you need to install a version of that package of your choice yourself in your app (though most likely that was already the case).

CORS * is now the default

In 2, the default CORS configuration for most packages is to serve an access-control-allow-origin: * header for all responses.

However, this is not the case in 2 for apollo-server-lambda, apollo-server-cloud-functions, or apollo-server-azure-functions (these serve no CORS headers by default), or for apollo-server-koa (this serves an access-control-allow-origin header with a value matching the request's origin by default).

In 3, all implementations of ApolloServer that support CORS configuration have the same default of access-control-allow-origin: *.

To use your framework's 2 default CORS behavior in your Apollo Server 3 application, provide the options specified below to the createHandler/getMiddleware method.

Lambda / Cloud Functions

server.createHandler({expressGetMiddlewareOptions: {cors: false}})

Azure Functions

server.createHandler({cors: false})


server.getMiddleware({cors: {}})

apollo-server-micro and apollo-server-cloudflare do not support CORS configuration in 2 or 3.


In 2, apollo-server-express officially supports both the express framework and the older connect framework.

In 3, we no longer guarantee future support for connect. We do still run a test suite against it and we will try not to unintentionally break functionality under connect, but if future changes are easier to implement in an Express-only fashion, we reserve the right to break connect compatibility within 3.


Now based on Express

In 2, apollo-server-lambda implements parsing AWS Lambda events and performing standard web framework logic inside the package itself. As Lambda added new capabilities, the logic had to adapt, and as added new features, they had to be implemented from scratch in apollo-server-lambda. Additionally, there was no way to add additional HTTP functionality (e.g., "middleware") to apollo-server-lambda.

In 3, apollo-server-lambda is implemented as a wrapper around apollo-server-express, using an actively maintained package to parse AWS Lambda events into Express requests. This simplifies the implementation and enables us to support new Lambda integrations such as Application Load Balancers. Additionally, because it now uses Express internally, you can extend HTTP functionality via Express middleware using the new expressAppFromMiddleware option to createHandler.

createHandler arguments changed

As part of the update described above, the to createHandler have changed. Instead of taking only cors and onHealthCheck, the method takes an expressGetMiddlewareOptions option, which is an object that supports any of the options that the apollo-server-express applyMiddleware and getMiddleware can take (other than app).

For example, instead of:


you now write

server.createHandler({expressGetMiddlewareOptions: {onHealthCheck}})`

Handler is always async

In 2, the handler returned by createHandler can be called either as a function that takes a callback as a third , or (starting with v2.21.2) as an async function that returns a Promise.

In 3, the handler returned by createHandler is always an async function that returns a Promise. Any callback passed in will be ignored.

All currently supported Lambda runtimes support async handlers. However, if you are currently wrapping the handler returned from createHandler in your own larger handler and passing a callback into it, this no longer works. Rewrite your outer handler to be an async function and await the Promise returned from the handler returned by createHandler.

For example, if your server looked like this:

const apolloHandler = server.createHandler();
exports.handler = function (event, context, callback) {
apolloHandler(event, context, (error, result) => {
callback(error, result);

you can change it to look like this:

const apolloHandler = server.createHandler();
exports.handler = async function (event, context) {
try {
return await apolloHandler(event, context);
} finally {


In 2, apollo-server-cloud-functions implements standard web framework logic inside the package itself. Even though the Node.js API for Google Cloud Functions provides its request and response in the form of Express request and response objects, 2 has a bespoke implementation unrelated to apollo-server-express and does not support all features supported by apollo-server-express. Additionally, there is no way to add additional HTTP functionality (e.g., "middleware") to apollo-server-cloud-functions.

In 3, apollo-server-cloud-functions is implemented on top of apollo-server-express and supports all features supported by apollo-server-express. You can add additional HTTP functionality via Express middleware using the new expressAppFromMiddleware option to createHandler.

As part of this, the to createHandler have changed. Instead of taking only cors, it takes an expressGetMiddlewareOptions option, which is an object taking any of the options that the apollo-server-express applyMiddleware and getMiddleware can take (other than app). For example, instead of:


you now write:

server.createHandler({expressGetMiddlewareOptions: {cors}})


In 2, apollo-server-fastify supports Fastify v2 and does not support Fastify v3. There is no dependency or peer dependency making this requirement clear.

In 3, apollo-server-fastify supports Fastify v3. It has not been tested with versions of Fastify older than v3. There is a peer dependency on fastify v3.


We are not certain exactly which versions of Hapi are supported by apollo-server-hapi in 2. We are certain that only @hapi/hapi v20.1.2 and higher are supported by Node 16, and 2 was not tested with versions of hapi newer than v17.8.5. (Hapi's package name changed from hapi to @hapi/hapi between v18.1.0 and v18.2.0.)

In 3, apollo-server-hapi works with @hapi/hapi v20.1.2 and Node 16. It is not tested with older versions of Hapi. Note that the Hapi project believes that all versions older than v20 have security issues.

Choosing which package to use
Schema basics
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy