Rails Application Templates in the Real World
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 Gemfilegem_group
– Create agroup
block in the Gemfile with the gems in the supplied blockadd_source
– Add gem source to Gemfile, for example, a private Gem serverenvironment
– Add a line to the application.rb or the supplied environment file. This is aliased toapplication
as well. I’ll show you an example of it working in my final template.git
– Run git commandsvendor
– Create a file in the vendor directorylib
– Create a file or directory in librakefile
– Create a new Rakefileinitializer
– Create a new initializer file with the supplied contentgenerate
– Run a Rails generatorrake
– Run a Rake taskcapify
– Runcapify
route
– Create an entry in routes.rbreadme
– Prints the contents of the supplied file to the consoleafter_bundle
– A callback that runs (you guessed it) oncebundle 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 direcotry 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.
-
Matt Campbell
-
ggsp
-
Matt Campbell
-
-
-
http://about.me/tamouse Tamara Temple