GraphQL The Rails Way: Part 2 - Writing standard and custom mutations
In this episode you will learn how to create custom GraphQL mutations using graphql-ruby as well as reusable mutations to easily create, update and delete your API resources.
In this episode you will learn how to create custom GraphQL mutations using graphql-ruby as well as reusable mutations to easily create, update and delete your API resources.
TL;DR; Thinking GraphQL mutations are a bit too verbose to define? A bit of introspection can help define base mutations for common operations. Need to do something else? Writing your own custom mutations is simple, really.
In the last episode we covered how to dynamically define queryable resources using graphql-ruby, making it almost a one-liner to get fully functional API resources.
In today's episode we are going to talk about mutations and how to define them The Rails Way™.
Mutations you say? Yes. If you're familiar with REST, you know how CRUD operations are codified. One uses POST for create actions, PUT or PATCH for update actions and DELETE for destroy actions.
This HTTP verb mapping always makes developers wonder whether they should use POST, PUT or PATCH when actions fall outside of traditional CRUD operations. For example, let's consider a REST action which is "approve a transaction". Which verb should you use? Well it depends.
If you consider that the transaction is getting updated, you should use PUT or PATCH. Now if you consider that you create an approval, which in turns updates the transaction then you should use POST.
I dislike these dilemmas because each developer will have a different view on it. And as more developers work on your API well...inconsistencies will spawn across your REST actions.
With GraphQL there are no such questions. You keep hitting the /graphql endpoint with POST requests!
A mutation is simple: it's an operation which leads to a write, somewhere. It can be anything, such as:
So let's see how to define mutations with graphql-ruby and - more importantly - how to make them reusable ;)
In this episode I will be reusing the Book(name, page_size, user_id) and User(name, email) models we defined in the previous episode
All mutations must be registered in the Types::MutationType file, the same way queryable resources must be declared in the Types::QueryType file.
Mutations can be declared in block form inside the Types::MutationType file but that's kind of messy. We'll use a proper mutation class to define our mutation.
First, let's register our mutation. The class doesn't exist yet but we'll get it soon.
Nothing complicate here. It's fairly straightforward.
Now to write a mutation, we need to define four aspects:
That sounds quite reasonable, you would expect this sequence from any controller action. So let's see what the mutation looks like.
That's all you need. Now let's try to update a book using GraphiQL. You'll note that, once again, our mutation is properly documented on the right side :)
Now let's try to update a book that doesn't exist:
Finally let's add some validation on the pages attribute of our ActiveRecord model:
Now let's try to update our book with a negative page size:
As expected, the update is rejected and we get a proper message describing the error.
All in all I find that defining mutations is quite an easy process, much more than defining queryable resources. This is mainly due to their atomic nature, where each mutation does only one thing. On the other side queryable resources have a greater complexity due to the many functionalities we expect from them (pagination, filtering, sorting, embedded resources etc.)
Now as you can guess, we can simplify the definition of mutations for CUD operations with some metaprogramming. The gain is not going to be as obvious as with queryable resources, but it's still there - especially because we'll standardize the way they are defined and returned.
As part of this simplification process, we'll also improve error formatting. Returning error messages is a bit rough. Having a message, a code and a path (= which input triggered the error) would be much nicer.
Let's get to work!
In order to standardize our mutations we'll need to define three things:
If you've read our previous episode you should be comfortable with types already.
The mutation error type is defined as follow:
We're done here. Let's use that new type in our standard return type for mutations.
All mutations should inherit from Mutations::BaseMutation.
You can then define child base classes for each mutation flavor (e.g. create/update/delete). But all should be inheriting from Mutations::BaseMutation - this way all your mutations will be consistent.
So what do we want to enforce in all our mutations? The following concepts:
With the above in mind, here is what our Mutations::BaseMutation class looks like:
Most of the complicated code is related to error formatting, which requires a bit of array hula hoop. That code aside, we're really just enforcing standard return fields (success and errors).
Let's tackle our CUD operations now.
The create mutation is going to handle the following aspects:
What it is NOT going to handle is:
The use of Pundit (or similar framework) is optional but strongly recommended. Look at the authorized? method and adapt based on your needs.
The base create mutation looks like this:
It looks like a lot of code but it doesn't do that much. Think about our previous updateBook mutation - the rest is just metaprogramming overhead.
Now let's implement the actual create mutation for our book model.
And register our create mutation:
You can now create books via GraphiQL:
If validation happens to fail, you'll get nicely structured errors:
The Mutations::BaseCreateMutation comes with a few additional goodies. Let's say you need to set the book ownership to the current user and perform some post-create actions.
You could rework your book mutation the following way. Of course it assumes that the GraphQL context has a current_user attribute defined. You should read our previous episode to know more about this.
The base update and delete mutations will be very similar. Let's go over them briefly.
The base update mutation is a bit more complex than the base create mutation because:
The base update mutation looks like this:
And our book update mutation can now be reworked like this:
Do not forget to register your book update mutation:
We can now update our book via GraphiQL again. The only difference with our previous approach is that the errors field is now an array of objects.
The base delete mutation is very similar to the base update one. The only difference is:
The base delete mutation looks like this:
And the actual delete mutation looks like this:
The mutation type now has all the CUD operations defined:
Now let's destroy a book via GraphiQL:
🎉 Tada! 🎉
Remember that mutations can be any mutating action.
CUD operations will be covered by the standard mutations we have written but in case you need to do something different, just go back to Mutations::BaseMutation - don't try to bend our standard CUD mutations.
This base mutation simply defines the response style of your mutations. It is responsible for making your API consistent. In the end all you need to do is:
Let's create a "copy book" mutation for instance. This mutation doesn't really fit within the scope of our standard mutations so we'll write it from scratch.
Once again, do not forget to update your mutation type:
And now copy your book!
I have come to really like GraphQL mutations because I find them simple and consistent. One defines the input, output, authorization and business logic and you're done. There is no real magic - it's very readable.
The metaprogramming we presented above does not do much in the end, but it does save you a lot of boilerplate code when you need to expose a lot of resources - especially if you are working on an existing project.
The key takeaway here? Standard CUD mutations are nice but if you need to do anything specific, just go back to using Mutations::BaseMutation. Bending your standard mutations to fit exotic use cases will just make your code difficult to read.
Mutations are simple and atomic, keep them that way.
Keypup's SaaS solution allows engineering teams and all software development stakeholders to gain a better understanding of their engineering efforts by combining real-time insights from their development and project management platforms. The solution integrates multiple data sources into a unified database along with a user-friendly dashboard and insights builder interface. Keypup users can customize tried-and-true templates or create their own reports, insights and dashboards to get a full picture of their development operations at a glance, tailored to their specific needs.
---
Code snippets hosted with ❤ by GitHub