Using FormObject pattern in Ruby APIs
Tags: ruby, rails, oop, patterns
Published: 2023-01-16 07:19:59

FormObject is a powerful pattern that can be used to encapsulate logic and validation in Ruby on Rails applications. While the name "FormObject" suggests that they should only be used for forms, this is not the case. FormObject can be used for a variety of purposes and can be a valuable addition to your toolset for keeping your code organized and maintainable.

One of the main benefits of using FormObject is that it allows you to separate concerns in your application. By encapsulating logic and validation within a dedicated object, you can keep your controllers and models focused on their specific responsibilities. This can make your code more readable and easier to understand, as well as more testable.

A FormObject is a plain Ruby object that is responsible for handling the logic and validation for a specific action. It can be used in place of the controller to handle the creation or update of a model.

To use a FormObject, you would first create a new class that inherits from ActiveModel::Model. This gives your FormObject all the same features as a Rails model, such as validations and errors. Next, you would define the attributes and validations for the form within the class.

Here's an example of a FormObject for creating a new user:

```ruby
class UserForm
  include ActiveModel::Model

  attr_accessor :name, :email, :password

  validates :name, presence: true
  validates :email, presence: true, email: true
  validates :password, presence: true
end
```

In your controller, you would then use the FormObject to handle the creation of a new user. Here's an example of how you would use the UserForm in the `UsersController`:

```ruby
class UsersController < ApplicationController
  def create
    form = UserForm.new(user_params)
    if form.valid?
      user = User.create(form.attributes)
      render json: user, status: :created
    else
      render json: form.errors, status: :unprocessable_entity
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :password)
  end
end
```

As you can see in the above example, using FormObjects can make your controller code more readable, as it separates the validation and creation of a user into a dedicated class. This also allows to have better testability and maintainability, as the logic and validation for the form are encapsulated within the FormObject class.

## Model validations VS FormObject

Rails model validations and FormObject validations are both used to ensure that the data being saved to a model is valid. However, there are some key differences between the two:

1. Scope of validation: Rails model validations are typically used to validate the attributes of a model before they are saved to the database. FormObject validations, on the other hand, can be used to validate data that may not necessarily be saved to a model or even to validate multiple models at once.

2. Location of validation: Rails model validations are typically defined within the model itself, while FormObject validations are defined within a separate FormObject class.

3. Complex validation: Rails model validations are limited to validating individual attributes, whereas FormObject validations can be used to handle more complex validation logic that may involve multiple attributes or even multiple models.

4. Separation of concerns: Rails model validations are closely tied to the model and its persistence, whereas FormObjects can be used to separate the validation logic from the model and the persistence, making the code more readable and maintainable.

5. Testing: Rails model validations are typically tested by writing unit tests for the model, whereas FormObject validations can be tested by writing unit tests for the FormObject class. This can make it easier to test complex validation logic that involves multiple attributes or models.

# It's not an "all or nothing" dilemma

As mentioned before, FormObject is a nice addition to your application and it does not have to replace all model validations. It is important to note that moving **all** of the validation logic to FormObjects can also make your code more complex. Certain types of validations might be better left in the model. 

The validations that should be left in the model are those that are specific to the attributes of the model and are closely tied to the persistence of the model. Here are a few examples of validations that should typically be left in the model:

1. Presence validations: These ensure that a required attribute is present and is not blank.

2. Format validations: These ensure that an attribute is in a specific format, such as an email address or a phone number.

3. Uniqueness validations: These ensure that an attribute is unique across all instances of the model.

4. Association validations: These ensure that a required association is present or that an attribute belongs to a specific association.

5. Data type validations: These ensure that an attribute is of a specific data type, such as an integer or a boolean.

6. Length validations: These ensure that an attribute is within a specific length range.

These validations are closely tied to the persistence and the attributes of the model and should be left in the model. They can be easily tested and are easily understandable for other developers who work on the same codebase.

On the other hand, validations that are related to the business logic or the context of the form submission, such as checking if a user has sufficient funds to make a purchase, or if the user can make a reservation on a specific date, are better suited to be placed in the FormObject.

It's worth mentioning that there's no hard and fast rule and it depends on the specific needs of your application. The important thing is to evaluate your validations and decide what should stay in the model, and what should be moved to FormObject, to have a maintainable and readable codebase.

## Summary

In summary, FormObject is a powerful pattern that can help you keep your Rails controllers clean and maintainable by encapsulating form logic and validation. It can be a great addition to your toolset to make your code more organized and testable.

### Further readings

I recommend checking my a bit more advanced topic about using FormObject pattern in combination with a Service patter in my [FormService: a PORO ServiceObjects with a state](https://dev.to/povilasjurcys/formservice-a-poro-serviceobjects-with-a-state-4a75) post.

### Authorship sidenote

This post was created based on my personal experience and research which I did using chat-gpt tool. So some content might not be 100% unique and is taken from other sources (which just proves the wild spread of the FormObject pattern, btw)