Rails api how to handle service object exceptions in controller

1.8k Views Asked by At

I wanted use Service Object in my Rails API. Inside my Service Object I want to save Model and return true if saving was successful, but if saving was unsuccessful then I want to return false and send error messages. My Service Object looks like that:

class ModelSaver
  include ActiveModel::Validations
  
  def initialize(params)
    @params = params
  end

  def save_model
    model ||= Model.new(params)

    return false unless model.valid?
    model.save!

  rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
    model.errors.add(:base, 'here I want default error message')
    false
  end

  private

  attr_reader :params
end

The problem is that I don't know how to send errors messages in response. When I try to send service_object.errors.messages it displays an empty array to me. It looks like that in Controller:

class ModelController < ApplicationController
...
  def create
    service_object = ModelSaver.new(params)

    if service_object.save_model
       render json: service_object
    else
       render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
    end
  end
...
end

So how can I get Model errors from Service Object inside Controller?

2

There are 2 best solutions below

2
On BEST ANSWER

You can solve this by providing methods to your service object that allow returning the model or the errors even after save_model returned with false.

I would change the service object to

class ModelSaver
  include ActiveModel::Validations

  attr_reader :model

  def initialize(params)
    @params = params
  end

  def errors
    @model.errors
  end

  def save_model
    @model ||= Model.new(params)

    return false unless model.valid?
    @model.save!

  rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
    @model.errors.add(:base, 'here I want default error message')
    false
  end

  private

  attr_reader :params
end

and the controller method to

def create
  service_object = ModelSaver.new(params)

  if service_object.save_model
     render json: service_object.model
  else
     render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
  end
end
3
On

I would refactor the service object so that it just delegates to the underlying model:

class ModelSaver
  attr_reader :model
  delegate :errors, to: :model
  
  def initialize(**params)
    @model = Model.new(**params)
  end

  def save_model
    if model.save
      true
    else
      model.errors.add(:base, 'you forgot to frobnobize the whatchamacallit')
      false
    end
  end
end
class ModelController < ApplicationController
  def create
    service_object = ModelSaver.new(**params)

    if service_object.save_model
       render json: service_object
    else
       render json: { errors: service_object.errors.messages }, status: :unprocessable_entity
    end
  end
end