GraphQL Tour: Directives
The word “directive” might sound quite technical and maybe a bit scary, but we’re going to take this time to explore GraphQL directives and show that they’re not really all that bad.
First of all, what is a directive? Syntactically, directives are the @at-sign markers you might find in some GraphQL queries:
See that @skip? That’s the skip directive in action, which lets the GraphQL executor omit certain fields at runtime.
In general, the purpose of directives is to allow the GraphQL executor to alter the result in ways that would otherwise be invalid or unsupported. GraphQL defines two standard directives, skip and include.
When we say “alter the result”, that can really mean anything, right? The default directives in GraphQL allow us to just show or hide different fields, which is pretty straightforward to understand, but directives in the abstract come off as a bit more mysterious. You may be wondering, “What else would we possibly want to do?” (or at least I was).
A promising hint at better utilizing directives comes from the GraphQL spec:
As future versions of GraphQL adopts new configurable execution capabilities, they may be exposed via directives.
The next clue is that directives are actually allowed to be on fragments and even whole queries. The skip and include directives only operate on fields, but something like this is valid GraphQL:
If you’re curious, you can read the relevant Relay directive source to see all the options available — but the important thing is that you can extend new types of directives that aren’t supported by GraphQL out-of-the-box.
Now let’s take a look at how we would do such a thing using a GraphQL-JS server.
Creating a New Directive
You can skip to the final source on Github, but we’ll walk through the big ideas.
One limitation of creating our own directives at this point is that GraphQL-JS (as of 0.5) doesn’t expose many hooks for altering the execution of GraphQL queries. How it implements skip and include are baked into the core of the execution steps, not using a public API. I’m sure this is because it’s not clear what kind of APIs are needed for directives — so if you do end up implementing a new directive and need an API, shoot them an issue!
In the meantime, we’re going to define an @instrument directive. When you add it to a field, the server will instrument how long it takes to resolve and send it to a metrics service.
It’s a bit contrived, I don’t know if you’d actually want to do such a thing in a real app, but hopefully it gets you thinking about what other directives could be created. At the end of the day, we want to be able to construct queries like this:
To get started, we define a directive just like we would define a new GraphQL type:
Note that we have a locations property, which states where the directive can be used (remember we said directives can be placed on fields, fragments, and queries). We also specify that the directive has a tag argument, which is what we’ll use to track performance in our system.
For this example, we’re going to inject our directive-powered behavior at the resolve level (like we said earlier, the GraphQL executor doesn’t provide much room for adding custom behaviors at that level). Here’s a search-result type that we showed in the example earlier:
Notice that we’re using a new function, resolveWithInstrumentation. We’ll go into the details in a moment, but for now imagine what that function would need to do internally:
- Return a function, which is what resolve expects
- Inspect the query and determine whether or not to instrument the inner resolve function
- Eventually invoke the real resolve function we passed as an argument
Make sense? Let’s build it up one step a time. First, we need resolveWithInstrumentation to return a new function:
Next we have to determine whether the new instrument directive is present on the field. Luckily GraphQL passes a ton of helpful information, including the GraphQL AST, in the info object (and if you’re wondering about the complete list of things in the info bag, check out the source). We can use fieldASTs to figure out what directives are present:
Pretty straight-forward — if the directive isn’t present, then we just call the original resolve function. Finally we need to instrument the resolve. There’s a few ways of doing this, and for brevity’s sake we’ll simply wrap the underlying resolve in a promise:
Remember that the point here isn’t to show how to actually instrument GraphQL resolution, but to highlight one technique for using custom directives at runtime.
Finally we need to inform our schema that our directive exists! Just writing the code that detects it isn’t enough — if our schema isn’t aware of the directive, any incoming queries using it will throw an error.
The directives property takes an array of all the directives your schema understands. Note that if override that property with any value, we also have to pass in the original GraphQL include and skip directives.
To see it all in action, check out the source on Github.