Simple form object

Form objects are an additional layer between the controller and the ActiveRecord model.
This layer maps a specific form to the concerned model and contains form-specific business logic (validations, virtual attributes, persistence of other depending ActiveRecord objects). This is how the ActiveRecord model can be relieved of some responsibilities:

  1. Callbacks / observer, which also modify and persist other model objects
  2. Context-dependent model validations (conditional validations with if - else)
  3. Virtual ActiveRecord accessor methods, which are used only for caching form values
  4. ActiveRecord models with dependencies to external systems (e.g., mailers)

By having form objects address those tasks, ActiveRecord objects can deal with their main responsibility: CRUD.

The controller

An example is the bank account registration. An address can also be specified during registration.
The Models:

rails g model BankAccount iban:string address:references
rails g model Address name:string family_name:string street:string zip:string

The controller meets a standard controller. The create action accepts the request, calls the business logic (in this case the form object) and responds with JSON:

# app/controllers/bank_accounts_controller.rb
class BankAccountsController < ApplicationController
  def create
    @bank_account = BankAccountRegistration.new params.require(:bank_account).permit!
    if @bank_account.save
      render nothing: true, status: :created
    else
      render json: { errors: @bank_account.errors }, status: :bad_request
    end
  end
end

The only noteworthy is the missing strong parameters definition. The form object does only pass through the form attributes that are necessary anyway.

The form object

The form object itself corresponds to ActiveModel. Moreover it contains:

  • Getter and setter methods for the form fields input
  • Validations
  • A persistence method save

The save method includes the necessary business logic. After the form input validation, the database transaction starts like:

# app/forms/bank_account_registration.rb
class BankAccountRegistration
  include ActiveModel::Model

  attr_accessor :iban,
                  :name,
                  :family_name,
                  :street,
                  :zip

  validates :iban, :name, :family_name,
    presence: true

  def save
    return false if invalid?
    bank_account.transaction do
      bank_account.address = address
      bank_account.save!
      # do more stuff like mailer ...
    end
  end

  private

  def bank_account
    @bank_account ||= BankAccount.new iban: iban
  end

  def address
    @address ||= Address.new name: name,
                             family_name: family_name,
                             street: street,
                             zip: zip
  end
end

One of the form objects advantages is their great testability in isolation. If complexity increases, form objects can deal with it very well without much additional effort.
A simple curl request to the form object with the response:

curl -X POST -d 'bank_account[iban]=DE450123456789' localhost:3000/bank_accounts
{"errors":{"name":["can't be blank"],"family_name":["can't be blank"]}}