Cyrus Stoller home about consulting

Using Rails templates

One of the hardest parts of starting a new project (of any kind) is the initial setup. Code generators have helped me to overcome this mental friction when working with software. In this tutorial, I’ll share how I’ve constructed my Rails template to jumpstart my development process and quickly start digging into the features that make a particular project unique instead of getting bogged down writing boilerplate code.

Rule of thumb: if I find myself doing the same thing for the third time, I automate it.

If you haven’t used Rails templates before, I highly recommend reading the official guide. I wish someone had told me to read this when I was first learning Rails.

Structuring my template file

When setting up a new Rails project I have a checklist of things I need to do regardless of what makes the project unique:

Fortunately, a Rails template is just a plain ruby DSL. The one I’ll show you uses fileutils and shellwords, so it can be loaded locally or from the web, which is a trick I learned from mattbrictson/rails-template on Github. In effect, the template will clone its own git repo into a temp directory if it’s being loaded from the web, so the file structure of the repository can be used by Thor actions like copy_file.

def add_template_repository_to_source_path
  if __FILE__ =~ %r{\Ahttps?://}
    git_repo = "<<LINK TO YOUR TEMPLATE REPO>>"

    require "tmpdir"
    source_paths.unshift(tempdir = Dir.mktmpdir("jumpstart-"))
    at_exit { FileUtils.remove_entry(tempdir) }
    git clone: [
      "--quiet",
      git_repo,
      tempdir
    ].map(&:shellescape).join(" ")

    if (branch = __FILE__[%r{<<REPO_NAME>>/(.+)/template.rb}, 1])
      Dir.chdir(tempdir) { git checkout: branch }
    end
  else
    source_paths.unshift(File.dirname(__FILE__))
  end
end

Basic file structure

If you’ve worked with Rails, this file structure should look familiar. By closely mirroring the standard Rails file structure, it’s easy to keep files up to date.

.
├── Procfile # To make it easy to deploy to Heroku
├── README.md
├── app
│   ├── assets (*)
│   ├── helpers (*)
│   ├── javascript (*)
│   ├── models (*)
│   ├── policies (*)
│   └── views (*)
├── config
│   ├── cable.yml
│   ├── initializers (*)
│   ├── locales (*)
│   └── sidekiq.yml
├── env
├── lib
│   └── templates (*)
├── ruby-version
├── template.rb # Where the main logic lives
└── test
    ├── controllers (*)
    ├── fixtures (*)
    └── policies (*)

Subdirectories in app, config, lib, and test will be copied directly into the new Rails project. In this tutorial, I’ll go through some examples to give you an idea of how to do this, but since these are a matter of preference, you’ll probably be best served writing these yourself.


Installing dependencies

Here are some methods that I’ve written in template.rb to install common dependencies for my Rails projects.

def add_root_directory_files
  copy_file "env", ".env" 
  copy_file "Procfile" 

  template "ruby-version.tt", ".ruby-version", force: true
  append_to_file ".gitignore", "\n.env*\n!.env.example"
end

def add_dependencies
  gem 'devise'
  gem 'font-awesome-sass'
  gem 'friendly_id'
  gem 'kaminari'
  gem 'pundit'
  gem 'sidekiq'

  run "yarn add bulma"
end

I’ve found that I prefer the following included in my local .env file. I imagine you’ll find that you have preferences you’ll want included as well.

# env
RACK_ENV=development
RUBYOPT='-W:no-deprecated -W:no-experimental'

Once these files are in place, the gems need to be installed and the remaining commands will be run inside the following block:

after_bundle do
  # add code generators described below here

  rails_command "db:create"
  rails_command "db:migrate"
  git :init
  git add: "."
  git commit: %Q{ -m 'Initial commit' }
end

Setting up basic layouts and font awesome

For every Rails project that I work on, I find that I start by adding these files.

def copy_templates
  run "rm app/assets/stylesheets/application.css"

  directory "app", force: true
  directory "config", force: true
  directory "lib", force: true
  directory "test", force: true

  # These templates incorporate the name of the project
  template "app/helpers/shared_helper.rb.tt"
  template "app/views/layouts/_header.html.erb.tt"
  template "app/views/layouts/_social_headers.html.erb.tt"
  template "app/views/users/mailer/confirmation_instructions.html.erb.tt"
end

Then, I always add a couple static pages: one to serve as a landing page and one to explain what the web application does.

def create_pages_controller
  generate "controller pages home about"
  route "get 'home', to: 'pages#home'"
  route "get 'about', to: 'pages#about'"

  gsub_file "config/routes.rb", "get 'pages/home'\n", ""
  gsub_file "config/routes.rb", "get 'pages/about'\n", ""
end

Adding js to power disappearing flash messages and hamburger menus

This is included in app/javascript/packs/ directory that was copied over in copy_templates described above.

Configuring user authentication

With Devise already installed above, this code ensures that it’s been configured properly. I’ve found that I often customize the controller logic and want the ability to designate certain users as admins. Note: The production mail url options still need to be configured once a domain has been procured.

def create_user_model_and_controllers
  # Install Devise
  generate "devise:install"

  # Configure Devise
  environment "config.action_mailer.default_url_options =" +
    " { host: 'localhost', port: 5000 }", env: 'development'
  environment "#config.action_mailer.default_url_options =" + 
    " { host: 'localhost' }", env: 'production'
  route "root to: 'pages#home'"

  # Create Devise User
  generate :devise, "User", "admin:integer"

  # Set admin default to false
  in_root do
    migration = Dir.glob("db/migrate/*").max_by{ |f| File.mtime(f) }
    gsub_file migration, /:admin/, ":admin, default: 0"
    gsub_file migration, /\# t/, "t" # uncommenting columns
    gsub_file migration, /\# a/, "a" # uncommenting indices
  end

  # Using explicit controllers
  generate "devise:controllers", "users"

  gsub_file "config/routes.rb", "devise_for :users",
  %Q(devise_for :users, controllers: {
    confirmations: 'users/confirmations',
    invitations: 'users/invitations',
    # omniauth_callbacks: 'users/omniauth_callbacks',
    passwords: 'users/passwords',
    registrations: 'users/registrations',
    sessions: 'users/sessions',
    unlocks: 'users/unlocks'
  })
end

Adding user authorization

This logic configures Pundit to check user permissions and adds helper methods to make writing tests easier.

def install_pundit
  insert_into_file "app/controllers/application_controller.rb",
    "\n  include Pundit",
    after: "class ApplicationController < ActionController::Base"
  insert_into_file "app/controllers/application_controller.rb",
    "\n  protect_from_forgery",
    after: "include Pundit"

  generate "pundit:install"

  # Add convenience methods to the test_helper

  convenience_class = %q{
class PolicyTest < ActiveSupport::TestCase
  def permit(current_user, record, action)
    self.class.to_s.gsub(/Test/, '').constantize
      .new(current_user, record).public_send("#{action.to_s}?")
  end

  def forbid(current_user, record, action)
    !permit(current_user, record, action)
  end
end}

  append_to_file "test/test_helper.rb", convenience_class
end

Adding pagination

For my installation, all of the customization is present in app/views/kaminari/.

Setting up transactional email

To support Sendgrid, I add the following so that I can easily use the Heroku Add-on:

# config/initializers/setup_mail.rb
ActionMailer::Base.smtp_settings = {
  :user_name => ENV['SENDGRID_USERNAME'],
  :password => ENV['SENDGRID_PASSWORD'],
  :domain => 'localhost',
  :address => 'smtp.sendgrid.net',
  :port => 587,
  :authentication => :plain,
  :enable_starttls_auto => true
}

Setting up workers for background jobs

This will configure Sidekiq with ActiveJob and mount the dashboard in the routes file.

def add_sidekiq
  environment "config.active_job.queue_adapter = :sidekiq"

  insert_into_file "config/routes.rb",
    "require 'sidekiq/web'\n\n",
    before: "Rails.application.routes.draw do"

  insert_into_file "config/routes.rb",
    "  authenticate :user, lambda { |u| u.admin? } do\n" +
    "    mount Sidekiq::Web => '/sidekiq'\n  end\n",
    after: "https://guides.rubyonrails.org/routing.html\n"
end
def update_test_helper
  insert_into_file "test/test_helper.rb", "require 'minitest/autorun'\n" +
    "require 'sidekiq/testing'\n\n",
    before: "class ActiveSupport::TestCase"
end

Configuring Vue.js with webpacker

This adds Vue.js to webpacker and supports it working in conjunction with Turbolinks. You can learn more about the split chunks configuration here.

def add_vuejs
  rails_command 'webpacker:install:vue'

  # Remove base files
  run "rm app/javascript/app.vue"
  run "rm app/javascript/packs/application.js"
  run "rm app/javascript/packs/hello_vue.js"

  run "yarn add vue vue-loader vue-turbolinks"

  insert_into_file "config/webpack/environment.js",
    "const WebpackAssetsManifest = require('webpack-assets-manifest')\n",
    before: "module.exports = environment"
  insert_into_file "config/webpack/environment.js",
    "environment.splitChunks()\n",
    before: "module.exports = environment"
end

To make Vue.js play nice with Turbolinks, I add the following:

// app/javascripts/packs/vue_container.js

// import axios from 'axios'
import Vue from 'vue/dist/vue.esm'
import TurbolinksAdapter from 'vue-turbolinks'

Vue.use(TurbolinksAdapter)
Vue.config.productionTip = false
// Vue.prototype.$http = axios

document.addEventListener('turbolinks:load', () => {
  // const csrfTokenEl = document.querySelector('meta[name="csrf-token"]')
  // axios.defaults.headers.common = {
  //   "Accept": "application/json, text/plain, */*",
  //   "X-CSRF-Token": csrfTokenEl.getAttribute('content'),
  //   "X-Requested-With": "XMLHttpRequest"
  // }

  var container = document.querySelector('[vue-enabled]')
  if (container != null) {
    const app = new Vue({
    }).$mount(container)
  }
})

Updating the scaffold generator

Add the relevant template files in lib/templates/erb/scaffold/. In my case, I’ve written some files to make it so that the generated forms make use of Bulma CSS classes.


Conclusion

This isn’t a lot of code. But, it makes it much easier to start building a prototype that may still be a fragile idea in my head. I think this type of setup without automation used to take me a couple hours. Now it’s just a few minutes. I hope this will help you feel more inclined to experiment and spend more time building. Happy hacking.

Category Tutorial