

GraphQLon Rails: on the way to perfection
source link: https://www.tuicool.com/articles/qM3UFfJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

A hitchhiker’s guide to developing GraphQL applications with Rails on the back-end and React/Apollo on the front-end. 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 outPart 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 back-end 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 front-end code. What kind of duplication do we have here?
Front-end 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 front-end 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 back-end 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 front-end 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 front-end 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:
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 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;
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
Recommend
-
34
Javascript rants and findings, by kangax
-
26
Enlarge / We're so close to having an emul...
-
5
On the way to perfection — Martian Chronicles, Evil Martians’ team blogA 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...
-
8
The M1 MacBook Air: PerfectionIf it’s not there, it’s very, very close…
-
14
Voici déjà la troisième version du casque sans fil à réduction de bruit active de Sony. Deux ans après un MDR-1000X original déjà excellent, et un an apr...
-
7
‘Diablo II: Resurrected’ won’t reinvent the wheel when the circle was already perfect(Blizzard Entertainment)By Gene Par...
-
9
@wolffJosh WolffAI, blockchain, bioengineeringThis article was written entirely by GPT-J. It is published here unedited. You can try GPT-J
-
6
Free your mind... again — Matrix: Resurrections trailer is sheer perfection, both fresh and familiar "After all these years, to be going back to where it all started. Back to the...
-
1
Oh Dear is the all-in-one monitoring tool for your entire website. We monitor uptime, SSL certificates, broken links, scheduled tasks and more. You'll get a notifications for us when something's wr...
-
9
Oh Dear is the all-in-one monitoring tool for your entire website. We monitor uptime, SSL certificates, broken links, scheduled tasks and more. You'll get a notifications for us when something's wr...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK