If you’re interested in translating or adapting this post, please email us first .

A hitchhiker’s guide to developing GraphQL applications with Rails on the backend and React/Apollo on the frontend. The third and final part of this tutorial is all about real-time updates, as well as about DRY-ing up our code and implementing better error handling.

In the previous parts of this tutorial we have built the prototype of a Martian Library application: a user can dynamically manage a list of artifacts related to the Red Planet in a modern SPA-fashion. It’s not quite the time to sit back and relax though, because we still have some refactoring to do.

Check out Part 1 and Part 2 of this guide.

If you have been coding along for the past two parts—feel free to keep using your code, if not—pull it from this repo.

All you need is DRY

Don’t forget to run yarn && bundle install if you decide to start from scratch.

Let’s start with the backend and DRY-up our items’ mutations ( AddItemMutation and UpdateItemMutation ) a bit. We have some duplicated code that verifies whether a user is logged in:

# app/graphql/mutations/add_item_mutation.rb module Mutations class AddItemMutation < Mutations :: BaseMutation # ... def resolve if context [ :current_user ]. nil? raise GraphQL :: ExecutionError , "You need to authenticate to perform this action" end save_item end end end

Let’s move it into the BaseMutation class:

# app/graphql/mutations/base_mutation.rb module Mutations class BaseMutation < GraphQL :: Schema :: Mutation def check_authentication! return if context [ :current_user ] raise GraphQL :: ExecutionError , "You need to authenticate to perform this action" end end end

After this change, you can replace the above snippet in the AddItemMutation and UpdateItemMutation with the check_authentication! call. That is just one example of how we can use the BaseMutation . In a real-world application it can contain many useful helpers for repetitive tasks.

Now, let’s take a look at our frontend code. What kind of duplication do we have here?

Frontend duplication

Splitting your queries into fragments is more art than science. Wise use of fragments can greatly improve your code! Try to use them as often as possible, but don’t put all the fields into one fragment. That leads to overfetching!

These queries look very similar: the fields we select in Item queries are almost the same. How can we avoid repetition?

Luckily, GraphQL has its own “variables” called fragments. A fragment is a named set of fields on a specific type.

Time to create our first fragment:

$ mkdir -p app/javascript/fragments && touch app/javascript/fragments/Item.graphql

Put all the repeating fields into it:

fragment ItemFragment on Item { id title imageUrl description }

Note that named import ItemFragment from '../../fragments/Item.graphql' will not work here

Now we need to add fragments to all operations in AddItemForm , UpdateItemForm and Library . For instance, this is how the query should look in the Library component:

# app / javascript / components / Library / operations . graphql # import ' ../../fragments/Item.graphql ' query LibraryQuery { items { ... ItemFragment user { id email } } }

Dealing with errors

As we know, GraphQL always responds with 200 OK, if the action has not caused the server-side error. Two types of errors usually happen: user input-related errors (validations) and exceptions.

Validation errors can appear only in mutations and they are included in the data that is sent back. They are meant to provide useful feedback to the user and could be displayed in the UI.

Exceptions could happen in any query and signal that something went wrong with the query: for example, authentication/authorization issues, unprocessable input data, etc. (see below). The client must “fail hard” if a response contains an exception (e.g., show an error screen).

What can we do with errors from the frontend perspective?

First, we can set up an error logger to quickly detect and fix errors (we already configured it in the first part of this guide).

Second, it would be a good idea to wrap components in error boundaries and show error screens with sad developer faces when something goes wrong.

Third, we should try to avoid common mistakes by looking at the documentation. Beware of dots and handle nullable fields properly! Look up the me query in your GraphiQL docs:

GraphiQL auto-generated documentation

Libraries like ramda and lodash provide special functions that can retrieve the value from the object at a given path even if an object or a path do not exist anymore. However, don’t use it in all cases—it might result in unpredictable scenarios. Also, you will lose logs (no errors, no logs). Use it only if you need to grab values from a set of nullable fields.

According to the documentation, me is a nullable field. We cannot use an expression like me.email , for example. We need to make sure that the user exists.

Finally, we should process GraphQL errors inside the render prop function. We will show you how to do this soon.

When the user submits the invalid data, our backend returns a list of error messages as strings. Let’s change the way we resolve errors: we will return an object, containing the same list of error messages, but also some JSON-encoded details. Details could be used to generate messages client-side or provide additional feedback to users (e.g., highlight the invalid form field).

First of all, let’s define a new ValidationErrorsType :

# app/graphql/types/validation_errors_type.rb module Types class ValidationErrorsType < Types :: BaseObject field :details , String , null: false field :full_messages , [ String ], null: false def details object . details . to_json end end end

Now, we need to change our AddItemMutation to use the new type we defined (please do the same thing for the UpdateItemMutation ):

# app/graphql/mutations/add_item_mutation.rb module Mutations class AddItemMutation < Mutations :: BaseMutation argument :title , String , required: true argument :description , String , required: false argument :image_url , String , required: false field :item , Types :: ItemType , null: true field :errors , Types :: ValidationErrorsType , null: true # this line has changed def resolve ( title :, description: nil , image_url: nil ) check_authentication! item = Item . new ( title: title , description: description , image_url: image_url , user: context [ :current_user ] ) if item . save { item: item } else { errors: item . errors } # change here end end end end

Finally, let’s add a couple of validations to the Item model:

# app/models/item.rb class Item < ApplicationRecord belongs_to :user validates :title , presence: true validates :description , length: { minimum: 10 }, allow_blank: true end

For this example, we will use only fullMessages of ValidationErrorType . details can be used in production to construct more sophisticated error messages.

Now we need to use these validations in our interface. We should update logic for AddItemForm and UpdateItemForm . We will show you how to do it for AddItemForm . The code for UpdateItemForm we will leave as an exercise for the reader (you can check the solution here).

Let’s add an errors field to operations.graphql first:

# / app / javascript / components / AddItemForm / operations . graphql # import ' ../../fragments/Item.graphql ' mutation AddItemMutation ( $title : String ! $description : String $imageUrl : String ) { addItem ( title : $title , description : $description , imageUrl : $imageUrl ) { item { ... ItemFragment user { id email } } errors { # new field fullMessages } } }

Now we need to make a minor change in AddItemForm and its parent ProcessItemForm to add a new element for errors:

We are adding a new errors property to ProcessItemForm and a new element to show errors.

// app/javascript/components/ProcessItemForm/index.js const ProcessItemForm = ({ // ... errors , }) => { // ... return ( < div className = { cs . form } > { errors && ( < div className = { cs . errors } > < div className = " error " > { errors . fullMessages . join ( ' ; ' )} < /div > < /div > )} { /* ... */ } < /div > ); }; export default ProcessItemForm ;

When working with the Mutation component, we are grabbing errors from the data property:

// app/javascript/components/AddItemForm/index.js // ... < Mutation mutation = { AddItemMutation } > {( addItem , { loading , data }) => ( // getting data from response < ProcessItemForm buttonText = " Add Item " loading = { loading } errors = { data && data . addItem . errors } / > // ... ) } < /Mutation >

If you want to make your errors appear a little bit nicer, add the following styles to

/app/javascript/components/ProcessItemForm/styles.module.css :

.form { position : relative ; } .errors { position : absolute ; top : -20px ; color : #ff5845 ; }

Now, let’s talk about the second type of GraphQL errors: exceptions. In the previous part, we have implemented the authentication, but we did not implement a way to handle a user with a non-existent email. It is not the expected behavior, so let’s make sure to raise an exception:

# app/graphql/mutations/sign_in_mutation.rb module Mutations class SignInMutation < Mutations :: BaseMutation argument :email , String , required: true field :token , String , null: true field :user , Types :: UserType , null: true def resolve ( email :) user = User . find_by! ( email: email ) token = Base64 . encode64 ( user . email ) { token: token , user: user } rescue ActiveRecord :: RecordNotFound raise GraphQL :: ExecutionError , "user not found" end end end

We need to change our frontend code to handle this situation gracefully. Let’s do it for the UserInfo component. Grab the error parameter from the object provided by render prop function for the Mutation component:

// app/javascript/components/UserInfo/index.js const UserInfo = () => { // ... {( signIn , { loading : authenticating , error /* new key */ }) => { }} // ... }

And add an element that displays an error just before the closing </form> tag:

// app/javascript/components/UserInfo/index.js const UserInfo = () => { < form > // ... { error && < span > { error . message } < /span> } < /form > // ... }

Apollo client provides many interesting features for handling errors. For example, you can ignore all gql errors or add a special logic on how to store your data in the cache when an error occurs. See here for more details.

Handling input data

Let’s come back to AddItemMutation and UpdateItemMutation mutations again. Take a look at the argument list and ask yourself, why do I have two almost identical lists? Every time we add a new field to the Item model we would need to add a new argument twice, and that is not good.

The solution is fairly simple: let’s use a single argument containing all the fields we need. graphql-ruby comes with a special primitive called BaseInputObject , which is designed to define a type for arguments like this. Let’s create a file named item_attributes.rb :

# app/graphql/types/item_attributes.rb module Types class ItemAttributes < Types :: BaseInputObject description "Attributes for creating or updating an item" argument :title , String , required: true argument :description , String , required: false argument :image_url , String , required: false end end

CQRS stands for “Command Query Responsibility Segregation”

This looks a lot like the types we have created before, but with a different base class and argument s instead of fields. Why is that? GraphQL follows CQRS principle and comes up with two different models for working with data: read model (type) and write model (input).

Heads up: you cannot use complex types as the argument type—it can only be a scalar type or another input type!

Now we can change our mutations to use our handy argument. Let’s start with AddItemMutation :

# app/graphql/mutations/add_item_mutation.rb module Mutations class AddItemMutation < Mutations :: BaseMutation argument :attributes , Types :: ItemAttributes , required: true # new argument field :item , Types :: ItemType , null: true field :errors , Types :: ValidationErrorsType , null: true # <= change here # signature change def resolve ( attributes :) check_authentication! item = Item . new ( attributes . to_h . merge ( user: context [ :current_user ])) # change here if item . save { item: item } else { errors: item . errors } end end end end

As you can see, we have replaced a list of arguments with a single argument named attributes , changed #resolve signature to accept it, and slightly changed the way we create the item. Please make the same changes in UpdateItemMutation . Now we need to change our frontend code to work with these changes.

The only thing we need to do is to add one word and two brackets to our mutation (the same change should be done for UpdateItem ):

# / app / javascript / components / AddItemForm / operations . graphql # import ' ../../fragments/Item.graphql ' mutation AddItemMutation ( $title : String ! $description : String $imageUrl : String ) { addItem ( attributes : { # just changing the shape title : $title description : $description imageUrl : $imageUrl } ) { item { ... ItemFragment user { id email } } errors { fullMessages } } }

Server-initiated updates are common in modern applications: in our case, it might be helpful for our user to have the list updated when someone adds a new or changes an existing item. This is exactly what GraphQL subscriptions are for!

Subscription is a mechanism for delivering server-initiated updates to the client. Each update returns the data of a specific type: for instance, we could add a subscription to notify the client when a new item is added. When we send Subscription operation to the server, it gives us an Event Stream back. You can use anything, including post pigeons, to transport events, but Websockets are especially suitable for that. For our Rails application, it means we can use ActionCable for transport. Here is what a typical GraphQL subscription looks like:

Subscriptions example

Laying the cable

First, we should create app/graphql/types/subscription_type.rb and register the subscription, which is going to be triggered when the new item is added.

# app/graphql/types/subscription_type.rb module Types class SubscriptionType < GraphQL :: Schema :: Object field :item_added , Types :: ItemType , null: false , description: "An item was added" def item_added ; end end end

Second, we should configure our schema to use ActionCableSubscriptions and look for the available subscriptions in the SubscriptionType :

# app/graphql/martian_library_schema.rb class MartianLibrarySchema < GraphQL :: Schema use GraphQL :: Subscriptions :: ActionCableSubscriptions mutation ( Types :: MutationType ) query ( Types :: QueryType ) subscription ( Types :: SubscriptionType ) end

Third, we should generate an ActionCable channel for handling subscribed clients:

$ rails generate channel GraphqlChannel

Let’s borrow the implementation of the channel from the docs:

# app/channels/graphql_channel.rb class GraphqlChannel < ApplicationCable :: Channel def subscribed @subscription_ids = [] end def execute ( data ) result = execute_query ( data ) payload = { result: result . subscription? ? { data: nil } : result . to_h , more: result . subscription? } @subscription_ids << context [ :subscription_id ] if result . context [ :subscription_id ] transmit ( payload ) end def unsubscribed @subscription_ids . each do | sid | MartianLibrarySchema . subscriptions . delete_subscription ( sid ) end end private def execute_query ( data ) MartianLibrarySchema . execute ( query: data [ "query" ], context: context , variables: data [ "variables" ], operation_name: data [ "operationName" ] ) end def context { current_user_id: current_user & . id , current_user: current_user , channel: self } end end

Make sure to pass :channel to the context. Also, we pass current_user to make it available inside our resolvers, as well as :current_user_id , which can be used for passing scoped subscriptions.

Now we need to add a way to fetch current user in our channel. Change ApplicationCable::Connection in the following way:

# app/channels/application_cable/connection.rb module ApplicationCable class Connection < ActionCable :: Connection :: Base identified_by :current_user def connect self . current_user = current_user end private def current_user token = request . params [ :token ]. to_s email = Base64 . decode64 ( token ) User . find_by ( email: email ) end end end

Triggering the event is fairly simple: we should pass the camel-cased field name as the first argument, options as the second argument, and the root object of the subscription update as the third argument. Add it to the AddItemMutation :

# app/graphql/mutations/add_item_mutation.rb module Mutations class AddItemMutation < Mutations :: BaseMutation argument :attributes , Types :: ItemAttributes , required: true field :item , Types :: ItemType , null: true field :errors , [ String ], null: false def resolve ( attributes :) check_authentication! item = Item . new ( attributes . merge ( user: context [ :current_user ])) if item . save MartianLibrarySchema . subscriptions . trigger ( "itemAdded" , {}, item ) { item: item } else { errors: item . errors . full_messages } end end end end

Argument hash can contain the arguments, which are defined in the subscription (which will be passed as resolver arguments). There is an optional fourth argument called :scope which allows you to limit the scope of users who will receive the update.

Let’s another subscription, this time for updating our items:

# app/graphql/types/subscription_type.rb module Types class SubscriptionType < GraphQL :: Schema :: Object field :item_added , Types :: ItemType , null: false , description: "An item was added" field :item_updated , Types :: ItemType , null: false , description: "Existing item was updated" def item_added ; end def item_updated ; end end end

This is how we should trigger this kind of update in the UpdateItemMutation :

# app/graphql/mutations/update_item_mutation.rb module Mutations class UpdateItemMutation < Mutations :: BaseMutation argument :id , ID , required: true argument :attributes , Types :: ItemAttributes , required: true field :item , Types :: ItemType , null: true field :errors , [ String ], null: false def resolve ( id :, attributes :) check_authentication! item = Item . find ( id ) if item . update ( attributes . to_h ) MartianLibrarySchema . subscriptions . trigger ( "itemUpdated" , {}, item ) { item: item } else { errors: item . errors . full_messages } end end end end

For AnyCable users this is no longer a problem—the graphql-anycable gem extracted from our work on eBay projects brings performant GraphQL subscriptions to everyone

We should mention that the way subscriptions are implemented in graphql-ruby for ActionCable can be a performance bottleneck: a lot of Redis round-trips, and query re-evaluation for every connected client (see more in this in-depth explanation here).

Plugging in

To teach our application to send data to ActionCable, we need some configuration. First, we need to install some new modules to deal with Subscriptions via ActionCable:

$ yarn add actioncable graphql-ruby-client

Then, we need to add some new magic to /app/javascript/utils/apollo.js

// /app/javascript/utils/apollo.js ... import ActionCable from ' actioncable ' ; import ActionCableLink from ' graphql-ruby-client/subscriptions/ActionCableLink ' ; ... const getCableUrl = () => { const protocol = window . location . protocol === ' https: ' ? ' wss: ' : ' ws: ' ; const host = window . location . hostname ; const port = process . env . CABLE_PORT || ' 3000 ' ; const authToken = localStorage . getItem ( ' mlToken ' ); return ` ${ protocol } // ${ host } : ${ port } /cable?token= ${ authToken } ` ; }; const createActionCableLink = () => { const cable = ActionCable . createConsumer ( getCableUrl ()); return new ActionCableLink ({ cable }); }; const hasSubscriptionOperation = ({ query : { definitions } }) => definitions . some ( ({ kind , operation }) => kind === ' OperationDefinition ' && operation === ' subscription ' ); //.. // we need to update our link link : ApolloLink . from ([ createErrorLink (), createLinkWithToken (), ApolloLink . split ( hasSubscriptionOperation , createActionCableLink (), createHttpLink (), ), ]), //..

Despite the fact that the code looks a bit scary, the idea is simple:

we create a new Apollo link for subscriptions inside createActionCableLink ;

; we decide where to send our data inside ApolloLink.split;

if hasSubscriptionOperation returns true, the operation will be sent to actionCableLink .

Now we need to create a new component by using our generator:

$ npx @hellsquirrel/create-gql-component create /app/javascript/components/Subscription

Let’s add our subscription to operations.graphql :

# / app / javascript / components / Subscription / operations . graphql # import ' ../../fragments/Item.graphql ' subscription ItemSubscription { itemAdded { ... ItemFragment user { id email } } itemUpdated { ... ItemFragment user { id email } } }

Nothing new, right? Let’s create the Subscription component:

// /app/javascript/components/Subscription/index.js import React , { useEffect } from ' react ' ; import { ItemSubscription } from ' ./operations.graphql ' ; const Subscription = ({ subscribeToMore }) => { useEffect (() => { return subscribeToMore ({ document : ItemSubscription , updateQuery : ( prev , { subscriptionData }) => { if ( ! subscriptionData . data ) return prev ; const { itemAdded , itemUpdated } = subscriptionData . data ; if ( itemAdded ) { const alreadyInList = prev . items . find ( e => e . id === itemAdded . id ); if ( alreadyInList ) { return prev ; } return { ... prev , items : prev . items . concat ([ itemAdded ]) }; } if ( itemUpdated ) { return { ... prev , items : prev . items . map ( el => el . id === itemUpdated . id ? { ... el , ... itemUpdated } : el ), }; } return prev ; }, }); }, []); return null ; }; export default Subscription ;

If you need a simpler subscription component, react-apollo provides its own version. This version is perfect for notification bars.

One more hook! Now it’s useEffect . It’s called on initial render and reruns whenever the user changes.

We are asking our hook to subscribe to add and update event streams. We are adding or updating items when the corresponding event is fired.

The last step is to add Subscription component to Library at the end of the last div inside Query component:

import Subscription from ' ../Subscription ' ; //... const Library = () => { const [ item , setItem ] = useState ( null ); return ( < Query query = { LibraryQuery } > {({ data , loading , subscribeToMore /* we need subscribe to more arg */ }) => ( < div > // ... < Subscription subscribeToMore = { subscribeToMore } / > < /div > )} < /Query > ); }; //...

The Query component from the react-apollo library provides the special function subscribeToMore which is used by the Subscription component. We are passing this function to our Subscription component.

Now we are ready to test our subscriptions! Try to add a new item or change the existing one in a different browser tab—you should see the changes appearing in all the tabs you have open.

Congratulations! 🥳

It is the end of our exciting and adventurous journey through Ruby-GraphQL-Apollo world. Using our small application as an example, we practiced all the basic techniques, highlight common problems, and introduce some advanced topics.

This might have been a challenging exercise, but we are certain you will benefit from it in the future. In any case, you now have enough theory and practice to create your own Rails applications that leverage the power of GraphQL!

Part 1 | Part 2 | Part 3