The Power of Rails Generators
One of the things that initially drew me to rails, aside from ruby itself was the ability to generate boilerplate code and build CRUD actions in a matter of seconds. The classic build a blog example.
Since then, a lot of teams that I've been on have had system patterns that would have been 10x faster to implement if they used a simple generator. A great candidate that I saw was a set of admin functions that were called to perform common or even one-off actions (like manually reset a user's password, invalidate caches, clean certain db records, etc.). Luckily Rails provides a generator generator rails g generator ...
. That's great if you want a service object, form object, or component generator, for example.
The way that rails implements generators makes it easy to override the templates that it uses as well. The lookup path prioritizes local lib paths over the paths from the rails gems like active_record.
For example, you could add # frozenstringliteral: true to all your generated files, or null: false
to the timestamps statement in migrations, or even add column annotations as comments to model files.
As an aside, if you use neovim, you can add combined language grammars for embedded ruby (eruby) and ruby in your init.lua
file:
vim.filetype.add({
pattern = {
[".*%.rb%.tt"] = "eruby.ruby",
},
})
Here, I added a couple of things to the migration template like null: false, default: -> { "now()" }
, and the VERSION string as a comment:
lib/templates/active_record/migration/create_table_migration.rb.tt
# frozen_string_literal: true
# Created at <%= Time.current %>
# VERSION=<%= migration_number %>
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
def change
create_table :<%= table_name %><%= primary_key_type %> do |t|
<%- attributes.each do |attribute| -%>
<%- if attribute.password_digest? -%>
t.string :password_digest<%= attribute.inject_options %>
<%- elsif attribute.token? -%>
t.string :<%= attribute.name %><%= attribute.inject_options %>
<%- elsif attribute.reference? -%>
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %><%= foreign_key_type %>
<%- elsif !attribute.virtual? -%>
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
<%- end -%>
<%- end -%>
<%- if options[:timestamps] -%>
t.timestamps null: false, default: -> { "now()" }
<%- end -%>
end
<%- attributes.select(&:token?).each do |attribute| -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>, unique: true
<%- end -%>
<%- attributes_with_index.each do |attribute| -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<%- end -%>
end
end
And here, I added some annotations to the model generator:
lib/templates/active_record/model/model.rb.tt
# frozen_string_literal: true
<%- module_namespacing do -%>
# | column | type | index? | unique | null |
# |----------------------|------------|--------|--------|--------|
<%- attributes.each do |attribute| -%>
# | <%=- attribute.name.ljust(20) -%> | <%=- attribute.type.to_s.ljust(10) -%> | <%= (attribute.has_index? ? "true" : "").ljust(6) -%> | <%=- (attribute.has_uniq_index? ? "true" : "").ljust(6) -%> | <%=- attribute.attr_options[:null].to_s.ljust(6) -%> |
<%- end -%>
class <%= class_name %> < <%= parent_class_name.classify %>
<%- attributes.select(&:reference?).each do |attribute| -%>
belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
<%- end -%>
<%- attributes.select(&:rich_text?).each do |attribute| -%>
has_rich_text :<%= attribute.name %>
<%- end -%>
<%- attributes.select(&:attachment?).each do |attribute| -%>
has_one_attached :<%= attribute.name %>
<%- end -%>
<%- attributes.select(&:attachments?).each do |attribute| -%>
has_many_attached :<%= attribute.name %>
<%- end -%>
<%- attributes.select{|a| a.name.end_with?('token')}.each do |attribute| -%>
has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
<%- end -%>
<%- if attributes.any?{|a| a.name == 'password_digest'} -%>
has_secure_password
<%- end -%>
end
<%- end -%>
Now if I run the model generator:
rails g model Company name:citext\!:uniq website bio:text
I get:
# frozen_string_literal: true
# Created at 2025-11-02 16:28:56 UTC
# VERSION=20251102162856
class CreateCompanies < ActiveRecord::Migration[8.0]
def change
create_table :companies do |t|
t.citext :name, null: false
t.string :website
t.text :bio
t.timestamps null: false, default: -> { "now()" }
end
add_index :companies, :name, unique: true
end
end
app/models/company.rb
# frozen_string_literal: true
# | column | type | index? | unique | null |
# |----------------------|------------|--------|--------|--------|
# | name | citext | true | true | false |
# | website | string | | | |
# | bio | text | | | |
class Company < ApplicationRecord
end
Pretty cool!