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.
Other parts:
- GraphQL on Rails: From zero to the first query
- GraphQL on Rails: Updating the data
- GraphQL on Rails: On the way to perfection
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.
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
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?
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
}
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:
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
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>
// ...
}
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
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
}
}
}
Implementing real-time updates
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:
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
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 toactionCableLink
.
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;
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!