As a Ruby on Rails developer, I'm always looking for the best tools and practices to apply to my day-to-day development. This framework has a fantastic structure where all its inner parts communicate very well. But, as applications start growing, or developers want to start using more modern technologies, several things start getting out of control.
Recent versions of Rails implemented Webpack, facilitating the use of modern JavaScript frameworks, which added another level of robustness to everyone's applications. Not only is this becoming the new standard, but it also enables bigger code-bases without falling into madness.
Facebook, as the creator of ReactJS (one of these modern JS frameworks and the most popular one) introduced to the world another of its technologies, GraphQL. This tool, like any other tool, can be a great asset, as long as it's in the right hands, meaning that you first need to know what role it plays before exploring how to use it in your RoR application.
From my experience, developers have been using this solution in the wrong way. Not because of the choice of gem or technologies associated with it, but because it is conceptually done in the wrong way. The intention here then it's to review and understand the concepts involved in using GraphQL on a Ruby on Rails application.
The Rails core provides us a fantastic beginning on MVC applications, which is the scaffold
generator. Although rarely used in most real-world applications, it still fits us very well because it's the most fundamental building block of any application.
Let's start this review by running the following scaffold
command and investigating some of the files created.
$ rails g scaffold Post title description author:belongs_to likes:integer \
enabled:boolean
~ invoke active_record
~ create db/migrate/XXXXXXXXXXXXXX_create_posts.rb
~ create app/models/post.rb
~ invoke resource_route
~ route resources :posts
~ invoke scaffold_controller
~ create app/controllers/posts_controller.rb
~ invoke erb
~ create app/views/posts
~ create app/views/posts/index.html.erb
~ create app/views/posts/edit.html.erb
~ create app/views/posts/show.html.erb
~ create app/views/posts/new.html.erb
~ create app/views/posts/_form.html.erb
This result may differ from versions and configurations, but the idea here is to focus only on a couple of these files (we are going to skip views for this article):
# config/routes.rb
Sample::Application.routes.draw do
resources :posts
end
# app/models/post.rb
class Post < ActiveRecord::Base
belongs_to :user
end
# app/controllers/posts_controller.rb
class MicropostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :destroy]
# GET /posts
def index
@posts = Post.all
end
# GET /posts/1
def show
end
# GET /posts/new
def new
@post = Post.new
end
# GET /posts/1/edit
def edit
end
# POST /posts
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: 'Post was successfully created.'
else
render :new
end
end
# PATCH/PUT /posts/1
def update
if @post.update(post_params)
redirect_to @post, notice: 'Post was successfully updated.'
else
render :edit
end
end
def destroy
@post.destroy
redirect_to posts_path, notice: 'Post was successfully destroyed.'
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :description, :author_id,
:likes, :enabled)
end
end
One other thing to observe is the routes created by resources :posts
:
$ rails routes
~ Prefix Verb URI Pattern Controller#Action
~ posts GET /posts(.:format) posts#index
~ POST /posts(.:format) posts#create
~ new_post GET /posts/new(.:format) posts#new
~ edit_post GET /posts/:id/edit(.:format) posts#edit
~ post GET /posts/:id(.:format) posts#show
~ PATCH /posts/:id(.:format) posts#update
~ PUT /posts/:id(.:format) posts#update
~ DELETE /posts/:id(.:format) posts#destroy
With all these pieces in place, a user is now allowed to do all the CRUD operations for Posts records.
A sample of some of these possible operations that a user can perform under this setup can be represented as the following:
The first step to understanding GraphQL role is to move away from standard applications to an API-like application. When we do that, it means that we stop handling the frontend and focus exclusively on the backend. Again, Rails vanilla implementation already allows us to do this change very easily, by activating its API mode.
# config/application.rb
module Sample
class Application < Rails::Application
config.api_only = true
When we do this and run the same scaffold again, a couple of things change and the result is the perfect starting point that we need.
# config/routes.rb still the same
# app/models/post.rb still the same
# app/controllers/posts_controller.rb
class MicropostsController < ApplicationController
before_action :set_post, only: [:show, :update, :destroy]
# GET /posts
def index
@posts = Post.all
render json: @posts
end
# GET /posts/1
def show
render json: @post
end
# POST /posts
def create
@post = Post.new(post_params)
if @post.save
render json: @post, status: :created, location: @post
else
render json: @post, status: :unprocessable_entity
end
end
# PATCH/PUT /posts/1
def update
if @post.update(post_params)
render json: @post
else
render json: @post, status: :unprocessable_entity
end
end
def destroy
@post.destroy
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :description, :author_id,
:likes, :enabled)
end
end
$ rails routes
~ Prefix Verb URI Pattern Controller#Action
~ posts GET /posts(.:format) posts#index
~ POST /posts(.:format) posts#create
~ post GET /posts/:id(.:format) posts#show
~ PATCH /posts/:id(.:format) posts#update
~ PUT /posts/:id(.:format) posts#update
~ DELETE /posts/:id(.:format) posts#destroy
These changes mean that, from now on, Rails will only answer requests with a JSON response of whatever the operation was. It also reflects a difference in how applications are designed because now we need something to connect the user's browser with the API and display the information nicely.
Now that we have isolated our backend by setting it as API only, we have two actors that need to interact with each other to display the information nicely formatted back to users. In the non-API mode this interaction still existed, but since the view and the controller layers were under the same framework and language, the way they exchanged information was much more straightforward (i.e., by setting @posts
on the index action, you could access it on the view and iterate over it).
We now have a backend that's responsible for performing and returning data for us, and a frontend responsible for requesting these operations so then it can be displayed. The most basic implementation of this can be done by having a simple HTML file with a couple of JavaScript functions to perform the requests (the AJAX way).
Another common approach is to use modern frontend Frameworks like Vue, React, and Angular to produce these pages. Nevertheless, the idea is still the same and can be represented as below: This approach has several advantages, including better separation of responsibilities, simplified testing, asynchronous operations, and better performance. Those are the main reasons why this approach has been growing in the past few years, which is also why people started developing other solutions to ease the implementation of this architecture and seize all its power.
However, this model leaves a considerable hole, its worst enemy, and a terrible drawback. How do we communicate something from the frontend to the backend? And how do we interpret and use its response?
This exchange of information is a challenge that developers have been trying to solve for quite some time since it can make all the advantages of choosing this structure disappear.
GraphQL is one of the solutions to this problem. In general terms, it defines ways that the frontend can query or ask the backend to do things while also specifying what it expects as an exact response.
From our previous example, the backend was the one defining the result of the operations. Although this works in most of the cases, it has a couple of problems, like it can send way more information that the frontend needs (i.e., If we want just a list of titles of all posts, we don't need the other fields), or it can also send not enough information that we want to display (i.e., not sending the authors' data).
It's worth reading the overview from the GraphQL spec if you are not too familiar with its principles and proposals. What's important for us now is how it changes our model. We will be focusing on its server side since it works with almost anything at the client side (even AJAX). This way, it's easy to see that under a Rails application, the role of GraphQL is to replace the Controller layer. It also replaces the pre-controller layer (routing layer), since the endpoint will be the same and what will change is the query we will be requesting.
Now, we will be deconstructing our RESTful architecture and build a GraphQL one, so here we have the two things that need to be replaced: the routes and the controller actions.
A GraphQL Schema represents everything that a GraphQL server can perform, receive, and return. Its parallel would be all the routes, controllers, actions, and JSON responses that an API application has.
Under a schema, we have the essential elements that we will be implementing and translating from our RESTful example. They are:
types
: Contains all the types of exchangeable objects (inputs and outputs);queryType
: Contains all the root fields that can be queried (so it replaces our GET routes);mutationType
: Contains all the root operations that can be performed (so it replaces our POST/PUT/PATCH/DELETE routes);
Although GraphQL schemas support other things like subscriptionType and directives, we won't be needing those to replace during this translation process.
The main idea here is to explore how we can represent our RESTful implementation using GraphQL. At the end of this article, we will have a sample of how that can be done using Rails. Nevertheless, any GraphQL implementation must be able to achieve the same result proposed here.
The term introspection will be used to describe the GraphQL representation of our server, similar to
rails routes
which is an introspection of all the routes in a rails application.
Let's focus on our GET-based routes first. Their purpose is to return the list of all the Posts (index
) and to return a single Post based on its id (show
). Since they are not considered "operations" that manipulate records, they live under the queryType
.
schema {
types: []
queryType: {
posts: [Post!]!
post(id: ID!): Post!
}
mutationType: {}
}
The translation here is pretty simple: each GET-based action becomes a field on queryType
. The plural form returns the Posts list, while the singular form returns a single Post. We also translated the :id
argument into an (id: ID!)
field argument. They both return a Post
-type object, one is an array of not null elements, while the other is a not null object.
The not null here is very important, since Post.find
will raise an exception if it doesn't find the record, and Post.all
will always return an array and never containing a nil
element within its result; hence the both not null marks.
The remaining routes are the ones that manipulate records, which means that they mutate the data before returning what the frontend wants. In the same way as our controller, we will be manipulating only a single record either by creating a new one, updating an existing one, or destroying an existing one.
schema {
types: []
queryType: {
posts: [Post!]
post(id: ID!): Post!
}
mutationType: {
createPost(post: PostParams!): Post!
updatePost(id: ID!, post: PostParams!): Post!
deletePost(id: ID!): Boolean
}
}
The same thing happens here; each action from the controller became a field here, we also translated the attributes accordingly, and the result types are pretty much the same not null Post
type.
One small difference is the deletePost
that now has a boolean
type because we must return something from a field. On an API request, on the other hand, we can assume that when the HTTP response was something like 200 Ok
for a DELETE
request, we don't have to look at its body to know that it was done successfully.
We also introduce an essential type in here, which is the PostParams
as a direct representation of the post_params
method. Its purpose is the same, ensure what parameters the caller may provide while mutating a Post record.
This is one of the places where I see most people using GraphQL on Rails in the wrong way. Developers usually forget about Input Objects, which leads them to do something like:
createPost(title: String!, description: String!, authorId: ID!, likes: Int, enabled: Boolean!): Post!
. Even though it works, doing this two times (one for create and another for update) already displays pour reusability and definition of a GraphQL service.
Now comes the fun part, which is defining our input and output types. GraphQL is all about types since it communicates through them, so we have to acknowledge it all the time; otherwise, we would be overdoing something that was supposed to facilitate our lives. They are also the best thing about GraphQL, since well-defined types can share so many things that the frontend developers can almost feel like programming.
schema {
types: [Post, PostParams]
queryType: {
posts: [Post!]
post(id: ID!): Post!
}
mutationType: {
createPost(post: PostParams!): Post!
updatePost(id: ID!, post: PostParams!): Post!
deletePost(id: ID!): Boolean
}
}
The good part here is that we only have to define two new types: Post and PostParams, since any other type is considered default Scalars from any GraphQL implementation. Plus, good implementations of it will also provide other scalars (like Date and DateTime) that are represented as strings and thus accepted by any JavaScript implementation.
Let's then define our types:
type Post {
id: ID!
title: String
description: String
authorId: ID!
author: Author!
likes: Int
enabled: Boolean
createdAt: DateTime!
updatedAt: DateTime!
}
input PostParams {
title: String
description: String
authorId: ID
likes: Int
enabled: Boolean
}
This is also very straighforward, we translate all the keys on permit(:title, :description, :author_id, :likes, :enabled)
to an input type PostParams
object and all the attributes of the Post
model into the Post
type.
We also added another type named Author
. With this addition, we allow callers to access the author information through the Post.author
field. So, this tree behavior is one of the best features and also enlightens the beauty of GraphQL.
We can summarize these 1-to-1 translations as follows:
- Model attributes become scalar fields
- Any field validated in the model as
presence
must be marked as not null (same for inputs)
- Any field validated in the model as
- Any
belongs_to
becomes a field returning a single object of the association (it's not null by default as Rails convention) - Any
has_one
becomes a field returning a single nullable object of the association - Any
has_many
orhas_and_belongs_to_many
becomes a field returning a nullable array with not null objects of the association
Another mistake that I see very often is the overuse or lack of not null outputs. Any null restrictions on the database or the model should be reflected on GraphQL as it is. Ideally, the constraint would be enforced on all sides (database, model, GraphQL, and even on frontend forms).
Let's assume that our Author
and Post
models are defined like the following:
# app/models/author.rb
# id :bigint not null, primary key
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
class Author < ApplicationRecord
has_many :posts
validates :name, presence: true
end
# app/models/post.rb
# id :bigint not null, primary key
# title :string not null
# description :string
# author_id :bigint not null
# likes :integer not null default(0)
# enabled :boolean not null default(true)
# created_at :datetime not null
# updated_at :datetime not null
class Post < ApplicationRecord
belongs_to :author
validates :title, presence: true
validates :likes, presence: true
validates :enabled, inclusion: [true, false] # since presence of 'false' is false
accepts_nested_attributes_for :author # which allows creating the author through a post
end
A corresponding schema would look like:
schema {
types: [Post, PostParams, Author, AuthorParams]
queryType: {
posts: [Post!]
post(id: ID!): Post!
}
mutationType: {
createPost(post: PostAttributes!): Post!
updatePost(id: ID!, post: PostAttributes!): Post!
deletePost(id: ID!): Boolean
}
}
type Post {
id: ID!
title: String
description: String
authorId: ID!
author: Author!
likes: Int!
enabled: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
}
type Author {
id: ID!
name: String!
posts: [Post!]
createdAt: DateTime!
updatedAt: DateTime!
}
input PostParams {
title: String!
description: String
authorId: ID
authorAttributes: AuthorParams
likes: Int! = 0
enabled: Boolean! = true
}
input AuthorParams {
id: ID
name: String!
}
Although the
belongs_to
enforces not null onauthor_id
, we shouldn't mark it as nullable because we can create an author using theauthorAttributes
field, so we can only pass the responsibility to the server/model.Another approach would be to remove this field and accept only the
authorAttributes
, but we wouldn't be able to provide only the id inside of it since it will also try to change the name of the author.
With this GraphQL schema, we have finished, meaning that any implementation that can reproduce this exact introspection is correctly translated from RESTful in its most ideal way. There is plenty to explore from here onwards, like interfaces
, enum
, and unions
. But they are more complex concepts.
Now that we have our schema defined, we could use any means to reach our Rails application, with any implementation of a GraphQL server, and get whatever we want. We could even use CURL as an example:
$ QUERY='{"query": "{ posts { title description author { name } likes } }"'
$ HEADERS='Content-type: application/json'
$ curl -XPOST -H $HEADERS -d $QUERY 'http://localhost:3000/graphql'
Here are some samples of what we can do now using JavaScript and a little bit of jQuery:
const query = `
posts {
title
description
author {
name
}
likes
}
`
jQuery.post('/graphql', { query: query }, function(result) {
console.dir(result.data.posts);
});
This is an example of how we could query all the Post
records while also returning the name of the author associated with them.
const mutation = `
mutation($post: PostParams!) {
createPost(post: $post) {
id
author {
name
}
createdAt
}
}
`
const post = { title: 'New Post', authorId: 1 };
const params = { query: mutation, variables: { post: post } };
jQuery.post('/graphql', params, function(result) {
console.log(result.data.createPost.id);
});
In this example, we are creating a new Post
record associated with the Aurhor
of id = 1
. The result will be the id
of the created record, the author's name, and the date and time when the record was created.
We have successfully translated a Rails API structure to a GraphQL one, and we were also able to see what's its role and where it fits in this architecture. A important thing to acknowledge here is the misconception that Models
are only represented as GraphQL Types
. We can say that assuming GraphQL as a representation of a Model
, not a Controller
, is the wrong approach whatsoever. We can better illustrate the deconstruction of a model in the following way:
Here we can see something that I call ownership of fields and types, where a model Post
owns, and it is responsible for defining and keeping updated, a series of types and fields in a GraphQL schema. Once we implement that, we can avoid reimplementing operations that should be available regardless of where they were defined.
If we decide to allow frontend developers to filter posts only by their enabled
column, they would have to be implemented twice. See the example from our schema:
# Just a subset of the whole schema
schema {
queryType: {
# Here on the root query
posts(enabled: Boolean): [Post!]
}
}
type Author {
# Also here since we could ask for the posts only for a specific author
posts(enabled: Boolean): [Post!]
}
This concept is out of the scope of GraphQL, and it can also escalate to scopes
and sorting
records. I'll be diving deeper into this concept in another article.
This is a little sample using the rails-graphql
gem on how we could define our schema form our examples.
Again, we can achieve the same result with another implementation, as long as you follow the concepts explored in here.
# app/graphql/objects/author_object.rb
class GraphQL::AuthorObject < GraphQL::Object
field :id, :id, null: false
field :name, :string, null: false
field :posts, :post, array: true, nullable: false
field :created_at, :datetime, null: false
field :updated_at, :datetime, null: false
end
# app/graphql/objects/post_object.rb
class GraphQL::PostObject < GraphQL::Object
field :id, :id, null: false
field :title, :string, null: false
field :description, :string
field :author_id, :id, null: false
field :author, :author, null: false
field :likes, :int, null: false
field :enabled, :boolean, null: false
field :created_at, :datetime, null: false
field :updated_at, :datetime, null: false
end
# app/graphql/objects/query_object.rb
class GraphQL::QueryObject < GraphQL::Object::Query
field :posts, :post, array: true, nullable: false
field :post, null: false, arguments: arg(:id, :id, null: false)
end
# app/graphql/inputs/author_params_input.rb
class GraphQL::AuthorParamsInput < GraphQL::Input
field :id, :id
fiedl :name, :string, null: false
end
# app/graphql/inputs/post_params_input.rb
class GraphQL::PostParamsInput < GraphQL::Input
field :title, :string, null: false
field :description, :string
field :author_id, :id
field :author_attributes, :author_params
field :likes, :int, null: false, default: 0
field :enabled, :boolean, null: false, default: true
end
# app/graphql/mutations/post/create_mutation.rb
class GraphQL::Post::CreateMutation < GraphQL::Mutation
rename! 'createPost'
argument :post, :post_params, null: false
returns :post
def perform
Post.create!(args[:post])
end
end
# app/graphql/mutations/post/update_mutation.rb
class GraphQL::Post::UpdateMutation < GraphQL::Mutation
rename! 'updatePost'
id_argument
argument :post, :post_params, null: false
returns :post
def perform
Post.find(args[:id]).update!(args[:post])
end
end
# app/graphql/mutations/post/delete_mutation.rb
class GraphQL::Post::DeleteMutation < GraphQL::Mutation
rename! 'deletePost'
id_argument
returns :boolean
def perform
Post.find(args[:id]).destroy! && true
end
end
# app/graphql/objects/mutation_object.rb
class GraphQL::MutationObject < GraphQL::Object::Mutation
import :create_post
import :update_post
import :delete_post
end
# app/graphql/application_schema.rb
class GraphQL::ApplicationSchema < GraphQL::Schema
end
# config/routes.rb
Sample::Application.routes.draw do
mount GraphQL::ApplicationSchema.engine, at: '/graphql'
end
We were able to achieve our goal to translate a traditional MVC Ruby on Rails application into a Rails + JavaScript + GraphQL application. We explored how we correctly turn the elements from one into another while observing which role each component of either side plays in the application architecture, delivering quality, modernity, and robustness.
Reviewing these concepts should be the first thing done by any developer in charge of starting a GraphQL server on a Rails application. Not only for GraphQL specifically, but any tool that we developers decide to use, we must start from the concept and understand its role.
The concepts explained here should be internalized by everyone developing under similar architectures; GraphQL replaces the Controller and is also responsible for serialization, we should not threat models just as types, we must be aware of input types, we should translate models' attributes and settings to their specific things in the GraphQL server.
Plus, everyone should try to follow all the other conventions of any RoR application:
- Convention over configuration;
- Never write business or complex logic in the view layer (in this case in the serialization layer);
- Take advantage of scopes and filtered associations as much as possible
- Keep things attached to the model (not specifically fat models, but attached to them).
If you and your team understand and apply the things described in this post, your' applications won't fall into the madness territory, and the development done on top of such a solid basis will be very productive and with top-notch quality.
- GraphQL June 2018 Spec - The most recent GraphQL spec definition;
- The Rails Doctrine - Ruby on Rails official commandments;
- Getting up and running quickly with scaffolding - The Rails guide for scaffolding;
- Using Rails for API-only Applications - Rails guide for API-only applications;
- Rails GraphQL gem - A new open-source gem for working with GraphQL on Rails applications (still a work in progress).