GraphQL Schema Directives as ORM
A new era of ORMs
GraphQL is booming. Anyone starting an app today will require talking to an API and will likely come across GraphQL. Working with GraphQL daily it seems to me GraphQL is everywhere like the 5 times in this intro.
Not only has the ecosystem around GraphQL rapidly evolved over the past months but it has also moving towards best practices and common patterns how to use GraphQL in the client and server as well as combining legacy systems and REST APIs.
Now giving you are getting started with GraphQL many boilerplates, documentations and tutorial hardly go past using “hard-coded” resolvers with maybe two types defined. While this get’s you started you will bump into problems very soon by either thinking GraphQL is overkill repeating resolver logic over and over again or you may be trying to look beyond “plain” resolvers. Lucky enough — many roads lead to Rome — or in GraphQL case to resolve a request.
If you are at this stage — this article might be for you or you just want to know more about the power of GraphQL Schema Directives read on. ….just want Schema Directive examples — scroll fast :)
GraphQL Resolver Patterns
GraphQL has the concept of using resolvers to return exact data to your request according to your query or mutation return. No matter what server-side language or server you run there are certain patterns how you might or should resolve requests in GraphQL. Then again there is no right or wrong but each pattern can give you certain benefits over the other.
I see Resolver Patterns the same as Architecture Patterns or Fashion Trends you either follow them or not — up to you — but sometimes you better do or have to and most of the time it will be based on personal choice/flavor or some kind of corporate force or madness.
Below is a by no means a complete list of common resolver patterns or approaches to structure your resolvers:
- Root resolvers
- Type & Field resolvers
- Context injection
- Apollo Resolvers
- GraphQL Middleware
- Schema directives
- Database / Backend as a service / “Generated Resolvers”
Now there is many options but based on the past 2 years working with GraphQL with heavily changing requirements and let’s call it client-side-requirement-driven-development your GraphQL server need to be flexible and abstract at the same time.
Using Schema Directives enables you to go all-in on using a Schema-first approach and build your resolvers around it. How that might work we will see below.
If you look around today at managed GraphQL services like Prisma or AppSync you will notice the initial input and single source of truth is the Schema defined in SDL or some abstracted JS to generated your resolvers. Now this is for a reason — SDL being more or less JSON — the GraphQL Schema can be used as universal layer to all data sources, easily readable and understandable across technical levels or business stakeholders. Read more here on this topic.
Towards Schema Directives
Not only with seeing GraphQL Schema as single source of truth but also unleashing its power of using Schema Directives it will become clear why we might be up to a new Era of ORMs.
A GraphQL Schema enables you to define Types your objects being used in your application. Then we have Arguments to access Types and its Fields based and certain Variables. Also we have Query, Mutation and Subscription to access or manipulate our data — which means all operations and accessible fields are normally defined in the schema. So why not go beyond defining operations but also defining relationships, caching behavior, data sources, access rights and hooks?
Sounds familiar? Yes, this is what most ORMs or somewhat for example npm packages like sequelize have been offering in the past.
Schema Directives can be defined on OBJECT, FIELD and FIELD DEFINITION levels so pretty much everywhere using custom directives with common or custom data types — String, Boolean and you name it.
It does not stop at defining directives solely. All defined directives may be used before AND after the default resolver which makes it more powerful than middleware or root resolvers for example. To read more on this topic I highly recommend reading Ben’s article on Schema Directives which finally sold me.
A New Era of ORMs
With all the excitement on Schema Directives and it’s superpowers and Ben’s vision on an ecosystem of directives, we can also call it a new era of ORMs.
Implementing GraphQL server-side may leads to the question on where to define what have been defined previously in your ORM defining fields, types, tables, table names and relations. In case of GraphQL these times shall be over — with a Schema-first approach you should not be defining relationships or data types twice in your GraphQL Schema and then in your ORM again.
Such approach is not only calling off double work here but also enables your to define your ORM on a database level but in the data layer — where it actually belongs. In many cases you will rely on multiple databases, database technologies or maybe different data providers or external data sources which all shall be part of your ORM.
In a nutshell with a GraphQL Schema by default it enables you to have proper Object and Relationship definitions already but adding Schema Directives it empowers to pretty much everything a ORM has been in the past with powers beyond as universal data-layer.
Schema Directives Example
Enough talking let’s look at some examples what it all means using Schema Directives as an ORM.
Let’s go by the examples of the Apollo documentation:
Now let’s add some Schema Directives:
Let’s digest above on what we did here.
myEvents: [Event] @auth(role:”event.owner”)
We defined a “auth” directives with role-based permissions to limited the access to the event owner only which enables you to define role based permission in schema level on query, type or field level. The authorization itself can be implemented using custom directives or use common packages such as graphql-shield. Your custom directive could read roles from a token and then decide to resolve each field to the client based on its permission.
Data source samples:
type Event @dynamoDB(table:”Event”)
searchEvents: [Event] @myElasticCluster(index:”allEvents”)
upcomingEvents: [Event] @dynamoDB(index:”upcoming”)
weather: WeatherInfo @rest(url:”weather.com”,lat:$lat,lon:$lon)
Here we go using our own custom directives defining our data source in the schema itself along with the data source equivalent key or index for example. In case of DynamoDB you might have different secondary indexes defined for each query you run on your table which then can be your custom DynamoDB directive to query data from the index specified in your schema.
We also may define result sources on field level such as getting the WeatherInfo from a rest endpoint e.g. weather.com with query strings based on our root data (lat, lon).
pastEvents: [Event] @cacheControl(maxAge: 60000) @myLongTermStorage(index:”past”)
Here we have a sample on defining cache control inside the schema for past events we likely want to cache and maybe event store in another storage. cacheControl directive is handled by default by Apollo Server exposing it to Apollo Engine or your implementation of a Cache Key Value store of your choice.
To build your own Schema Directives you can use the widely popular package of graphql-tools exposing several helpers to implement proper directives which you can then pass into your executable schema. Go ahead and build your own!
Limitations and alternatives
Schema directives are powerful but there are limitation you may run into or when solely relying on Schema Directives.
Your GraphQL Schema can be exposed using Introspection, so normally in a production you would not want to expose your Schema for introspection. Directives defined in your schema can also be uncovered — so do block introspection and be aware of anyone with access to the schema will have access to directives which would expose way more than just data structure.
Also implementing your own directives may lead to fatal results if for example you handle authorization through schema directives but forget to protect certain fields or even worse edges between nodes which lead to expose multiple types of your graph due to GraphQL’s nested structure.
Implementing directives on a field level can lead to poor performance if you will run heavy calculations or useless business logic for each field pre data resolution. For example you will want to avoid initilizing your database connection for every field again but rather just query data according to your field or type. Also using methods like visitSchema can slow down performance being run on every query or even every field depending on your setup and size of schema — so you may weight where high-level abstraction shall be put in place with least impact on performance.
Provisioning and migrations
Depending on your data storage database or table provisioning could be more complicated than using DynamoDB for example. Using Schema Directives as ORM shines using NoSQL and Elasticsearch for example but it might get harder using MySQL for example taking care of migrations on schema changes.
Centralise business logic
As mentioned above do weight in the impact in abstracting your ORM into the schema. You likely be able to abstract authorization and at least defining high-level data sources such as type of data source in the schema to then forward/resolve the request into its proper path. Luckily enough with schema directives you would also be able to for example just inject these into the context or resolver itself.
For simple CRUD operation without business logic or repeating business logic you could write the resolver itself into the schema directives i.e. your directives will resolve data based on directive attributes. For example the dynamoDB directive: Let’s assume you have one DynamoDB table with multiple indexes. The schema directives will pass the index name into your high-level schema resolver to return the data. If you have specific business logic you may just take the directive attributes as input and pass them into the context to process in the default resolver.
Writing your own schema directives can be a breeze but you may just want to get the benefits of schema directives and use built-in best practices with managed services then there quite a few options to pick from.
You may go with a services like Prisma which heavily simplifies resolver implementation. Prisma is one of the leading services to provide guided process from defining a type schema to generating actual resolvers for you alongside integrating a lot best practises building GraphQL servers providing another useful data layer in front of your database.
Or you can go with AWS’ AppSync which also helps your to go from schema to resolvers in record time while providing all of the common benefits going with one AWS offerings. AppSync has standardized resolving data for simple relations to DynamoDB and ElasticSearch built-in.
For even further abstraction you could go with a service like Hasura offering to convert your Postgres into a GraphQL endpoint without the need of an actual GraphQL Schema (not exposed). Hasura takes the approach of moving the business logic behind common CRUD operations to event-based triggers from the database.
Enter the new era of ORMs with GraphQL
Just getting started with GraphQL? Do not be overwhelmed by all the different ways of writing resolvers. Do get your hands dirty first and write a couple of resolvers yourself to understand the inner workings of GraphQL.
Do forget about the way you have used ORMs in the past and cherish the benefits of combining relations and logic in on place where its needed.
The real power of using schema directives may only be achieved with getting towards an ecosystem of directives to supply demands for various database technologies and sources. As much as schema stitching is an interesting approach it would be much more beneficial to standardize data sharing using common schema directives.
Common schema directives then maybe lead to globally unique data using schema id + type name + identifier to merge data schema across services and organisations. For example Github:User:username or LinkedIn:User:username — let’s call it ? 😃