apps/blog/content/blog/graphql-middleware-zie3iphithxy/index.mdx
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.
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.
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 ...
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`
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'))
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!"
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:
The logInput and logResult functions receive five input arguments each:
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).
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:
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.
graphql-middlewareThe 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).
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! 💚
Matic also already built several libraries on top of graphql-middleware that you might find useful for your GraphQL server development:
graphql-middleware-apollo-upload-server: Manage file uploads.graphql-shield: Easily implement permission rules in your resolvers.graphql-middleware-sentry: Reports errors to Sentry.We're excited to see what you're going to build with GraphQL Middleware!