Form Objects

Form Objects is a design pattern for handling complex form submissions that involve multiple models or custom validation logic, keeping that complexity out of controllers and ActiveRecord models.

Table of Contents

What is a Form Object in Ruby on Rails?

A Form Object is a plain Ruby object that represents a form and handles its submission. Instead of binding a form directly to an ActiveRecord model, a Form Object acts as an intermediary that accepts input, validates it, and coordinates the creation or update of one or more models.

This pattern is useful when a single form needs to interact with multiple models simultaneously, or when the validation and persistence logic does not map cleanly to a single ActiveRecord model.

Why Are Form Objects Useful?

As forms grow in complexity, controllers and models can accumulate logic that does not belong to either. Form Objects help by:

  • Keeping controllers thin by moving form handling logic out
  • Avoiding bloated models with validations that only apply to specific forms
  • Handling forms that interact with multiple models in one place
  • Providing a single object to validate and test form submissions
  • Separating persistence logic from presentation and input handling
  • Making complex form workflows easier to maintain and reuse

They are especially useful for registration forms, checkout flows, or any form that creates or updates more than one record at a time.

How Do Form Objects Work?

A Form Object is a plain Ruby class that includes validation and persistence logic for a specific form.

Key components:

  • Form Class – A plain Ruby object that represents the form and its fields
  • Validations – Defined on the form object rather than directly on models
  • Persistence – The form object coordinates saving one or more models on valid submission
  • Controller Integration – The controller passes params to the form object and checks its validity
  • Separation of Concerns – Keeps model and controller focused on their primary responsibilities

Examples

Scenario 1: User Registration Form

A registration form that creates both a User and an Account record on submission.

Form Object (app/forms/registration_form.rb)

class RegistrationForm 
  include ActiveModel::Model 
 
  attr_accessor :first_name, :last_name, :email, :password, :account_name 
 
  validates :first_name, :last_name, :email, :password, :account_name, presence: true 
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } 
 
  def save 
    return false unless valid? 
 
    ActiveRecord::Base.transaction do 
      account = Account.create!(name: account_name) 
      User.create!( 
        first_name: first_name, 
        last_name: last_name, 
        email: email, 
        password: password, 
        account: account 
      ) 
    end 
 
    true 
  rescue ActiveRecord::RecordInvalid 
    false 
  end 
end 

Including ActiveModel::Model gives the form object access to Rails validations, error messages, and form helper compatibility.

Controller

class RegistrationsController < ApplicationController 
  def new 
    @form = RegistrationForm.new 
  end 
 
  def create 
    @form = RegistrationForm.new(registration_params) 
 
    if @form.save 
      redirect_to root_path, notice: "Registration successful" 
    else 
      render :new 
    end 
  end 
 
  private 
 
  def registration_params 
    params.require(:registration_form).permit( 
      :first_name, :last_name, :email, :password, :account_name 
    ) 
  end 
end 

View

<%= form_with model: @form, url: registrations_path do |f| %> 
  <%= f.text_field :first_name %> 
  <%= f.text_field :last_name %> 
  <%= f.email_field :email %> 
  <%= f.password_field :password %> 
  <%= f.text_field :account_name %> 
  <%= f.submit "Register" %> 
<% end %> 

The view remains clean because it interacts with the form object just like a regular ActiveRecord model.

Scenario 2: Handling Validation Errors

Since the form object includes ActiveModel::Model, validation errors work the same way as with ActiveRecord models.

form = RegistrationForm.new(email: "invalid-email", account_name: "") 
form.valid?        # => false 
form.errors.full_messages 
# => ["First name can't be blank", "Email is invalid", "Account name can't be blank"] 

This makes error display in views straightforward using the standard Rails error helpers.

Where to Use Form Objects?

  • Registration or onboarding flows that create multiple records
  • Checkout or order forms involving several models
  • Forms with validations that only apply in a specific context
  • Multi-step form workflows
  • Any form where the input does not map cleanly to a single model

In Summary

A Form Object is a plain Ruby object that handles the validation and persistence logic for complex form submissions, keeping controllers thin and models focused on business logic. It is particularly valuable when a single form needs to interact with multiple models or apply context-specific validations.