DEV Community

Cover image for Intro to GraphqlRails
Povilas Jurčys
Povilas Jurčys

Posted on • Edited on

Intro to GraphqlRails

Introduction

In the ever-evolving landscape of software development, GraphQL has emerged as a powerful tool for crafting flexible and efficient APIs. As Ruby on Rails enthusiasts, we're no strangers to dynamic and elegant nature of Rails applications. Yet, with the rise of GraphQL, new possibilities beckon, challenging us to explore innovative ways of integrating this technology into our Rails projects.

Imagine a world where building robust, secure, and maintainable GraphQL APIs seamlessly aligns with the Rails ecosystem you've come to know and love. Enter GraphqlRails, a gem that transcends the boundaries of conventional development paradigms. It's been three years since the release of GraphqlRails 1.0.0. In that time, it has grown from being a mere extension to the established graphql-ruby library to becoming a transformative framework that redefines GraphQL development within the Rails context.

In this journey of discovery, we'll delve into the unique philosophy of GraphqlRails, one that doesn't just view GraphQL as an isolated entity, but rather, seamlessly intertwines it with the familiar entities of models, controllers, and routes that form the backbone of every Ruby on Rails project. The question that arises naturally is, why opt for GraphqlRails over the well-established graphql-ruby? Strap in as we embark on an exploration of the reasons that make this transition not just worthwhile, but game-changing.

Not just extension for graphql-ruby

In our exploration of GraphqlRails, it's important to grasp that this gem extends far beyond the scope of being a simple extension for graphql-ruby. Initially, GraphqlRails emerged as a compilation of tools geared towards GraphQL-related tasks. Over time, however, it has grown in diverse directions, gradually transforming into a full-fledged framework that stands on its own.

One of the most compelling distinctions that sets GraphqlRails apart from graphql-ruby is its underlying philosophy. In graphql-ruby, GraphQL is treated as a distinct entity with its separate unique ecosystem including it's special directories, types and schemas. On the other hand, GraphqlRails takes a fundamentally different viewpoint. Here, GraphQL is seamlessly integrated into your existing Rails application, harmonizing with the familiar entities of models, controllers, and routes that are ubiquitous in every Ruby on Rails project.

But enough talking. For me, it's always been easier represent my ideas in ruby then in english.

Getting hands dirty

Let's start with graphql-ruby style minimal app:

# app/models/post.rb
class Post < ActiveRecord::Base
  has_many :comments
end

# app/models/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :post
end

# app/graphql/types/post_type.rb
module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :title, String, null: false
    field :body, String, null: false
    field :comments, [Types::CommentType]
  end
end

# app/graphql/types/comment_type.rb
module Types
  class CommentType < Types::BaseObject
    field :id, ID, null: false
    field :post, PostType, null: false
  end
end

# app/graphql/types/query_type.rb
class QueryType < GraphQL::Schema::Object
  field :post, PostType, "Find a post by ID" do
    argument :id, ID
  end

  # Then provide an implementation:
  def post(id:)
    Post.find(id)
  end
end

# graphql/schema.rb
class Schema < GraphQL::Schema
  query QueryType
end
Enter fullscreen mode Exit fullscreen mode

Now let's write same minimal app in GraphqlRails style:

# app/models/post.rb
class Post < ActiveRecord::Base
  include GraphqlRails::Model

  has_many :comments

  graphql.attribute(:id).type('ID!')
  graphql.attribute(:title).type('String!')
  graphql.attribute(:body).type('String!')
  graphql.attribute(:comments).type('[Comment]')
end

# app/models/comment.rb
class Comment < ActiveRecord::Base
  include GraphqlRails::Model

  belongs_to :post

  graphql.attribute(:id).type('ID!')
  graphql.attribute(:post).type('Post')
end

# app/controllers/graphql/posts_controller.rb
class PostsController < GraphqlRails::Controller
  action(:show).returns('Post')
  def show
    Post.find(id)
  end
end

# graphql/graphql_router.rb
GraphqlRouter = GraphqlRails::Router.draw do
  resources :posts, only: :show
end
Enter fullscreen mode Exit fullscreen mode

As we observe, the end result between the two styles would be almost identical. As projects grow larger, you'll find GraphqlRails becoming more extensible. Just like with Ruby on Rails controllers and models, you'll begin modularizing code through extraction into modules or inheritance. This flexibility is a distinct advantage over the graphql-ruby architecture.

Attaching graphql types to models

At first glance, graphql-ruby's separation of types into distinct classes offers a remarkable degree of flexibility. The concept is simple: return an object with methods matching your GraphQL type or even a hash with keys corresponding to the type's structure. However, this flexibility comes at a significant cost. As types become detached from their models, ensuring robust security, privacy, and system-wide consistency becomes increasingly challenging.

A real-world example from my current workplace, SameSystem, highlights this challenge. We rely on a sophisticated privacy rules with custom permissions for each user, such as emails:read or own_data:read. So for instance, when user is lacking the emails:read permission, API should always return "email": "null". GraphQL's inherent support for deeply nested data demands a careful consideration of permissions at all levels.

To solve this problem, we could wrap our unprotected user with class called ProtectedUser which looks somewhat like this:

To address this issue, we've created a wrapper called ProtectedUser, illustrated below:

class ProtectedUser
  include GraphlqRails::Model

  graphql.name('User')
  graphql.attribute(:email).type('String')

  def initialize(user, viewer)
    @user = user
    @viewer = viewer
  end

  def email
    return nil unless @viewer.permitted?('emails:read', @user)

    @user.email
  end
end
Enter fullscreen mode Exit fullscreen mode

Side note: you are seeing a bit tailored version of policy handling. In practice we use resource_policy gem which handles all the protection logic out of the box. I wrote dedicated article called Protect your GraphQL data with resource_policy on this topic.

GraphqlRails keeps graphql types pure, meaning that they do not change output by their own. The transformation occurs within your model layer.

If your graphql types are not bound to your models, it's very common mistake to return unprotected User instance instead of ProtectedUser and accidentally break the privacy.

When you need to write a controller, there is no hidden magic too:

class Graphql::UsersController < GraphqlRails::Controller
  action(:show).permit(id: 'ID!').returns('ProtectedUser!')
  def show
    user = User.find(params[:id])
    ProtectedUser.new(user, current_user)
  end
end
Enter fullscreen mode Exit fullscreen mode

As you can see, you are expected to return instance of the same type as it was written in the controller's action configuration. This design makes it straightforward to validate output and ensure the controller's action adheres to expectations.

Testing

Testing is my personal obsession. I have done extensive research on graphql-ruby types testing and almost everyone suggests only integration-level specs, rather than unit testing. graphql-ruby itself has a statement about unit it:

By testing these (and other) application-level behaviors without GraphQL, you can reduce the overhead of your test suite and simplify your testing scenarios.

To rephrase it: so you wrote some custom logic in your type and want to make sure it works as expected? Well, you can't!.

Now, here's where GraphqlRails shines. In GraphqlRails, the model and type are essentially one and the same. When you test the model, you inherently test the type. Unlike graphql-ruby, GraphqlRails style types does not have any hidden logic that you should be worried about. Let's break down how you can conduct unit testing for your graphql type/model:

describe ProtectedUser do
  let(:protected_user) { described_class.new(user, viewer) }
  let(:user) { create(:user) }
  let(:viewer) { create(:user) }

  describe '#email' do
    subject(:email) { protected_user.email }

    context 'without "emails:read" permission' do
      it { is_expected.to be_nil }
    end

    context 'with "emails:read" permission' do
      before { viewer.add_permission('emails:read', user) }
      it { is_expected.to eq(user.email) }
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

With our type thoroughly tested, we can move on to creating straightforward controller tests to ensure our outputs align with expectations:

describe Graphql::UsersController, graphql_controller: true do
  describe '#show' do
    let(:request) do 
      query(:show, params: params, graphql_context: gql_context)
    end
    let(:params) { { id: user.id } }
    let(:gql_context) { { viewer: viewer } }
    let(:user) { create(:user) }
    let(:viewer) { create(:user) }   

    it 'returns ProtectedUser' do
      expect(request.result).to be_a(ProtectedUser)
    end 
  end
end
Enter fullscreen mode Exit fullscreen mode

This approach effectively lifts the veil off GraphQL as a black box, enabling testing beyond integration scenarios. GraphqlRails provides a familiar controller-model interplay that can be tested step by step using unit testing techniques.

Decorators

In various scenarios, you might not want to blend your GraphQL type definitions directly into your Ruby on Rails models. You might want slightly different logic for your GraphQL responses. This is where GraphqlRails::Decorator comes to the rescue. Under the hood, it functions same as GraphqlRails::Model, but it's better suited for situations involving paginated responses. What makes it particularly useful is the introduction of the .decorate method, which simplifies the process of decorating a collection.

Let's illustrate the significance of GraphqlRails decorators by creating a paginated users controller's index action:

Let's begin by examining the index action without using a decorator:

class Graphql::UsersController < GraphqlApplicationController
  action(:index).paginated.returns('[ProtectedUser!]!')
  def index
    User.all.map do |user| 
      ProtectedUser.new(usr, current_user)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Both graphql-ruby and GraphqlRails supports pagination out of the box. You do not need to think on how to split your collection into smaller batches in order to show limited amount of data (by default it's a 200 items per page). However, there's a catch - pagination works efficiently with ActiveRecord records only.

In the provided index example, I demonstrated a somewhat inefficient approach without using GraphqlRails::Decorated. The inefficiency stems from loading all users from the database and then constructing ProtectedUser instances for each user. Yet, for pagination, we only require a subset of users, not the entire collection. If you have a million users, this inefficiency becomes a significant problem. The simplest way to solve this is by turning ProtectedUser into a decorator. This is easily accomplished by replacing GraphqlRails::Model with GraphqlRails::Decorator:

class ProtectedUser
  include GraphlqRails::Decorator

  graphql.name('User')
  graphql.attribute(:email).type('String')

  def initialize(user, viewer)
    @user = user
    @viewer = viewer
  end

  def email
    return nil unless @viewer.permitted?('emails:read', @user)

    @user.email
  end
end
Enter fullscreen mode Exit fullscreen mode

With just one line replaced in our type class, we can update our controller and return decorated records:

class Graphql::UsersController < GraphqlApplicationController
  action(:index).paginated.returns('[ProtectedUser!]!')
  def index
    ProtectedUser.decorate(User.all, current_user)
  end
end
Enter fullscreen mode Exit fullscreen mode

The .decorate method takes a list or query as its first argument, and any subsequent arguments are shared across all decorated items. This method emulates the behavior of an ActiveRecord query, ensuring it fetches only the necessary records for the specific page.

Rendering errors

In the realm of Rails, encountering errors is an everyday occurrence. Sometimes, things don't go as planned and the system crashes. In these situations, GraphqlRails comes to the rescue by automatically rendering system errors. However, there are situations when you'd like to take charge of error rendering manually, without causing the system to crash. This is where the render method within the controller comes into play:

action(:create)
  .permit_input(:input, type: 'User')
  .returns('User!')

def create
  new_user = User.new(params[:input])
  if current_user.forbidden_to?(:create, new_user)
    return render(errors: ['Create is forbidden'])
  end

  return new_user if new_user.save

  render(errors: new_user.errors)
end
Enter fullscreen mode Exit fullscreen mode

Notice the straightforward error handling within this controller action. If you take a closer look, you will notice, that we render different type of errors in different places.

In this part we render stings as errors:

return render(errors: ['Create is forbidden'])
Enter fullscreen mode Exit fullscreen mode

And in this part we render validation errors:

render(errors: new_user.errors)
Enter fullscreen mode Exit fullscreen mode

It's not a coincidence that this gem has Rails word in it.

Final Thoughts

Our journey through the world of GraphqlRails has been a revelation. From its inception as an extension to graphql-ruby to its transformation into a full-fledged framework, GraphqlRails has evolved into a valuable asset for developers seeking a streamlined, consistent, and secure approach to GraphQL development within Ruby on Rails applications.

Embracing GraphqlRails isn't just a matter of adopting a gem; it's about embracing a philosophy that melds GraphQL seamlessly into the Rails ecosystem. This unique integration philosophy bridges the gap between familiar Rails constructs and the dynamic world of GraphQL. By doing so, GraphqlRails redefines how we approach building APIs that are robust, secure, and maintainable.

The power of GraphqlRails lies in its ability to unify models and types, simplifying the management of permissions, privacy, and consistency. By treating GraphQL types as extensions of models, the gem not only preserves Rails conventions but also enhances the security of your application. This streamlined approach not only simplifies development but also fosters confidence in the security of your data.

Moreover, GraphqlRails introduces a paradigm shift in testing. Unlike traditional GraphQL testing, which often leans heavily on integration tests, GraphqlRails empowers developers to conduct unit testing on GraphQL types/models. This approach aligns testing practices with the principles of object-oriented programming and promotes encapsulation, making your application's behavior more comprehensible and testable.

As we conclude our journey, let's not just view GraphqlRails as a technical tool but as an enabler of innovation. It redefines how we craft GraphQL APIs, offering an alternative path that harmonizes GraphQL with Ruby on Rails. Whether you're starting a new project or considering a transition, the insights gained from GraphqlRails can pave the way for more efficient, secure, and maintainable GraphQL development.

Top comments (0)