- mercurius
- Federation
- Federation metadata support
- Federation with __resolveReference caching
- Use GraphQL server as a Gateway for federated schemas
- Periodically refresh federated schemas in Gateway mode
- Programmatically refresh federated schemas in Gateway mode
- Using Gateway mode with a schema registry
- Flag service as mandatory in Gateway mode
- Batched Queries to services
- Using a custom errorHandler for handling downstream service errors in Gateway mode
- Securely parse service responses in Gateway mode
- Federation
Federation support is managed by the plugin @mercuriusjs/federation
and the plugin @mercuriusjs/gateway
The signature of the method is the same as a standard resolver: __resolveReference(source, args, context, info)
where the source
will contain the reference object that needs to be resolved.
'use strict'
const Fastify = require('fastify')
const mercuriusWithFederation = require('@mercuriusjs/federation')
const users = {
1: {
id: '1',
name: 'John',
username: '@john'
},
2: {
id: '2',
name: 'Jane',
username: '@jane'
}
}
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['1']
}
},
User: {
__resolveReference: (source, args, context, info) => {
return users[source.id]
}
}
}
app.register(mercuriusWithFederation, {
schema,
resolvers,
})
app.get('/', async function (req, reply) {
const query = '{ _service { sdl } }'
return app.graphql(query)
})
app.listen({ port: 3000 })
Just like standard resolvers, the __resolveReference
resolver can be a performance bottleneck. To avoid this, the it is strongly recommended to define the __resolveReference
function for an entity as a loader.
'use strict'
const Fastify = require('fastify')
const mercuriusWithFederation = require('@mercuriusjs/federation')
const users = {
1: {
id: '1',
name: 'John',
username: '@john'
},
2: {
id: '2',
name: 'Jane',
username: '@jane'
}
}
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['1']
}
}
}
const loaders = {
User: {
async __resolveReference(queries, context) {
// This should be a bulk query to the database
return queries.map(({ obj }) => users[obj.id])
}
}
}
app.register(mercuriusWithFederation, {
schema,
resolvers,
loaders,
})
app.get('/', async function (req, reply) {
const query = '{ _service { sdl } }'
return app.graphql(query)
})
app.listen({ port: 3000 })
A GraphQL server can act as a Gateway that composes the schemas of the underlying services into one federated schema and executes queries across the services. Every underlying service must be a GraphQL server that supports the federation.
In Gateway mode the following options are not allowed (the plugin will throw an error if any of them are defined):
schema
resolvers
loaders
Also, using the following decorator methods will throw:
app.graphql.defineResolvers
app.graphql.defineLoaders
app.graphql.extendSchema
const gateway = Fastify()
const mercuriusWithGateway = require('@mercuriusjs/gateway')
gateway.register(mercuriusWithGateway, {
gateway: {
services: [
{
name: 'user',
url: 'http://localhost:4001/graphql',
rewriteHeaders: (headers, context) => {
if (headers.authorization) {
return {
authorization: headers.authorization
}
}
return {
'x-api-key': 'secret-api-key'
}
},
setResponseHeaders: (reply) => {
reply.header('set-cookie', 'sessionId=12345')
}
},
{
name: 'post',
url: 'http://localhost:4002/graphql'
}
]
}
})
await gateway.listen({ port: 4000 })
The Gateway service can obtain new versions of federated schemas automatically within a defined polling interval using the following configuration:
gateway.pollingInterval
defines the interval (in milliseconds) the gateway should use in order to look for schema changes from the federated services. If the received schema is unchanged, the previously cached version will be reused.
const gateway = Fastify()
const mercuriusWithGateway = require('@mercuriusjs/gateway')
gateway.register(mercuriusWithGateway, {
gateway: {
services: [
{
name: 'user',
url: `http://localhost:3000/graphql`
}
],
pollingInterval: 2000
}
})
gateway.listen({ port: 3001 })
The service acting as the Gateway can manually trigger re-fetching the federated schemas programmatically by calling the application.graphql.gateway.refresh()
method. The method either returns the newly generated schema or null
if no changes have been discovered.
const Fastify = require('fastify')
const mercuriusWithGateway = require('@mercuriusjs/gateway')
const server = Fastify()
server.register(mercuriusWithGateway, {
graphiql: true,
gateway: {
services: [
{
name: 'user',
url: 'http://localhost:3000/graphql'
},
{
name: 'company',
url: 'http://localhost:3001/graphql'
}
]
}
})
server.listen({ port: 3002 })
setTimeout(async () => {
const schema = await server.graphql.gateway.refresh()
if (schema !== null) {
server.graphql.replaceSchema(schema)
}
}, 10000)
The service acting as the Gateway can use supplied schema definitions instead of relying on the gateway to query each service. These can be updated using application.graphql.gateway.serviceMap.serviceName.setSchema()
and then refreshing and replacing the schema.
const Fastify = require('fastify')
const mercuriusWithGateway = require('@mercuriusjs/gateway')
const server = Fastify()
server.register(mercuriusWithGateway, {
graphiql: true,
gateway: {
services: [
{
name: 'user',
url: 'http://localhost:3000/graphql',
schema: `
extend type Query {
me: User
}
type User @key(fields: "id") {
id: ID!
name: String
}
`
},
{
name: 'company',
url: 'http://localhost:3001/graphql',
schema: `
extend type Query {
company: Company
}
type Company @key(fields: "id") {
id: ID!
name: String
}
`
}
]
}
})
await server.listen({ port: 3002 })
server.graphql.gateway.serviceMap.user.setSchema(`
extend type Query {
me: User
}
type User @key(fields: "id") {
id: ID!
name: String
username: String
}
`)
const schema = await server.graphql.gateway.refresh()
if (schema !== null) {
server.graphql.replaceSchema(schema)
}
Gateway service can handle federated services in 2 different modes, mandatory
or not by utilizing the gateway.services.mandatory
configuration flag. If a service is not mandatory, creating the federated schema will succeed even if the service isn't capable of delivering a schema. By default, services are not mandatory. Note: At least 1 service is necessary to create a valid federated schema.
const Fastify = require('fastify')
const mercuriusWithGateway = require('@mercuriusjs/gateway')
const server = Fastify()
server.register(mercuriusWithGateway, {
graphiql: true,
gateway: {
services: [
{
name: 'user',
url: 'http://localhost:3000/graphql',
mandatory: true
},
{
name: 'company',
url: 'http://localhost:3001/graphql'
}
]
},
pollingInterval: 2000
})
server.listen(3002)
To fully leverage the DataLoader pattern we can tell the Gateway which of its services support batched queries.
In this case the service will receive a request body with an array of queries to execute.
Enabling batched queries for a service that doesn't support it will generate errors.
const Fastify = require('fastify')
const mercuriusWithGateway = require('@mercuriusjs/gateway')
const server = Fastify()
server.register(mercuriusWithGateway, {
graphiql: true,
gateway: {
services: [
{
name: 'user',
url: 'http://localhost:3000/graphql'
allowBatchedQueries: true
},
{
name: 'company',
url: 'http://localhost:3001/graphql',
allowBatchedQueries: false
}
]
},
pollingInterval: 2000
})
server.listen({ port: 3002 })
Service which uses Gateway mode can process different types of issues that can be obtained from remote services (for example, Network Error, Downstream Error, etc.). A developer can provide a function (gateway.errorHandler
) that can process these errors.
const Fastify = require('fastify')
const mercuriusWithGateway = require('@mercuriusjs/gateway')
const server = Fastify()
server.register(mercuriusWithGateway, {
graphiql: true,
gateway: {
services: [
{
name: 'user',
url: 'http://localhost:3000/graphql',
mandatory: true
},
{
name: 'company',
url: 'http://localhost:3001/graphql'
}
],
errorHandler: (error, service) => {
if (service.mandatory) {
server.log.error(error)
}
},
},
pollingInterval: 2000
})
server.listen({ port: 3002 })
Note: The default behavior of errorHandler
is call errorFormatter
to send the result. When is provided an errorHandler
make sure to call errorFormatter
manually if needed.
Gateway service responses can be securely parsed using the useSecureParse
flag. By default, the target service is considered trusted and thus this flag is set to false
. If there is a need to securely parse the JSON response from a service, this flag can be set to true
and it will use the secure-json-parse library.
const Fastify = require('fastify')
const mercuriusWithGateway = require('@mercuriusjs/gateway')
const server = Fastify()
server.register(mercuriusWithGateway, {
graphiql: true,
gateway: {
services: [
{
name: 'user',
url: 'http://localhost:3000/graphql',
useSecureParse: true
},
{
name: 'company',
url: 'http://localhost:3001/graphql'
}
]
},
pollingInterval: 2000
})
server.listen({ port: 3002 })