el-feo

rails-generators

Create expert-level Ruby on Rails generators for models, services, controllers, and full-stack features. Use when building custom generators, scaffolds, or code generation tools for Rails applications, or when the user mentions Rails generators, Thor DSL, or automated code generation.

el-feo 10 1 Updated 5mo ago

Resources

1
GitHub

Install

npx skillscat add el-feo/ai-context/rails-generators

Install via the SkillsCat registry.

SKILL.md
Build production-ready Rails generators that automate repetitive coding tasks and enforce architectural patterns. This skill covers creating custom generators for models, service objects, API scaffolds, and complete features with migrations, tests, and documentation. Create a simple service object generator:
# lib/generators/service/service_generator.rb
module Generators
  class ServiceGenerator < Rails::Generators::NamedBase
    source_root File.expand_path('templates', __dir__)

    def create_service_file
      template 'service.rb.tt', "app/services/#{file_name}_service.rb"
    end

    def create_service_test
      template 'service_test.rb.tt', "test/services/#{file_name}_service_test.rb"
    end
  end
end

Template file (templates/service.rb.tt):

class <%= class_name %>Service
  def initialize
  end

  def call
    # Implementation goes here
  end
end

Invoke with: rails generate service payment_processor
</basic_generator>

**Generator location**: `lib/generators/[name]/[name]_generator.rb` **Template location**: `lib/generators/[name]/templates/` **Test location**: `test/generators/[name]_generator_test.rb` </usage_pattern> </quick_start> Create custom generators when you need to:
  • Enforce architectural patterns: Service objects, form objects, presenters, query objects
  • Reduce boilerplate: API controllers with standard CRUD, serializers, policy objects
  • Maintain consistency: Team conventions for file structure, naming, and organization
  • Automate complex setup: Multi-file features with migrations, tests, and documentation
  • Override Rails defaults: Customize scaffold behavior for your application's needs</when_to_create>
Rails 8 introduced the authentication generator (`rails generate authentication`) which demonstrates modern generator patterns including: - ActionCable integration for real-time features - Controller concerns for shared behavior - Mailer generation with templates - Comprehensive view scaffolding

Study Rails 8 built-in generators for current best practices.
</rails_8_updates>

**Choose base class**:
  • Rails::Generators::Base: Simple generators without required arguments
  • Rails::Generators::NamedBase: Generators requiring a name argument
class ServiceGenerator < Rails::Generators::NamedBase
  # Automatically provides: name, class_name, file_name, plural_name
end
</step_1> **Define source root and options**:
source_root File.expand_path('templates', __dir__)

class_option :namespace, type: :string, default: nil, desc: "Namespace for the service"
class_option :skip_tests, type: :boolean, default: false, desc: "Skip test files"

Access options with: options[:namespace]
</step_2>

**Add public methods** (executed in definition order):
def create_service_file
  template 'service.rb.tt', service_file_path
end

def create_test_file
  return if options[:skip_tests]
  template 'service_test.rb.tt', test_file_path
end

private

def service_file_path
  if options[:namespace]
    "app/services/#{options[:namespace]}/#{file_name}_service.rb"
  else
    "app/services/#{file_name}_service.rb"
  end
end
</step_3> **Create ERB templates** (`.tt` extension):
<% if options[:namespace] -%>
module <%= options[:namespace].camelize %>
  class <%= class_name %>Service
    def call
      # Implementation
    end
  end
end
<% else -%>
class <%= class_name %>Service
  def call
    # Implementation
  end
end
<% end -%>

Important: Use <%% to output literal <% in generated files.
</step_4>

**Test the generator** (see [Testing](#testing) section):
rails generate service payment_processor --namespace=billing
rails generate service notifier --skip-tests
</step_5> **Custom model with associations and scopes**:
class CustomModelGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  class_option :parent, type: :string, desc: "Parent model for belongs_to"
  class_option :scope, type: :array, desc: "Scopes to generate"

  def create_migration
    migration_template 'migration.rb.tt',
      "db/migrate/create_#{table_name}.rb"
  end

  def create_model_file
    template 'model.rb.tt', "app/models/#{file_name}.rb"
  end

  def create_test_file
    template 'model_test.rb.tt', "test/models/#{file_name}_test.rb"
  end
end

See references/model-generator.md for complete example.
</model_generator>

**Service object with result object pattern**:
class ServiceGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  class_option :result_object, type: :boolean, default: true

  def create_service
    template 'service.rb.tt', "app/services/#{file_name}_service.rb"
  end

  def create_result_object
    return unless options[:result_object]
    template 'result.rb.tt', "app/services/#{file_name}_result.rb"
  end
end

See references/service-generator.md for complete example.
</service_object_generator>

**API controller with serializer**:
class ApiControllerGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  class_option :serializer, type: :string, default: 'active_model_serializers'
  class_option :actions, type: :array, default: %w[index show create update destroy]

  def create_controller
    template 'controller.rb.tt',
      "app/controllers/api/v1/#{file_name.pluralize}_controller.rb"
  end

  def create_serializer
    template "serializer_#{options[:serializer]}.rb.tt",
      "app/serializers/#{file_name}_serializer.rb"
  end

  def add_routes
    route "namespace :api do\n    namespace :v1 do\n      resources :#{file_name.pluralize}\n    end\n  end"
  end
end

See references/api-generator.md for complete example.
</api_controller_generator>

**Complete feature scaffold**:
class FeatureGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  class_option :api, type: :boolean, default: false

  def create_model
    invoke 'model', [name], migration: true
  end

  def create_controller
    if options[:api]
      invoke 'api_controller', [name]
    else
      invoke 'controller', [name], actions: %w[index show new create edit update destroy]
    end
  end

  def create_views
    return if options[:api]
    %w[index show new edit _form].each do |view|
      template "views/#{view}.html.erb.tt",
        "app/views/#{file_name.pluralize}/#{view}.html.erb"
    end
  end

  def create_tests
    invoke 'test_unit:model', [name]
    invoke 'test_unit:controller', [name]
    invoke 'test_unit:system', [name] unless options[:api]
  end
end

See references/scaffold-generator.md for complete example.
</full_stack_scaffold>
</common_patterns>

**Generator hooks** enable modular composition and test framework integration:
class ServiceGenerator < Rails::Generators::NamedBase
  hook_for :test_framework, as: :service
end

This automatically invokes test_unit:service or rspec:service based on configuration.

Creating hook responders:

# lib/generators/rspec/service/service_generator.rb
module Rspec
  module Generators
    class ServiceGenerator < Rails::Generators::NamedBase
      source_root File.expand_path('templates', __dir__)

      def create_service_spec
        template 'service_spec.rb.tt',
          "spec/services/#{file_name}_service_spec.rb"
      end
    end
  end
end

See references/hooks.md for hook patterns and fallback configuration.

**Invoke other generators** from your generator:
def create_dependencies
  invoke 'model', [name], migration: true
  invoke 'service', ["#{name}_processor"]
  invoke 'serializer', [name]
end

Conditional invocation:

def create_optional_files
  invoke 'mailer', [name] if options[:mailer]
  invoke 'job', ["#{name}_job"] if options[:background_job]
end
</generator_composition> **Namespace generators** for organization:
# lib/generators/admin/resource/resource_generator.rb
module Admin
  module Generators
    class ResourceGenerator < Rails::Generators::NamedBase
      source_root File.expand_path('templates', __dir__)

      def create_admin_resource
        template 'resource.rb.tt',
          "app/admin/#{file_name}.rb"
      end
    end
  end
end

Invoke with: rails generate admin:resource User

Generator search path:

  1. rails/generators/[name]/[name]_generator.rb
  2. generators/[name]/[name]_generator.rb
  3. rails/generators/[name]_generator.rb
  4. generators/[name]_generator.rb

See references/namespacing.md for fallback patterns.

**Thor::Actions methods** available in generators:
# Create files
create_file 'config/settings.yml', yaml_content
copy_file 'template.rb', 'config/template.rb'
template 'config.rb.tt', 'config/settings.rb'

# Modify existing files
insert_into_file 'config/routes.rb', route_content, after: "Rails.application.routes.draw do\n"
gsub_file 'config/application.rb', /old_value/, 'new_value'
comment_lines 'config/environments/production.rb', /config.assets.compile/

# Directory operations
empty_directory 'app/services'
directory 'templates/views', 'app/views/admin'

# Rails-specific helpers
initializer 'service_config.rb', config_content
lib 'custom_validator.rb', validator_content
rakefile 'import_tasks.rake', rake_content
route "namespace :api do\n  resources :users\n end"

See references/file-actions.md for complete reference.
</file_manipulation>

<template_system> **ERB template features**: <pre><code class="language-erb" data-language="erb">&lt;%# Variables from generator available automatically %&gt; class &lt;%= class_name %&gt;Service &lt;%- if options[:async] -%&gt; include AsyncService &lt;%- end -%&gt; def initialize(&lt;%= attributes.map(&amp;:name).join(&#39;, &#39;) %&gt;) &lt;%- attributes.each do |attr| -%&gt; @&lt;%= attr.name %&gt; = &lt;%= attr.name %&gt; &lt;%- end -%&gt; end end</code></pre><p><strong>Escaping for nested ERB</strong> (template generates another template):</p> <pre><code class="language-erb" data-language="erb">&lt;%# This will be evaluated when user uses the generated file %&gt; &lt;%%= render partial: &#39;form&#39;, locals: { &lt;%= singular_name %&gt;: @&lt;%= singular_name %&gt; } %&gt;</code></pre><p><strong>Conditional whitespace control</strong> with <code>-</code>:</p> <ul> <li><code>&lt;%-</code> suppresses leading whitespace</li> <li><code>-%&gt;</code> suppresses trailing whitespace</li> </ul> <p>See <a href="#" class="file-link" data-file-path="references/templates.md">references/templates.md</a> for template patterns.<br></template_system><br></advanced_features></p> <testing> <rails_test_framework> **Testing with Rails::Generators::Testing::Behavior**: <pre><code class="language-ruby" data-language="ruby">require &#39;test_helper&#39; require &#39;generators/service/service_generator&#39; class ServiceGeneratorTest &lt; Rails::Generators::TestCase tests ServiceGenerator destination File.expand_path(&#39;../tmp&#39;, __dir__) setup :prepare_destination test &quot;generates service file&quot; do run_generator [&quot;payment_processor&quot;] assert_file &quot;app/services/payment_processor_service.rb&quot; do |content| assert_match(/class PaymentProcessorService/, content) assert_match(/def call/, content) end end test &quot;generates with namespace option&quot; do run_generator [&quot;payment&quot;, &quot;--namespace=billing&quot;] assert_file &quot;app/services/billing/payment_service.rb&quot; do |content| assert_match(/module Billing/, content) assert_match(/class PaymentService/, content) end end test &quot;skips tests when flag provided&quot; do run_generator [&quot;payment&quot;, &quot;--skip-tests&quot;] assert_file &quot;app/services/payment_service.rb&quot; assert_no_file &quot;test/services/payment_service_test.rb&quot; end end</code></pre><p><strong>Available assertions</strong>:</p> <ul> <li><code>assert_file(path) { |content| ... }</code> - File exists with expected content</li> <li><code>assert_no_file(path)</code> - File does not exist</li> <li><code>assert_migration(path)</code> - Migration file exists</li> <li><code>assert_class_method(method, content)</code> - Class method defined</li> <li><code>assert_instance_method(method, content)</code> - Instance method defined</li> </ul> <p>See <a href="#" class="file-link" data-file-path="references/testing-rails.md">references/testing-rails.md</a> for comprehensive testing patterns.<br></rails_test_framework></p> <rspec_testing> **Testing with RSpec and generator_spec**: <p>Add to Gemfile: <code>gem &#39;generator_spec&#39;, group: :development</code></p> <pre><code class="language-ruby" data-language="ruby">require &#39;generator_spec&#39; RSpec.describe ServiceGenerator, type: :generator do destination File.expand_path(&#39;../../tmp&#39;, __FILE__) before do prepare_destination end context &quot;with default options&quot; do before do run_generator [&quot;payment_processor&quot;] end it &quot;creates service file&quot; do expect(destination_root).to have_structure { directory &quot;app/services&quot; do file &quot;payment_processor_service.rb&quot; do contains &quot;class PaymentProcessorService&quot; contains &quot;def call&quot; end end } end it &quot;creates test file&quot; do expect(destination_root).to have_structure { directory &quot;test/services&quot; do file &quot;payment_processor_service_test.rb&quot; end } end end context &quot;with namespace option&quot; do before do run_generator [&quot;payment&quot;, &quot;--namespace=billing&quot;] end it &quot;creates namespaced service&quot; do assert_file &quot;app/services/billing/payment_service.rb&quot; do |content| expect(content).to match(/module Billing/) expect(content).to match(/class PaymentService/) end end end end</code></pre><p><strong>Alternative: Test against dummy app</strong>:</p> <pre><code class="language-ruby" data-language="ruby">RSpec.describe &quot;ServiceGenerator&quot; do it &quot;generates correct service file&quot; do Dir.chdir(dummy_app_path) do `rails generate service payment_processor` service_file = File.read(&#39;app/services/payment_processor_service.rb&#39;) expect(service_file).to include(&#39;class PaymentProcessorService&#39;) # Cleanup FileUtils.rm_rf(&#39;app/services/payment_processor_service.rb&#39;) end end end</code></pre><p>See <a href="#" class="file-link" data-file-path="references/testing-rspec.md">references/testing-rspec.md</a> for RSpec patterns and generator_spec usage.<br></rspec_testing></p> <manual_testing> **Manual testing workflow**: <ol> <li>Generate in test Rails app:</li> </ol> <pre><code class="language-bash" data-language="bash">cd test/dummy rails generate service payment_processor --namespace=billing</code></pre><ol start="2"> <li>Verify generated files:</li> </ol> <pre><code class="language-bash" data-language="bash">tree app/services cat app/services/billing/payment_processor_service.rb</code></pre><ol start="3"> <li>Test destruction (if implemented):</li> </ol> <pre><code class="language-bash" data-language="bash">rails destroy service payment_processor --namespace=billing</code></pre><ol start="4"> <li>Test edge cases:</li> </ol> <pre><code class="language-bash" data-language="bash">rails generate service payment_processor --skip-tests rails generate service payment_processor --namespace=very/deep/namespace</code></pre></manual_testing> </testing> <validation> <checklist> Before considering a generator complete, verify: <ul> <li><input disabled="" type="checkbox"> Generator inherits from appropriate base class</li> <li><input disabled="" type="checkbox"> <code>source_root</code> points to templates directory</li> <li><input disabled="" type="checkbox"> All options have appropriate types and defaults</li> <li><input disabled="" type="checkbox"> Public methods execute in correct order</li> <li><input disabled="" type="checkbox"> Templates use correct ERB syntax (<code>.tt</code> extension)</li> <li><input disabled="" type="checkbox"> File paths handle namespacing correctly</li> <li><input disabled="" type="checkbox"> Generator works with and without options</li> <li><input disabled="" type="checkbox"> Tests cover default behavior and all options</li> <li><input disabled="" type="checkbox"> Generator can be destroyed (if applicable)</li> <li><input disabled="" type="checkbox"> Documentation includes usage examples</li> <li><input disabled="" type="checkbox"> Edge cases handled (special characters, deep namespacing)</checklist></li> </ul> <common_issues> **Missing source_root**: Templates not found ```ruby # Add this to generator class source_root File.expand_path('templates', __dir__) ``` <p><strong>Incorrect template syntax</strong>: File generated with wrong ERB tags</p> <pre><code class="language-ruby" data-language="ruby"># Use &lt;%% for literal ERB in generated files &lt;%%= @user.name %&gt; # Generates: &lt;%= @user.name %&gt;</code></pre><p><strong>Option not recognized</strong>: Check option definition</p> <pre><code class="language-ruby" data-language="ruby">class_option :namespace, type: :string # Not :namespace_option # Access with: options[:namespace]</code></pre><p><strong>Method order issues</strong>: Methods execute in definition order</p> <pre><code class="language-ruby" data-language="ruby"># This runs first def create_model end # This runs second def create_controller end</code></pre></common_issues> </validation> <examples> <service_with_dependencies> **Service generator with dependency injection**: <pre><code class="language-ruby" data-language="ruby">class AdvancedServiceGenerator &lt; Rails::Generators::NamedBase source_root File.expand_path(&#39;templates&#39;, __dir__) class_option :dependencies, type: :array, default: [] class_option :async, type: :boolean, default: false def create_service template &#39;service.rb.tt&#39;, service_path end def create_test template &#39;service_test.rb.tt&#39;, test_path end private def service_path &quot;app/services/#{file_name}_service.rb&quot; end def test_path &quot;test/services/#{file_name}_service_test.rb&quot; end def dependency_params options[:dependencies].map { |dep| &quot;#{dep}:&quot; }.join(&#39;, &#39;) end end</code></pre><p>Template (service.rb.tt):</p> <pre><code class="language-erb" data-language="erb">class &lt;%= class_name %&gt;Service &lt;%- if options[:async] -%&gt; include ActiveJob::Helpers &lt;%- end -%&gt; &lt;%- if options[:dependencies].any? -%&gt; def initialize(&lt;%= dependency_params %&gt;) &lt;%- options[:dependencies].each do |dep| -%&gt; @&lt;%= dep %&gt; = &lt;%= dep %&gt; &lt;%- end -%&gt; end &lt;%- else -%&gt; def initialize end &lt;%- end -%&gt; def call # Implementation end end</code></pre></service_with_dependencies> <query_object_generator> **Query object generator**: <pre><code class="language-ruby" data-language="ruby">class QueryGenerator &lt; Rails::Generators::NamedBase source_root File.expand_path(&#39;templates&#39;, __dir__) class_option :model, type: :string, required: true class_option :scopes, type: :array, default: [] def create_query template &#39;query.rb.tt&#39;, &quot;app/queries/#{file_name}_query.rb&quot; end def create_test template &#39;query_test.rb.tt&#39;, &quot;test/queries/#{file_name}_query_test.rb&quot; end end</code></pre></query_object_generator> </examples> <reference_guides> **Detailed references**: <ul> <li><a href="#" class="file-link" data-file-path="references/model-generator.md">Model generator patterns</a> - Custom models with migrations and associations</li> <li><a href="#" class="file-link" data-file-path="references/service-generator.md">Service generator patterns</a> - Service objects with result objects</li> <li><a href="#" class="file-link" data-file-path="references/api-generator.md">API generator patterns</a> - Controllers, serializers, and routes</li> <li><a href="#" class="file-link" data-file-path="references/scaffold-generator.md">Scaffold patterns</a> - Full-stack feature generation</li> <li><a href="#" class="file-link" data-file-path="references/hooks.md">Hooks and composition</a> - Generator hooks and fallbacks</li> <li><a href="#" class="file-link" data-file-path="references/namespacing.md">Namespacing</a> - Organizing generators with namespaces</li> <li><a href="#" class="file-link" data-file-path="references/file-actions.md">File manipulation</a> - Thor::Actions reference</li> <li><a href="#" class="file-link" data-file-path="references/templates.md">Template system</a> - ERB template patterns</li> <li><a href="#" class="file-link" data-file-path="references/testing-rails.md">Rails testing</a> - Rails::Generators::TestCase patterns</li> <li><a href="#" class="file-link" data-file-path="references/testing-rspec.md">RSpec testing</a> - generator_spec and RSpec patterns</reference_guides></li> </ul> <success_criteria> A well-built Rails generator has: <ul> <li>Clear inheritance from Rails::Generators::Base or NamedBase</li> <li>Properly configured source_root pointing to templates</li> <li>Well-defined class_option declarations with appropriate types</li> <li>Public methods that execute in logical order</li> <li>ERB templates with correct syntax and variable interpolation</li> <li>Comprehensive tests covering default and optional behaviors</li> <li>Error handling for edge cases (missing arguments, invalid options)</li> <li>Documentation with usage examples</li> <li>Consistent naming following Rails conventions</li> <li>Works correctly with generator hooks and composition</success_criteria></li> </ul> <p><strong>Sources</strong>:</p> <ul> <li><a href="https://guides.rubyonrails.org/generators.html" target="_blank" rel="noopener noreferrer nofollow">Creating and Customizing Rails Generators &amp; Templates — Ruby on Rails Guides</a></li> <li><a href="https://blog.saeloun.com/2025/05/12/rails-8-adds-built-in-authentication-generator/" target="_blank" rel="noopener noreferrer nofollow">Rails 8 adds built in authentication generator | Saeloun Blog</a></li> <li><a href="https://blog.saeloun.com/2023/05/31/customizing-rails-generators-using-thor-templates/" target="_blank" rel="noopener noreferrer nofollow">Shaping Rails to Your Needs, Customizing Rails Generators using Thor Templates | Saeloun Blog</a></li> <li><a href="https://github.com/rails/thor/wiki/Generators" target="_blank" rel="noopener noreferrer nofollow">Generators · rails/thor Wiki · GitHub</a></li> <li><a href="https://github.com/stevehodgkiss/generator_spec" target="_blank" rel="noopener noreferrer nofollow">GitHub: generator_spec</a></li> <li><a href="https://blog.appsignal.com/2024/02/07/a-deep-dive-into-rspec-tests-in-ruby-on-rails.html" target="_blank" rel="noopener noreferrer nofollow">A Deep Dive Into RSpec Tests in Ruby on Rails | AppSignal Blog</a></li> </ul>