Forms and Validations in Rails

· 6 min read · Updated March 28, 2026 · beginner
rails forms validations ruby

Every Rails app needs to collect user input. Whether it’s a login form, a blog post editor, or a checkout flow, you need a way to get data from the browser into your database — and you need to make sure that data is worth keeping. That’s what forms and validations are for.

This tutorial covers form_with, Rails’ modern form builder; strong parameters, which protect you from mass-assignment attacks; and the validation system that keeps bad data out of your models.

Building Forms with form_with

Rails 5.1 introduced form_with as the unified replacement for the older form_tag and form_for helpers. It handles both model-bound forms and standalone forms.

Model-Bound Forms

When you have a model instance, pass it with model: and Rails figures out the URL and HTTP method:

# app/views/articles/new.html.erb
<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>
  <div>
    <%= form.label :body %>
    <%= form.text_area :body %>
  </div>
  <%= form.submit %>
<% end %>

If @article is a new record, Rails generates a POST to /articles. If it has an id, it generates a PATCH to /articles/:id. You don’t have to specify anything — the form reads the state of the model.

By default form_with generates remote (AJAX) forms. For a regular full-page POST, add local: true:

form_with model: @article, local: true do |form|

Non-Model Forms

For things like search boxes that don’t correspond to a model, use url: instead:

form_with url: "/search", method: :get do |form|
  form.text_field :query
  form.submit "Search"
end

Scoped Forms

When you want field names prefixed under a namespace but don’t have a model, use scope::

form_with scope: :subscription, url: subscriptions_path do |form|
  form.email_field :email
  form.password_field :password
end

This prefixes every input with subscription[...], so params arrive as params[:subscription][:email].

Strong Parameters

Form data hits your controller as part of the params hash. Strong parameters are the gatekeepers that decide which fields your application will accept.

Without them, a malicious user could send extra fields and overwrite things like admin: true or user_id: another_user. Strong parameters block that.

# app/controllers/articles_controller.rb
def article_params
  params.require(:article).permit(:title, :body, :author_id)
end

require(:article) raises ActionController::ParameterMissing if the :article key is missing entirely. permit(:title, :body, :author_id) returns a hash containing only those three fields — everything else is silently dropped.

Use this in your create and update actions:

def create
  @article = Article.new(article_params)
  if @article.save
    redirect_to @article, notice: "Article created!"
  else
    render :new
  end
end

Model Validations

Validations run before Rails saves anything to the database. If validation fails, save returns false and your controller can re-display the form with error messages.

Common Validation Types

class Article < ApplicationRecord
  validates :title, presence: true
  validates :body, length: { minimum: 10 }
  validates :slug, format: { with: /\A[a-z0-9-]+\z/ }
  validates :price, numericality: { greater_than: 0 }
  validates :email, uniqueness: true
end

Each validation type covers a different risk: is the field filled in? Is it the right length? Does it match a pattern? Is it a number in the right range? Is it already in the database?

Conditional Validation

Run a validation only under certain conditions:

validates :card_number, presence: true, if: :paid_with_card?
validates :terms, acceptance: true, unless: :guest?

Rails evaluates the symbol or lambda you pass to if: or unless: and skips the validation when the condition is false.

Custom Validations

When the built-in helpers aren’t enough, write your own:

class Order < ApplicationRecord
  validate :delivery_date_not_in_past

  private

  def delivery_date_not_in_past
    if delivery_date.present? && delivery_date < Date.today
      errors.add(:delivery_date, "cannot be in the past")
    end
  end
end

The errors.add method takes the attribute name and a message. You can also use errors.add(:base, ...) to attach an error to the model as a whole rather than a specific field.

The Uniqueness Gotcha

The uniqueness validation has a race condition problem. Two requests can both pass validation before either one saves, and both end up inserting the same value.

validates :slug, uniqueness: true  # good first line of defence

The real safety net is a database-level unique index:

add_index :articles, :slug, unique: true

The validation catches the common case fast; the index prevents duplicates from ever entering the database.

Displaying Errors in Views

When validation fails, Rails populates model.errors with the problems. You can check for errors in your view:

@article.errors.any?
# => true
@article.errors[:title]
# => ["can't be blank"]
@article.errors.full_messages
# => ["Title can't be blank", "Body is too short (minimum is 10 characters)"]

Render them in the form:

<% if @article.errors.any? %>
  <div class="errors">
    <ul>
      <% @article.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %>
    <%= form.text_field :title %>
    <% @article.errors[:title].each do |msg| %>
      <span class="error"><%= msg %></span>
    <% end %>
  </div>
  <!-- rest of form -->
<% end %>

Showing errors per-field gives users clear, actionable feedback without making them hunt for what went wrong.

render :new vs redirect_to After Failed Validation

This catches a lot of beginners. When validation fails, you must use render :new, not redirect_to.

# WRONG — loses all form data on error
def create
  @article = Article.new(article_params)
  if @article.save
    redirect_to @article
  else
    redirect_to new_article_path  # form will be empty!
  end
end

# RIGHT — preserves @article and its errors
def create
  @article = Article.new(article_params)
  if @article.save
    redirect_to @article
  else
    render :new  # re-renders the form with @article intact
  end
end

render :new re-displays the current view with the @article object still in scope — including the values the user typed and the errors that validation produced. redirect_to sends a 302 to the browser, which then makes a fresh GET request. That new request creates a new controller instance with a fresh @article, and the form comes back empty.

flash.now vs flash

When you redirect after a successful action, you can pass notice: or alert: directly and Rails puts it in the flash:

redirect_to @article, notice: "Article saved!"

But after render, there is no redirect — the current request just finishes. Use flash.now for that case:

def create
  @article = Article.new(article_params)
  if @article.save
    redirect_to @article, notice: "Article saved!"
  else
    flash.now[:alert] = "Please fix the errors below."
    render :new
  end
end

flash survives one redirect (the next request). flash.now lasts only for the current rendered response. Mixing them up means your message shows up on the wrong page.

CSRF Protection

Rails embeds a signed authenticity token in every form automatically via form_with. This token is verified on the server side and prevents cross-site request forgery attacks.

In a typical web app, leave this alone. If you’re building a purely API-only Rails app that uses token-based authentication and doesn’t use cookies or sessions, you might skip verification for those specific endpoints:

skip_before_action :verify_authenticity_token, if: :api_request?

Disabling CSRF protection in a normal browser-based app is a security vulnerability, not a shortcut.

Conclusion

Forms are how your users talk to your application. form_with gives you a clean, flexible way to build them. Strong parameters keep bad data from ever reaching your models. Validations catch problems early with clear messages. And understanding the difference between render and redirect after a failed save is the difference between a smooth user experience and a frustrating one with empty forms.

The patterns here — check validation, re-render on failure — apply everywhere in Rails. Once you internalise them, building any data-entry flow feels familiar.

See Also