| name | rails-generators |
| description | 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. |
# 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
- 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
Study Rails 8 built-in generators for current best practices.
Rails::Generators::Base: Simple generators without required argumentsRails::Generators::NamedBase: Generators requiring a name argument
class ServiceGenerator < Rails::Generators::NamedBase
# Automatically provides: name, class_name, file_name, plural_name
end
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]
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
<% 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.
rails generate service payment_processor --namespace=billing
rails generate service notifier --skip-tests
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.
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.
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.
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.
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.
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
# 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:
rails/generators/[name]/[name]_generator.rbgenerators/[name]/[name]_generator.rbrails/generators/[name]_generator.rbgenerators/[name]_generator.rb
See references/namespacing.md for fallback patterns.
# 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.
<%# Variables from generator available automatically %>
class <%= class_name %>Service
<%- if options[:async] -%>
include AsyncService
<%- end -%>
def initialize(<%= attributes.map(&:name).join(', ') %>)
<%- attributes.each do |attr| -%>
@<%= attr.name %> = <%= attr.name %>
<%- end -%>
end
end
Escaping for nested ERB (template generates another template):
<%# This will be evaluated when user uses the generated file %>
<%%= render partial: 'form', locals: { <%= singular_name %>: @<%= singular_name %> } %>
Conditional whitespace control with -:
<%-suppresses leading whitespace-%>suppresses trailing whitespace
See references/templates.md for template patterns.
require 'test_helper'
require 'generators/service/service_generator'
class ServiceGeneratorTest < Rails::Generators::TestCase
tests ServiceGenerator
destination File.expand_path('../tmp', __dir__)
setup :prepare_destination
test "generates service file" do
run_generator ["payment_processor"]
assert_file "app/services/payment_processor_service.rb" do |content|
assert_match(/class PaymentProcessorService/, content)
assert_match(/def call/, content)
end
end
test "generates with namespace option" do
run_generator ["payment", "--namespace=billing"]
assert_file "app/services/billing/payment_service.rb" do |content|
assert_match(/module Billing/, content)
assert_match(/class PaymentService/, content)
end
end
test "skips tests when flag provided" do
run_generator ["payment", "--skip-tests"]
assert_file "app/services/payment_service.rb"
assert_no_file "test/services/payment_service_test.rb"
end
end
Available assertions:
assert_file(path) { |content| ... }- File exists with expected contentassert_no_file(path)- File does not existassert_migration(path)- Migration file existsassert_class_method(method, content)- Class method definedassert_instance_method(method, content)- Instance method defined
See references/testing-rails.md for comprehensive testing patterns.
Add to Gemfile: gem 'generator_spec', group: :development
require 'generator_spec'
RSpec.describe ServiceGenerator, type: :generator do
destination File.expand_path('../../tmp', __FILE__)
before do
prepare_destination
end
context "with default options" do
before do
run_generator ["payment_processor"]
end
it "creates service file" do
expect(destination_root).to have_structure {
directory "app/services" do
file "payment_processor_service.rb" do
contains "class PaymentProcessorService"
contains "def call"
end
end
}
end
it "creates test file" do
expect(destination_root).to have_structure {
directory "test/services" do
file "payment_processor_service_test.rb"
end
}
end
end
context "with namespace option" do
before do
run_generator ["payment", "--namespace=billing"]
end
it "creates namespaced service" do
assert_file "app/services/billing/payment_service.rb" do |content|
expect(content).to match(/module Billing/)
expect(content).to match(/class PaymentService/)
end
end
end
end
Alternative: Test against dummy app:
RSpec.describe "ServiceGenerator" do
it "generates correct service file" do
Dir.chdir(dummy_app_path) do
`rails generate service payment_processor`
service_file = File.read('app/services/payment_processor_service.rb')
expect(service_file).to include('class PaymentProcessorService')
# Cleanup
FileUtils.rm_rf('app/services/payment_processor_service.rb')
end
end
end
See references/testing-rspec.md for RSpec patterns and generator_spec usage.
- Generate in test Rails app:
cd test/dummy
rails generate service payment_processor --namespace=billing
- Verify generated files:
tree app/services
cat app/services/billing/payment_processor_service.rb
- Test destruction (if implemented):
rails destroy service payment_processor --namespace=billing
- Test edge cases:
rails generate service payment_processor --skip-tests
rails generate service payment_processor --namespace=very/deep/namespace
- Generator inherits from appropriate base class
-
source_rootpoints to templates directory - All options have appropriate types and defaults
- Public methods execute in correct order
- Templates use correct ERB syntax (
.ttextension) - File paths handle namespacing correctly
- Generator works with and without options
- Tests cover default behavior and all options
- Generator can be destroyed (if applicable)
- Documentation includes usage examples
- Edge cases handled (special characters, deep namespacing)
Incorrect template syntax: File generated with wrong ERB tags
# Use <%% for literal ERB in generated files
<%%= @user.name %> # Generates: <%= @user.name %>
Option not recognized: Check option definition
class_option :namespace, type: :string # Not :namespace_option
# Access with: options[:namespace]
Method order issues: Methods execute in definition order
# This runs first
def create_model
end
# This runs second
def create_controller
end
class AdvancedServiceGenerator < Rails::Generators::NamedBase
source_root File.expand_path('templates', __dir__)
class_option :dependencies, type: :array, default: []
class_option :async, type: :boolean, default: false
def create_service
template 'service.rb.tt', service_path
end
def create_test
template 'service_test.rb.tt', test_path
end
private
def service_path
"app/services/#{file_name}_service.rb"
end
def test_path
"test/services/#{file_name}_service_test.rb"
end
def dependency_params
options[:dependencies].map { |dep| "#{dep}:" }.join(', ')
end
end
Template (service.rb.tt):
class <%= class_name %>Service
<%- if options[:async] -%>
include ActiveJob::Helpers
<%- end -%>
<%- if options[:dependencies].any? -%>
def initialize(<%= dependency_params %>)
<%- options[:dependencies].each do |dep| -%>
@<%= dep %> = <%= dep %>
<%- end -%>
end
<%- else -%>
def initialize
end
<%- end -%>
def call
# Implementation
end
end
class QueryGenerator < Rails::Generators::NamedBase
source_root File.expand_path('templates', __dir__)
class_option :model, type: :string, required: true
class_option :scopes, type: :array, default: []
def create_query
template 'query.rb.tt', "app/queries/#{file_name}_query.rb"
end
def create_test
template 'query_test.rb.tt', "test/queries/#{file_name}_query_test.rb"
end
end
- Model generator patterns - Custom models with migrations and associations
- Service generator patterns - Service objects with result objects
- API generator patterns - Controllers, serializers, and routes
- Scaffold patterns - Full-stack feature generation
- Hooks and composition - Generator hooks and fallbacks
- Namespacing - Organizing generators with namespaces
- File manipulation - Thor::Actions reference
- Template system - ERB template patterns
- Rails testing - Rails::Generators::TestCase patterns
- RSpec testing - generator_spec and RSpec patterns
- Clear inheritance from Rails::Generators::Base or NamedBase
- Properly configured source_root pointing to templates
- Well-defined class_option declarations with appropriate types
- Public methods that execute in logical order
- ERB templates with correct syntax and variable interpolation
- Comprehensive tests covering default and optional behaviors
- Error handling for edge cases (missing arguments, invalid options)
- Documentation with usage examples
- Consistent naming following Rails conventions
- Works correctly with generator hooks and composition
Sources:
- Creating and Customizing Rails Generators & Templates — Ruby on Rails Guides
- Rails 8 adds built in authentication generator | Saeloun Blog
- Shaping Rails to Your Needs, Customizing Rails Generators using Thor Templates | Saeloun Blog
- Generators · rails/thor Wiki · GitHub
- GitHub: generator_spec
- A Deep Dive Into RSpec Tests in Ruby on Rails | AppSignal Blog