Kickstart a New Rails Project
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:
Hitting F12 will jump into the code, for example:
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 projectGemfile
. - The
environment
command will add the specified line toconfig/application.rb
. - The
run
command will run any shell command. Place these in theafter_bundle
block to have the commands run afterbundle 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.