DEV Community

Lucas Barret
Lucas Barret

Posted on • Edited on

Services Object with GraphQL And Rails

Rails is an opinionated framework, but unlike other frameworks, it does not provide a service layer.
You end up having either a lot of code in your controller or a lot of code in your model; this is a known issue called a fat model or fat controller.

Graphql services

With GraphQL, you do not have Controllers. Nevertheless, I like to think of it as a controller, and it is nice to keep these graphql queries or mutations clean; often, code in your query or mutation becomes messy!

If I may quote ToughBot: Make sure the only logic that lives in your controller is related to handling your requests and responses: Get your request parameter, and manage authentication in short :

Let's see an example

As you are going to see, we have a graphql query.
This query greets your users; it retrieves the user by id and greets the user with its name. If the user is outside our database, it greets it with Welcome Guest.

module Queries
  class YouRockQuery < Queries::BaseQuery

    argument :id, ID, required: false
    type String, null: false

    def resolve(input)

      input.tap do |e|
        @id = e[:current_user_id]
      end

      name = User.where(@id).pluck(:name).first
      if name.present?
        "You rock, #{name}"
      else
        "You rock, Guest"
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Extracting in a service layer

First a Service Object is an adaptation of the Command Pattern. Which enables you to extract some logic in a class and group implementation details. When you want to perform a specific action you can the method of this Plain Ol' Ruby Object (PORO).

You could want to export this in the service object for several reasons:

  • It could be used in another query. They are plenty of occasions to greet a user :D
  • You are mixing and handling requests and logic and want to avoid that.
  • This enables to have better tests with PORO

You want to respect the MVC principles and especially the separation of concerns, and generalize it. To avoid high coupling with another part of your application.

Rule of Thumb

This one is from my manager, and I found it pretty helpful:

Most of the time, remember to KISS (Keep Is Simple Stupid).
If your query or mutation is not doing much and is not reused, exporting it would not be much more readable. We make code cleaner and more readable for people.

Nevertheless, if you know that this behavior will be reused, export it, or if the inside of your mutation or query is becoming too big, like more than 20 lines, export it.

This is a rule of thumb so that you may have your threshold!

Separation of Concerns

Separation of concerns is a simple principle that follows a law that I tend to see as the generalization at the object level of 2 directions:

  • Methods should do one thing,
  • Methods should be one level of abstraction.

From Wikipedia :
In computer science, separation of concerns is a design principle for separating a computer program into distinct sections. Each section addresses a separate concern, a set of information that affects the code of a computer program.

MVC is one of these principles. But as you can see, MVC can be insufficient, and we must go further.

Extracting and refactoring

So we can extract our greeting logic in a service, and as an exercise, we can also do refactoring our code.

module Greeting
  class YouRock
    attr_accessor :id

    def initialize(id:)
      @id = id
    end

    def perform(id: nil)
      you_rock(user_name: user_name_by(id:))
    end

    private 
    def user_name_by(id:)
      User.where(id: id).pluck(:name).first
    end

    def you_rock(user_name: 'Guest')
      "You rock #{user_name}"
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

I will not dig into the principles we've applied here for the refactor not to make this article too much complicated. But for sure, when exporting code in a service layer, this is a great time to refactor it if possible and needed.

Key point

We are defining our perform method, which will greet the user.
Then we have two private methods defined for refactoring our code.
Defining the private method helps clarify what action can be taken outside the service.

Testing Services

Another benefit of services is better testing. The best test you can run is a test with an input and an output. With the first graphql query, to test your query, you need to make an HTTP request, retrieve the answer and check the answer.

With GraphQL, you must evaluate a new query in each test. When multiplied by hundreds of tests, this costs a lot. Now with your services, you can make a plain ruby rspec like this without testing the network part.

require 'rails_helper'
require_relative "../lib/you_rock.rb"

RSpec.describe 'Greeting::YouRock' do

  subject { Greeting::YouRock.new.perform(id: 1) }

  before { create(:user) }

  it { expect(subject.you_rock(user_name: "Lucas")).to  
  eq("You rock Lucas") }

end
Enter fullscreen mode Exit fullscreen mode

After a discussion with some of my colleagues, they told me about what we could do about testing user_name_by and you_rock :

Testing is mandatory, but is testing everything worth it?

This is a fundamental question, which means we need to make a trade-off; of course, you will feel super confident if you test every function of your codebase.

Nevertheless, this has a cost. Adding a test case, if necessary in the future, could be better.
I am unsure about all this and would love to hear your feelings.

Should I put it in lib?

It seems this depends on people's preference; some will create a services folder in an app like app/services or use the lib lib/services.
You can also put it under lib in his folder as is; it is really up to you and the coding convention of your company.
Some people consider lib as code usable between different projects and is not specialized to the current application, a gem that is not already a gem...

Conclusion

All we see here, Service Layer/Objects, enables us to make our code cleaner. There might be better ways, or the only one. Some people are against the service layer, but IMHO, this makes code cleaner and encourages refactoring and clean code.

This is a trade-off; this would not be necessary if your query retrieves a user or updates a field.
But if it begins to become messy, a lot of calls, and you feel that what you are doing is dirty, feel free to create a service for it.

Resources

For Service Layer :

https://www.toptal.com/ruby-on-rails/rails-service-objects-tutorial

Against Service Layer :
https://intersect.whitefusion.io/the-art-of-code/why-service-objects-are-an-anti-pattern

Keep in Touch

On Twitter : @yet_anotherDev

On Linkedin : Lucas Barret

Top comments (0)