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.
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
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.
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
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
This is included in app/javascript/packs/
directory that was copied over in
copy_templates
described above.
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
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
For my installation, all of the customization is present in
app/views/kaminari/
.
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
}
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
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)
}
})
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.
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.