Ruby
Article

Rails Application Templates in the Real World

By Glenn Goodrich

railstemplate

I’m about to start a new project that will have many Rails (and rails-api) based services, and I want a way to make sure all the services are created equal. There are a couple of other developers on the project, so I don’t want us each creating wildly different application structures or using different gems unless there’s a good reason.

For example, I’d like us all to start with the same version of Rails and PostgreSQL is the repository of choice, so installing the SQLite gem is silly. In the test and development environments, it’d be great to already have Rspec and Guard ready to go when the application is generated. Also, having Pry available from the get-go is just plain smart. It’s a myriad of these and similar changes that have me wanting to modify our starting point.

There are tons of ways to do this, but I didn’t really want to start with something like RailsBricks or Rails Composer. I have nothing against either of those, quite the opposite. I actually wrote about RailsBricks and I think Daniel Kehoe (the creator of the RailsApps tutorials and Rails Composer) should be knighted. Simply put, I want to do this without adding another dependency, if possible.

The Rails core team offers an approach in Rails Application Templates, so why not take a shot at using what the core team uses.

Spoiler: It works great.

Template API

The Rails Template API is a Domain-Specific Language (DSL) this is powered by Thor. It adds the following actions on top of the core Thor actions:

  • gem– Add a gem to the Gemfile
  • gem_group – Create a group block in the Gemfile with the gems in the supplied block
  • add_source – Add gem source to Gemfile, for example, a private Gem server
  • environment – Add a line to the application.rb or the supplied environment file. This is aliased to application as well. I’ll show you an example of it working in my final template.
  • git – Run git commands
  • vendor – Create a file in the vendor directory
  • lib – Create a file or directory in lib
  • rakefile – Create a new Rakefile
  • initializer – Create a new initializer file with the supplied content
  • generate – Run a Rails generator
  • rake – Run a Rake task
  • capify – Run capify
  • route – Create an entry in routes.rb
  • readme – Prints the contents of the supplied file to the console
  • after_bundle – A callback that runs (you guessed it) once bundle install is complete

It turns out that these few actions, along with the core Thor actions, are more than enough to make a robust starting point for a real Rails application.

The Example

As I previously mentioned, I want to change some items from the core rails new generated site. Here’s the complete list of things I want to tailor:

  • Add the pg gem to the Gemfile and database configuration
  • Use Rails API. This means that I will change which core Railties are included.
  • Use ROAR for my presenters
  • Use Pry (via the byebug gem) in development
  • In test, Use Rspec, Mutant, SimpleCov, and Guard, along with running the generators/install
  • Create a Dockerfile and docker-compose.yml for the application
  • Add a better .gitignore file
  • Add some tools around JSON Schema. The fine folks at Heroku have a ton of them
  • Add a route for a health check endpoint

As you can see, it’s not too much, but having a template that developers can use when generating a service will save some time and remove some of the incongruencies between development environments. Once this template is complete, a developer will be able to type

rails new service_name -m our_template.rb

and then go straight into development. Things like guard will work immediately, without any need to set them up. Just like it should be.

The Template

Here’s the entire template:

# Add the current directory to the path Thor uses
# to look up files
def source_paths
  Array(super) + 
    [File.expand_path(File.dirname(__FILE__))]
end

remove_file "Gemfile"
run "touch Gemfile"
add_source 'https://rubygems.org'
gem 'rails', '4.2.1'
gem 'rails-api'
gem 'puma'

gem 'pg'
gem 'roar-rails'

gem 'committee'

gem_group :development, :test do
  gem 'spring'
  gem 'pry-rails'
  gem 'web-console', '~> 2.0'
  gem 'prmd'
  gem 'rspec-rails', require: false
end

gem_group :test do
  gem 'simplecov', require: false
  gem 'simplecov-rcov', :require => false
  gem 'guard-rspec'
  gem 'mutant'
  gem 'mutant-rspec'
end

copy_file "Dockerfile"
copy_file "docker-compose.yml"
remove_file ".gitignore"
copy_file ".gitignore"

inside 'config' do
  remove_file 'database.yml'
  create_file 'database.yml' do <<-EOF
default: &default
  adapter: postgresql
  host: db 
  port: 5432
  pool: 5
  timeout: 5000
  user: postgres
  password: postgres

development:
  <<: *default
  database: #{app_name}_development

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: #{app_name}_test
  host: 192.168.59.103

production:
  <<: *default
  database: #{app_name}_production

EOF
  end
end

create_file 'schema/meta.json' do <<-EOF
{
"description": "Service",
"id":"service-uu",
"links": [{
"href" : "https://api.esalrugs.com",
"rel" : "self"
}],
"title" : "UU Service"
}
EOF
end

# JSON Schema
empty_directory "schema/schemata"

rakefile("schema.rake") do <<-EOF
require 'prmd/rake_tasks/combine'
require 'prmd/rake_tasks/verify'
require 'prmd/rake_tasks/doc'

namespace :schema do
    Prmd::RakeTasks::Combine.new do |t|
      t.options[:meta] = 'schema/meta.json'    
      # use meta.yml if you prefer YAML format
      t.paths << 'schema/schemata'
      t.output_file = 'schema/api.json'
    end

    Prmd::RakeTasks::Verify.new do |t|
      t.files << 'schema/api.json'
    end

    Prmd::RakeTasks::Doc.new do |t|
      t.files = { 'schema/api.json' => 'schema/api.md' }
    end
  task default: ['schema:combine', 'schema:verify', 'schema:doc']
end
EOF
end

after_bundle do
  remove_dir "app/views"
  remove_dir "app/mailers"
  remove_dir "test"

  insert_into_file 'config/application.rb', after: "require 'rails/all'\n" do <<-RUBY
require "active_record/railtie"
require "action_controller/railtie"
  RUBY
  end

  gsub_file 'config/application.rb', /require 'rails\/all'/, '# require "rails/all"'

  application do <<-RUBY
    config.assets.enabled = false
    config.generators do |g|
      g.test_framework :rspec, fixture: true
      g.view_specs false
      g.helper_specs false
    end

    # Validates the supplied and returned schema. 
    # docs: https://github.com/interagent/committee
    config.middleware.use Committee::Middleware::RequestValidation, schema: JSON.parse(File.read("./schema/api.json")) if File.exist?("./schema/api.json")
  RUBY
  end

  gsub_file 'config/environments/development.rb', /action_mailer/, ''
  gsub_file 'config/environments/test.rb', /.*action_mailer.*/n/, ''

  gsub_file 'app/controllers/application_controller.rb', /protect_from_forgery/, '# protect_from_forgery'

  run "spring stop"
  generate "rspec:install"
  run "guard init"

  # Health Check route
  generate(:controller, "health index")
  route "root to: 'health#index'"

  git :init
  git add: "."
  git commit: "-a -m 'Initial commit'"
end

Hmph, that looks long, doesn’t it? Let’s break down the template.

# Add the current directory to the path Thor uses
# to look up files
def source_paths
  Array(super) + 
    [File.expand_path(File.dirname(__FILE__))]
end

Thor uses source_paths to look up files that are sent to file-based Thor actions, such as copy_file and remove_file. It is redfined here so I can add the template directory and copy files from it to the generated application.

The default Rails Gemfile has a bunch of stuff we don’t need (sqlite, all the asset stuff, turbolinks), so rather than tediously remove gems, let’s just explicitly rebuild the file. Of course, every Gemfile needs a source to search, so that’s added to the top of the file.

# We'll be building the Gemfile from scratch
remove_file "Gemfile"
run "touch Gemfile"

add_source 'https://rubygems.org'

Here are all the gems we need, :

gem 'rails', '4.2.1'
gem 'rails-api'
gem 'puma'

gem 'pg'
gem 'roar-rails'

gem 'committee'

gem_group :development, :test do
  gem 'spring'
  gem 'pry-rails'
  gem 'web-console', '~> 2.0'
  gem 'prmd'
  gem 'rspec-rails', require: false
end

gem_group :test do
  gem 'simplecov', require: false
  gem 'simplecov-rcov', :require => false
  gem 'guard-rspec'
  gem 'mutant'
  gem 'mutant-rspec'
end

gem and gem_group are supplied by the Rails Template API and do exactly what you’d expect.

One of the goals of this template is to make Dockerizing the app very simple. Explaining Docker and its benefits (and cons) is well beyond the scope of this article. Instead, I’ll encourage you to look into Docker (I hope to write a post on that soon) and look at the following as I just needed to copy some custom files into the generated app.

copy_file "Dockerfile"
copy_file "docker-compose.yml"

The Dockerfile and docker-compose.yml are files in my template directory. The files are just copied in using Thor’s copy_file action.

Let’s make a shiny, new .gitignore file. The reason the file is removed first is to avoid the rails new command from prompting the user to overwrite the file:

remove_file ".gitignore"
copy_file ".gitignore"

In what may be a bit more controversial decision, there’s a core database.yml file that I’d like to distribute. Thanks to Docker, we can standardize this a bit. The cool inside method makes sure the file ends up in the right folder:

inside 'config' do
  remove_file 'database.yml'
  create_file 'database.yml' do <<-EOF
default: &default
  adapter: postgresql
  host: db 
  port: 5432
  pool: 5
  timeout: 5000
  user: postgres
  password: postgres

development:
  <<: *default
  database: #{app_name}_development

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: #{app_name}_test
  host: 192.168.59.103

production:
  <<: *default
  database: #{app_name}_production

EOF
  end
end

Notice that I use app_name to create the database names. app_name and app_path are available variables to the template.

Time to leverage some JSON Schema tools to make sure I have good schema and doc generation, as well as some validation. The JSON Schema flow is still a bit clumsy in Rails, but the end justifies the means, IMO:

create_file 'schema/meta.json' do <<-EOF
{
"description": "Service",
"id":"service-uu",
"links": [{
"href" : "https://api.esalrugs.com",
"rel" : "self"
}],
"title" : "UU Service"
}
EOF
end

# JSON Schema
empty_directory "schema/schemata"

rakefile("schema.rake") do <<-EOF
require 'prmd/rake_tasks/combine'
require 'prmd/rake_tasks/verify'
require 'prmd/rake_tasks/doc'

namespace :schema do
    Prmd::RakeTasks::Combine.new do |t|
      t.options[:meta] = 'schema/meta.json'    
      # use meta.yml if you prefer YAML format
      t.paths << 'schema/schemata'
      t.output_file = 'schema/api.json'
    end

    Prmd::RakeTasks::Verify.new do |t|
      t.files << 'schema/api.json'
    end

    Prmd::RakeTasks::Doc.new do |t|
      t.files = { 'schema/api.json' => 'schema/api.md' }
    end
  task default: ['schema:combine', 'schema:verify', 'schema:doc']
end
EOF
end

First, create a meta.json file that will hold the metadata about this service. Title, URL, etc. Next, create a schema/schemata directory to hold the schema files for my resources. I am not going to go into the whats and hows of JSON Schema in this post, but I encourage you to check it out. Finally, I add some rake tasks to generate the schema and markdown documents that will document the API. The rakefile method is provided by the Rails Application Template API and it’s cool.

Now we’ve reached the post-bundle callback. There’s a lot done after bundling, some of which could probably be moved to pre-bundle, but here we are. This is a Rails API applicsation, so we don’t have a need for views and mail won’t be handled by these services. Also, we’re using Rspec, so just remove the test directory:

after_bundle do
  remove_dir "app/views"
  remove_dir "app/mailers"
  remove_dir "test"

Since Rails API doesn’t use all the core Railties, we can gain some performance here by only requiring what we need. Thor’s cool insert_into_file method allows us to put it right where we want it. Once done, we can comment out the line that includes everything, using gsub_file:

insert_into_file 'config/application.rb', after: "require 'rails/all'\n" do <<-RUBY
    require "active_record/railtie"
    require "action_controller/railtie"
  RUBY
  end

  gsub_file 'config/application.rb', /require 'rails\/all'/, '# require "rails/all"'

Since there are no assets or views, disabling assets altogether is a good idea. Also, while I want Rspec as the test framework, I don’t want view specs. Here, I use the Rails Template API method application which, if you remember, is just an alias for environment:

application do <<-RUBY
    config.assets.enabled = false
    config.generators do |g|
      g.test_framework :rspec, fixture: true
      g.view_specs false
      g.helper_specs false
    end

Here is some more JSON Schema fun in the form of middleware that will perform validation:

# Validates the supplied and returned schema. 
# docs: https://github.com/interagent/committee
config.middleware.use Committee::Middleware::RequestValidation, schema: JSON.parse(File.read("./schema/api.json")) if File.exist?("./schema/api.json")

RUBY
end

The committee gem provides the middleware to validate the JSON Schema in requests and responses. Input validation is groovy.

Next, get rid of all the Action Mailer config:

gsub_file 'config/environments/development.rb', /.*action_mailer.*/n/, ''
  gsub_file 'config/environments/test.rb', /.*action_mailer.*/n/, ''

Since we won’t be using cookies with the API (because that is utter lunacy), the protect_from_forgery method is not needed:

gsub_file 'app/controllers/application_controller.rb', /protect_from_forgery/, '# protect_from_forgery'

I’d like to have the test/spec environment ready to go, so that means running the Rspec generator and initializing Guard. I learned the hard way that if I don’t stop Spring, the generator will hang:

run "spring stop"
  generate "rspec:install"
  run "guard init"

Now, I want each service to have a heath check endpoint, so let’s make one:

# Health Check route
generate(:controller, "health index")
route "root to: 'health#index'"

Almost done, just need to setup Git:

git :init
  git add: "."
  git commit: "-a -m 'Initial commit'"
end

Thats a wrap! I can tell our development team to use my template to generate their Rails services, and we will have a common foundation along with a ready-to-go environment. Just cd into the new application directory and guard or docker-compose up and you’re off. Not bad for very little work. I have put a lot of things into place to encourage some best practices and get rid of cruft. When people say Rails makes you more productive, this is a great example, in my humble opinion.

Gotchas

Of course, there’s a couple of things to be aware of using this particular example. The big one is, since I decided to copy files from the template directory, you can’t use an HTTP endpoint for the template file. In other words, this won’t work:

rails new service_name -m https://raw.githubusercontent.com/skookum/rails-api-template/master/rails-api-template.rb

because it won’t find the Dockerfile to copy. This could be fixed by inlining all the file content into the template itself using heredocs. So, if you’re going to use this, be sure to clone the repository to a local spot and refer to the local path in the -m parameter.

Also, some of you might be asking why I am not using rails-api new service_name. It’s a good question. Basically, I looked at what rails-api new generates and made my template do something similar. For what it’s worth, you can pass the same -m parameter to the rails-api generator and you’ll end up in a similar place.

Finally, this template hasn’t been proven out yet. As we use it and find things that it doesn’t do or is missing, I plan to update the repository. As such, the template may diverge from the article.

Template All the Things

Hopefully, this post will encourage you to take a similar approach to your Rails team-based development. We are only really just starting with templates, so I am sure I’ve missed something or one of you has some great advice on how to make this even better. Put that stuff in the comments!

Thanks for reading!

UPDATE: A previous version of this article incorrectly stated that Rails Composer was created by M. Hartl when, in fact, it was created by Daniel Kehoe. Our apologies for getting this wrong. Those responsible have been sacked.

  • mbrictson

    Great article!

    Regarding the gotcha, “since I decided to copy files from the template directory, you can’t use an HTTP endpoint for the template file” – one solution is to have the application template clone itself to a local temporary directory, and add that directory into Thor’s source_paths.

    This is the solution I’m using for my Rails template here: https://github.com/mattbrictson/rails-template

    You can use -m [GitHub URL] and it “just works”. :)

    Further explanation: https://github.com/mattbrictson/rails-template#how-does-it-work

  • Matt Campbell

    Nice article!

    You mention: “Since Rails API doesn’t use all the core Railties, we can gain some performance here by only requiring what we need.”

    It made me wonder – how much performance is actually gained? Is it just faster startup time for the application? That word “performance” always makes me wonder where and how speed is effected.

    • ggsp

      It’s just startup time. I should really benchmark startup and see if I am full of it or not.

      • Matt Campbell

        Ha, no that’s fair man. I know nothing about “performance” and “optimization” but I think that’s because so many of the CPU resources (RAM, CPU GHZ, memory, etc.) have improved to the point that your run-of-the-mill web development work these days simply doesn’t require knowledge of it. Servers are powerful.

  • Totally agree!! “Template All The Things!!” (With nods to Allie Brosh). The Lazy Programmer is the Productive Programmer. :D

  • ggsp

    Holy awesome, Batman! I am trying this….now.

    • ggsp

      YUP! Works a treat. Thanks @mbrictson:disqus!

  • Check out a gem I put on together to mimic this kind of behavior, it is called platter https://github.com/IcaliaLabs/platter

  • frankis

    Awesome timing! Thanks for sharing this guide (that even a newbie can understand!).
    If you find the time / are willing to: It would be great to learn to bring it even further and see how I can get dotenv into this, keys generated & related variables set within secrets.yml and maybe(?!) even within an example like papertrail and amazon S3 api key?

  • Thom Parkin

    [Another] Great article, Glenn.
    I have always been hesitant (afraid?) to try building templates. Actually I never took the time to **really** understand them. You have removed the veil of mystery for me.
    Thanks!!

  • ggsp

    Wow! That’s like what we did, but superheroed up…we may have to copy your style/brains….etc :)

Recommended
Sponsors
Get the latest in Ruby, once a week, for free.