DEV Community

Cover image for Clean Code Tip: Create a Service Object and Improve Your GraphQL Mutations in Rails
Sulman Baig
Sulman Baig

Posted on • Originally published at sulmanweb.com

Clean Code Tip: Create a Service Object and Improve Your GraphQL Mutations in Rails

As a developer, one of the most important aspects of writing code is making it easy to understand, maintain, and scale. This is where the concept of "clean code" comes in. Writing clean code is not just about writing code that works but also code that is readable, easy to modify, and follows best practices.

In this blog post, we will explore one clean code tip that can help you write more maintainable and scalable GraphQL APIs in Ruby on Rails: creating a service object and calling it in the mutation.

What is a service object?

A service object is a design pattern that separates the business logic of an application from the controller or model. It encapsulates a specific business task, such as sending an email or updating a user's information, into a reusable and modular object. Service objects promote code reuse, reduce complexity, and make testing and maintaining the code easier.

In the context of a GraphQL API in Ruby on Rails, a service object can encapsulate a mutation’s business logic. This can help make the mutation code more modular and reusable and make it easier to add or modify functionality in the future.

Creating a service object for a GraphQL mutation in Rails

Let's say we have a GraphQL mutation that signs up the user. The mutation might look something like this:

# app/graphql/mutations/sign_up.rb
module Mutations
  # @note: This is the mutation that will be used to sign up a new user.
  class SignUp < Mutations::BaseMutation
    description 'Sign up a new user'

    argument :email, String, required: true, description: 'The email of the user'
    argument :name, String, required: true, description: 'The name of the user'
    argument :password, String, required: true, description: 'The password of the user'
    argument :username, String, required: true, description: 'The username of the user'

    field :auth_token, String, null: true, description: 'The auth token of the user'
    field :errors, [String], null: true, description: 'The errors of the mutation'
    field :user, Types::Objects::UserType, null: true, description: 'The user'

    def resolve(username:, password:, email:, name:)
      user = User.new(username:, email:, password:, name:)
      return { auth_token: nil, errors: user.errors.full_messages, user: nil } unless user.valid?

      ActiveRecord::Base.transaction do
        user.save!
        session = user.sessions.create!
      end
      auth_token = "Bearer #{JsonWebToken.encode({ token: session.token })}"
      { auth_token:, user:, errors: nil }
    rescue StandardError => e
      { auth_token: nil, user: nil, errors: [e.message]
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Now we will add send confirmation email, may be we add some slack notifer or other code that we need to execute after user is created through sign up. So soon, this resolve code gets bloated, and many business actions will make our code dirty. Here comes service objects.

While this mutation code works, it can be improved using a service object. Here's how we can create a service object to handle the business logic of updating a user's email:

# app/services/users/signup_service.rb
module Users
  # @note: This is the service that will be used to create a new user and a new session.
  # @param [String] username
  # @param [String] email
  # @param [String] password
  # @param [String] name
  # @return [Struct] result with the user, auth token and error
  # @example
  #   Users::SignupService.call(username: 'username', email: 'email', password: 'password', name: 'name')
  # @example result
  #   result.user # => User
  #   result.auth_token # => String
  #   result.errors # => Array
  # @step: 1 - Validate the user.
  # @step: 2 - Create the user and the session.
  # @step: 3 - Encode the session token.
  # @step: 4 - Return the result.
  class SignupService
    attr_accessor :username, :email, :password, :name, :result

    # @note: This is the constructor of the service.
    def initialize(username:, email:, password:, name:)
      @username = username
      @email = email
      @password = password
      @name = name
      # @note: This is the struct that will be used to return the result of the service.
      @result = Struct.new(:user, :auth_token, :errors)
    end

    # @note: This is the method that will be called to execute the service.
    def call # rubocop:disable Metrics/AbcSize
      session = nil
      user = User.new(username:, email:, password:, name:)
      # @note: This is the validation of the user.
      return result.new(nil, nil, user.errors.full_messages) unless user.valid?

      # @note: This is the transaction that will be used to create the user and the session.
      ActiveRecord::Base.transaction do
        user.save!
        session = user.sessions.create!
      end
      # Add more business actions
      auth_token = "Bearer #{JsonWebToken.encode({ token: session.token })}"
      result.new(user, auth_token, nil)
    rescue StandardError => e
      result.new(nil, nil, [e.message])
    end

    class << self
      # @note: This is the method that will be called to execute the service.
      def call(username:, email:, password:, name:)
        new(username:, email:, password:, name:).call
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

https://github.com/sulmanweb/twitter-clone-ruby-gql/blob/20bf99b2740dc059b68e258b1d6e69c21e7d19f0/app/services/users/signup_service.rb

Now we can call this service object in our mutation code:

module Mutations
  # @note: This is the mutation that will be used to sign up a new user.
  class SignUp < Mutations::BaseMutation
    description 'Sign up a new user'

    argument :email, String, required: true, description: 'The email of the user'
    argument :name, String, required: true, description: 'The name of the user'
    argument :password, String, required: true, description: 'The password of the user'
    argument :username, String, required: true, description: 'The username of the user'

    field :auth_token, String, null: true, description: 'The auth token of the user'
    field :errors, [String], null: true, description: 'The errors of the mutation'
    field :user, Types::Objects::UserType, null: true, description: 'The user'

    def resolve(username:, password:, email:, name:)
      Users::SignupService.call(username:, password:, email:, name:)
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

https://github.com/sulmanweb/twitter-clone-ruby-gql/blob/20bf99b2740dc059b68e258b1d6e69c21e7d19f0/app/graphql/mutations/sign_up.rb

By using a service object, we have encapsulated the business logic of updating a user's email into a modular and reusable object. If we need to add or modify this functionality in the future, we can do so easily by modifying the service object.

Benefits of using a service object in GraphQL mutations

Using a service object in GraphQL mutations has several benefits:

  • Encapsulates business logic: The service object encapsulates the business logic of the mutation, making it easier to read, test, and modify.
  • Promotes code reuse: By separating the business logic from the mutation code, we can reuse the service object in other application parts.
  • Makes code more modular: The service object makes the mutation code more modular and easier to maintain.
  • Easier to test: The service object can be tested independently of the mutation code, making it easier to write automated tests.

Conclusion

Creating a service object and calling it in the mutation of GraphQL in Rails is an important clean code tip. It helps keep the code clean and organized and allows the logic to be reused in different application parts. When creating and calling the service object, it is important to consider the inputs and outputs, purpose, expected behavior, and expected outcome.

This code is part of the public repo of project twitter_clone_ruby_gql, which I am working on these days. Link: https://github.com/sulmanweb/twitter-clone-ruby-gql.


Happy Coding!
Contact me at sulman@hey.com

Top comments (3)

Collapse
 
yet_anotherdev profile image
Lucas Barret • Edited

Really good article ! Do you think that it is always worth it to create a service layer for your queries in graphql?
I feel that often we just fetch a model with find or something like this, and it will be much work to do it like this.
Of course when you have more than just boilerplate code it's worth.
The more I gain experience in coding, the more I feel like it is always a tradeoff. 👀
What do you think :D

Collapse
 
sulmanweb profile image
Sulman Baig • Edited

You are right sometimes it looks like more than the required effort. But this is code in progress l. So in future if I need more validations, some extra service to be called like subscribing user to newsletters, or send email to user, or sending slack message when user signs up then business logic separation would look very nice. And most importantly when repo will get a lot big then this separation of models actions and business logic separation will look very beautiful and easy for onboarding engineers. Thinking of long term project makes it worth it in my opinion.

Collapse
 
yet_anotherdev profile image
Lucas Barret

Of course I understand, like I said I think it is a question of tradeoff.
What has the most value in your opinion, I mostly agree with you for the developper experience of the future onboarded.