Skip to content

Instantly share code, notes, and snippets.

@crashtech
Last active September 22, 2024 21:59
Show Gist options
  • Save crashtech/284eff2f73ee5b395a97ff7dc4fd61dd to your computer and use it in GitHub Desktop.
Save crashtech/284eff2f73ee5b395a97ff7dc4fd61dd to your computer and use it in GitHub Desktop.
RoR, from MVC to GraphQL

Ruby on Rails, from MVC to GraphQL

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 MVC 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: MVC Architecture

Changing to a RESTful API

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.

Entering JavaScript

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: API-mode Architecture 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 as a solution

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). GraphQL Architecture 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.

The GraphQL schema

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:

  1. types: Contains all the types of exchangeable objects (inputs and outputs);
  2. queryType: Contains all the root fields that can be queried (so it replaces our GET routes);
  3. 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.

The queryType part

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 mutationType part

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.

The types part

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 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 or has_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).

The final version

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 on author_id, we shouldn't mark it as nullable because we can create an author using the authorAttributes 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.

Querying our schema

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.

ActiveRecord point-of-view

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: From ActiveRecord to GraphQL 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.

Implementation sample

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

Summary

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.

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment