Mercurius and Apollo Interoperability

What is GraphQL

GraphQL is a query language for APIs; it provides a clear and complete description for your data and allows the client to retrieve the data it needs.

The API can be defined using a schema that allows the client to know which operations can be executed by the server — the performance of the operations is determined by the resolvers. Using these concepts makes it easy to define and extend your API.

Once the schema and the resolvers are defined, you can select from any available GraphQL implementations and pick the one that best meets your preferences/requirements.

Initially developed by Facebook, which provided just a reference implementation, GraphQL was mostly implemented in the JS ecosystem in Apollo and Mercurius . Speaking of references, I recommend you view the mercurius-apollo-playground repo created by Andrea Carraro . It provides you with invaluable context because it contains the code and the benchmark commands for the project that inspired this blog post.

Creating a GraphQL server that manages a data structure like the one in the diagram below can be achieved quickly by using a single GraphQL server.

However, real-world applications are more complex than that seen above. Indeed, it’s pretty common for most applications to require hundreds of entities and access to multiple data sources, as the image underneath demonstrates:

We can use a single GraphQL service to build an application as complex as the one above, but the issue with this approach is that a huge codebase is difficult to maintain and split into different teams — some parts can load heavily, others are only called a few times, etc.

A microservices-like approach enables you to split the responsibilities into different services that can be maintained by separate teams and deployed in different ways.

According to the Integrity Principles , having a single entry point with a unified schema is essential. For example, it’s essential in order to be able to access all data from a single query and avoid duplication of graph implementation.

This feature can be created using Apollo federation v1 , one of the most well-known methods of implementing GraphQL in microservices.

Define a sample architecture with supergraph and subgraph

Let’s define a schema for the well-known example of a blog page's basic architecture.

Our system will have two entities:

  • User: This implements the user data and can retrieve the blog post written by a user
  • Post: This implements the post data and can retrieve the information about the author (that’s a specific type of user)

Below, you can see an example of a schema that defines the above entities:

JSON
type Query {
  me: User
  users: [User]
  posts: [Post]
}
  
type User {
  id: ID!
  name: String!
  posts: [Post]
}

type Post {
  id: ID!
  title: String
  content: String
  author: User
}

Based on the above example, we would have two services — the first one manages the users and the second one looks after the posts .

We can split the schema into 2 subgraphs:

JSON
// Subgraph 1 - Users

extend type Query {
  me: User
  users: [User]
}

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

// Subgraph 2 - Posts

type Query {
  posts: [Post]
}
  
type Post @key(fields: "id") {
  id: ID!
  title: String
  content: String
  author: User
}

type User @key(fields: "id") @extends {
  id: ID! @external
  name: String @external
  posts: [Post]
}

We define the application architecture with three services:

  • Gateway service with a GraphQL endpoint that acts as supergraph service — this exposes a merged schema that’s created using the subgraphs schemas
  • User service with a GraphQL endpoint that serves the supergraph 1 schema
  • Posts service with a GraphQL endpoint that serves the supergraph 2 schema

Implement the application with a gateway

To implement the solution, we can use either Mercurius or Apollo. However, the key thing to note is that we also have the option of picking both libraries and deciding service-by-service which one to use.

First, we define the schema and the resolvers that can be created with an agnostic approach.

JavaScript
// Post subgraph with resolvers

const postSubgraph = {
  schema: `
  extend type Query {
    posts: [Post]
  }
  
  type Post @key(fields: "id") {
    id: ID!
    title: String
    content: String
    author: User
  }

  type User @key(fields: "id") @extends {
    id: ID! @external
    name: String @external
    posts: [Post]
  }
`,
  resolvers: {
    Query: {
      posts: (parent, args, context) => {
        // Get the posts from a service
        return getPosts()
      }
    },
    Post: {
      author: post => {
        return {
          __typename: 'User',
          id: post.authorId
        }
      }
    },
    User: {
      posts: user => {
        return getPostsByUserId(user.id)
      }
    }
  }
}

export { postSubgraph }

The User subgraph service with Apollo

The User subgraph service is created using the Apollo framework. It is built by passing a schema created by the buildSubgraphSchema method to the ApolloServer instance. The ApolloServer is then passed to the startStandaloneServer function that exposes the /graphql endpoint.

JavaScript
import gql from 'graphql-tag'
import { ApolloServer } from '@apollo/server'
import { buildSubgraphSchema } from '@apollo/subgraph'
import { startStandaloneServer } from '@apollo/server/standalone'
import { userSubgraph } from './userSubgraph'

async function runApollo()  {
  const service = new ApolloServer({
    schema: buildSubgraphSchema({
      typeDefs: gql`
        ${userSubgraph.schema}
      `,
      resolvers: userSubgraph.resolvers
    }),
    resolvers: userSubgraph.resolvers
  })

  const { url } = await startStandaloneServer(service, {
    listen: { port: 4001 }
  })
}

runApollo()

The Post subgraph service with Mercurius

The Post subgraph service is created using the Mercurius framework. A Fastify  application is created with the @mercuriusjs/federation plugin. The plugin exposes a GraphQL federated service on the /graphql endpoint based on the schema and the resolvers defined in the postSubgraph file.

JavaScript
import Fastify from "fastify";
import mercurius from "@mercuriusjs/federation";
import { postSubgraph } from './postSubgraph'

async function runMercurius() {
  const service = Fastify({});

  service.register(mercurius, {
    schema: postSubgraph.schema,
    resolvers: postSubgraph.resolvers,
    jit: 1
  });
  await service.listen({ port: 4002 });
};

runMercurius()

The code above shows that the two technologies can be switched quickly if the resolvers are kept agnostic. However, in a real-world example, keeping the agnostic rule valid is more complex and requires serious attention. Authentication, authorization, caching, validation, etc… are created using modules that can be strictly related to either of the platforms.

A good design and a Hexagonal Architecture  pattern will help you to achieve the goal of being able to switch the two technologies by keeping the resolvers agnostic.

Create the gateway

We can use the @apollo/gateway or @mercuriusjs/gateway modules to create the gateway. The integration of both technologies is similar:

  • Firstly, they call the subgraph services to get the exposed schema
  • Secondly, they merge them in the supergraph schema, automatically creating the resolvers

The services created above expose the GraphQL endpoint and run on ports 4001 and 4002 . The introspection action of the gateway calls the endpoint, merges the subgraph schemas, and provides the client with a unified schema.

Implement the supergraph gateway with Apollo gateway

The Apollo library provides a gateway that can be used as a supergraph server. Setting it up is enough to pass the subgraph endpoints as parameters. You can learn more about this by reading the official documentation for implementing a gateway with Apollo server .

The snippet below creates an Apollo gateway for the services defined above.

JavaScript
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway'

function runApolloGateway() {
  const gateway = new ApolloGateway({
    supergraphSdl: new IntrospectAndCompose({
      subgraphs: [
        { name: 'user', url: 'http://localhost:4001/graphql' },
        { name: 'post', url: 'http://localhost:4002/graphql' }
      ]
    })
  })

  // Pass the ApolloGateway to the ApolloServer constructor
  const server = new ApolloServer({ gateway })

  const { url } = await startStandaloneServer(server, { port: 4000 })
}

runApolloGateway()

Implement the supergraph gateway with Mercurius gateway

The Mercurius library provides a module ( @mercuriusjs/gateway ) that creates a supergraph server. As with the Apollo server, setting it up is enough to pass the subgraph endpoints as parameters. You can discover more about this by checking out the official documentation published by Mercurius .

The snippet below creates a Mercurius gateway for the services defined above.

JavaScript
import Fastify from 'fastify'
const mercuriusGateway = require('@mercuriusjs/gateway')

export function runMercuriusGateway() {
  const gateway = Fastify()

  gateway.register(mercuriusGateway, {
    gateway: {
      services: [
        { name: 'user', url: 'http://localhost:4001/graphql' },
        { name: 'post', url: 'http://localhost:4002/graphql' }
      ]
    }
  })

  await gateway.listen({ port: 4000 })
}

runMercuriusGateway()

 Create a supergraph gateway with Apollo Router

In addition to the library I showed you previously in this blog post, Apollo provides a high-performance router that is built in Rust .

A supergraph definition file is required to run the Apollo router. This file can be generated using rover , a command-line interface for managing graphs with GraphOS: Install rover and follow the instructions to generate the key .

Login to rover.

Plain Text
rover config auth

Create a YAML config file that declares how to retrieve the subgraph schema.

YAML
## rover-config.yaml
federation_version: 2
subgraphs:
  user:
    routing_url: http://localhost:4001/graphql
    schema:
      subgraph_url: http://localhost:4001/graphql
  post:
    routing_url: http://localhost:4002/graphql
    schema:
      subgraph_url: http://localhost:4002/graphql

You now generate the supergraph file. To do this, the subgraph services should be run and be accessible.

JavaScript
APOLLO_ELV2_LICENSE=accept rover supergraph compose --config ./rover-config.yaml > ./supergraph.graphql

Install the Apollo router.

Prepare a config file for the router.

YAML
# router.yaml
supergraph:
  listen: 127.0.0.1:4000
  path: /graphql

Run the router.

Plain Text
router --config ./router.yml --supergraph ./supergraph.graphql

Run a sample query

Our federation is now up and running. Let’s define a query that uses both subgraphs.

JSON
query {
  users {
    id
    name
    posts {
      title
      author {
        name
      }
    }
  }
}

According to the schema defined above, the following query will be executed:

  • Get the user list and the name of each user in the user service
  • Obtain the post related to each user from the post service
  • Ask the user service again for the information related to the author

Run the test using a CURL command.

Plain Text
curl -X POST http://localhost:4000/graphql \
-H "Content-Type:application/json" \
-d '{"query": "query { users {id name posts { title author { name } } } }"}'

The server will return a similar result.

JSON
{
  data: {
    users: [
      {
        id: 'u1',
        name: 'John',
        posts: [
          { title: 'Post 1', author: { name: 'John' } },
          { title: 'Post 3', author: { name: 'John' } }
        ]
      },
      {
        id: 'u2',
        name: 'Jane',
        posts: [
          { title: 'Post 2', author: { name: 'Jane' } },
          { title: 'Post 4', author: { name: 'Jane' } }
        ]
      },
      { id: 'u3', name: 'Jack', posts: [] }
    ]
  }
}

Benchmark Overview

The examples above can be merged differently. For instance, they could employ a full Mercurius or Apollo implementation or use a hybrid implementation with the Apollo router and the subgraph managed by Mercurius. There are a number of options.

A simple case like this is easy to convert and to create some benchmarks for the performance that are based on the configuration. Configurations tested:

  • Mercurius gateway, Mercurius subgraph services
  • Apollo gateway, Mercurius subgraph services
  • Apollo gateway configured with subgraph, Mercurius subgraph services
  • Apollo router (rust), Mercurius subgraph services
  • Apollo gateway, Apollo subgraph services
  • Apollo router (rust), Apollo subgraph services

Test details:

  • The test is run on a MacBookPro 6-Core Intel Core i7, 32GB
  • All the data is mocked in memory and a no database call is carried out
  • The query tested is the sample query defined above
  • The test is done using autocannon with 10 concurrent connections for 10 seconds

It’s important to be aware that this is not a production test and should not be taken as a reference for the performance provided by the library. An accurate production application adds many layers that affect the system's performance. Request responses in 10 seconds (the higher the better)

1xx 2xx 3xx 4xx 5xx
apollo-router-with-mercurius-services 0 33119 0 0 0
mercurius-gateway-with-mercurius-services 0 15660 0 0 0
apollo-router-with-apollo-services 0 7468 0 0 0
mercurius-gateway-with-apollo-services 0 7419 0 0 0
apollo-gateway-with-mercurius-services-and-supergraph 0 7341 0 0 0
apollo-gateway-with-mercurius-service 0 5680 0 0 0
apollo-gateway-with-apollo-services 0 3999 0 0 0

Requests per second (the higher the better)

average stddev min max
apollo-router-with-mercurius-services 3011.19 129.66 2733 3223
mercurius-gateway-with-mercurius-services 1566 39.64 1500 1630
apollo-router-with-apollo-services 746.8 39.14 654 807
apollo-gateway-with-mercurius-services-and-supergraph 734.1 25.78 670 768
mercurius-gateway-with-apollo-services 674.46 20.02 621 700
apollo-gateway-with-mercurius-service 568 22.2 535 604
apollo-gateway-with-apollo-services 399.9 18.61 354 421

Conclusions

As I’ve explained in this article, the creation of a hybrid system with Mercurius and Apollo can be achieved quite easily. Furthermore, it is possible to choose other JS libraries or even different languages. Check the supported feature list in the federation-compatible subgraph implementations document to discover what options are available to you.

The configuration you should choose depends on many factors. If the system is well-designed and tested, it is relatively easy to move from one technology to another and run some benchmarks to understand better which solution fits the product requirements.

Now all that's left is for you to create your own hybrid system with Mercurius and Apollo, when the time is right for you to do so.

Resources

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact