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 assupergraph
service — this exposes a merged schema that’s created using the subgraphs schemasUser
service with a GraphQL endpoint that serves thesupergraph 1
schemaPosts
service with a GraphQL endpoint that serves thesupergraph 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 serviceObtain the
post
related to each user from the post serviceAsk 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
Apollo GraphQL – Apollo GraphQL hub.
Mercurius – Mercurius GraphQL hub.
Autocannon – An HTTP/1.1 benchmarking tool written in node.
mercurius-apollo-playground – The project used to write this article created by Andrea Carraro. It contains the code and the benchmark commands.