You are going to read about a GraphQL project that is perfect for playing with to skill up on GraphQL by using:
- The Mercurius GraphQL server
- The Fastify web framework
- The GraphQL Federation architecture
This project is a playground to test the GraphQL Federation architecture with the Mercurius server.
The Federation architecture is a way to share data between distributed GraphQL servers applying the microservices architecture into the GraphQL standard.
The project spins up 2 + 1 nodes:
user
: it represents the user datateam
: it represents a set of users- the gateway: it connects all the nodes into a federation graph
⚠️ As prerequisites, you should be familiar with the GraphQL basic concepts. Before continuing with this article, you can take a tour of the official tutorial.
Creating a GraphQL Node
Creating a node by using the Fastify+Mercurius toolkit is very easy. You need to:
- Create a Fastify instance
- Register the Mercurius plugin
async function buildService (name, { schema, resolvers, loaders }) {
const app = Fastify({
logger: {
name,
level: 'info'
}
})
app.register(GQL, {
schema, // the GraphQL schema
resolvers, // the GraphQL resolvers
loaders, // the GraphQL loaders
federationMetadata: true,
allowBatchedQueries: true
})
await app.listen() // start the node
}
By implementing the buildService
function, you can focus on the business logic of your node without worrying about the infrastructure.
The user
GraphQL Node
Now we can focus on the business logic of the node by creating a node-1.js
file.
The User
is made of a name
property and it always has a bestFriend
.
We need a simple Query
to get a user too.
So, the node must:
- access to the users dataset
- expose the
User
GraphQL type - implement a simple query to get a user
The schema will look like this:
type Query {
zero: User
}
type User @key(fields: "id") {
id: ID!
name: String
bestFriend: User
}
The implementation of this schema requires:
- A
Query.zero
resolver that returns the user with id0
- A
User.bestFriend
resolver that returns the best friend of the user
Implementing the resolvers is relatively straightforward:
// a fake database
const users = [
{ id: 0, name: 'Mathew Deckow', bestFriendId: 1 },
{ id: 1, name: 'Van McKenzie', bestFriendId: 8 },
{ id: 2, name: 'Jay Roob', bestFriendId: 2 },
{ id: 3, name: 'Deborah Spencer', bestFriendId: 7 },
{ id: 4, name: 'Vivian Murphy', bestFriendId: 6 },
{ id: 5, name: 'Meredith Mitchell', bestFriendId: 0 },
{ id: 6, name: 'Kate Deckow', bestFriendId: 5 },
{ id: 7, name: 'Beth Hodkiewicz IV', bestFriendId: 3 },
{ id: 8, name: 'Sheryl Schaden', bestFriendId: 5 },
{ id: 9, name: 'Heather Veum', bestFriendId: 9 }
]
module.exports = {
schema,
resolvers: {
Query: {
zero: async function () {
return users[0]
}
},
User: {
bestFriend: async function (user) {
return users[user.bestFriendId]
}
}
},
}
Now you can call this simple node by instantiating it in a start.js
file:
const serviceOne = await buildNode('user', require('./node-1'))
const response = await serviceOne.inject({
method: 'POST',
url: '/graphql',
body: {
query: `{
zero {
name
bestFriend {
name
}
}
}`
}
})
And you can see the result:
{
"data": {
"zero": {
"name": "Charlie Pacocha",
"bestFriend": {
"name": "Glenda Ankunding"
}
}
}
}
The team
GraphQL Node
The team
node will have a Query
that returns a list of users that compose a team.
The schema will look like this:
type Query {
myTeam: Team
}
type Team {
components: [User]
}
type User @key(fields: "id") @extends {
id: ID! @external
}
As you can see, the @extends
directive is used to extend the User
GraphQL type and declare the id
field as an external field. This is part of the GraphQL Federation standard.
The node-2.js
resolver will be very basic, but you can add more complex logic to it. Note that we set the async
keyword: you will be able to run asynchronous code in every resolver function!
module.exports = {
schema,
resolvers: {
Query: {
myTeam: async function () {
return {
components: [
{ id: 1 },
{ id: 2 }
]
}
}
}
}
}
🔮 Every time you set the @external
directive, you must remember to set the special __resolveReference
resolver function in the target node.
In this case, we need to update the node-1.js
file adding a loader:
module.exports = {
schema,
resolvers: {
Query: {
zero: function () {
return users[0]
}
},
User: {
bestFriend: function (user) {
return users[user.bestFriendId]
}
}
},
loaders: {
User: {
async __resolveReference (queries, context) {
return queries.map(({ obj }) => users[+obj.id])
}
}
}
}
Note that the __resolveReference
function is defined as loader
to solve the N+1 problem.
You can add the __resolveReference
as resolver
too, but you slow down your node: I encourage you to add some console.log
and experiment. You will see that:
- The loader's
__resolveReference
is called grouping the queries by the target type. - The resolver's
__resolveReference
is called multiple times for each query with nested fields. - If the
__resolveReference
is defined in both the loader and the resolver, the loader's one is called.
The Gateway GraphQL Node
Finally, the last step is to create the gateway
node.
It is a node that will connect the other nodes with a dedicated GraphQL configuration:
const gateway = Fastify()
gateway.register(GQL, {
graphiql: true,
pollingInterval: 2000,
gateway: {
services: [
{
name: 'user',
url: 'http://localhost:53114/graphql',
},
{
name: 'team',
url: 'http://localhost:53115/graphql',
}
]
}
})
gateway.listen(3000)
Now you can access the GraphQL API from the browser: http://localhost:3000/graphiql and you will be able to run the query:
{
myTeam {
components {
name
bestFriend {
name
bestFriend {
name
bestFriend {
name
}
}
}
}
}
}
It is interesting to note the logs of the nodes:
- During the startup of the gateway, the
node-1
andnode-2
are queried by the gateway getting the GraphQL schemas. - The gateway receives the query to execute, and it will call the
team
node to get the list of users. - The gateway receives the
team
node's response, and it will call theuser
node to get the users. - The
user
node resolves thebestFriend
field internally using the loader and type's resolver.
# startup
{"level":30,"name":"user","msg":"Server listening at http://127.0.0.1:53176"}
{"level":30,"name":"team","msg":"Server listening at http://127.0.0.1:53177"}
{"level":30,"name":"team","reqId":"req-1","req":{"method":"POST","url":"/graphql","hostname":"localhost:53177","remoteAddress":"127.0.0.1","remotePort":53178},"msg":"incoming request"}
{"level":30,"name":"team","reqId":"req-1","res":{"statusCode":200},"responseTime":6.589166045188904,"msg":"request completed"}
{"level":30,"name":"user","reqId":"req-1","req":{"method":"POST","url":"/graphql","hostname":"localhost:53176","remoteAddress":"127.0.0.1","remotePort":53179},"msg":"incoming request"}
{"level":30,"name":"user","reqId":"req-1","res":{"statusCode":200},"responseTime":1.2786250114440918,"msg":"request completed"}
{"level":30,"name":"gateway","msg":"Server listening at http://127.0.0.1:53208"}
# query execution
{"level":30,"name":"gateway","reqId":"req-1","req":{"method":"POST","url":"/graphql","hostname":"localhost:80","remoteAddress":"127.0.0.1"},"msg":"incoming request"}
{"level":30,"name":"team","reqId":"req-2","req":{"method":"POST","url":"/graphql","hostname":"localhost:53177","remoteAddress":"127.0.0.1","remotePort":53178},"msg":"incoming request"}
{"level":30,"name":"team","reqId":"req-2","res":{"statusCode":200},"responseTime":0.835334062576294,"msg":"request completed"}
{"level":30,"name":"user","reqId":"req-2","req":{"method":"POST","url":"/graphql","hostname":"localhost:53176","remoteAddress":"127.0.0.1","remotePort":53179},"msg":"incoming request"}
{"level":30,"name":"user","queries":[{"obj":{"__typename":"User","id":"1"},"params":{}},{"obj":{"__typename":"User","id":"2"},"params":{}}],"msg":"User.__resolveReference"}
{"level":30,"name":"user","reqId":"req-2","res":{"statusCode":200},"responseTime":2.0201669931411743,"msg":"request completed"}
{"level":30,"name":"gateway","reqId":"req-1","res":{"statusCode":200},"responseTime":7.0754170417785645,"msg":"request completed"}
Now jump into the source code on GitHub and start to play with the GraphQL Federation implemented in Fastify. Comment and share if you enjoyed this article!