Jason Fleetwood-Boldt’s Rails Cookbook

Now updated for Rails 7.1

Here is my unique Rails cookbook for bootstrapping any Rails project on Rails 7.

While most developers should pay attention to the Gems and setup you are adding to your codebases, these quick start recipies are great as a teaching tool and for setting demo apps up quickly.

1/ Rails 7 JS Strategy Setup (pick A, B, C or D)

Ensure you understand if you are building a Rails app using (A) Importmap, (B) JSBundling, (C ) Shakapacker, or (D) Vite Rails. To help you decide, read this post.

These scripts use your current Ruby and Node versions (to switch, use a manager like RVM and NVM).

Be sure to switch to the desired Ruby and Node versions before copying & pasting below. You will be prompted for your new app name.

1A: Make a new JSBundling Rails App (recommended by me for people getting started with Rails 7)

echo "please give your new JSBundling Rails app a TitleCase name:" && read APP_NAME && rails new $APP_NAME --javascript=esbuild --database=postgresql && cd $APP_NAME && CURRENT_NODE=$(nvm current) && echo $CURRENT_NODE >> .node-version && printf "Node + Ruby versions are in \`.node-version\` and \`.ruby-version\`, respectively.\n\n# Setup\n\n\`bin/setup\`\n\n# Start Rails\n\n\`bin/dev\`\n\n# Run Specs\n\nrun with \`bin/rake\`" > README.md && git add . && git commit -m "initial commit with $(rails -v), Node $CURRENT_NODE, Ruby $(more ./.ruby-version)" && sed -i '' -e 's/ruby-//g' .ruby-version && RUBY_STRING="ruby \"$(more ./.ruby-version)\"" && sed -i '' -e "s/$RUBY_STRING/ruby File.read('.ruby-version').strip/g" Gemfile && git add . && git commit -m "fixes .ruby-version file and sets Gemfile to use .ruby-version file" && bin/rails db:create db:migrate && git add . && git commit -m "adds schema file" 

For the complete JS Bundling setup guide, see this post.

Be sure to start up your app with ./bin/dev

Typescript install for JSBundling app (optional)

First, confirm that you have correctly installed the tsc executable on your system. Assuming you are using Homebrew on macOS, type which tsc to confirm that you see this as the output:

/opt/homebrew/bin/tsc

If you don’t see this, run

brew install typescript

Next, install Typescript, the watcher, and modify the Procfile with this quick script:

1A-Typescript

yarn add --dev tsc-watch && yarn add typescript && git add . && git commit -m "adds typescript and tsc-watch as node packages" && tsc --init && sed -i '' -e "s/\/\/ \"noImplicitAny\": true,/\"noImplicitAny\": true,   /g" tsconfig.json && git add . && git commit -m "initial tsconfig.json file with noImplicitAny" && sed -i '' -e "s/\"scripts\": {/\"scripts\": {\n    \"failure:js\": \"rm .\/app\/assets\/builds\/application.js \&\& rm .\/app\/assets\/builds\/application.js.map\", \n    \"compile:typescript\": \"tsc-watch --noClear -p tsconfig.json --onSuccess 'yarn build' --onFailure 'yarn failure:js'\", /g" package.json && sed -i '' -e "s/js: yarn build --watch/js: yarn compile:typescript --watch/g" Procfile.dev && touch app/javascript/empty.ts && git add . && git commit -m "adds typescript support"

1B: Make a new Rails app using ImportMapNo Node.

Use this setup for a Node-free Rails 7 app.

echo "please give your new Importmap Rails app a TitleCase name:" && read APP_NAME && rails new $APP_NAME --database=postgresql && cd $APP_NAME && git add . && git commit -m "initial commit with $(rails -v), Ruby $(more ./.ruby-version)" && printf "Ruby version is in \`.ruby-version\`.\n\n# Setup\n\n\`bin/setup\`\n\n# Start Rails\n\n\`bin/rails s\`\n\n# Run Specs\n\nrun with \`bin/rake\`" > README.md  && sed -i '' -e 's/ruby-//g' .ruby-version && RUBY_STRING="ruby \"$(more ./.ruby-version)\"" && sed -i '' -e "s/$RUBY_STRING/ruby File.read('.ruby-version').strip/g" Gemfile  && git add . && git commit -m "fixes .ruby-version file and set Gemfile to use .ruby-version file" && ./bin/setup && git add . && git commit -m "adds schema file"

Start up your app with rails server

For more information about Importmap, see this post.

1C: Shakapacker

echo "please give your new Shakapcker Rails app a TitleCase name:" && read APP_NAME && rails new $APP_NAME --database=postgresql --skip-javascript && cd $APP_NAME && git add . && git commit -m "initial commit with $(rails -v), Ruby $(more ./.ruby-version)" && printf "Ruby version is in \`.ruby-version\`.\n\n# Setup\n\n\`bin/setup\`\n\n# Start Rails\n\nbin/rails s\\n\n# Run Specs\n\nrun with \bin/rake" > README.md  && sed -i '' -e 's/ruby-//g' .ruby-version && RUBY_STRING="ruby \"$(more ./.ruby-version)\"" && sed -i '' -e "s/$RUBY_STRING/ruby File.read('.ruby-version').strip/g" Gemfile  && git add . && git commit -m "fixes .ruby-version file and set Gemfile to use .ruby-version file" && bundle exec rails db:create db:migrate && git add . && git commit -m "adds schema file"  && bundle add react_on_rails shakapacker && bundle install && git add . && git commit -m "Adds shakapacker and react_on_rails to gemfile" && bundle exec rails webpacker:install && git add . && git commit -m "webpacker install" && rails generate react_on_rails:install && git add . && git commit -m "react_on_rails install" && rm app/controllers/hello_world_controller.rb && rm app/views/layouts/hello_world.html.erb && rm -rf app/views/hello_world/ && sed -i '' -e "s/get \'hello_world\', to: \'hello_world#index\'//g" config/routes.rb && git add . && git commit -m "removing shakapacker hello world" && rails generate controller Welcome &&
sed -i '' -e 's/class WelcomeController < ApplicationController/class WelcomeController < ApplicationController\n  def index\n\n  end/g' app/controllers/welcome_controller.rb &&
printf "Hello Shakapacker" > app/views/welcome/index.html.erb &&
sed -i '' -e  's/# root "articles#index"//g' config/routes.rb &&
sed -i '' -e  's/Rails.application.routes.draw do/Rails.application.routes.draw do\n  root to: "welcome#index"/g' config/routes.rb && git add . && git commit -m "generates Welcome controller" && echo "\nimport './hello-world-bundle'"  >> app/javascript/packs/application.js && echo '\n\n<%= react_component("HelloWorld", props: {name: '\"`echo $APP_NAME`\"'}, prerender: false) %>' >> app/views/welcome/index.html.erb && git add . && git commit -m "adding the HelloWorld react component to our Welcome index"

For more about Shakapacker, see this blog post

1C-StimulusJS

To add Stimulus JS on top of script 1C, use this:

bundle add stimulus-rails && echo "\nimport '../controllers'" >> app/javascript/packs/application.js && bin/rails stimulus:install && git add . && git commit -m "adds stimulus"

1D: Vite Rails with React

echo "please give your new Vite Rails app a TitleCase name:" && read APP_NAME  && rails new $APP_NAME --skip-javascript --database=postgresql  && cd $APP_NAME && git checkout -b main && CURRENT_NODE=$(nvm current) && printf $CURRENT_NODE >> .node-version && printf "Node + Ruby versions are in \`.node-version\` and \`.ruby-version\`, respectively.\n\n# Setup\n\n\`bin/setup\`\n\n# Start Rails\n\n\`bin/vite dev\` + \`bin/rails s\` in two separate windows\nor run \`bin/dev\` in single window and access site at http://127.0.0.1:5100\n\n# Run Specs\n\nrun with \`bin/rake\`" > README.md && git add . && git commit -m "initial commit with $(rails -v), Node $CURRENT_NODE, Ruby $(more ./.ruby-version)" &&  ./bin/setup  && sed -i '' -e 's/ruby-//g' .ruby-version && RUBY_STRING="ruby \"$(more ./.ruby-version)\"" && sed -i '' -e "s/$RUBY_STRING/ruby File.read('.ruby-version').strip/g" Gemfile  && git add . && git commit -m "fixes .ruby-version file and sets Gemfile to use .ruby-version file" && npm init -y && git add . && git commit -m "initalizes npm" && bundle add vite_rails && bundle install && git add . && git commit -m "adds vite-rails gem" && bundle exec vite install && git add . && git commit -m "vite rails default setup" && yarn add react react-dom && git add . && git commit -m "yarn add react react-dom" && rails generate controller Welcome && sed -i '' -e 's/class WelcomeController < ApplicationController/class WelcomeController < ApplicationController\n  def index\n\n  end/g' app/controllers/welcome_controller.rb &&
printf "<div id=\"${APP_NAME}App\"></div>" > app/views/welcome/index.html.erb &&
sed -i '' -e  's/# root "articles#index"//g' config/routes.rb &&
sed -i '' -e  's/Rails.application.routes.draw do/Rails.application.routes.draw do\n  root to: "welcome#index"/g' config/routes.rb &&  git add . && git commit -m "generates Welcome controller" && printf "import App from '~/components/App'\nimport React from 'react'\nimport { createRoot } from 'react-dom/client'\nconst root = createRoot(document.getElementById('${APP_NAME}App'))\nroot.render(React.createElement(App))" >> app/frontend/entrypoints/application.js && mkdir app/frontend/components  && printf "import React from 'react';\nexport default function App () {\n  return (<h1>Hello Vite Rails</h1>)\n}" >> app/frontend/components/App.jsx && git add . && git commit -m "basic shell for React app" && printf '#!/usr/bin/env sh\n\nif ! gem list foreman -i --silent; then\n  echo "Installing foreman..."\n  gem install foreman\nfi\n\nexec foreman start -f Procfile.dev "$@"' > bin/dev && chmod 0755 bin/dev && git add . && git commit -m "adding bin/dev option to start both vite + rails in one terminal window using Foreman"

You will find your new React app root component (the root of your React app) in app/frontend/components/App.jsx. Look at the bottom of the file app/frontend/entrypoints/application.js to see how it gets mounted into the Welcome controller’s index view using a simple DOM ID.

You can use two windows to start Vite + Rails according to the Vite instructions(bin/vite dev + bin/rails s) and access your site at the normal http://localhost:3000/. Alternatively, you can avoid two-terminal windows by running bin/dev (which runs Foreman). Under this setup, your app is accessible at port 5100 (http://localhost:5100)

When you go to the home page, you should see this in your browser:

Important: Vite server boots by default only at http://localhost:3036/vite-dev/ (not 127.0.0.1). Because the Rails server passes its own domain onto where it looks for the the Vite server (at a different port), you must then also develop locally at localhost, or when accessing Rails, at http://localhost:3000.

If you try to access your site at 127.0.0.1, you will see errors like this:

GET http://127.0.0.1:3036/vite-dev/ net::ERR_CONNECTION_REFUSED

Prefer Typescript over JS? If so, run this additional script, which will switch your existing JSX setup to Typescript.

1D-TypeScript

sed -i '' -e 's/vite_javascript_tag/vite_typescript_tag/g' app/views/layouts/application.html.erb && mv app/frontend/components/App.jsx app/frontend/components/App.tsx && mv app/frontend/entrypoints/application.js app/frontend/entrypoints/application.ts && sed -i '' -e "s/import App from '~\/components\/App'/import App from '..\/components\/App'/g" app/frontend/entrypoints/application.ts && git add . && git commit -m "switches to typescript" 

2/ Testing Setup

Recommend: Always install this section. You will install either A or B (not both).

Choose your testing paradigm: Minitest or Rspec. Minitest is for you if you like the simplicity of Ruby’s raw structures. Rspec is a language DSL where you’ll need to learn some extra syntax to write specs. Minitest is older and more elite and for people who prefer Rubyish syntax. Rspec is more popular and — to some — feels more natural than Minitest for many developers. (If you are starting out, I recommend trying both to see how the ergonomics feel— that is, how efficiently they make you code.)

With either choice (Minitest or Rspec), these scripts also add Dotenv, FactoryBot, FFaker, VCR, and SimpleCov. These are excellent defaults for beginners just getting started with TDD.

Adds to and modifies .gitignore for DotEnv and configures coverage reports with SimpeCov, setting them up to print out automatically after every test (see coverage/ — a folder not checked into your repo).

Remember to install one option (Option 2A for Minitest or Option 2B for Rspec) — not both. Both 2A and 2B have an extra block to run for Capybara: marked as 2A-Capy and 2B-Capy below.

2A: Minitest + Friends

bundle add minitest-rails minitest-spec-rails factory_bot_rails ffaker vcr simplecov dotenv-rails --group "development, test" && bundle add simplecov-rcov launchy --group "test" && git add . && git commit -m "adds minitest, factory_bot_rails, ffaker, VCR, simplecov, dotenv-rails" && printf "\n.env\n.env.local\n.env.*.local\n\ncoverage/" >> .gitignore && printf "" >> .env.local && git add . && git commit -m "adds .env, etc and coverage/ to .gitignore file"  && sed -i '' -e "s/require \"rails\/test_help\"/require \"rails\/test_help\"\nrequire 'simplecov'\nrequire 'simplecov-rcov'\nclass SimpleCov::Formatter::MergedFormatter\n  def format(result)\n    SimpleCov::Formatter::HTMLFormatter.new.format(result)\n    SimpleCov::Formatter::RcovFormatter.new.format(result)\n  end\nend\nSimpleCov.formatter = SimpleCov::Formatter::MergedFormatter\nSimpleCov.start 'rails' do\n  add_filter \"\/vendor\"\nend\n\n/g" test/test_helper.rb && 
git add . && git commit -m "configuring simplecov"

2A-Capy: Capybara for Minitest

printf "require 'minitest/autorun'\nrequire 'application_system_test_case'\n\n\nclass HomepageTest < Minitest::Test \n  test 'can load' do\n    visit '/'\n    assert(page.has_content?('Hello World'), 'page missing Hello World')\n  end\nend" >  test/system/homepage_test.rb && printf "Capybara.register_driver :selenium do |app|\n  options = Selenium::WebDriver::Chrome::Options.new(\n    # It's the headlese arg that make Chrome headless\n    # + you also need the disable-gpu arg due to a bug\n    args: ['headless', 'disable-gpu window-size=1366,1200'],\n    )\n\n  Capybara::Selenium::Driver.new(\n    app,\n    browser: :chrome,\n    options: options\n  )\nend\n\nCapybara.default_driver = :selenium" >> test/test_helper.rb && git add . && git commit -m "basic capybara example"

2B: Rspec + Friends

rm -rf test/ && bundle add rspec-rails rspec-wait factory_bot_rails ffaker vcr simplecov dotenv-rails webmock --group "development, test" && bundle add simplecov-rcov launchy --group "test" &&  
rails generate rspec:install && 
git add . && git commit -m "adds rspec, factory bot, ffaker, vcr, simplecov, and launchy" && printf "\n.env\n.env.local\n.env.*.local\n\ncoverage/" >> .gitignore && printf "" >> .env.local && git add . && git commit -m "adds .env, etc and coverage/ to .gitignore file" && sed -i '' -e 's/RSpec.configure do |config|/RSpec.configure do |config|\n  config.include FactoryBot::Syntax::Methods\n/g' spec/rails_helper.rb && sed -i '' -e "s/RSpec.configure do |config|/require 'simplecov'\nrequire 'simplecov-rcov'\nclass SimpleCov::Formatter::MergedFormatter\n  def format(result)\n    SimpleCov::Formatter::HTMLFormatter.new.format(result)\n    SimpleCov::Formatter::RcovFormatter.new.format(result)\n  end\nend\nSimpleCov.formatter = SimpleCov::Formatter::MergedFormatter\nSimpleCov.start 'rails' do\n  add_filter \"\/vendor\"\nend\n\nVCR.configure do |config|\n  config.cassette_library_dir = \"spec\/fixtures\/vcr_cassettes\"\n  config.hook_into :webmock\n  config.ignore_request do |request|\n    [\"127.0.0.1\", \"chromedriver.storage.googleapis.com\" ,  \"googlechromelabs.github.io\", \"edgedl.me.gvt1.com\"].include? URI(request.uri).host\n  end\nend\n\n\nRSpec.configure do |config|/g" spec/rails_helper.rb  &&  git add . && git commit -m "adding factorybot and simplecov to Rspec config" && sed -i '' -e 's/< Rails::Application/< Rails::Application\n	  config.generators do |generate|\n      generate.helper false\n\n      generate.assets false\n      generate.helper false\n      generate.stylesheets false\n      generate.test_framework :rspec,\n                              request_specs: false,\n                              view_specs: false,\n                              controller_specs: false,\n                              helper_specs: false,\n                              routing_specs: false,\n                              fixture: false,\n                              fixture_replacement: "factory_bot"\n    end\n/g' config/application.rb && git add . && git commit -m "disables extraneous generators"

2B-Capy: Capybara for Rspec

mkdir spec/features  && printf "require 'rails_helper'\n\ndescribe 'homepage' do\n  it 'can load' do\n    visit '/'\n    expect(page).to have_content('Hello World')\n  end\nend" >>  spec/features/homepage_spec.rb && printf "Capybara.register_driver :selenium do |app|\n  options = Selenium::WebDriver::Chrome::Options.new(\n    # It's the headlese arg that make Chrome headless\n    # + you also need the disable-gpu arg due to a bug\n    args: ['headless', 'disable-gpu window-size=1366,1200'],\n    )\n\n  Capybara::Selenium::Driver.new(\n    app,\n    browser: :chrome,\n    options: options\n  )\nend\n\nCapybara.default_driver = :selenium" >> spec/rails_helper.rb && git add . && git commit -m "basic capybara example"

To get the Hello World working (to make the spec pass), see section #3.

If you are not on Rails 7.0.7 or higher, be sure to run bundle remove webdrivers.

3/ Generate a Default Welcome Controller

Recommend: Install for a Quick Home Page

Use this for quick demo apps that need a “home page.” All you get here is a root route pointing to Welcome#index, a corresponding WelcomeController with an index action, and a view at app/views/welcome/index.html.erb. This can be considered your “home page” for your new app.

You can mix this with a default setup from Section 1 if you have chosen JSBundling (1A) or Importmap (1B). Shakapacker (1C) and Vite Rails (1D) have their own hello world page (created in React) which is already included.

rails generate controller Welcome &&
sed -i '' -e 's/class WelcomeController < ApplicationController/class WelcomeController < ApplicationController\n  def index\n\n  end/g' app/controllers/welcome_controller.rb &&
printf "Hello World" > app/views/welcome/index.html.erb &&
sed -i '' -e  's/# root "articles#index"//g' config/routes.rb && 
sed -i '' -e  's/Rails.application.routes.draw do/Rails.application.routes.draw do\n  root to: "welcome#index"/g' config/routes.rb && git add . && git commit -m "generates Welcome controller"

4/ Misc Options: Generator Settings, Lint, Typecheck, GUID, Github Actions, or CircleCI

In this section, we have six optional setup tools (unrelated to each other). You may want to start with if you are starting a new Rails app. If you don’t know these, skip this section and move on to section 5.

4A/ Disables generators (Rspec)

Disables Rails from generating auxiliary files — empty helpers, assets, and stylesheets. Also disables all but model specs from being generated automatically by Rspec. I don’t like having the empty files around, which happens as you use the generators and don’t actually add anything to the empty files it creates. I realize that the empty files are supposed to remind you to use them, but I prefer to have them not created at all.

sed -i '' -e "s/class Application < Rails::Application/class Application < Rails::Application\n   config.generators do |generate|\n      generate.helper false\n\n      generate.assets false\n      generate.stylesheets false\n      generate.test_framework :rspec,\n                              request_specs: false,\n                              view_specs: false,\n                              controller_specs: false,\n                              helper_specs: false,\n                              routing_specs: false,\n                              fixture: false,\n                              fixture_replacement: 'factory_bot'\n    end\n/g" config/application.rb && git add . && git commit -m "disables auxillary files in generators, disables most rspec generators"

4B/ Add Rubocop (Linting)

bundle add rubocop && printf "AllCops:\n NewCops: enable\n SuggestExtensions: false\n\nStyle/Documentation:\n Enabled: false\n\nStyle/FetchEnvVar:\n Enabled: false\n\nMetrics/MethodLength:\n Enabled: false\n\nLayout/LineLength:\n Max: 120\n\nMetrics/BlockLength:\n Enabled: false\n\n\nLint/EmptyBlock:\n  Enabled: false\n\n" >> .rubocop.yml && printf '#!/usr/bin/env ruby\n# frozen_string_literal: true\n\n#\n# This file was generated by Bundler.\n#\n# The application 'rubocop' is installed as part of a gem, and\n# this file is here to facilitate running it.\n#\n\nrequire "pathname"\nENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",\n  Pathname.new(__FILE__).realpath)\n\nbundle_binstub = File.expand_path("../bundle", __FILE__)\n\nif File.file?(bundle_binstub)\n  if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/\n    load(bundle_binstub)\n  else\n    abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.\nReplace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")\n  end\nend\n\nrequire "rubygems"\nrequire "bundler/setup"\n\nload Gem.bin_path("rubocop", "rubocop")' >> bin/rubocop && chmod 0755 bin/rubocop && git add . && git commit -m "adds rubocop" && (bin/rubocop -A) || git add . && git commit -m "rubocop autocorrections" 

Run rubocop with bin/rubocop -A

Because the default Rails code produces code that does not lint correctly with Rubocop, you’ll need to fix the Rubocop errors, check in the fixes, and re-run bin/rubocop -A until you see it lint cleanly. For details, see this post.

When bin/rubocop -A runs with only green dots, it will say “no offenses detected” like so:

4C/ Sorbet (Ruby Type checking)

bundle add sorbet-static-and-runtime && bundle add tapioca --group development &&  bundle exec srb typecheck -e 'puts "Hello, world!"' && bundle exec ruby -e 'puts(require "sorbet-runtime")' && bundle exec tapioca init && git add . && git commit -m "adds sorbet"

Now run srb tc to run the type checker.

4D/ Postgres UUIDs

Makes all new models in Postgres use UUIDs. Be sure that you create foreign keys using the uuid field type. It will not work retroactively, so I recommend you do this at the start of your app. Sets your database up for Postgres UUIDs. After running this script, new models that are created with the model generator will have id: :uuid in the create_table syntax in the migration. This will mean that their primary keys use UUID and not integers for the id column.

mkdir db/migrate && echo  "# frozen_string_literal: true\n\nclass CreatePostgresExtensions < ActiveRecord::Migration[7.0]\n  def change\n    ActiveRecord::Base.connection.execute('CREATE EXTENSION pgcrypto;')\n  end\nend\n" >> db/migrate/00000000000000_create_postgres_extensions.rb && sed -i '' -e 's/class Application < Rails::Application/class Application < Rails::Application\n    config.generators do |generate|\n      generate.orm :active_record, primary_key_type: :uuid\n    end\n/g' config/application.rb && git add . && git commit -m "sets up for postgres UUIDs"

When creating a foreign key to another table, be sure to use the uuid field type instead of an integer like so. Notice that the author_id field is a uuid and not an integer as a typical foreign key.

bin/rails generate model Book title:string author_id:uuid

4E/ Add Postgres Enum Extension

echo 'class EnablePostgresEnums < ActiveRecord::Migration[7.1]\n  def up\n    enable_extension "plpgsql"\n  end\nend\n\n' >> db/migrate/00000000000000_enable_postgres_enums.rb

4F/ Github Actions CI Setup — Adds a .github/workflows/test_suite.yml file for a Rails app with Node and Postgres

You can use this script after 2A or 2B to set up your app for running on Github Actions as your CI runner. Use this for a JSBundling, Vite, or Shakapacker app only.


mkdir .github && mkdir .github/workflows && printf '# This workflow uses actions that are not certified by GitHub.  They are\n# provided by a third-party and are governed by separate terms of service,\n# privacy policy, and support documentation.\n#\n# This workflow will install a prebuilt Ruby version, install dependencies, and\n# run tests and linters.\nname: "Test Suite"\non:\n  push:\n    branches: [ "main" ]\n  pull_request:\n    branches: [ "main" ]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    services:\n      postgres:\n        image: postgres:11-alpine\n        ports:\n          - "5432:5432"\n        env:\n          POSTGRES_DB: rails_test\n          POSTGRES_USER: rails\n          POSTGRES_PASSWORD: password\n      chrome:\n        image: selenium/standalone-chrome:latest\n        ports:\n          - 4444:4444\n    env:\n      RAILS_ENV: test\n      DATABASE_URL: "postgres://rails:password@localhost:5432/rails_test"\n\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n      - name: Install Ruby and gems\n        uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0\n        with:\n          bundler-cache: true\n\n      - name: Setup Node\n        uses: actions/setup-node@v2\n        with:\n          node-version: 18\n\n      - name: npm install\n        run: npm install\n        \n      - name: Set up database schema\n        run: bin/rails db:schema:load\n      - name: Run tests\n        run: bin/rake\n\n' > .github/workflows/test_suite.yml && bundle lock --add-platform x86_64-linux && git add . && git commit -m "github actions CI setup" && bin/rails db:migrate && git add db/schema.rb && git commit -m "db/schema.rb file"

If you are using Rubocop, add this lint job: (see Section 3B)

sed -i '' -e 's/jobs:/jobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v3\n      - uses: ruby\/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0\n    \n\n      - run: bundle install\n      - name: Rubocop\n        run: rubocop/g' .github/workflows/test_suite.yml && git add . && git commit -m "adds rubocop lint job to Github Actions"

4G/ Circle CI (Rspec)

bundle exec rails runner "puts Rails.application.class.module_parent_name" > $APP_NAME && bundle add rspec_junit_formatter && mkdir .circleci && printf "#  more about orbs: https://circleci.com/docs/2.0/using-orbs/\nversion: 2.1\n\norbs:\n  ruby: circleci/ruby@1.0\n  node: circleci/node@2\n  browser-tools: circleci/browser-tools@1.4.4\n\njobs:\n  build:\n    docker:\n      - image: cimg/ruby:3.2.2-browsers\n        auth:\n          username: mydockerhub-user\n          password: $DOCKERHUB_PASSWORD\n      - image: redis:6.2.6\n\n    steps:\n      - checkout\n      - ruby/install-deps\n      - node/install-packages:\n          pkg-manager: yarn\n          cache-key: 'yarn.lock'\n      - run:\n          name: Build assets\n          command: bundle exec rails assets:precompile\n\n  test:\n    parallelism: 1\n    docker:\n      - image: cimg/ruby:3.2.2-browsers\n        auth:\n          username: mydockerhub-user\n          password: $DOCKERHUB_PASSWORD\n      - image: redis:6.2.6\n      - image: cimg/postgres:13.7\n        auth:\n          username: mydockerhub-user\n          password: $DOCKERHUB_PASSWORD\n        environment:\n          POSTGRES_USER: circleci-demo-ruby\n          POSTGRES_DB: `$APP_NAME`_test\n          POSTGRES_PASSWORD: ''\n\n    environment:\n      BUNDLE_JOBS: '3'\n      BUNDLE_RETRY: '3'\n      PGHOST: 127.0.0.1\n      PGUSER: circleci-demo-ruby\n      POSTGRES_USERNAME: circleci-demo-ruby\n      POSTGRES_PASSWORD: ''\n      PGPASSWORD: ''\n      RAILS_ENV: test\n\n    steps:\n      - run: sudo apt-get update\n      - browser-tools/install-browser-tools:\n          chrome-version: 116.0.5845.96 # TODO: remove when chromedriver downloads are fixed\n          replace-existing-chrome: true\n      - browser-tools/install-chrome:\n          # TODO remove following line when fixed https://github.com/CircleCI-Public/browser-tools-orb/issues/90\n          chrome-version: 116.0.5845.96\n          replace-existing: true\n      - browser-tools/install-chromedriver\n      - checkout\n      - ruby/install-deps\n      - node/install-packages:\n          pkg-manager: yarn\n          cache-key: 'yarn.lock'\n      - run:\n          name: Wait for DB\n          command: dockerize -wait tcp://localhost:5432 -timeout 1m\n      - run:\n          name: Load schema\n          command: bin/rails db:schema:load RAILS_ENV=test\n      - ruby/rspec-test\n\nworkflows:\n  version: 2\n  build_and_test:\n    jobs:\n      - build\n      - test:\n          requires:\n            - build\n" >  .circleci/config.yml  && git add . && git commit -m "adds cirleci default config"

• bundle add rspec_junit_formatter

5/ Debugging Tools

Recommend: Always install

Adds Byebug, Bullet, Active Record Query Trace, and Ruby Critic as default debugging tools.

Byebug can be your go-to debugger and you can drop into a debugger anywhere by putting the statement byebug on its own line of code. (Tip: try not to put this at the end of a block or method.)

Bullet is used to detect and alert you any N+1 queries you have made by accident. It is always enabled (look for it in your Rails logs).

Active Record Query Trace allows you to examine each and every database query and see exactly which line of code it comes from — yours or lines of code in your gem code. This default setup does not enable it, but a config file is added for you. To use it, enable set ActiveRecordQueryTrace.enabled to true in config/initializers/active_record_query_trace.rb (then restart your Rails server and look for the trace output in your Rails logs). To get to understand the tool, try to adjust the setting for ActiveRecordQueryTrace.lines — this determines how long (many lines) the backtrace is for each traced database query. You use this tool to identify where in your code a specific database query is being invoked.

Finally, Ruby Critic is a tool for measuring your code’s cyclomatic complexity. You run it using bin/rubycritic .

bundle add bullet active_record_query_trace byebug --group "development, test" && sed -i '' -e "s/Rails.application.configure do/Rails.application.configure do\n  config.after_initialize do\n    Bullet.enable        = true\n    Bullet.alert         = false\n    Bullet.bullet_logger = true\n    Bullet.console       = true\n    Bullet.rails_logger  = true\n    Bullet.add_footer    = true\n  end\n/g" config/environments/development.rb && printf "if Rails.env.development?\n  ActiveRecordQueryTrace.enabled = false\n\n  ActiveRecordQueryTrace.level = :full # :app, :rails, or :full\n  ActiveRecordQueryTrace.lines = 10\n  \nend" >> config/initializers/active_record_query_trace.rb && echo "\n.byebug_history" >> .gitignore && git add . && git commit -m "adding byebug, bullet, active_record_query_trace" && bundle add rubycritic --group dev && bundle install && printf "bundle exec rubycritic" > bin/rubycritic && chmod 0755 bin/rubycritic && git add  . && git commit -m "adds rubycritic; run with bin/rubycritic" 

6/ Hot Glue (pick A or B)

Hot Glue is built by yours truly. It is a rapid prototype toolkit for building bespoke dashboard-like Rails Turbo interfaces quickly. It lets you build list & CRUD views instantly, plus a boatload more features. Be sure to run Rspec install first (see section 3), which is required. When you install Hot Glue, you need to pick a theme: Bootstrap, Tailwind, or no theme. (Tailwind is experimental, so I recommend Bootstrap for getting started.). Although to get it to work with Bootstrap, you’ll have to also install Bootstrap (see Section 7, below), you may run the Hot Glue step here using the Bootstrap theme before you install CSSBundling+Bootstrap below.

6A: Add Hot Glue With Bootstrap

bundle add hot-glue && git add . && git commit -m "adds hot-glue" && 
rails generate hot_glue:install --layout=bootstrap && git add . && git commit -m "hot glue setup (bootstrap)"

6B: Add Hot Glue with Tailwind (experimental)

bundle add hot-glue && git add . && git commit -m "adds hot-glue" && 
rails generate hot_glue:install --layout=tailwind && git add . && git commit -m "hot glue setup (bootstrap)"


see also

7/ CSS Options (pick A or B)

If you go through the steps above, you will have a JSBundlnig app without CSSBundling. Add cssbundling-rails with Bootstrap (A) or Tailwind (B).

7A: Bootstrap + SASSC-Rails

bundle add sassc-rails && bundle install && git add . && git commit -m "adds sassc-rails" && bundle add cssbundling-rails && rails css:install:bootstrap && sed -i '' -e 's/Rails.application.configure do/Rails.application.configure do\n  config.sass.inline_source_maps = true/g' config/environments/development.rb && git add . && git commit -m "adds cssbundling-rails with bootstrap" && sed -i '' -e 's/# gem "sassc-rails"/gem "sassc-rails"/g' Gemfile && bundle install && git add . && git commit -m "adds sassc-rails" 

Add Bootstrap Navbar:

bundle exec rails runner "puts Rails.application.class.module_parent_name" > $APP_NAME && sed -i '' -e 's/<body>/<body>\n<nav class="navbar navbar-expand-lg navbar-light bg-light">\n  <div class="container-fluid">\n    <a class="navbar-brand" href="#">'$APP_NAME'<\/a>\n    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">\n      <span class="navbar-toggler-icon"><\/span>\n    <\/button>\n    <div class="collapse navbar-collapse" id="navbarSupportedContent">\n      <ul class="navbar-nav me-auto mb-2 mb-lg-0">\n        <li class="nav-item">\n          <a class="nav-link active" aria-current="page" href="#">Home<\/a>\n        <\/li>\n        <li class="nav-item">\n          <a class="nav-link" href="#">Link<\/a>\n        <\/li>\n        <li class="nav-item dropdown">\n          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">\n            Dropdown\n          <\/a>\n          <ul class="dropdown-menu" aria-labelledby="navbarDropdown">\n            <li><a class="dropdown-item" href="#">Action<\/a><\/li>\n            <li><a class="dropdown-item" href="#">Another action<\/a><\/li>\n            <li><hr class="dropdown-divider"><\/li>\n            <li><a class="dropdown-item" href="#">Something else here<\/a><\/li>\n          <\/ul>\n        <\/li>\n        <li class="nav-item">\n          <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled<\/a>\n        <\/li>\n      <\/ul>\n      <form class="d-flex">\n        <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">\n        <button class="btn btn-outline-success" type="submit">Search<\/button>\n      <\/form>\n    <\/div>\n  <\/div>\n<\/nav>/g' app/views/layouts/application.html.erb && git add . && git commit -m "adds bootstrap navbar"

Be sure to confirm that the drop-down menu works in the navbar. It will look like this when you click on (or tap for mobile) the menu “Dropdown”.

If the drop-down menu does not show up, you have not installed the Javascript correctly. Try to fix by taking these steps manually.

Also check the mobile view, which shows a hamburger menu like so:

QUICK TROUBLESHOOTING STEPS FOR BOOTSTRAP ON RAILS 7

  • If you have a JSBundlnig, Vite Rails, or Shakapacker app, be sure you are starting it correctly using Foreman (bin/dev. If you boot Rails only the old way (rails server or rails s), your CSS compiler isn’t recompiling your assets.
  • run yarn add @popperjs/core bootstrap bootstrap-icons sass and confirm that these are added as dependencies in package.json
  • confirm that the Procfile.dev contains css: yarn watch:css
  • confirm that the package.json file contains a scripts entry for watch:css and build:css
  • confirm there is a file at app/assets/sytlesheets/application.bootstrap.css that contains:
    @import 'bootstrap/scss/bootstrap';
    @import 'bootstrap-icons/font/bootstrap-icons';
  • confirm that app/javascript/application.js contains
    import * as bootstrap from "bootstrap"

7B: Tailwind

bundle add cssbundling-rails &&
./bin/rails css:install:tailwind &&
git add . && git commit -m "adds cssbundling-rails with tailwind" &&
rails generate controller Articles &&
printf '<div class="bg-blue-900 text-center py-4 lg:px-4"><div class="p-2 bg-blue-800 items-center text-blue-100 leading-none lg:rounded-full flex lg:inline-flex" role="alert"><span class="flex rounded-full bg-blue-500 uppercase font-bold px-2 py-1 text-xs mr-3">New</span><span class="font-semibold mr-2 text-left flex-auto">Hello Tailwind</span></div></div>' >> app/views/articles/index.html.erb
sed -i '' -e 's/# root "articles#index"/ root "articles#index"/g' config/routes.rb 
git add . && git commit -m "adds tailwind element" 

8/ Kaminari for Pagination

bundle add kaminari --git="https://github.com/jasonfb/kaminari.git" --branch="master" && 
rails generate kaminari:views bootstrap4 && 
sed -i '' -e "s/class: 'page-link'/class: 'page-link', 'data-turbo-action': 'advance'/g" app/views/kaminari/_first_page.html.erb app/views/kaminari/_gap.html.erb app/views/kaminari/_last_page.html.erb app/views/kaminari/_next_page.html.erb app/views/kaminari/_page.html.erb app/views/kaminari/_prev_page.html.erb && git add . && git commit -m "adding kaminari and views"

Note 1: This generates default Kaminari views and adds 'data-turbo-action': 'advance' to all links.

Note 2: The templates for bootstrap4 work identically for Bootstrap 5, and there is no option to generate for Bootstrap 5, so use bootstrap4 here, even though you are building for Bootstrap 5

9/ 12-Factor, Nonschema Migrations (for Heroku/ Fly.io/any other 12-Factor provider)

Note that 12-Factor apps have special requirements (like not writing to the disk). Read more about 12-factor apps here. The most important parts of building a 12 Factor app are:

  • Dev-prod parity (you run things the same way in dev and prod)
  • Nothing can be written to the localfile system, so you must use an external service for things like images
  • The logs themselves should not be written to the filesystem. Instead, they’ll get piped out to an external service.

For all apps that you intend to deploy to a cloud environment. Not needed for demo apps or teaching apps that you will only run locally. Here, we add:

  • a Procfile that defines for Heroku the types of processes your app will run
  • a release-tasks.sh file that tells Heroku what to do on releasing
  • my gem Nondestructive Migrations

(Note that the old “12Factor Gem” is no longer needed for new Rails apps, so it is not installed anymore even though this step is called the “12 Factor” step in this guide.)

printf "web: bundle exec rails server -p \$PORT\nrelease: ./release-tasks.sh" >> Procfile && printf "# Step to execute\nbundle exec rails db:migrate data:migrate\n# check for a good exit\nif [ $? -ne 0 ]\nthen\n  puts '*** RELEASE COMMAND FAILED'\n  # something went wrong; convey that and exit\n  exit 1\nfi" >> ./release-tasks.sh && chmod 0755 release-tasks.sh && sed -i '' -e 's/# config.force_ssl = true/config.force_ssl = true/g' config/environments/production.rb && bundle install && bundle lock --add-platform x86_64-linux && git add . && git commit -m "setup for Heroku" && bundle add nonschema_migrations  && rails generate data_migrations:install && rails db:migrate && git add . && git commit -m "adds nonschema_migrations"

Recipe 10/ Users & Roles (with Devise)

10A/ Install Devise for Rails

Devise 4.9.0 and above now works with Rails 7 with no issues

bundle add devise && bundle install && 
git add . && git commit -m "adding devise gem" && 
rails generate devise:install && 
git add . && git commit -m 'devise install' && 
sed -i '' -e  's/Rails.application.configure do/Rails.application.configure do\n  config.action_mailer.default_url_options = { host: "localhost", port: 3000 }/g' config/environments/development.rb && 
git add . && git commit -m 'devise setup'

To make an authenticated user. This also adds a quick Logout button in the main layout app/views/layouts/application.html.erb, but you can move that anywhere (like your nav bar).

10B/ Create a user model

rails generate devise User name:string && rails db:migrate && git add . && git commit -m "generating devise user" && sed -i '' -e 's/<body>/<body><\% if user_signed_in? %>\n   <div>  Logged in as <%= current_user.email %>\\n      <%= button_to "Logout", destroy_user_session_path, method: :delete %><hr \/><\/div>\n  <% elsif ! controller.class.to_s.include?("Devise")%>\n    <%= link_to "Login", new_user_session_path %> | <%= link_to "Signup", new_user_registration_path %><hr \/><\/div>\n  <% end %>/g' app/views/layouts/application.html.erb

10C/ Create a Role model and Join it to User using UserRole

Note: This requires 9, 10A, 10B

bin/rails generate model Role name:string label:string && bin/rails generate model UserRole user_id:integer role_id:integer && sed -i '' -e 's/end/\n  has_many :user_roles\n  has_many :roles, through: :user_roles\nend/g' app/models/user.rb && sed -i '' -e 's/end/\n  has_many :user_roles\n  has_many :roles, through: :user_roles\nend/g' app/models/user.rb && sed -i '' -e 's/class UserRole < ApplicationRecord/class UserRole < ApplicationRecord\n  belongs_to :user\n  belongs_to :role\n/g' app/models/user_role.rb && sed -i '' -e 's/class Role < ApplicationRecord/class Role < ApplicationRecord\n  has_many :user_roles\n  has_many :users, through: :user_roles\n/g' app/models/role.rb && git add . && git commit -m "adds UserRole and Role"

Make some new roles with

rails generate data_migration AddRoles

Then add some roles to your newly created data migration which was created for your in the folder db/data_migrate/

Add roles appropriate for your app. Notice here that I use a name field which will be queried off directly from the code (eg Role.find_by(name: "admin")), but display it using a label field. This design allows easy changing of the label but hard-codes the role name into your codebase.

class AddRoles < ActiveRecord::Migration[7.0]
  def change
    Role.create(name: "admin", label: "Admin")
    Role.create(name: "user", label: "User")
    Role.create(name: "superadmin", label: "Superadmin")
  end
end

Recipe 10D: Use Hot Glue to build a Roles Checkbox Set

Requires: 2B (Rspec), 6 (Hot Glue), 10A, 10B, and 10C.

rails generate hot_glue:scaffold User --related-sets=roles --include=email,roles --namespace=admin --gd

Be sure to add this to your config/routes.rb file too:

  namespace :admin do
    resources :users
  end

In this example, I’ve created a Gd controller in the admin namespace to edit users, which isn’t properly authenticated.

Notice that even if it were properly authenticated, it has two large problems:

(1) Privileged escalation — When a user has access to upgrade their own role to a role allowing them more access than the are supposed to have. (This is a common security vulnerability.) In most designs, only users with superadmin role can assign roles to other users. Alterntiavely, a system can allow you to assign roles that are your role or below. A privileged escalation vulnerability would be if an admin user could assign the superadmin role to themselves, giving themselves more power than they should have.

(2) Accidental plunge — The opposite of privileged escalation, when an user accidentally unassigns themselves (or someone else) from the role giving them the ability to assign & unassign roles. (For example, you accidentally unassign youself the superadmin role or you accidentally unassign yourself the admin role and you can no longer access your own interface.)

To fix both of these problems, see Example 17 in the HOT GLUE TUTORIAL

Recipe 11/ Image Processing with Amazon S3 + Dropzone

if ( command -v vips &> /dev/null ); then; printf "vips already installed...";  else; echo "installing vips with brew..."; brew install vips; fi && bundle add image_processing aws-sdk-s3 && ./bin/rails active_storage:install && ./bin/rails db:migrate && git add . && git commit -m "installing active storage" && sed -i '' -e 's/config.active_storage.service = :local/config.active_storage.service = :amazon/g' config/environments/development.rb && sed -i '' -e 's/config.active_storage.service = :local/config.active_storage.service = :amazon/g' config/environments/production.rb  && yarn add dropzone && yarn add @rails/activestorage &&
sed -i '' -e 's/# amazon:/amazon:/g' config/storage.yml  && sed -i '' -e 's/#   service: S3/  service: S3/g' config/storage.yml &&
sed -i '' -e 's/#   access_key_id: \<%= Rails.application.credentials.dig(:aws, :access_key_id) %\>/  access_key_id: \<%= Rails.application.credentials.dig(:aws, :access_key_id) %\>/g' config/storage.yml && 
sed -i '' -e 's/#   secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>/  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>/g' config/storage.yml &&
sed -i '' -e 's/#   region: us-east-1/  region: us-east-1/g' config/storage.yml && git add . && git commit -m "adds image processing, amazon s3, configures for S3" && echo "REMEMBER TO 1) Set your bucket name is config/storage.yml and 2) set your access_key_id and secret_access_key in Rails credentials and 3) Configure CORS on the bucket"

Also, add this to your github actions

      - name: Install libvips library ubuntu
        run: |
          sudo apt-get update
          sudo apt-get install -y libvips

12/ Sidekiq & Sidekiq Scheduler

For all apps that will require jobs or scheduled jobs.

• Be sure to run the 12-Factor (Heroku) setup above first.

bundle add sidekiq sidekiq-scheduler && printf "require 'sidekiq/web'\n" >> config/initializers/sidekiq.rb && printf "\nworker: bundle exec sidekiq" >> Procfile && printf "\nworker: bundle exec sidekiq" >> Procfile.dev && sed -i '' -e 's/Rails.application.routes.draw do/Rails.application.routes.draw do\n  mount Sidekiq::Web => "\/sidekiq" /g' config/routes.rb && printf "#:scheduler:\n# add your scheduled jobs here and uncomment line above" >> config/sidekiq.yml && git add . && git commit -m "adds sidekiq with configuration"

13/ Pundit

Add Pundit

bundle add pundit && sed -i '' -e 's/ApplicationController < ActionController::Base/ApplicationController < ActionController::Base\n  include Pundit::Authorization/g' app/controllers/application_controller.rb && bundle install && rails g pundit:install && git add . && git commit -m "pundit gem"

14/ Stripe

TODO: WIP

bundle add stripe-rails

Add to config/environments/production.rb,

config.stripe.publishable_key = 'pk_live_XXXYYYZZZ'

Add to config/environments/development.rb

Add

config.stripe.secret_key = Rails