Rails Generators help streamline spinning up new code. Whether it’s a new model, controller, resource, migration, job, or something else, generators remove a lot of the tedious leg work of bootstrapping new functionality. While anybody that’s used Rails has likely run one of the built-in generators at once or twice, there’s some real power in creating custom generators.

I’ve been spending more time creating custom generators, but the more I have, the more I’ve collected notes on the rough edges to make it a little easier every time. So if you had no idea you could create your own generators, we’ll explore how it works by getting into some of the lower-level details for a basic generator.

On the other hand, if you already knew and you’ve even created some custom generators yourself but stumbled enough that you don’t turn to them unless you really need to, hopefully some of these notes and references can help.

A Little Preparation

We’re not going to go super-deep in customizing the generators to support different test suites, ORMs, or any of that. We’ll keep things simple and focus on building a smaller generator to dial in the basics. Then you can create generators to help with small repetitive tasks in your project or use this as a stepping stone towards more advanced generators. Before we get started, here’s a few links to keep handy for additional reference:

  1. The Rails Generators Guide
  2. Rails::Generators Docs
  3. Rails::Generators::TestCase Docs
  4. The Thor Wiki - Thor is the underlying toolkit that powers Rails’ generators.
  5. The Source for the Built-in Rails Generators - These can be fairly complex because they’re some of the more advanced examples you can find, but some like the task generator provide good self-contained examples to learn from.
  6. The Rails Tests for its own Generators - Like the generators themselves, the tests can be a bit overwhelming at first for the more complex generators, and some of the testing functionality has been extracted into shared modules. Again, some like the task generator tests are a good starting point that’s less overwhelming.

It’s definitely worth it to spend at least a little time perusing some of those links before diving in. I’ll do my best to provide everything you need right here, but coming into the process with more context would likely help before going much further.

Cover of Frictionless Generators Interested in moving faster with Rails?

I’m writing a book to help streamline creating custom Rails generators so you can save time and skip all that copying and pasting and searching and replacing. Join the list to be notified about the release in September 2023.

No-nonsense, one-click unsubscribes.

In the interest of keeping things simple, we’ll create a generator that makes it easier to spin up a PORO and a corresponding test file to go with it. For now, the generated code isn’t the critical part. We’re going to focus purely on the generation portion of the process and keep the generated code as boring as possible.

Generating a Generator

Not to get too meta, but Rails provides a built-in generator for generating generators. We’ll use that and jump right in, but remember that you can append the --pretend option if you prefer to see a preview of the files it will generate ahead of time.

bin/rails generate generator Plain
Figure 1

An example of using a generator to generator a generator.


You should end up seeing results that look like this:

create lib/generators/plaincreate lib/generators/plain/plain_generator.rbcreate lib/generators/plain/USAGEcreate lib/generators/plain/templatesinvoke test_unitcreate test/lib/generators/plain_generator_test.rb
Figure 2

The generator creates a skeleton with the directories and test placeholder for your new generator command. Rails provides some great tooling for testing the generators.


The generator provides a few things for us inside of the lib/generators/plain. It adds the generator itself, a file for explaining the usage, a folder for the templates that we’ll use to generate our actual files, and the test file for the generator. So far so good. Let’s look at how the generator starts off for each of these.

The Generator File

The generator itself starts fairly simply, but don’t let that fool you. (Figure 3) There’s some pretty great functionality exposed to your generator through its parent class, so let’s zoom in on the parent class: Rails::Generators::NamedBase. The parent class is interesting both for what it provides directly and what it exposes from the grandparent class, Rails::Generators::Base, which which provides a plethora of convenience methods for the types of file system work you’ll likely want to perform in your generator.

While Rails::Generators::NamedBase is the default parent used in a new generator, it’s not strictly necessary to use it. In some cases, you may want to use Rails::Generator::Base directly instead , but we’ll get to that in detail later.

lib/generators/plain/plain_generator.rb class PlainGenerator < Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__)end
Figure 3

We have two key things to note here. The generator inherits from Rails::Generators::NamedBase, and it sets the source root so the generator knows where to look for any templates/files. This is usually the ‘templates’ directory in the generator’s directory, but you can change it if you have a reason to.


For the most part, NamedBase adds convenience methods related to the name passed to the generator, but it also makes a key assumption about arguments in that the first value passed to it gets parsed as the name upon which many of the additional convenience methods act. So with NamedBased your generator command restricts you a bit with that first argument. (Figure 4)

bin/rails generate generator <name>
Figure 4

With Rails::Generators::NamedBase, the first argument represents the name attribute for the generator, so if you want a different type of argument for the first value, you’ll likely want to use the Rails::Generators::Base class instead.


In most cases, using NamedBase is what you want, but in some simple cases, I’ve found it handier to use Base instead. With the former, the first command line argument will be captured as the name, but with the latter, there are no pre-defined arguments so you have complete control over that first argument.

For example, I use a generator to start to new blog posts. When I start a new post, the generator really only needs a title in quotes, but with NamedBase, the command “swallows” the initial portion of the title string up to the first space. So it would require passing an argument before the title in order to fulfill the name argument’s position. For this context, however, I don’t have a use for the name argument. So by inheriting from Base instead, I’m able to skip passing an otherwise unused name value. (Figure 5)

bin/rails generate entry <name> "Blog Post Title"bin/rails generate entry "Blog Post Title"
Figure 5

Using the Base version instead of NamedBase means you don’t get the name helpers, but you can skip the pre-defined <name> argument and control how you use that first argument.


So while you likely want the default NamedBase in most cases, don’t forget you can also use Base for simpler cases. We’ll use NamedBase for our example here, but I wanted to make sure and draw attention to this detail because it’s not explicitly discussed in the guides or documentation. It wasn’t until reading through the source code that I found out why the arguments I was passing in my tests were’t captured the way I expected them to be. Once you know, you know, but knowing ahead of time would have saved me some time when I first started digging into generators.

The Usage File

The USAGE file provides a home for the help content displayed when you run bin/rails generate plain --help. (Figure 6) (Remember plain is the name for the generator we’re building.) Depending on the complexity of your generator, the usage file may not be critical, but even with a single argument, it’s worth having the ability to surface quick reminders for yourself or your teammates.

lib/generators/plain/USAGE Description: Generates a plain Ruby class with the given NAMEExample: bin/rails generate plain NAME This will create: app/models/name.rb test/models/name_test.rb
Figure 6

The usage file for our PORO generator is pretty simple, but it’s still worth taking the time to fill it in based on our initial plans.


It’s also worth noting that the --help flag is optional. If you run the a generator without arguments, it will display the help by default. So the more info or examples in the usage file, the easier people will be able to use it. In addition to your usage file, Rails will automatically include the details about arguments and parameters based on the values defined in your generator by using the value you define in the banner option. So you can think of the USAGE file as the bonus content for you to provide a friendly description as well as specific examples of the way the command could be used with various options.

At this point, we don’t have any additional arguments, but it could be handy to support passing a list of attribute names similar to how the ActiveRecord model generator does. We’ll save that for later, though. For now, we’ll stay focused on the basics with the default generator.

The Templates Folder

It’s initially empty, but the templates folder serves as the home for the file templates that we’ll use to generate our files. If you take a look at the templates folder for the task generator, you’ll see the basic structure of a rake task with some ERb. You may also notice that the templates use the tt extension which stands for “Thor Template”.

The Tests

Finally, the generator also provides the test files for ensuring our generator does what we expect. (Figure 7) While tests are always important, with generators, the automated tests go far beyond that because manually testing and cleaning up any incorrectly-generated files is tedious and error-prone with even the simplest generators.

Let’s look at the starting point for our test file:

test/lib/generators/plain_generator_test.rb require "test_helper"require "generators/plain/plain_generator"class PlainGeneratorTest < Rails::Generators::TestCase tests PlainGenerator destination Rails.root.join("tmp/generators") setup :prepare_destination # test "generator runs without errors" do # assert_nothing_raised do # run_generator ["arguments"] # end # endend
Figure 7

The default test file includes the critical setup bits by default, and those lines help ensure testing your generator doesn’t unintentionally leave behind some remnants of manual testing.


Lines five through seven are where the magic happens for testing generators. The tests line lets it know what generator to run when you call run_generator in the test. The destination line ensures that files generated by the test don’t end up in your actual codebase where they could accidentally be committed to your repo. Instead, it puts them in the tmp/generators folder where they won’t interfere.

And finally, the setup line ensures that the destination folder is clean before each test. A teardown method is also provided, but as long as the destination is prepared in the setup step, you shouldn’t need the teardown unless your tests generate some additional files that need to be cleaned up or your project isn’t setup to ignore the tmp directory. If you don’t tear down the resulting files, it’s also easier to manually inspect the results after running a single test since the generated files will be untouched.

The safety and automated cleanup you get from these makes it much easier to build and test new generators without worrying about unintended consequences. As long as your generator uses the destination_root method when specifying paths, your tests should only generate files in the specified test directory. For the most part, it’s as simple as using Rails.root.join(destination_root, ...) any time you need to reference a folder. That will ensure that your generator uses the currently defined destination, and since your test specified that to be tmp/generators, you’ll be all set both when testing and when running the command.

Rails also provides some generator-specific assertions to help make generator tests a little easier to create as well as easier to read after the fact. If you’re using RSpec, you’ll likely have to build some tooling to ensure a smooth test-writing experience.

Connecting Everything

Now that we’ve set the table with the default files and context, we’re ready to make the generator actually do something. Ultimately, we’re going to generate two files: the model file and the model test file. So we’ll start by creating the templates for those files. And remember, the NamedBase class provides quite a few convenience methods.

The Templates

Before we can have the generator do stuff, we have to provide the template files that serve as the source material for generating our final files. We’ll put all of these in the lib/generators/plain/templates folder and tack on a .tt (Thor Template) extension. The templates can use ERb and have access to all of the methods that NamedBase provides. (Figures 8 & 9)

lib/generators/templates/model.rb.tt class <%= class_name %>end
Figure 8

We’ll create a basic model file that’s little more than a placeholder. The class_name method is available via NamedBase. It’s not much right now, but in the future, it could have an include statement for ActiveModel or add attributes that could be passed via the command line.

lib/generators/templates/model_test.rb.tt require "test_helper"class <%= class_name %>Test < ActiveSupport::TestCase setup do @<%= singular_name %> = <%= class_name %>.new end test "the truth" do assert_predicate @<%= singular_name %>, present? endend
Figure 9

The test file has a little more going, and like the model file, the class_name and singular_name methods are available via the NamedBase parent class.


Now we have some bare bones templates. They don’t do much at this point, but that’s alright. We’ll enhance them later.

The Tests

Automated tests are great, but automated tests for generators really help save time. So before we implement the generator, let’s add some tests based on our expected results from running the generator.

test/lib/generators/plain_generator_test.rb require "test_helper"require "generators/plain/plain_generator"class PlainGeneratorTest < Rails::Generators::TestCase tests PlainGenerator destination Rails.root.join("tmp/generators") setup :prepare_destination test "generates the model file" do run_generator ["Point"] do # Let's make sure the model file exists... assert_file "app/models/Point.rb" do |content| # ...and that it includes the class declaration... assert_match(/class Point/, content) end end end test "generates the model test file" do run_generator ["Point"] # Let's make sure the model's test file exists... assert_file "test/models/point_test.rb" do |content| # ...and that it includes the test class declaration... assert_match(/class Point < ActiveSupport::TestCase/, content) end endend
Figure 10

We’ll add two tests—one for each file we expect the generator to create. Each test runs the generator, asserts that the expected file exists, and then asserts that the file contains the content we want.


The Generator

So far, it’s probably felt like a whole lotta setup, and you’re ready for something more substantial, and that’s what we’ll do next. Fortunately, at this point, the generator is fairly straightforward, and we’re creating a fairly simple generator. Before we do, though, there’s one more thing to know about generators: they will call all of the public methods in the generator in the order they’re defined.

Since the source_root method establishes the generator’s local templates directory as the source for the template files, we can use the template command from the Thor::Actions. With everything prepared, the template method only needs two arguments to work: the name of the source file in the templates folder (without the .tt extension) and the file path for the file you want generated after the ERb is parsed in the template. (Figure 11)

lib/generators/plain/plain_generator.rb class PlainGenerator < Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) def create_model_file template "model.rb", File.join("app/models", "#{file_name}.rb") end def create_model_test_file template "model_test.rb", File.join("test/models", "#{file_name}_test.rb") endend
Figure 11

Our generator does precisely two things: it generates the model file and it generates the model test file. They can do plenty more, but keeping it simple and concise works better for easing into the power of generators.


With everything else in place, you may want to take a moment to familiarize yourself with the available methods via the inheritance chain. We won’t use any of the others in our example, but even skimming the available methods can help reveal the capabilities of generators. I’ll include a few interesting methods from each right here, but it’s still worth reading the documentation to know all of your options. I’ve found that keeping these links handy helps when I want to make sure there’s not already a built-in way to do something where I might otherwise reach for FileUtils.

With all of the tools and methods available through these options, you can perform some pretty robust manipulation of files and directories with concise, readable, and predictable commands. But let’s look at one more feature of generators that can help save time and then use that feature to make a minor enhancement to our generator.

Enhancing Our Generator

So far, we’ve kept our generator relatively simple so we could focus on how generators work rather than being distracted by what they’re generating, but where’s the fun in that? Passing arguments and options to generators is where things start to get really interesting.

Command Line Arguments

Our generator can receive a name via the command line, but it would be nice if it could accept some additional arguments or options and generate a few extra bits on our PORO. I’ve got two ideas in particular that be nice conveniences and give us an opportunity to see how to add arguments and options/flags.

First, we’ll update the generator to support an arbitrary number of arguments for attributes that we would like to have on our PORO. We’d like to be able to run something like this to add attributes onto our object:

bin/rails g plain Point x y
Figure 12

An example of the new command supporting positional arguments from the command line.


So we need to specify that we’d like to capture the additional arguments that follow the name argument. You can take a peek at the built-in Rails model generator to see how it handles the array of attribute fields. (Figure 13) We want something fairly similar, but since our PORO isn’t database-backed, so we can skip the type and index options used with standard model generator.

# The built-in model generator accepts field type and index detailsargument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"# We can use a simplified version since our PORO doesn't correlate # to database columnsargument :attributes, type: :array, default: [], banner: "attribute attribute"# Instead of providing a default value, you can also make arguments# required, but that's not what we want hereargument :attributes, type: :array, required: true, banner: "attribute attribute"
Figure 13

You can see how the default Rails model generator handles accepting an arbitrary number of attributes, so we’ll follow Rails’ lead but simplify our arguments. Most of the parameters should be self-explanatory, but the banner portion is somewhat special as it represents the string displayed for help so you know how to pass arguments to the generator.


Seems straightforward enough, so let’s add the argument declaration to our generator. (Figure 14) Once that’s taken care of, our generator can capture the additional arguments, but it won’t yet be doing anything with them. Let’s see where we’re at, and then we’ll have the generator put the additional info to use.

lib/generators/plain/plain_generator.rb class PlainGenerator < Rails::Generators::NamedBase # Remember that NamedBase defines the `name` argument for us, so anything we # declare here will be passed in addition to the `name` argument. argument :attributes, type: :array, default: [], banner: "attribute attribute" source_root File.expand_path("templates", __dir__) def create_model_file template "model.rb", File.join("app/models", "#{file_name}.rb") end def create_model_test_file template "model_test.rb", File.join("test/models", "#{file_name}_test.rb") endend
Figure 14

With the additional of a single line, our PORO generator can accept an arbitrary number of attribute names as arguments.


Now that we can accept the arguments, what do we do with them? We head over to the model template file and iterate over the array of attributes to add the attr_accessor declaration to our PORO, but before we do, let’s go ahead and update our tests so have a failing test prior to updating the template.

For the most part, we’ll be able to reuse our existing test for the model file, but now we need the test to run the generator with the attribute name arguments. (Figure 15) Fortunately, that only requires adding additional values to the array passed to the run_generator method. Then we need to update the content match assertion to expect our file to have the correct attr_accessor declaration.

test "supports specifying attr_accessor fields for the model" do run_generator ['Point', 'x', 'y'] # %w[Point x y] works too! assert_file "app/models/point.rb" do |content| assert_match(/attr_accessor :x, :y/, content) endend
Figure 15

We add a test to verify that when we pass arguments to the generator, they captured and converted to an attr_accessor declaration in our generated model file. You’ll notice that we’re now passing three values to the run_generator command so that it has additional attributes to parse into the attributes array.


Now we just need our model template to do the work and add the declaration, but since attributes are optional, we’ll need to remember to only add them if the generator parse any from the command line. While the argument name is attributes, that provides a collection of Rails::Generators::GeneratedAttribute values. While the instances of GeneratedAttribute can be used via the attributes, we’ll use the attributes_names method that returns simply the names of the attributes passed to the generator. (Figure 16)

lib/generators/plain/plain_generator.rb class <%= class_name %><% if attributes_names.any? -%> attr_accessor <%= attributes_names.map { |name| ":#{name}" }.join(', ') %><% end -%>end
Figure 16

We check to make sure the generator has attributes to fill in, and if it does we generate the attr_accessor line.


Now if you run your tests, they should be passing no problem. Our generator is still pretty basic though, and we need to touch on how to pass options/flags to it. So that’s what we’ll dig into next.

Command Line Options/Flags

In addition to arguments, Rails generators support options specified via flags thanks to Thor’s class_options declaration. Values passed via class options can be of type string, hash, array, numeric, or boolean.

For our generator, we’ll keep it simple and use a boolean to support an --active-model/--no-active-model flag that will optionally auto-include ActiveModel.11One could extend this to support any subset of the Active Model modules, but we’ll keep it simple to focus purely on adding a single option to the generator. With that, we’d be able to automatically insert the include ActiveModel::Model statement by default but support skipping it if our scenario doesn’t need it.

If all goes well, we could run something like the following:

bin/rails g Plain Point x ybin/rails g Plain Point x y --active-modelbin/rails g Plain Point x y --no-active-model
Figure 12

An example of the new command supporting positional arguments from the command line.


But before we implement it, let’s go add tests to ensure we get it right. First, since we want to add ActiveModel as the default we’ll update the original test to verify that the include statement is present. (Figure 17)

test/lib/generators/plain_generator_test.rb test "generates the model file" do run_generator ["Point"] assert_file "app/models/point.rb" do |content| assert_match(/class Point/, content) assert_match(/include ActiveModel::Model/, content) endend
Figure 17

We add an assertion to verify that our default includes ActiveModel.


We’re almost there. With the addition of one more line, we should have our first generator wrapped up. (Figure 18)

lib/generators/plain/plain_generator.rb class PlainGenerator < Rails::Generators::NamedBase # Remember that NamedBase defines the `name` argument for us, so anything we declare here will be passed in addition # to the `name` argument. argument :attributes, type: :array, default: [], banner: "attribute attribute" class_option :active_model, type: :boolean, default: true source_root File.expand_path("templates", __dir__) def create_model_file template "model.rb", File.join("app/models", "#{file_name}.rb") end def create_model_test_file template "model_test.rb", File.join("test/models", "#{file_name}_test.rb") end private def active_model? options[:active_model] endend
Figure 18

Add a boolean class_option for including ActiveModel is our final step. You can add additional class options to have your generator adjust its output in endless ways.


The Finished Generator

Now we have a generator for table-less plain old Ruby objects with some attributes and the ability to automatically include ActiveModel. It’s not the most advanced generator, but it certainly makes it easier to spin up simpler objects so we’re less tempted to bolt functionality onto ActiveRecord models.

I’ve made all the files available as a public Gist. If you notice any mistakes, opportunities for improvements, or suggestions for better approaches for creating custom Rails generators, I’d love to hear about it.