How we migrated to Apollo Server 4
Tags: graphql, apollo, programming, api
Published: 2023-02-22 16:44:00

At [Propel](https://www.propeldata.com/), it’s no secret we’re fans of GraphQL: our entire product is centered around serving in-product analytics and customer dashboards using this flexible and performant technology.

We chose GraphQL first because it empowers clients to fetch exactly the data they need, usually in a single round-trip to the server. This is great for building customer dashboards, which may include multiple time series, counters, and leaderboards in a single page. It’s part of what makes using Propel so snappy.

Second, the developer experience around GraphQL is amazing, and we’ve been fortunate to use some great tools from [The Guild](https://the-guild.dev/) and [Apollo](https://www.apollographql.com/) in building our product. For example, we publish our GraphQL schemas to [Apollo Studio](https://studio.apollographql.com/public/Propel-API/home?variant=production), we embed the Apollo Studio Explorer in [our docs](https://www.propeldata.com/docs), and our GraphQL API is actually built on top of [Apollo Server](https://www.apollographql.com/docs/apollo-server/).

I want to take a moment to share part of our journey on the server-side, since we just recently upgraded our GraphQL APIs from Apollo Server 3 to 4. Apollo did a great job documenting the expected effort in their “[Migrating to Apollo Server 4](https://www.apollographql.com/docs/apollo-server/migration/)” guide, but we still encountered (and solved!) some unexpected differences in Apollo Server 4.

> 💡 Apollo Server 3 is officially deprecated and will reach end-of-life October 22, 2023. Read along to see how we at Propel migrated to Apollo Server 4.

## Basic changes

### The apollo-server library is now @apollo/server

**In Apollo Server 3**, you depended on version 3.x of the apollo-server library and any supporting libraries, like apollo-server-errors or apollo-server-core.

**In Apollo Server 4**, everything is published in a single, namespaced library. You only have to depend on version 4.x of @apollo/server.

### New `startStandaloneServer` method

**In Apollo Server 3**, the apollo-server library wrapped apollo-server-express and offered a “batteries-included” HTTP server for handling GraphQL requests.

**In Apollo Server 4**, the “batteries-included” approach is now `startStandaloneServer`. Migrating to this new method is straightforward and well-described in the migration guide. At Propel, we tried to use this new method; however, for reasons described below, we ultimately needed to use `expressMiddleware`. 

### The `gql` template literal tag has been removed

**In Apollo Server 3**, you could import the `gql` template literal tag directly from the apollo-server library. This template literal tag is provided by [the graphql-tag library](https://github.com/apollographql/graphql-tag) and allows parsing a GraphQL query string to an AST that can be used by Apollo and other GraphQL libraries.

```tsx
import { gql } from 'apollo-server'
```

**In Apollo Server 4**, this template literal tag is no longer exported. In fact, it’s no longer depended on by the @apollo/server library at all. At Propel, we had a few usages of `gql` in our backend, so we had to add a dependency on graphql-tag and change our import in order to fix this. Easy fix. 👍

```tsx
import { gql } from 'graphql-tag'
```

### ApolloError is replaced by GraphQLError

**In Apollo Server 3**, the apollo-server-errors library defined an ApolloError class. At Propel, we sub-classed ApolloError in our backend to create a hierarchy of errors. Any time we needed to throw an error from a resolver and have it appear in the GraphQL response’s `errors` array, we would use this class. For example, one of our NotFoundErrors looked like this:

```tsx
import { ApolloError } from 'apollo-server-errors'

export class NotFoundError extends ApolloError {
  readonly name = 'NotFoundError'
  readonly code = 'NOT_FOUND'

  constructor (resourceName: string) {
    super(`${resourceName} not found`, this.code)
    Object.defineProperty(this, 'name', { value: this.name })
  }

  // Members omitted…
}
```

**In Apollo Server 4**, ApolloError is removed. Instead, we should now use the GraphQLError class from the graphql library. We already had a dependency on graphql, so we just needed to update our sub-classes. After migrating, our NotFoundError looked like this:

```tsx
import { GraphQLError } from 'graphql'

export class NotFoundError extends GraphQLError {
  readonly name = 'NotFoundError'
  readonly code = 'NOT_FOUND'

  constructor (resourceName: string) {
    super(`${resourceName} not found`, {
      extensions: {
        code: this.code
      }
    })
    Object.defineProperty(this, 'name', { value: this.name })
  }

  // Members omitted…
}
```

Additionally, Apollo Server 4 adds the ability to control HTTP response status codes by including an `http` extension in the GraphQLError. For example, throwing a GraphQLError like the following will cause the HTTP server to respond with [418 I’m a teapot](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418). 🫖

```tsx
throw new GraphQLError("I'm a teapot", {
  extensions: {
    code: 'TEAPOT',
    http: { status: 418 }
  }
})
```

## Plugin changes

### Plugin error-handling has changed

**In Apollo Server 3**, plugins could receive the incoming request object in their `requestDidStart` method. At Propel, we used this to build an authentication and authorization plugin (our “AuthPlugin”).

Our AuthPlugin determines if incoming requests are authentic and if they are authorized. If an incoming request is inauthentic or unauthorized, we throw an HttpQueryError with status code 403. This would cause Apollo Server to return a 403 Unauthorized HTTP response back to the user. It looked something like this:

```tsx
import { ApolloServer } from 'apollo-server'
import { HttpQueryError } from 'apollo-server-core'

class AuthPlugin {
  requestDidStart ({ request, context }) {
    if (!isAuthenticated || !isAuthorized) {
      throw new HttpQueryError(403, 'Unauthorized')
    }
    context.authCtx = authCtx
  }
}

const server = new ApolloServer({
  typeDefs,
  plugins: [new AuthPlugin()],
  context: () => ({})
})
```

**In Apollo Server 4**, not only is HttpQueryError removed, but throwing an error from a plugin’s `requestDidStart` method results in a 500 Internal Server Error. We already knew we would need to migrate from HttpQueryError to GraphQLError, but how could we control the HTTP status code if Apollo Server was re-throwing plugin errors as 500s?

After some head-scratching, I opened [an issue](https://github.com/apollographql/apollo-server/issues/7278) on Apollo Server’s GitHub repository. There, Apollo Server contributor **@​glasser** shared a helpful suggestion: why not invoke our AuthPlugin from Apollo Server’s `context` function? Throwing from `context` would ensure we can control the HTTP status response without having to introduce more methods and error checks to our AuthPlugin (like `unexpectedErrorProcessingRequest`). With that suggestion in mind, we rewrote our AuthPlugin as follows:

```tsx
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { GraphQLError } from 'graphql'

class AuthPlugin {
  getAuthContext (req) { // ← Method renamed (not a true plugin anymore).
    if (!isAuthenticated || !isAuthorized) {
      throw new GraphQLError('Unauthorized', {
        extensions: {
          code: 'UNAUTHORIZED',
          http: { status: 403 }
        }
      })
    }
    return authCtx
  }
}

const server = new ApolloServer({
  typeDefs,
  plugins: [] // ← AuthPlugin is omitted here.
})

const authPlugin = new AuthPlugin()

startStandaloneServer(server, {
  context: async ({ req }) => {
    // Plugin invoked inside of `context`. Read on for details.
    return { authCtx: await authPlugin.getAuthContext(req) }
  }
})
```

It’s a few extra lines, but it works well. 👍

### Plugins no longer receive the request URL

**In Apollo Server 3**, the incoming request object passed to `requestDidStart` included a URL property. Our AuthPlugin would read the request’s URL and headers to determine if the request was authentic and authorized.

**In Apollo Server 4**, this URL property has been removed. I was curious about this change, so [I asked about it](https://github.com/apollographql/apollo-server/issues/7277) on Apollo Server’s GitHub repository. Apollo Server contributor **@​glasser** helpfully pointed out that the URL was still accessible in Apollo Server’s `context` function. Because we had already moved our AuthPlugin inside of `context`, we could accept the request object provided by `startStandaloneServer` and extract the URL from there. The fix was straightforward:

```tsx
startStandaloneServer(server, {
  context: async ({ req }) => {
    // `req` here is an `http.IncomingMessage` with `url`.
    return { authCtx: await authPlugin.requestDidStart(req) }
  }
})
```

## Server changes

### HTTP-level health check endpoint removed

**In Apollo Server 3**, there was an HTTP-level health check endpoint included at `/.well-known/apollo/server-health`. This would return 200 OK for any GET request. While too simple to evaluate if the server process was truly healthy, it did indicate whether the Node.js process and HTTP server were running. At Propel, we deploy our GraphQL API servers as Fargate tasks behind Application Load Balancers (ALBs), so this was a natural choice to configure as our target groups’ health check endpoint:

```bash
https://your.server/.well-known/apollo/server-health
```

**In Apollo Server 4**, the HTTP-level health check endpoint has been removed. Apollo Server recommends issuing a simple GraphQL query instead. For example,

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

Indeed, this is a more sophisticated health check than the HTTP-level health check in Apollo Server 3: by executing a GraphQL query, we can ensure that Apollo Server is working in addition to the Node.js process and HTTP server.

At Propel, two issues prevented us from adopting this:

- Our entire GraphQL API is protected by our AuthPlugin, so by default the ALBs’ health check requests would be rejected with status code 403.
- Apollo Server’s CSRF protection would require the ALB to send an “Apollo-Require-Preflight” header with each health check request, which is unsupported.

Instead, we followed Apollo Server’s “[Swapping to `expressMiddleware`](https://www.apollographql.com/docs/apollo-server/api/standalone#swapping-to-expressmiddleware)” guide. By swapping to `expressMiddleware`, we would get access to the underlying `express` HTTP server. This way, we can install a health check endpoint at a new URL, `/.well-known/server-health`. This health check endpoint is completely private, and so it’s safe to leave unauthenticated.

```tsx
const app = express()

// Setup Apollo Server with `expressMiddleware`…

app.get('/.well-known/server-health', (_, res) => {
  // Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
  res.type('application/health+json')
  res.json({ status: 'pass' })
})
```

We followed the same response format as Apollo Server 3. Indeed, this is a very simple check; however, we plan to incorporate more checks in the future.

## Conclusion

Migrating from Apollo Server 3 to Apollo Server 4 took us less than a week, and we received a lot of help from Apollo’s own migration guide as well as on Apollo Server’s GitHub issues tracker. If any of this interests you — be it GraphQL or in-product analytics — reach out, follow us on Twitter [@PropelDataCloud](https://twitter.com/@propeldatacloud), or [subscribe to our email newsletter](https://www.propeldata.com/newsletter?__hstc=17958374.7add73943c8f554c1c478c947e376657.1661881287613.1667148400062.1667167207872.21&__hssc=17958374.2.1667167207872&__hsfp=2429260968).

If you’d like to learn more about our GraphQL API, [check out our GraphQL API documentation](https://www.propeldata.com/docs/api/about-the-graphql-api) or [request access](https://www.propeldata.com/request-access)!