Customizing Rails Generators for Views and Controllers
Rails scaffold generators get you running fast, but the views and controllers they produce rarely match how your app actually looks. Instead of generating and then rewriting, you can customize the templates so the output matches your conventions from the start.
Configuring What Gets Generated
First, turn off the stuff you don’t use. In config/application.rb:
config.generators do |g|
g.test_framework :minitest, spec: false, fixture: true
g.helper false
g.stylesheets false
g.javascripts false
g.jbuilder false
end
Now rails generate scaffold won’t create helper files, stylesheets, JS files, or jbuilder templates.
Where the Templates Live
Every built-in generator uses ERB templates. Override any of them by placing a file at the matching path inside lib/templates/. Rails checks there before falling back to its own defaults.
Key paths for view and controller customization:
lib/templates/
erb/scaffold/
index.html.erb.tt
show.html.erb.tt
new.html.erb.tt
edit.html.erb.tt
_form.html.erb.tt
_partial.html.erb.tt
rails/scaffold_controller/
controller.rb.tt
The .tt extension means it’s a template that generates a template. <%% outputs a literal <% in the generated file. <%= runs at generation time.
To find the originals, look in the Rails source under railties/lib/rails/generators/erb/scaffold/templates/.
Example: Index View with a Table and Turbo
The default index view is pretty bare. Here’s a lib/templates/erb/scaffold/index.html.erb.tt that generates a table with Turbo Frame support:
<h1><%= plural_table_name.titleize %></h1>
<%%= turbo_frame_tag "<%= plural_table_name %>" do %>
<table>
<thead>
<tr>
<% attributes.reject(&:password_digest?).each do |attribute| -%>
<th><%= attribute.human_name %></th>
<% end -%>
<th></th>
</tr>
</thead>
<tbody>
<%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %>
<tr>
<% attributes.reject(&:password_digest?).each do |attribute| -%>
<td><%%= <%= singular_table_name %>.<%= attribute.column_name %> %></td>
<% end -%>
<td><%%= link_to "Show", <%= singular_table_name %> %></td>
</tr>
<%% end %>
</tbody>
</table>
<%% end %>
<%%= link_to "New <%= singular_table_name.titleize %>", new_<%= singular_table_name %>_path %>
Example: Controller with Strong Params and Flash Messages
lib/templates/rails/scaffold_controller/controller.rb.tt:
class <%= controller_class_name %>Controller < ApplicationController
before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy]
def index
@<%= plural_table_name %> = <%= orm_class.all(class_name) %>
end
def show
end
def new
@<%= singular_table_name %> = <%= orm_class.build(class_name) %>
end
def edit
end
def create
@<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
if @<%= singular_table_name %>.save
redirect_to @<%= singular_table_name %>,
notice: "<%= human_name %> was created."
else
render :new, status: :unprocessable_entity
end
end
def update
if @<%= singular_table_name %>.update(<%= singular_table_name %>_params)
redirect_to @<%= singular_table_name %>,
notice: "<%= human_name %> was updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@<%= singular_table_name %>.destroy
redirect_to <%= index_helper %>_url,
notice: "<%= human_name %> was deleted."
end
private
def set_<%= singular_table_name %>
@<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
end
def <%= singular_table_name %>_params
<%- if attributes_names.empty? -%>
params.fetch(:<%= singular_table_name %>, {})
<%- else -%>
params.require(:<%= singular_table_name %>)
.permit(<%= permitted_params %>)
<%- end -%>
end
end
Example: Form Partial with Stimulus
lib/templates/erb/scaffold/_form.html.erb.tt that wires up a Stimulus controller for form behavior:
<%%= form_with(model: <%= model_resource_name %>,
data: { controller: "form", action: "turbo:submit-end->form#onSubmit" }) do |form| %>
<%% if <%= singular_table_name %>.errors.any? %>
<div class="alert alert-danger">
<h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prevented saving:</h2>
<ul>
<%% <%= singular_table_name %>.errors.each do |error| %>
<li><%%= error.full_message %></li>
<%% end %>
</ul>
</div>
<%% end %>
<% attributes.each do |attribute| -%>
<% if attribute.password_digest? -%>
<div class="mb-3">
<%%= form.label :password %>
<%%= form.password_field :password, class: "form-control" %>
</div>
<div class="mb-3">
<%%= form.label :password_confirmation %>
<%%= form.password_field :password_confirmation, class: "form-control" %>
</div>
<% elsif attribute.attachments? -%>
<div class="mb-3">
<%%= form.label :<%= attribute.column_name %> %>
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, class: "form-control" %>
</div>
<% else -%>
<div class="mb-3">
<%%= form.label :<%= attribute.column_name %> %>
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, class: "form-control" %>
</div>
<% end -%>
<% end -%>
<div>
<%%= form.submit class: "btn btn-primary" %>
</div>
<%% end %>
The Show View
lib/templates/erb/scaffold/show.html.erb.tt:
<h1><%%= @<%= singular_table_name %>.<%= attributes.first&.column_name || 'id' %> %></h1>
<dl>
<% attributes.reject(&:password_digest?).each do |attribute| -%>
<dt><%= attribute.human_name %></dt>
<dd><%%= @<%= singular_table_name %>.<%= attribute.column_name %> %></dd>
<% end -%>
</dl>
<%%= link_to "Edit", edit_<%= singular_table_name %>_path(@<%= singular_table_name %>) %> |
<%%= link_to "Back", <%= index_helper %>_path %>
Tips
- Use
--pretendto preview what a generator will create without writing files:rails g scaffold Post title body:text --pretend - Check the Rails source for available variables. The scaffold templates have access to
attributes,singular_table_name,plural_table_name,class_name,human_name,orm_class,index_helper, and more. - Bootstrap or Tailwind classes go right in the templates. Set them once and every scaffold matches your design system.
- Test your templates by generating a throwaway scaffold. If the output has syntax errors, the template has a bug — fix it before you forget.
In the next post I extend this approach with ActiveRecord reflection to automatically detect associations and generate smarter views.