Introducing Mercurius Dynamic Schema

Introducing Mercurius Dynamic Schema

·

5 min read

By Alfonso Graziano

A NearForm package that supercharges your GraphQL server and extends the functionality of Mercurius

We’re pleased to introduce Mercurius Dynamic Schema, a new package by NearForm.

This package is designed to supercharge your GraphQL server running on Mercurius. It extends the functionality of Mercurius to allow serving different GraphQL schemas based on a custom strategy. This approach utilizes custom constraints, a feature of Fastify, to route requests to different Mercurius instances based on specified conditions.

If you’ve ever encountered the need to diversify the GraphQL schemas in your application, based on different request conditions, this package is crafted just for you.

Getting started with dynamic schema routing

First of all, you need to install the package:

npm i mercurius-dynamic-schema

We assume that fastify and mercurius are already installed in your project.

You can then create all the schemas you need. For this example, we will create two simple schemas:

const schema1 = `
    type Query {
      add(x: Int, y: Int): Int
    }
  `

const resolvers1 = {
  Query: {
    add: async (_, obj) => {
      const { x, y } = obj
      return x + y
    }
  }
}

const schema2 = `
    type Query {
      subtract(x: Int, y: Int): Int
    }
  `

const resolvers2 = {
  Query: {
    subtract: async (_, obj) => {
      const { x, y } = obj
      return x - y
    }
  }
}

Now we can use mercurius-dynamic-schema to load our schemas and route the request based on the strategy that we select.

const Fastify = require("fastify");
const mercuriusDynamicSchema = require("mercurius-dynamic-schema");
//Add here (or import) the schemas and the resolvers 

const app = Fastify({
  logger: true
})

app.register(mercuriusDynamicSchema, {
  schemas: [
    {
      name: 'schema1', 
      schema: schema1,
      resolvers: resolvers1,
    },
    {
      name: 'schema2',
      schema: schema2,
      resolvers: resolvers2
    }
  ],
  strategy: req => {
    return req.headers?.schema || 'schema1'
  },
})

app.listen({ port: 3000 })

In this example, we are registering the plugin and declaring two schemas.

Each schema must have its own name, a GraphQL schema and the corresponding resolvers.

After declaring the schemas, we must declare a strategy that will be used to evaluate what schema should be used for the current request. The strategy is a function that returns a schema name from a request object. In this case, we are using schema1 by default, but the user can customize the schema based on the schema HTTP request header.

If we run the server and then make an HTTP request without the schema header, we will pick the default one.

curl -X POST \
     -H 'content-type: application/json' \
     -d '{ "query": "{ add(x: 2, y: 2) }" }' \
     localhost:3000/graphql

Response: 
{"data":{"add":4}}

In the example below, we can set the schema to schema2 to use the subtract query

curl -X POST \
     -H 'content-type: application/json' \
     -H 'schema: schema2' \
     -d '{ "query": "{ subtract(x: 2, y: 1) }" }' \
     localhost:3000/graphql

Response:
{"data":{"subtract":1}}

In case we try to use a non-existing strategy, we will get a not found error.

By default, the path is /graphql, but we can customize it for each schema using the optional path property. This allows us to make our endpoints even more configurable but we need to keep that in mind when applying the strategy function. Most of the time, keeping the default path should be fine.

We have the option of using the **context** parameter to enrich our request-handling logic by passing a context object derived from the request. This context object can be used within our resolvers to access request-specific data, thus allowing for more tailored and informed resolver logic. This context function is injected as the Mercurius context.

Here is an example of the context usage:

const app = Fastify()
app.register(mercuriusDynamicSchema, {
  schemas: [
    {
      name: 'schema1',
      schema,
      resolvers
    },
    {
      name: 'schema2',
      schema: schema2,
      resolvers: {
        Query: {
          subtract: async (_, obj, ctx) => {
            const { x, y } = obj
            return x - y + ctx.additionalAdd
          }
        }
      }
    }
  ],
  strategy: req => {
    return req.headers?.schema || 'schema1'
  },
  context: req => ({
    additionalAdd: Number(req.headers['additional-add'])
  })

Use cases

Now that you learned how to use this new package, here are a few interesting use cases:

  1. Endpoint versioning: Manage different versions of your GraphQL schema to cater to various versions of your API.

  2. Multi-tenant applications: Serve different schemas for different tenants or user groups within the same application.

  3. Feature flagging: Route to different schemas based on feature flags to gradually roll out new features or changes.

  4. Environment-specific schemas: Serve different schemas for different environments (for example, development, staging and production).

  5. Domain-specific schemas: Route requests to different schemas based on domain logic or business requirements.

  6. A/B testing: Use different schemas to perform A/B testing of new features or changes in your API.

  7. Schema experimentation: Experiment with schema modifications without affecting the primary schema. This is useful for testing and development.

  8. Modular development: Maintain modularity by keeping separate schemas for different parts of your application, making it easier to manage in a large team or a complex project.

  9. Customized user experiences: Provide tailored schemas based on user preferences or settings. This enhances the user experience.

  10. Schema transitioning: Smoothly transition from an old to a new schema by routing traffic gradually.

Wrap up

The Mercurius Dynamic Schema package is a straightforward tool for managing different GraphQL schemas in your application using Mercurius and Fastify. With this package, you can easily route requests to different schemas based on a strategy, which can be useful in various scenarios.

In the setup outlined in this article, you’ve seen how to define multiple schemas, set up a routing strategy and optionally use a context parameter to pass request-specific data to your resolvers. This setup enables a more organized approach to handling different schemas and can be particularly useful in larger applications or when dealing with different types of data that require separate schemas.

In summary, if you have a use case that requires serving different GraphQL schemas based on the request, the Mercurius Dynamic Schema package could be a good fit for your project. It’s easy to set up and use, and it integrates well with the existing Mercurius and Fastify ecosystem. Give it a try and see if it meets your project’s needs.