For over a decade, form_for was the cornerstone of form-building in Ruby on Rails. Introduced in Rails 2.0, it offered a clean, model-centric API that automatically handled URLs, HTTP methods, and parameter scoping.
Then, in Rails 5.1, form_with arrived — not as a replacement, but as a unified evolution. By Rails 7.0, form_for et form_tag were officially deprecated.
Today, in 2026, form_with is the only supported form helper — and it’s more powerful, flexible, and future-ready than ever.
This is the complete guide every Rails developer needs to:
- Understand the why behind the change
- Migrate legacy
form_forcode safely - Master modern form patterns avec
form_with - Build production-grade forms with Hotwire, Stimulus, and Turbo
Understanding form_for in Ruby on Rails
form_forThe form_for helper has been a reliable part of the Rails ecosystem for years. It’s used to create forms that are tied to model objects, automatically handling naming conventions, routes, and parameters.
Exemple:
<%= form_for @user do |f| %> <%= f.label :name %> <%= f.text_field :name %> <%= f.submit "Create User" %> <% end %>
This simple syntax made form_for a go-to choice for Rails developers, as it automatically mapped form fields to model attributes. However, as web applications evolved to include AJAX, Hotwire, and more dynamic frontends, form_for began to show its limitations.
The Rise of form_with
Introduced in Rails 5.1, form_with was designed as a unified and more flexible API that combined the functionality of both form_for et form_tag. In essence, it’s a smarter, more modern way to handle forms in Rails.
Exemple:
<%= form_with model: @user do |f| %> <%= f.label :name %> <%= f.text_field :name %> <%= f.submit "Create User" %> <% end %>
At first glance, form_with looks similar — but it comes with major improvements under the hood.
1. The Evolution of Rails Form Helpers
| Year | Helper | Status | Key Innovation |
| 2004 | form_tag | Deprecated | Basic URL-based forms |
| 2007 | form_for | Deprecated | Model integration, URL inference |
| 2017 | form_with | Current Standard | Unified API, remote by default |
| 2022 | form_for | Deprecated in Rails 7 | — |
| 2025 | form_with | Only supported | Full Hotwire compatibility |
The unification principle: One helper. One API. All use cases.
2. Core Differences: form_for vs form_with
| Fonctionnalité | form_for | form_with |
| Binding | form_for(@post) | form_with(model: @post) |
| URL-only forms | Not supported | form_with(url: path) |
| Scope control | Automatic | model: ou scope: |
| Remote/Ajax | remote: true | local: false |
| Default submit | “Save Post” | “Create Post” / “Update Post” |
| File upload | html: { multipart: true } | multipart: true |
| HTML options | html: {} | Same |
| Hotwire support | Partial | Native |
| Explicitness | Implicit | Explicit by default |
3. Migration Roadmap: form_for → form_with
90% of migrations are mechanical.
Step 1: Replace form_for(@obj) → form_with(model: @obj)
Step 2: Replace remote: true → local: false
Step 3: Replace form_tag → form_with(url: ...)
Step 4: Replace html: { multipart: true } → multipart: true
4. Pattern 1: Basic Model-Backed Form
erb <!-- Legacy: form_for --> <%= form_for(@post) do |f| %> <%= f.label :title %> <%= f.text_field :title, class: "input", placeholder: "Enter title" %> <%= f.label :body %> <%= f.text_area :body, rows: 5 %> <%= f.submit %> <% end %> <!-- Modern: form_with --> <%= form_with(model: @post) do |f| %> <%= f.label :title %> <%= f.text_field :title, class: "input", placeholder: "Enter title" %> <%= f.label :body %> <%= f.text_area :body, rows: 5 %> <%= f.submit %> <% end %>
Generated HTML (identical):
html <form action="/posts" method="post" accept-charset="UTF-8"> <input name="authenticity_token" type="hidden" value="..." /> <label for="post_title">Title</label> <input type="text" name="post[title]" id="post_title" class="input" placeholder="Enter title"> <label for="post_body">Body</label> <textarea name="post[body]" id="post_body" rows="5"></textarea> <input type="submit" value="Create Post"> </form>
On edit: action="/posts/1", method="post" with hidden _method: patch, submit = “Update Post”
5. Pattern 2: URL-Only Form (Search, Filters)
erb <!-- Old: form_tag --> <%= form_tag(search_posts_path, method: :get) do %> <%= text_field_tag :q, params[:q], placeholder: "Search posts..." %> <%= submit_tag "Search" %> <% end %> <!-- New: form_with --> <%= form_with(url: search_posts_path, method: :get) do |f| %> <%= f.search_field :q, value: params[:q], placeholder: "Search posts..." %> <%= f.submit "Search" %> <% end %>
f.search_field adds type="search" and better accessibility.
6. Pattern 3: Ajax / Turbo Forms
erb
<%= form_with(model: @comment, local: false) do |f| %>
<%= f.text_area :body, placeholder: "Add a comment..." %>
<%= f.submit "Post Comment", data: { disable_with: "Posting..." } %>
<% end %>HTML output:
html <form data-remote="true" action="/comments" method="post">
Works seamlessly with:
- Turbo Drive (no full page reload)
- Turbo Streams (real-time updates)
- Stimulus (custom behavior)
7. Pattern 4: Namespaced Routes (admin/posts)
erb <%= form_with(model: [:admin, @post]) do |f| %> <%= f.text_field :title %> <%= f.text_area :body %> <%= f.submit %> <% end %>
Params: admin[post][title]
Route: admin_post_path(@post)
8. Pattern 5: File Uploads
erb <%= form_with(model: @user, multipart: true) do |f| %> <%= f.file_field :avatar, accept: "image/*", class: "file-input" %> <%= f.submit "Upload Avatar" %> <% end %>
multipart: true is shorthand — cleaner than html: {}.
9. Pattern 6: Nested Attributes (has_many)
erbe <%= form_with(model: @post) do |f| %> <h3>Add Comments</h3> <%= f.fields_for :comments, @post.comments.build do |cf| %> <div class="field"> <%= cf.label :author %> <%= cf.text_field :author %> </div> <div class="field"> <%= cf.label :body %> <%= cf.text_area :body %> </div> <% end %> <%= f.submit "Save Post" %> <% end %>
fields_for works identically — no migration needed.
10. Pattern 7: Custom Parameter Scope
erb <%= form_with(scope: :article, model: @post) do |f| %> <%= f.text_field :title %> <!-- article[title] --> <%= f.text_area :body %> <!-- article[body] --> <% end %>
Useful for:
- Legacy APIs
- JSON payloads
- Non-ActiveRecord objects
11. Pattern 8: Clone Form (Model + Custom URL)
erb <%= form_with(model: @post, url: clone_post_path(@post)) do |f| %> <%= f.hidden_field :title %> <%= f.hidden_field :body %> <%= f.submit "Clone This Post" %> <% end %>
Uses @post for field values, submits to custom path.
12. Pattern 9: Multiple Submit Buttons
erb <%= form_with(model: @post) do |f| %> <%= f.text_field :title %> <%= f.submit "Save as Draft" %> <%= f.submit "Publish Now", name: "commit", value: "publish" %> <% end %>
Manette:
ruby def update @post.update(post_params) if params[:commit] == "publish" @post.publish! end redirect_to @post end
13. Pattern 10: Stimulus + Autosave
erb
<%= form_with(
model: @post,
data: { controller: "autosave" }
) do |f| %>
<%= f.text_field :title,
data: { action: "input->autosave#save" } %>
<%= f.text_area :body,
data: { action: "input->autosave#save" } %>
<% end %>Stimulus Controller (app/javascript/controllers/autosave_controller.js):
js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
save() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.element.requestSubmit()
}, 800)
}
}14. Pattern 11: Conditional Fields
erbe <%= form_with(model: @user) do |f| %> <%= f.text_field :name %> <% if @user.admin? %> <div class="field"> <%= f.check_box :super_admin %> <%= f.label :super_admin, "Grant Super Powers" %> </div> <% end %> <%= f.submit %> <% end %>
15. Pattern 12: Inline Validation Errors
erbe
<%= form_with(model: @post) do |f| %>
<div class="field">
<%= f.label :title %>
<%= f.text_field :title %>
<% if @post.errors[:title].any? %>
<p class="error"><%= @post.errors[:title].join(", ") %></p>
<% end %>
</div>
<% end %>16. Advanced: Turbo Stream Forms
erb
<%= form_with(
model: @comment,
local: false,
data: { turbo_stream: true }
) do |f| %>
<%= f.text_area :body %>
<%= f.submit "Add Comment" %>
<% end %>Manette:
ruby
def create
@comment = @post.comments.build(comment_params)
respond_to do |format|
if @comment.save
format.turbo_stream
else
format.html { render :new }
end
end
end
create.turbo_stream.erb:
erb
<%= turbo_stream.append "comments", partial: "comment", locals: { comment: @comment } %>
<%= turbo_stream.replace "comment_form", partial: "form" %>17. Common Pitfalls & Debugging
| Issue | Cause | Fix |
undefined method 'model' | form_with(@post) | Use model: @post |
| Ajax not working | remote: true | Utilisation local: false |
| File not uploading | Missing multipart | Add multipart: true |
| Wrong params | scope: mismatch | Utilisation model: ou scope: consistently |
| Turbo not submitting | data-turbo="false" | Remove or set data-turbo="true" |
18. Testing Forms
ruby
# spec/system/posts_spec.rb
require "rails_helper"
RSpec.describe "Posts", type: :system do
it "creates a post" do
visit new_post_path
fill_in "Title", with: "My First Post"
fill_in "Body", with: "Hello, world!"
click_button "Create Post"
expect(page).to have_content("Post was successfully created.")
expect(page).to have_content("My First Post")
end
endWorks identically avec form_with.
19. Performance & Best Practices
- Utilisation
form_witheverywhere — even for simple search forms - Prefer
model:— it infers URL and scope - Utilisation local: false with Turbo for SPA-like UX
- Avoid
form_tag— it’s deprecated and slower - Audit your codebase:
bash grep -r "form_for\|form_tag" app/views
20. Deprecation Timeline
| Rails Version | Status |
| 5.1 | form_with introduced |
| 6.1 | form_for still supported |
| 7.0 | form_for deprecated |
| 7.1+ | form_for will be removed |
Action required: Migrate now to avoid breaking changes.
21. Quick Reference Table
| Cas d'utilisation | form_with Syntaxe |
| Model form | form_with(model: @post) |
| URL form | form_with(url: path) |
| Ajax | local: false |
| File upload | multipart: true |
| Namespace | model: [:admin, @post] |
| Custom scope | scope: :article |
| Clone form | model: @post, url: clone_path |
Conclusion
À RailsCarma, we emphasize building modern, efficient, and future-ready Ruby on Rails applications. The form_with helper isn’t just a replacement for form_for — it’s a superior, unified API that enhances how developers create and manage forms in Rails.
Avec form_with, you can:
- Eliminate the need for
form_tag - Simplify Ajax and Hotwire integrations
- Reduce repetitive boilerplate code
- Future-proof your Rails codebase for long-term scalability
In short — it’s time to evolve your forms. Stop using form_for. Start using form_with — today, with RailsCarma’s Développement Ruby on Rails expertise.