Kickstart a New Rails Project

Published 04 Aug 2024 · 12 min read
Discover the essential steps and gems for launching a new Rails project. From setting up services in Docker containers to harnessing the power of RSpec, FactoryBot, and other must-have tools.

Starting a new Rails project is an exciting time, but it also comes with its fair share of setup tasks to ensure your project kicks off on the right foot. This post will walk through some important steps that I like to follow to set up a Rails project for success. From configuring the database to ensuring code quality and style, and setting up essential development tools.

While this post is structured to guide you through each section manually, you can optionally skip to the automation section to learn how to customize the output of rails new using a template file, which will automate and streamline the process. Let's get started.

Database Setup

Use the rails new command to generate new project, specifying PostgreSQL as the database:

rails new my-awesome-app --database=postgresql

This assumes you're previously installed Postgres, for example: brew install postgresql@14. If you don't specify the --database flag in the generator command, it will default to SQLite.

After the project has been generated, I recommend running Postgres in a Docker container rather than directly on your laptop. This makes it convenient to add other services like Redis for Sidekiq or ActionCable later on. You can check out my detailed guide in this previous post Setup a Rails Project with Postgres and Docker.

Once you've set up Postgres, ensure that your Rails project is configured to generate a structure.sql file instead of schema.rb for your database schema. This can be done by adding the following line to your config/application.rb file:

config.active_record.schema_format = :sql

The benefit of this is you may in the future have a migration that executes some SQL that cannot be expressed by the DSL provided by Rails. See the Rails Guides on Schema Dumps for more details.

Before moving on to further setup, this is a good time to ensure database can be started and created with bin/setup. If the database is configured correctly, the output of this command should include:

== Preparing database ==
Created database 'my_awesome_app'
Created database 'my_awesome_app_test'

Then launch a psql session with bin/rails db, which will connect you to the development database. Then run \d at the psql prompt to list tables, you should see:

                 List of relations
 Schema |         Name         | Type  |    Owner
--------+----------------------+-------+-------------
 public | ar_internal_metadata | table | my_awesome_app
 public | schema_migrations    | table | my_awesome_app
(2 rows)

Annotate

For a clearer understanding of the database schema and to save time when working with models, I like to add the annotate gem to the development and test sections of the Gemfile.

# Gemfile
group :development, :test do
  gem "annotate"
  # ...
end

After adding it, run the installation command:

bundle install
bin/rails generate annotate:install

This ensures that anytime database migrations are run, the schema information will be automatically prepended to the models, tests, and factories (more on testing later). This is beneficial when working with a model, such as adding scopes or other methods, to see what columns, indexes, and constraints are available.

For example, given the following migration to create a products table:

class CreateProducts < ActiveRecord::Migration[7.0]
  def change
    create_table :products do |t|
      t.string :name, null: false
      t.string :code, null: false
      t.decimal :price, null: false
      t.integer :inventory, null: false, default: 0

      t.timestamps
    end
  end
end

And the corresponding Product model:

class Product < ApplicationRecord
  # ...
end

After running the migration with bin/rails db:migrate, the model class will be updated with the schema information as comments at the beginning of the file:

# == Schema Information
#
# Table name: products
#
#  id         :integer          not null, primary key
#  code       :string           not null
#  inventory  :integer          default(0), not null
#  name       :string           not null
#  price      :decimal(, )      not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class Product < ApplicationRecord
  # ...
end

The same schema comments will also get added to the model test spec/models/product_spec.rb and factory spec/factories/product.rb (given that these files exist at the time you run migrations).

Code Quality

Maintaining clean and consistent code is essential for any project. Here's how you can set up code quality and style checks for your Rails project.

Rubocop

For code quality checks, add Rubocop and some extensions to the development group in the Gemfile:

group :development do
  # Static code analysis for Ruby
  gem "rubocop"

  # Additional RuboCop rules for Ruby on Rails
  gem "rubocop-rails"

  # RuboCop rules for RSpec tests
  gem "rubocop-rspec"

  # Performance-related RuboCop rules
  gem "rubocop-performance"

  # RuboCop rules to check for thread safety
  gem "rubocop-thread_safety"

  # RuboCop rules for FactoryBot usage
  gem "rubocop-factory_bot"

  # RuboCop rules for Capybara tests
  gem "rubocop-capybara"
end

After adding these gems, create a .rubocop.yml file in your project's root directory with custom configurations. This is because you'll nearly always want to customize the Rubocop defaults. The details will vary by project, but here's where I like to start:

  • Require all the extensions specified in Gemfile.
  • Exclude generated files.
  • Disable code comment docs (although I'm a huge fan of engineering documentation, enforcing it with Style/Documentation can lead to useless comments like # This is the product model).
  • Increase some max lengths to account for modern high resolution monitors and to avoid arbitrarily splitting up cohesive methods.
require:
  - rubocop-rails
  - rubocop-rspec
  - rubocop-performance
  - rubocop-thread_safety
  - rubocop-factory_bot
  - rubocop-capybara

AllCops:
  NewCops: enable
  Exclude:
    - 'db/schema.rb'
    - 'Gemfile'
    - 'lib/tasks/*.rake'
    - 'bin/*'
    - 'node_modules/**/*'
    - 'config/puma.rb'
    - 'config/spring.rb'
    - 'config/environments/development.rb'
    - 'config/environments/production.rb'
    - 'spec/spec_helper.rb'

Style/FrozenStringLiteralComment:
  Enabled: true

Style/Documentation:
  Enabled: false

Style/StringLiterals:
  EnforcedStyle: double_quotes

Metrics/BlockLength:
  Exclude:
    - 'spec/**/*.rb'

Metrics/MethodLength:
  Max: 15

Layout/LineLength:
  Max: 120

RSpec/ExampleLength:
  Max: 15

Profiler

Uncomment the gem "rack-mini-profiler" line in the development group of the Gemfile and run bundle install to enable Rack Mini Profiler:

# Gemfile
group :development do
  # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
  gem "rack-mini-profiler"
end

This tool helps you identify performance bottlenecks in your application by providing real-time metrics on database queries, rendering times, and memory usage. The first time I saw it, I thought it only provides the time it took to render the current view, but it's so much more than that. See this guide for more details on how to use it.

Testing

While Rails comes with minitest for testing, I prefer using RSpec, together with FactoryBot for a BDD (Behavior Driven Development) approach to testing, and explicit test data creation. Here's how to set this up.

Add rspec-rails gem to the Gemfile. It's placed in the development and test groups so that generators and rake tasks don't need to be preceded by RAILS_ENV=test. See the installation docs for more details.

# Gemfile
group :development, :test do
  gem "rspec-rails"
end

After adding the gem, run the following commands to install and bootstrap RSpec:

bundle install
bin/rails generate rspec:install

Cleanup the old test directory from rails new generator to avoid confusion (assuming there's nothing important in here):

rm -rf test

Optionally, you can generate a binstub for RSpec to make running tests more convenient:

bundle binstubs rspec-core

Now instead of having to type bundle exec rspec to run tests, this can be shortened to bin/rspec.

Factories

By default, Rails supports fixtures, which are yaml files that represent test data. They will be automatically loaded into the test database before every test run. While they are fast (due to database constraints being dropped before data loading), as a project and the data model grows, particularly with associations, fixtures can become a source of complexity. I prefer to use FactoryBot for more explicit data creation for each test that needs it.

To create test data easily, add the factory_bot_rails gem to your development and test groups in the Gemfile:

group :development, :test do
  gem "factory_bot_rails"
end

This will cause Rails to generate factories instead of fixtures when running for example bin/rails generate model SomeModel. See the generator docs if you want different behavior.

Next, configure FactoryBot in spec/rails_helper.rb:

RSpec.configure do |config|
  # Other config...
  config.include FactoryBot::Syntax::Methods
end

The above configuration supports using the FactoryBot methods directly in tests, for example:

# With config
let(:product) { build_stubbed(:product) }

# Without config
let(:product) { FactoryBot.build_stubbed(:product) }

Performance tip: Use FactoryBot's build_stubbed method rather than create where possible to speed up your test suite. Read this post from Thoughtbot for more details.

Shoulda Matchers

For more expressive model testing, I like to add the shoulda-matchers gem. Add it to the test group in the Gemfile:

# Gemfile
group :test do
  gem "shoulda-matchers"
end

After adding the gem, configure it at the end of the spec/rails_helper.rb file:

# spec/rails_helper.rb

# Other config...
Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

Here's an example of using shoulda matchers in an RSpec model test. Given the following User model:

# app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true
end

The presence validations can be tested with one-liners as follows:

# spec/models/user_spec.rb
RSpec.describe User, type: :model do
  it { should validate_presence_of(:name) }
  it { should validate_presence_of(:email) }
end

See the docs on matchers for a full list of what can be expressed in tests.

Before moving on, make sure everything is wired up properly by generating an example model, and ensure both the rspec test and factory is generated for it. For example:

bin/rails generate model Product name:string description:text price:decimal available:boolean

The output of this command should show that the migration, model, model spec and product factory have been created:

invoke  active_record
create    db/migrate/20231002125314_create_products.rb
create    app/models/product.rb
invoke    rspec
create      spec/models/product_spec.rb
invoke      factory_bot
create        spec/factories/products.rb

Afterward, you can safely clean up the generated files since this was just a test:

bin/rails destroy model Product

Dev Tooling

Here are a few more tools I like to add to enhance my workflow.

Faker

The faker gem is a handy tool for generating seed data during development and can also be used in factories for test data. Add it to your development and test groups in the Gemfile:

# Gemfile
group :development, :test do
  gem "faker"
end

Here's an example of how it can be used to seed the development database:

# db/seeds.rb
10.times do
  product = Product.create(
    name: Faker::Commerce.product_name,
    description: Faker::Lorem.paragraph,
    price: Faker::Commerce.price(range: 10.0..1000.0),
    available: Faker::Boolean.boolean
  )
end

Faker can also be used in factories, for example:

FactoryBot.define do
  factory :product do
    name { Faker::Commerce.product_name }
    description { Faker::Lorem.paragraph }
    price { Faker::Commerce.price(range: 10.0..1000.0) }
    available { Faker::Boolean.boolean }
  end
end

Solargraph

Solargraph is a gem for Intelligent code assistance. When used together with the Solargraph VSCode extension, it supports code navigation, documentation, and autocompletion.

Add it to the development group in the Gemfile:

# Gemfile
group :development do
  gem "solargraph"
end

After running bundle install, solargraph will also install YARD. Run this command to generate documentation for installed gems:

bundle exec yard gems

Once Solargraph is setup, here's an example of it in action. Hovering over a Rails class in VSCode with Solargraph setup will show the documentation like this:

rails kickstart solargraph hover

Hitting F12 will jump into the code, for example:

rails kickstart solargraph jump into code

Dotenv

The last bit of dev tooling I like to add is the dotenv gem. This automatically loads environment files from a .env file in the project root, into ENV for development and testing. Add it to the development and test groups of the Gemfile:

group :development, :test do
  gem "dotenv-rails"
  # ...
end

Then modify the .gitignore file in the project root to ignore the .env file, this is because secrets (even dev secrets) should not be committed to version control:

# Ignore environment variables
.env

Then create .env and .env.template files in the project root. The first one will be ignored, the second one is committed to provide developers with an example of what environment variables the application needs:

touch .env .env.template

Editorconfig

While not Rails specific, editorconfig is useful to have in all projects to maintain consistent whitespace in every file. To use it, add a .editorconfig in the project root such as:

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2

And then install an editor plugin for your editor/IDE of choice.

Services

Lastly, consider configuring an app/services directory in your Rails project. While Rails is not opinionated about how you organize business logic, having a services layer from the beginning can keep your codebase organized as the project grows.

Add an empty .keep file in this directory so it will get committed to git:

# from project root
mkdir app/services
touch app/services/.keep

Then add the following line to config/application.rb file to include the app/services directory in Rails' autoload path:

# config/application.rb
class Application < Rails::Application
  # other config...
  config.autoload_paths << Rails.root.join("services")
  # other config...
end

Automation

Since originally writing this post, I have learned about the Rails config file that can make some of these steps faster. It turns out Rails allows you to specify much of this information in a config file, then it will be applied automatically any time rails new some_app is run.

To automate most of the steps covered in this post, add a .railsrc file to your home directory, and also create a template.rb file (this can go in any directory):

# The config file goes in your home directory
touch ~/.railsrc

# The template file can go anywhere,
# just remember where you put it!
touch ~/rails/template.rb

Then edit the .railsrc file with the following:

--database=postgresql

--template=~/rails/template.rb

You can add any option that's supported by the rails new command. For example, if you usually use TailwindCSS for styling, you could also add --css tailwind to the config file. The --template option points to wherever you created the template.rb file.

Now in template.rb, enter:

gem_group :development, :test do
  gem "annotate"
  gem "rspec-rails"
  gem "factory_bot_rails"
  gem "faker"
  gem "dotenv-rails"
end

gem_group :development do
  gem "rubocop"
  gem "rubocop-rails"
  gem "rubocop-rspec"
  gem "rubocop-performance"
  gem "rubocop-thread_safety"
  gem "rubocop-factory_bot"
  gem "rubocop-capybara"
  gem "solargraph"
end

gem_group :test do
  gem "shoulda-matchers"
end

# adds lines to `config/application.rb`
environment 'config.active_record.schema_format = :sql'
environment 'config.autoload_paths << Rails.root.join("services")'

# commands to run after `bundle install`
after_bundle do
  # setup model annotation
  run "bin/rails generate annotate:install"

  # setup RSpec testing
  run "bin/rails generate rspec:install"
  run "rm -rf test"
  run "bundle binstubs rspec-core"

  # documentation for solargraph
  run "bundle exec yard gems"

  # create directories and files
  run "mkdir app/services"
  run "touch app/services/.keep .rubocop.yml .env .env.template"

  # copy new files that should always be in project
  copy_file "/path/to/.editorconfig", ".editorconfig"
  copy_file "/path/to/.rubocop.yml", ".rubocop.yml"
end

Notes:

  • The gem_group sections will add the gems to the specified group in the project Gemfile.
  • The environment command will add the specified line to config/application.rb.
  • The run command will run any shell command. Place these in the after_bundle block to have the commands run after bundle install has completed.
  • The copy_file command will copy a source file from an absolute path to a target in the generated Rails application. In the example above I keep a copy of my standard .editorconfig, rubocop.yml in a directory on my laptop, then these get copied over to the project root.

See the Rails Guides on Templates for all the options that can be specified in this file.

With this in place, the next time you run rails new my_app, you'll have an app setup with all the tools and configuration you like to use.

This leaves just a small amount of configuration to be done manually for spec/rails_helper.rb to configure FactoryBot and Shoulda Matchers:

# spec/rails_helper.rb
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

Conclusion

This post has covered important steps when starting a Rails project, including database setup, code quality and style, testing, additional dev tooling, and introducing a service layer from the start. Some projects may require more (see this post from Evil Martians on Gemfile of Dreams), but this is the baseline that I like to start with. By following these steps and practices, your Rails project should be well prepared for efficient development and maintainability.