Forms and Validations in Rails
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
- Rails Routing — how URLs map to controller actions, so you know where your form POSTs go.
- ActiveRecord Basics — the model layer that your validations protect.
- Views and Partials — how ERB templates render your forms and error messages.