August 07, 2018

Open Sourcing GraphQL Middleware - A Library to Simplify Your Resolvers

GraphQL Middleware lets you run arbitrary code before or after a resolver is invoked. It improves your code structure by enabling code reuse and a clear separation of concerns.

Middleware keeps resolvers clean

A well-organized codebase is key for the ability to maintain and easily introduce changes into an app. Figuring out the right structure for your code remains a continuous challenge - especially as an application grows and more developers are joining a project.

A common problem in GraphQL servers is that resolvers often get cluttered with business logic, making the entire resolver system harder to understand and maintain.

GraphQL Middleware uses the middleware pattern (well-known from Express.js) to pull out repetitive code from resolvers and execute it before or after one your resolvers is invoked. This improves code modularity and keeps your resolvers clean and simple.

Understanding middleware functions

When using GraphQL Middleware, you're removing functionality from your resolvers and put it into dedicated middleware functions. These functions effectively wrap a resolver function, meaning they ...

  • ... have access to the same resolver input arguments.
  • ... decide what the resolver ultimately returns.
  • ... can catch and throws errors in the resolver chain.

A simple example

Here is how you would implement a simple example of a logging middleware that prints the input arguments and return value of a resolver:

const { makeExecutableSchema } = require('graphql-tools')
const { applyMiddleware } = require('graphql-middleware')

const loggingMiddleware = async (resolve, root, args, context, info) => {
  console.log(`Input arguments: ${JSON.stringify(args)}`)
  const result = await resolve(root, args, context, info)
  console.log(`Result: ${JSON.stringify(result)}`)
  return result
}

const typeDefs = `
type Query {
  hello(name: String): String
}
`
const resolvers = {
  Query: {
    hello: (root, { name }, context) => `Hello ${name ? name : 'world'}!`,
  },
}

const schema = makeExecutableSchema({ typeDefs, resolvers })
const schemaWithMiddleware = applyMiddleware(schema, loggingMiddleware)

// instantiate your GraphQL server with `schemaWithMiddleware`

Diving deeper

Applying middleware to all resolvers

Let's take a look at another example where we're using two middleware functions to log the query arguments and the returned result of all resolvers in our schema. The numbers at the beginning of each console.log statement indicate the execution order:

const { GraphQLServer } = require('graphql-yoga')

const typeDefs = `
type Query {
  hello(name: String): String
  bye(name: String): String
}
`
const resolvers = {
  Query: {
    hello: (root, args, context, info) => {
      console.log(`3. resolver: hello`)
      return `Hello ${args.name ? args.name : 'world'}!`
    },
    bye: (root, args, context, info) => {
      console.log(`3. resolver: bye`)
      return `Bye ${args.name ? args.name : 'world'}!`
    },
  }
}

const logInput = async (resolve, root, args, context, info) => {
  console.log(`1. logInput: ${JSON.stringify(args)}`)
  const result = await resolve(root, args, context, info)
  console.log(`5. logInput`)
  return result
}

const logResult = async (resolve, root, args, context, info) => {
  console.log(`2. logResult`)
  const result = await resolve(root, args, context, info)
  console.log(`4. logResult: ${JSON.stringify(result)}`)
  return result
}

const middlewares = [logInput, logResult]

const server = new GraphQLServer({
  typeDefs,
  resolvers,
  middlewares,
})
server.start(() => console.log('Server is running on http://localhost:4000'))
β–Ά Run Example
Understanding the middleware execution flow

Assume the GraphQL server receives the following query:

query {
  hello(name: "Bob")
}

Here is what will be printed to the console:

1. logInput: {"name":"Bob"}
2. logResult
3. resolver: hello
4. logResult: "Hello Bob!"
5. logInput

Execution of the middleware and resolver functions follow the "onion"-principle, meaning each middleware function adds a layer before and after the actual resolver invocation.

The order of the middleware functions in the middlewares array is important. The first resolver is the "most-outer" layer, so it gets executed first and last. The second resolver is the "second-outer" layer, so it gets executed second and second to last... And so forth.

If the two functions in the array were switched, the following would be printed:

2. logResult
1. logInput: {"name":"Bob"}
3. resolver: hello
5. logInput
4. logResult: "Hello Bob!"
Applying middleware to specific resolvers

Rather than applying your middlewares to your entire schema, you can also apply them to specific resolvers (on a field- as well as on a type-level). For example, to apply only the logInput to the Query.hello resolver and both middlewares to the Query.bye resolver, you can use the following syntax:

const middleware1 = {
  Query: {
    hello: logInput,
    bye: logInput
  }
}

const middleware2 = {
  Query: {
    bye: logResult
  }
}

const middlewares = [middleware1, middleware2]

const server = new GraphQLServer({
  typeDefs,
  resolvers,
  middlewares,
})

Processing the same hello query from above, this would produce the following console output:

1. logInput: {"name":"Bob"}
3. resolver: hello
5. logInput

Here is an illustration of the execution flow:

Input arguments of middleware functions

The logInput and logResult functions receive five input arguments each:

  • The first one is the resolver function to which the middleware is applied.
  • The remaining four represent the standard resolver arguments (learn more here).

Inside of the middleware function, you need to manually invoke the resolver at some point. Notice that you also need to actually return the resolver's result from the middleware function (this also lets you transform the return value of a resolver).

GraphQL Middleware vs Schema directives

Using GraphQL schema directives is another option to add functionality to your resolver system. The biggest differences between GraphQL Middleware and schema directives are twofold:

  • GraphQL Middleware is imperative while schema directives are declarative.
  • Middleware functions are more flexible since they can be applied to specific fields, types and/or the entire schema (meaning to all your resolvers at once) while schema directives can be applied only to specific fields and/or types.

Schema directives require you to annotate your SDL schema definition with special directives to add additional behaviour to your resolver system. If you prefer having your schema definition free from business logic and be only responsible for defining API operations and modeling data, GraphQL Middleware is the right tool for you.

Getting started with graphql-middleware

The graphql-middleware library can be installed via NPM:

npm install graphql-middleware --save
# or
yarn add graphql-middleware

When using with graphql-yoga, the middleware functions can be passed directly into the GraphQLServer constructor. Other servers require you to create an executable schema first and then apply the middleware functions to it (using the applyMiddleware function as shown in the first example).

Made by the awesome GraphQL community

At Prisma, we deeply care about the GraphQL ecosystem and are especially excited about this project as it was driven primarily by our awesome community. Most notably by Matic Zavadlal who did an amazing job as the core maintainer of the project! πŸ’š



🌟 Star on GitHub 🌟

Matic also already built several libraries on top of graphql-middleware that you might find useful for your GraphQL server development:

We're excited to see what you're going to build with GraphQL Middleware!

Comments

Comments

Don’t miss the next post!