Extending Graphql — Comparison of Apollo plugin, Envelop and Graphql Middleware

Han
6 min readApr 2, 2022

--

GraphQL is an awesome query language and we have been using it for many years. Over the years, we have had many needs to do some non-resolver level modifications of the server and we have discovered many strategies. This post aim to summarize some of them and discuss a good strategy.

TL; DR

Apollo Server Plugin is less powerful than Envelop, but it showed good design considerations and is more favorable.

Graphql Middleware

Graphql middlware is one of the first libraries that were created to modify graphql schema. This strategy, in their words, an “onion” principle, works similar to any web server middleware (expressjs middleware, Django middleware, Rails middleware), it allow users to wrap the existing resolver function with a new resolver.

const newResolver = (oldResolver, _,arg,ctx, info){
// add some code before resolve
const result = await oldResolver(_,arg,ctx,info);
// add code after resolve
return result;
}

But notice, this is a schema modification, given an old schema, developer can apply many middlewares and generate a new schema. Then use this new schema in process graphql requests.

Envelop Plugins

Envelop Plugin an upgrade of Graphql Middleware. In a normal process, a graphql query goes through parsing, validation, execute three steps. in parsing step, the graphql query text is converted into an AST(abstract syntax tree), in validation step, the AST is then matched against the schema. This is the step where it ensures that the user query is not request fields that the schema does not have.

// graphql query
query {
user {
id
name
age
}
}
// schema
User {
id: Int
name: Int
}
// Validation error, "age" field not found in schema.

Then inexecute step, the resolvers in schema are called accordingly to form the graphql response.

The library envelop provide ways for users to modify all three steps. It not only allows user to add preXXX and postXXX code for each step, it even allows user to completely replace the existing function in the plugin. via the setParseFn, setValidationFn, setExecuteFn function.

Envelop API for modify parse step

Similar api are also provided for validation and execute step. Besides these standard steps, the package also allows user to modify context creating (happened after validation), and onSubscribe, onSchemaChange(run code when schema change, this usually happen in federation, some times part of schema is available after server is up). onResolverCalled (works like middleware, triggered when every resolver is called). Example of wrap parse is shown below.

// 
const plugin = {
onParse:({parseFn, setParseFn})=>{
const newParse = (...params)=>{
// add your code before parse;
const result = parseFn(...params);
// add your code after parse;
return result;
}
}
}

Apollo Server Plugin

Apollo Server is a full featured server designed specifically for graphql, with the plugin system, users are also able to inject code in the three steps: parsing, validation, execute.

Apollo server plugin lifecycle From https://www.apollographql.com/docs/apollo-server/integrations/plugins/#request-lifecycle-event-flow

User implement the xxxDidStart function according to the api, and it would be executed right before xxx, if the xxxDidStart returns a function, the function will be executed right after xxx.

The main difference between the apollo plugin and envelop plugins, is that Apollo plugin implementations do not affect the graphql query parsing and validation, only the willResolveField (works as the onResolverCalled in envelop) can affect the execute step. So the main reason for the plugin functions are for logging purpose. For example, you could collect request and send to newrelic, or other server monitoring server.

Comparison of GraphQL Middleware, Envelop and Apollo Server Plugin

Now for the good stuff, the comparison. This is a quite opinionated section, I happened to have used all three of them in production and discovered some pros and cons in these frameworks.

  1. Most people only need to inject code in resolver (change schema), in this perspective, all three libraries are able to do it well. There is little to no performance difference between these three in terms of “wrapping resolver”, it is just a difference of syntax.
  2. Apollo server plugin takes a very restricted approach when it comes to plugin, it allows async functions to be injected, but it does not affect the main flow result. For example, in the source code below, the validationDidStart event is triggered right before validate and await to finish. Then validationDidEnd is called right after. But non of the returned result is affecting the main validate call. The dispatcher dispatch the event, all the validationDidStart function from plugins are called and collected with Promise.All . This is very restricted to the user of course, because you are not able to change the behavior of the server, but it has a great advantage.

Each plugin are always independent of each other, and called in parallel.

in other words, the order of plugins does not matter. And this is important! (I will talk about it more after I talked about envelop).

sourcecode from apollo server: invoke validation step.
source code from apollo, dispatcher.invoke

3. Envelop plugin, however, is completely opposite, it gives developer so much freedom that you can basically do anything. Because it offers a setXXXFn api, so developer, if choose to, can replace the validate, parsing, execute with any function they want.

4. Wrapper style plugin is powerful, but dangerous. Because it allows plugins to depend on another. The problem with plugin dependency is that it is always assumed to be independent and the order of dependency is not like “imports” which usually can be found in compile time. It is hard to check and maintain. It is also a very common problem too, in many frameworks, Django middleware, expressjs middleware, even Ruby on rails activerecord callbacks. (the after_xxx callbacks are execute in order, so it is easy to have some dependency without knowing it, see this issue).

Django middleware dependency for cache

As author of plugin, one should always try to make it an independent plugin and free of side effect, but it is hard to enforce the author to comply this rule. Some may add test cases, for example, we have an integration test that shuffles the plugins and run sample queries to ensure the results are still the same.

From my years of programming, I have seen developers doing enough crazy things to know that

in a team of many developers, you cannot rely on every developers to follow the style guide, you must enforce them. If you want to prevent an anti-pattern, either add lint/test check, code review yourself, or do not give them the ability to write it.

The “resolver wrapper style” plugin like Graphql Middleware, onResolverCalled in Envelop, executeDidStart in Apollo Plugin is hard to avoid, because people do tend to modify the result of resolvers. But validate and parse step are pretty standard and we should avoid doing this “wrapper style” as much as we can. Which is why I, in my personal opinion, think that Apollo Server Plugins is a better design. Also, Apollo server plugin’s functions are all async, which allows user to do things like “send preparsed query to Datadog, or some other data collector”, but Envelop’s validate parse are synchronous, which focus on modify the behavior of validate and parse .

Summary

In this blog, I compared three ways to extend graphql server. I focused on comparison of Apollo Plugin and Envelop. I briefly explained how powerful envelop is, but lack of design choices. In my opinion, totally opinionated framework is not good, but neither is a totally non-opinionated barebone. Apollo Server Plugin made some hard choices when designing the plugin interface and in my opinion, is the right choice. So I would favor Apollo Plugins for now. It definitely has some limitations, but I trust them having no problems address these limitations in the future.

--

--

Han
Han

Written by Han

Google SWE | Newly Dad | Computational Biology PhD | Home Automation Enthusiast

No responses yet