This article is an in-depth guide that walks you through an advanced use case of GraphQL directives. We will go through a hypothetical scenario in which using inheritance during schema development proves to be valuable and when incorporating such a pattern should be avoided.
Ultimately, I will provide you with a real-world example implementation of type inheritance in GraphQL using a dedicated directive.
By the end of this article you will be able to:
- recognize when and when not to use inheritance.
- re-write your schema to use incorporate inheritance.
- implement a GraphQL directive that makes inheritance possible in your schema.
- how to use your directive implementation.
- understand the limitations of inheritance
This article assumes you are already familiar with the basic concepts of GraphQL. If that’s the case you can jump directly into the implementation section.
A common scenario
We finally decided to develop a blog for ourselves. The goal is to share written articles on a small site. We heard throughout our development process that “GraphQL will kill REST” and therefore we change our mind about the technology we will use to query our data. So we decide to go with GraphQL, since it’s new, and because we are cool.
After becoming familiar with the core concepts of GraphQL we come up with the following schema for our blog:
All good! We go on and start setting up our Apollo Server, write resolvers for our schema and design a custom dashboard from where we publish articles to our blog. For the months to come, we are doing well.
A couple of months go by and we came to realize that we love photography.
Even though we are very active on Instagram, where we have been posting photos showing our cooking skills, this sole solution no longer fits our needs because we want to make the content we produce our own.
We decide to introduce a new feature to our blog that:
can transform an article into an Instagram-compatible post. allows us to share a post with our audience on both Instagram and our blog. We introduce a new type, CrossPlatformPost, to our schema:
A few months go by and during that time we became a neuroscience aficionado.
We have conducted research and would like to publish our findings somewhere. But while we can publish articles and cross-platform posts on our site, none of the types in our GraphQL schema provides an interface for publishing scientific papers.
To achieve that we must introduce a new GraphQL type to the family: the ScientificPaper type. Like the CrossPlatformPost type, the ScientificPaper type shares common fields with the Post type.
Following the same trajectory of designing a GraphQL schema, we can already foresee how it would look for the ScientificPaper type. We would copy and paste shared fields from the Post type and repeat this process for all other types to come.
So let’s stop for a moment and gain some perspective on our endeavor because we might end up losing our minds copying and pasting the same fields over and over again.
Re-evaluating our initial approach The current design approach is probably not an issue for small applications that solve a domain-specific problem that is unlikely to scale and includes domain models that barely share a common interface.
In contrast, in cases where a common interface represents the basis from which all (or many) domain models derive their fields, it might be worth reconsidering how we design our schema, such that we can easily compose types when our schema grows in size and complexity.
Some GraphQL specialists might argue that we should split our schemas using Federation — a technique to combine multiple GraphQL schemas from different, external sources into one single source of truth, a single GraphQL endpoint if you will.
This argument holds for mid to large-sized companies that have different teams working on a specific subset of domains and therefore have to access data across entities.
It has been my experience that for small teams and companies who, for the most part, work on a single domain, using federation can be a) very costly and b) introduces new, unnecessary complexities that are outside the scope of their domain.
Note: In the end, it depends on the complexity of your domain-specific requirements, and which solution solves the problem best.
Inheritance: An alternative solution
Why use inheritance
The main advantage of using inheritance when composing types is that it allows you to avoid duplicating fields that are shared by multiple types. This is certainly true in our hypothetical scenario where the Article, CrossPlatformPost and ScientificPaper types share fields from the Post type.
Instead of defining the same fields multiple times, you define them once in a parent type and then inherit them in the child types. Doing so greatly reduces the amount of code you need to write, and makes your schema more flexible, concise and easier to maintain.
Inheritance also proves to be more efficient and less error-prone than duplicating shared fields across multiple types, because any changes to the shared fields only need to be made in one place.
Rewriting our schema
Recap: What GraphQL directives are
Directives are used to add metadata and functionality to GraphQL types and/or their fields. This metadata can be read at runtime to modify the behavior of a GraphQL schema.
There are two types of directives in GraphQL:
Schema directives
Directives whose sole purpose is to attach metadata to a type or field, e.g @deprecated
Operation directives
Directives that manipulate the output of a GraphQL operation (query, mutation).
E.g: @skip
, @include
Use cases for directives include but are not limited to:
- including or excluding fields
- manipulating query arguments
- controlling access to specific fields
A GraphQL directive is a pure Schema Definition Language (SDL) feature.
We declare and use directives using the following SDL syntax:
Inheriting types using a dedicated directive Now that we have a schema that defines shared fields between different kinds of Post types, let’s introduce an imaginary @inherits directive to compose an Article type.
At the time of writing, there is no way other than using a special directive to derive fields from one or more types in GraphQL.
We do the same for our old CrossPlatformPost type:
Lastly, we apply the same technique to the ScientificPaper type:
Thus far, we have re-written our schema, so that types that share a common interface can derive fields from one another using an imaginary @inherits
directive. But the directive is still just purely descriptive and has no functionality attached to it.
In the next section, we will go through the implementation details of our directive’s functionality in digestible chunks.
Implementation
For the sake of clarity, I will include an abundance of comments in the code. It’s important to be cautious with excessive commenting. If the purpose of our code is not evident at a glance, it may be too smelly and require refactoring.
What we need
- TypeScript For better developer experience, tooling and type-checking support.
- GraphQL The JavaScript reference implementation for GraphQL.
- graphql-tools A Library provided by The Guild team that provides an intuitive API and handy tools for working with GraphQL schemas. Creating a reliable and reusable, non-trivial schema directive can be tough, even with the help of great tools and best practices. It’s a good idea to use a typed language like TypeScript since there are so many different schema types to consider.
Initial setup
Add types to represent our directive definition in TypeScript.
This step is optional, but since we are using TypeScript we should make use of its powerful type system. This might be redundant in some cases yet in most situations it helps catch bugs early on in our development process.
Clear and actionable errors It’s always a good practice to provide users with clear and actionable errors. For that reason, let’s write some common error classes that extend the built-in GraphQLError.
With this setup in place, we can move on to the next step where all the magic happens.
Putting it all together
We have nearly reached our destination. The final piece of our implementation is about exporting a factory function that returns an object in the shape of DirectiveDefinition. The factory should allow us to customize the final directive name being used in our schema.
Here is what’s happening inside the inheritsDirectiveDefinition factory above:
create the typeDefs as it would appear in SDL.
E.g.: directive @inherits(from: [String!]!) on OBJECT | INPUT_OBJECT | INTERFACE
assign the inheritsDirectiveMapperFn to the appropriate GraphQL mapper kinds
You may have noticed the mapSchema function returned by our factory.
Remember, we need to transform our schema to apply functionality provided by our directive implementation and return a schema object. This function does just that.
Example Usage
To use our @inherits
directive we need to apply the directive definition to our schema. This step might differ depending on how you have set up your GraphQL server, but here is an example of how you could do it:
Testing our solution
I will not go into detail about how to test the above directive implementation in this article. But for the aficionados amongst us who would like to go through a naive testing example for this directive, here is a GitHub link: Inherits directive test.
There is no great software without any proper testing. As with all features you write, you should test your implementations thoroughly. Implementing GraphQL directives is no exception to that rule.
Tests are a great way to outline a problem and find robust solutions for it while considering the edge cases of our initial problem statement. We are also less prone to implementation errors that would otherwise go undetected.
I would argue that we should start writing tests before even considering coding anything.
Limitations
Inheritance is not always the best solution for every use case. In some cases, it may be more appropriate to define fields explicitly in each type to ensure clarity and emphasize intent in the schema.
The @inherits
directive, for instance, can introduce some complexity and overhead in the schema by making it difficult to see what fields will be included in the final, transformed type output. This may not be justified for simpler use cases.
When using Federation (e.g. Apollo Federation), modifying a type in a subgraph that is inherited by other types may strip away the @inherits
directive’s metadata.
Regarding using directives
Directives should not be used in every situation. In some cases, they may lead to a noticeable decrease in performance or make your schema more complex and difficult to maintain.
It’s important to keep your schema as simple as possible and to avoid using directives when they’re not necessary. For example, you should avoid using directives for tasks that can be performed at the resolver level, such as data filtering or validation. Doing so can make it more difficult to debug and understand the flow of data in your application.
Final words
Type inheritance in GraphQL is a common task in scenarios in which you have a lot of types that derive (a subset of) fields from a parent type. However, it is not natively supported and therefore we must make our hands dirty and implement such a feature on our own, using a dedicated directive.
In this article, we looked at a scenario that sticks to the classical approach of designing our schema and discussed why it might inhibit our development process and make it more difficult to add new features to our existing schema.
We went on to re-write our schema to fit the needs for incorporating inheritance and discovered why implementing inheritance in the GraphQL schema for our hypothetical blogging site proves to be effective.
Finally, we wrote the actual implementation of our imaginary directive’s functionality and outlined the limitations of our solution.
This implementation is just an example and must be customized to fit your needs. You must also ensure to restrict inheritance by type, as I commented right at the beginning of the implementation.
Ultimately, your decision to incorporate inheritance in your schema boils down to the specific needs and requirements of your project, as well as the trade-offs between simplicity, flexibility, and maintainability you are willing to make.
Regarding using directives in your GraphQL schema Directives should not be used in every situation. In some cases, they may lead to a noticeable decrease in performance or make your schema more complex and difficult to maintain.
It’s important to keep your schema as simple as possible and to avoid using directives when they’re not necessary. For example, you should avoid using directives for tasks that can be performed at the resolver level, such as data filtering or validation. Doing so can make it more difficult to debug and understand the flow of data in your application.