Extending Graphql — Comparison of Apollo plugin, Envelop and Graphql Middleware
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.
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
.
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.
- 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. - 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 beforevalidate
andawait
to finish. ThenvalidationDidEnd
is called right after. But non of the returned result is affecting the mainvalidate
call. Thedispatcher
dispatch the event, all thevalidationDidStart
function from plugins are called and collected withPromise.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).
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).
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.