Apollo introduced federated GraphQL services a little less than a year ago, answering the need to implement GraphQL in a microservice architecture. The previous solution, schema stitching, had too many issues and usually led to fragile codebases, as explained in the Apollo Federation launch blog.

The federation architecture is built from multiple GraphQL services that comply with the Apollo Federation specification and a GraphQL gateway service. The GraphQL gateway collects and composes all the schemas into a single final schema and executes the incoming queries across the different services.

Fastify is a web framework highly focused on providing the best developer experience with the least overhead and a robust plugin architecture. The fastify-gql plugin provides the following:

  • GraphQL integration with caching of both query parsing and validation.
  • Automatic loader integration to avoid 1+N queries (see this video tutorial for more details).
  • An optional JIT compiler to further optimise query executions.

With the release of fastify-gql version 3.2.0, you can now implement GraphQL servers both in federation and gateway mode. This results in a high-performance GraphQL gateway architecture that is compatible with the Apollo federation specification.

Creating the GraphQL gateway

The first step is to create the federated GraphQL services, as follows:

'use strict'

const Fastify = require('fastify')
const GQL = require('fastify-gql')
const users = require('./data')

const app = Fastify()
const schema = `
 extend type Query {
  me: User
 }

 type User @key(fields: "id") {
  id: ID!
  name: String
  username: String
 }
`

const resolvers = {
 Query: {
  me: () => {
    return users.loadUser([1])
  }
 }
}

const loaders = {
  User: {
    __resolveReference: (queries, context) => {
      // This is a bulk query to the database
      return users.loadUser(queries.map(({ obj }) => obj.id))
    }
  }
}

app.register(GQL, {
  schema,
  resolvers,
  loaders,
  federationMetadata: true
})

app.listen(3001)

Creating a federated service is the same as standard GraphQL service apart from the following three differences:

  • The federationMetadata property must be set to true to enable the federation support on a GraphQL service.
  • The @key directive must be added to the User field. The @key directive tells the service which field uniquely identifies the type.
  • A __resolveReference method must be added to the resolver. The __resolveReference is a specific resolver that tells the service how to fetch a type by its unique field. It is strongly recommended that you add the __resolverReference method as a loader to avoid performance bottlenecks.

After implementing each GraphQL service, we then implemented the gateway itself, as follows:

'use strict'
const Fastify = require('fastify')
const GQL = require('fastify-gql')
const gateway = Fastify()

gateway.register(GQL, {
  gateway: {
    services: [{
      name: 'user',
      url: 'http://localhost:3001/graphql',
      rewriteHeaders: (headers) => {
        if (headers.authorization) {
          return {
            authorization: headers.authorization
          }
        }
      }
    }, {
     name: 'review',
     url: 'http://localhost:3002/graphql'
   }]
 }
})

await gateway.listen(3000)

When the server starts up, it requests the information it needs from the underlying services to build the final schema, then automatically generates the schema resolvers.

If the gateway property is set on the options object, the plugin runs in gateway mode. In gateway mode, the following settings are disabled: schema, resolvers, loaders and subscription options.

The gateway option has one property: services. Its value is an array of services that are part of the gateway. Every service must have a unique name and an endpoint url, and can also have a rewriteHeaders method. If the method is defined, the gateway uses this method to modify the header object before it sends the request to the service. The gateway creates a default request header (with content-type and content-length properties), then uses the method to add additional header values.

Optimising performance

When the gateway receives a request, it generates many sub-queries that are then sent to the underlying services. Parsing the original query and generating new queries are CPU intensive and can cause performance problems. Because of this, we spent considerable effort on performance tuning.

We used the original federation-demo repository and re-implemented it with fastify-gql. We ran a comparison between the two implementations using autocannon configured with five connections and measured the request handling capacity, latency and throughput of each for a five-second duration with the following query:

query mainQuery {
  me {
    id
    name
    username
    numberOfReviews
    reviews {
      id
      body
      product {
        upc
        name
        price
        weight
        inStock
        shippingEstimate
      }
    }
  }
  topProducts {
    upc
    name
    reviews {
      id
      author {
        id
        name
        numberOfReviews
      }
    }
  }
}

The results are quite promising. As the results below show, the Fastify GraphQL gateway executes more than twice as many requests per second as the Apollo gateway.

Implementing a GraphQL gateway with Fastify

Apollo
Fastify
Difference
Requests/s
236.80
758.60
220.35%
Latency (ms)
20.06
6.09
-70.44%
Throughput (kb/s)
501.76
1525.76
205.65%

A full working example (and the code for the comparison tests) is available on GitHub. If you are interested in the actual implementation of the gateway, you can visit the Fastify-gql repository.

View all posts  |  Technology  |  Business  |  Culture  |  Opinion  |  Design
Follow us for more information on this and other topics.
Published by Peter Balazs