Mercurius and Apollo Interoperability

Mercurius and Apollo Interoperability

·

10 min read

By Davide Fiorello

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.

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:

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:

// 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.

// User Subgraph with resolvers

const userSubgraph = {
  schema: `
  extend type Query {
    me: User
    users: [User]
  }

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

  type Address {
    id: ID!
    street: String
    city: String
    zip: String
  }
  `,

  resolvers: {
    Query: {
      me: (parent, params, context) => {
        return getLoggedUser(context.auth)
      },
      users: () => {
        return getUsers()
      }
    },
    User: {
      // The subgraph service should implement a resolver to expose the entities data
      // https://www.apollographql.com/docs/apollo-server/using-federation/api/apollo-subgraph/#__resolvereference
      __resolveReference: user => {
        return getUserById(user.id)
      },
      addresses: user => {
        return getAddresses(user)
      }
    },
    Address: {
      // The subgraph service should implement a resolver to expose the entities data
      // https://www.apollographql.com/docs/apollo-server/using-federation/api/apollo-subgraph/#__resolvereference
      __resolveReference: address => {
        return getAddressById(address.id)
      }
    }
  }
}

export { userSubgraph }
// 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.

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.

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.

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.

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 made using 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.

rover config auth

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

## 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.

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

Install the Apollo router.

Prepare a config file for the router.

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

Run the router.

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.

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.

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.

{
  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)

Requests per second (the higher the better)

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 hybrid system with Mercurius and Apollo, when the time is right for you to do so.

Resources