GraphQL Directives in Drupal

Philipp Melab / Jan 24, 2024

Amazee Labs is in a long-time relationship with GraphQL, but we always had rather unique perspectives and requirements. That led to some interesting developments we are excited to finally share.

Back in 2015, we were heavily involved in the creation of the GraphQL module for Drupal, and over the years we invested time and resources to participate in constant maintenance and improvements. And even though our focus on Drupal blurred a little, we still use it every day. The latest release - the famous “v4”, that changed everything - has been stable and reliable for years now, thanks to the work of our friends at jobiqo and open social.

Benefits of GraphQL

When researching the benefits of GraphQL the internet’s (or ChatGPT’s) first answers are “No over-fetching” and “Reduced number of requests”, which are valid, but not unique to GraphQL any more. Drupal’s built-in JSON:API module along with its Includes feature can provide the same technical benefits. On top of that, our main focus lies on building large-scale content websites, like caritas.ch or lgtcp.com, with Gatsby, a static-site-generation framework. The latter means that Drupal and the GraphQL API are not even involved when a regular visitor accesses the website, which has a lot of benefits for us and our clients, but reduces the technical benefits GraphQL has for us to zero. So why do we still bet on it?

The “Schema-first” approach

We already have been rather vocal about the real purpose of GraphQL, namely defining custom APIs that are not dictated by one specific system. The approach of JSON:API, REST and the previous version of the GraphQL module was to automatically expose all Drupal data structures the current user has access to. That sounds great on paper, but ultimately means that implementation details leak through the whole application and turn it into a monolith. More than 70% of the development work we do is invested into the “UI” layer, which is not in Drupal, but written in Typescript and React. And the remaining 30% are most of the time Drupal. But more often than not, SaaS solutions or Decap CMS are a great alternative. In these cases, we don’t want to re-invent 70% of our process. Custom GraphQL APIs provide us with a technology-agnostic contract that allows us to go full “Schema-first”, meaning that all work is preceded by definition or modification of GraphQL types and the operations executed against them. Doing this before actually configuring data structures in Drupal makes sure that the GraphQL schema is rather the distillation of the feature requirements than a mirror of how Drupal thinks about it. When switching to this “pure” schema, suddenly the positive side effects pop up left and right! Frontend and Backend teams can work fully in parallel, automated testing becomes way easier, estimations are more accurate, migrations suddenly become a manageable task and automatically generated type systems help with long-term maintenance and extension of projects. All rainbows and unicorns, right? Almost.

Developers pay the price

Version 4 of the GraphQL module already focuses on custom schemas and therefore supports this approach, but the development ergonomics leaves room for improvement. To implement a simple “echo” field, that would return whatever is passed in, in the “idiomatic” way, one has to touch three different files: First, define the field in the actual schema:

Then, add a GraphQL data producer plugin:

And then use a rather arcane builder API to inject this data producer into the schema implementation:

What seems relatively manageable for this example, quickly turns into a burden when data structures become more complex and every project has its own schema. Under pressure, we often had to resort to shortcuts like huge “one does everything” data producer plugins or just skipping the plugin and using closure callbacks …

… that are not composable or reusable, and on top of that may negate future performance improvements. In defence of this API, that I helped to shape back then, I have to mention that it was not intended to be the final solution. There has always been the vision of a “point and click” user interface that allows Drupal site builders to quickly assemble their API from all available data producer plugins. That’s why they are annotated with metadata about input and output types. But developers are actually more comfortable in text editors than visual interfaces and there was never enough interest to justify the effort. Also, in hindsight, making Drupal the source of truth for the schema definition would have invalidated the “Schema-first” approach explained before. So we decided to shift gears and look at the problem from a different angle.

Directable schemas

The GraphQL directives module is an extension to the GraphQL module for Drupal. It provides a single new “Directable” schema plugin that reads in a schema definition file, and interprets certain directives it finds. Directives are a GraphQL feature that allows one to annotate type definitions or operations with meta-information for the implementing system. And they look like this:

In this example, the built-in @route directive will use the path argument to resolve a Drupal route and hand it over to the @loadEntity directive which will load the node behind it. The module’s README.md has an extensive list of all available directives and their inner workings, but out of the box the system can deal with any Drupal entities, menus, and PHP structures like arrays and objects.

Installation and configuration

To get started, use composer to install and enable the GraphQL module, as well as the GraphQL directives module:

When configuring a new GraphQL server, there should be a new option in the schema selection called “Directable Schema”. When this is selected, two configuration options will appear, that both expect file paths to files we still need to create.

Schema definition

The first one is the schema definition, which has to point to a GraphQL configuration file that contains pointers to schema definition files. It helps the graphql_directives module to find and aggregate the whole schema. This is necessary because modules might provide their own directive definitions. The graphql_directives module itself ships a directives.gql file that needs to be included along with the main schema definition. In a Composer based Drupal this file could be placed at the root and would look like this:

This would prepend all shared directives before the actual schema. Another purpose of this file is to inform editors. Common GraphQL extensions, like the ones for VSCode, PHPStorm or Neovim, understand this format and will provide you with autocompletion and type checking. To test if it works correctly, create a very simple schema file:

After clearing Drupal cache, running the following query in GraphiQL should return the “admin” user name:

Autoload registry

The second configuration option is used for easily adding new directives. Let’s have a look at the “echo” example that was solved with data producer plugins before. First, we create a very simple class that provides the logic as a static method:

Now we extend the GraphQL schema with a new directive that calls the service method. Note the block comment above the directive. It tells Drupal to use our service method when resolving the value. If our class is registered as a service in the container, we could also use a symfony service identifier, like echo.service::echo.

Now we need to generate the autoload.json file. GraphQL Codegen with the Autoloader plugin helps us to do that. There are many ways to configure and use GraphQL Codegen, but the instructions on both projects should get you started. After configuring the autoload registry file path in our GraphQL server, the new echo field should show up in GraphiQL and work as expected. A drastic improvement! And all directives added this way can be chained and combined with the built-in ones and each other!

Way to go

This sums up our approach to easily create and maintain custom GraphQL APIs. Currently, this approach still uses data producer plugins under the hood. Simply because all the built-in directives rely on existing data producers. If we manage to port all of them into simple service classes, we could create a more efficient implementation of the GraphQL executor and also squeeze out some performance gains. The Autoloader codegen plugin also works with Javascript-based GraphQL implementations. That way we extend Gatsby’s internal GraphQL API. But that’s a story for another day.