How to Build a Ruby on Rails Engine
20 Mar 2016Ruby on Rails Engines are miniature applications whose purpose is to supplement a larger Ruby on Rails application. If functionality can exist independent from a main application, an Engine can provide a wonderful degree of encapsulation.
Recently, I created the and “gemified” the Passages Ruby on Rails Engine to help alleviate some routing frustration. This gem will be the be used as the example for demonstrating what goes into creating a Ruby on Rails Engine.
Revving Up
There are two ways to start building an Engine. One option is to use the built in generators to create directories and dummy classes. These generators behave in the same way as standard Rails generators.
To generate an Engine in this way, use the plugins
built in generator:
$ rails plugin new passages --mountable
Note: Engines and Plugins are not exactly the same in the Ruby on Rails world but the --mountable
flag tells the plugin generator to generate a full Engine.
The other approach is to simply create the needed directories and files by hand. This is was the way the Passages Engine was built, resulting in the following directory structure:
|-app
|--controllers
|---passages
|----<controller directories>
|--views
|---passages
|----<views directories>
|-config
|--routes.rb
|--initializers
|---assets.rb
|-lib
|--passages
|---engine.rb
Some directories that the rails
generator would have added are missing (i.e. models
, helpers
, mailers
). These directories might be necessary for some projects, but the Passages Engine did not have use for them.
The file at the heart of it all is engine.rb
. This file is responsible for defining the engine and will also be utilized later to add optional enhancements an Engine can take advantage of:
module Passages
class Engine < ::Rails::Engine
isolate_namespace(Passages)
end
end
An interesting line in this file is the isolate_namespace
method call. This method helps ensure encapsulation for the Engine by isolating its controllers, helpers, views, routes, and any other shared resources between the main application and the Engine.
With isolation, an Engine does not need to worry about conflicting class or module names in the main application. Additionally, an isolated Engine will set its own name according to its namespace, accessible later via Passages::Engine.engine_name
.
Since the Passages Engine is also a gem, it has a file named passages.rb
in its lib
directory:
module Passages
end
require 'passages/engine'
require 'controllers/passages/routes_controller'
This file is responsible for defining the Passages
module and requiring the engine. It is the entry point for this gem’s logic.
Encapsulate Everywhere
The overarching theme of designing a good Ruby on Rails Engine is encapsulation. An application should not be negatively affected by its underlying Engines, they should simply support and bring new functionality the application.
To help reinforce this theme, Ruby on Rails requires that an Engine’s controllers, views, and assets all be nested in namespace modules and corresponding directory folders.
Controllers
In the Passages Engine, the RoutesController
demonstrates this nesting.
|-app
|--controllers
|---passages
|----routes_controller.rb
module Passages
class RoutesController < ActionController::Base
# ...
end
end
The same folder structure is used for views and assets.
Routes
Of course what good is a controller without a route to use it? A Ruby on Rails Engine also can define its own routes similarly to a stand-alone application.
Unlike the controllers, the routes.rb
file is not contained in a passages
folder, nor is it within the module Passages
.
|-config
|--routes.rb
Passages::Engine.routes.draw do
root to: 'routes#index'
end
In a standard Ruby on Rails application, the first line in a routes file is: Rails.application.routes.draw
; however, within an Engine, the name of the Engine replaces Rails.application
.
With a simple routes file in place, an application using the Passages Engine can run rake routes
to see the new routes in action:
$ rake routes
Prefix Verb URI Pattern
passages /passages
users GET /users
POST /users
new_user GET /users/new
edit_user GET /users/:id/edit
user GET /users/:id
PATCH /users/:id
PUT /users/:id
DELETE /users/:id
Routes for Passages::Engine:
root GET / passages/routes#index
Note: This assumes the engine is mounted at /passages
, more about mounting routes below.
Neat, the Passages routes are in a separate section to help differentiate them from normal application routes.
Remember the isolate_namespace
method? One of the side-effects of not using an isolated namespace can be seen when asking for an applications routes.
If the namespace isolation is commented out:
module Passages
class Engine < ::Rails::Engine
# isolate_namespace(Passages)
end
end
Then rake routes
gives a different output:
$ rake routes
Prefix Verb URI Pattern
passages_engine /passages
users GET /users
POST /users
new_user GET /users/new
edit_user GET /users/:id/edit
user GET /users/:id
PATCH /users/:id
PUT /users/:id
DELETE /users/:id
Routes for Passages::Engine:
root GET / routes#routes
Notice that now the root
for the Passages Engine has had its prefix removed. This will cause Rails to look in the wrong place for the routes_controller
:
ActionController::RoutingError
(uninitialized constant RoutesController):
While this might not be a huge deal for some applications, the fact that an Engine triggers a top level controller to be fetched is worrysome. What if the main application had its own RoutesController
, the Passages Engine could incorrectly fetch that instead. Or if things are reversed, an Engine without an isolated namespace might incorrectly override an important controller in the main application.
Assets
Like controllers and views, an Engine’s assets are also nested under a folder bearing the Engine’s name.
|-app
|--assets
|---javascripts
|----passages
|-----application.js
|---stylesheets
|----passages
|-----application.css
This organization enables the layouts and other views in the Engine to only load the files it needs and not accidentally reference the main application’s application.js
and application.css
:
<%= stylesheet_link_tag 'passages/application', media: 'all' %>
<%= javascript_include_tag 'passages/application' %>
To enable precompiled assets, a few more lines need to be added in the same engine.rb
file:
module Passages
class Engine < ::Rails::Engine
isolate_namespace(Passages)
initializer("passages.assets.precompile") do |app|
app.config.assets.precompile += [
'application.css',
'application.js'
]
end
end
end
This initializer
line creates an initializer in the underlying railties to be evaluated when assets are precompiled via rake assets:precompile
. With this, an application can successfully compile its own assets and those of the Passages Engine.
Mount Up
A Ruby on Rails Engine must be mounted by an application for it to be accessible.
The normal place for this to occur is in a main application’s routes.rb
:
Rails.application.routes.draw do
mount Passages::Engine, at: '/passages'
end
The '/passages'
string can be replaced with any desired endpoint, the Engine does not care about the name.
Alternatively, an Engine can mount itself using the same initializer
method in engine.rb
:
module Passages
class Engine < ::Rails::Engine
isolate_namespace(Passages)
initializer('passages',
after: :load_config_initializers) do |app|
Rails.application.routes.prepend do
mount Passages::Engine, at: '/passages'
end
end
end
end
In this case, the initializer has a specific placement: after the configuration initializers are loaded. This initializer then writes to the main application with Rails.application.routes.prepend
and, as the name suggests, prepends the mount to the application’s routes.
Since the mount is added to the beginning of an application’s routes, it is possible that this mounted path (in this case '/passages'
) will be overridden by a route with the same name later on in the main application’s routes.rb
file.
After an application mounts the Passages Engine, it can navigate to /passages
. This request would be served by the Passages Engine like a normal Ruby on Rails application request.
Use With Caution
Auto-mounted Engines may sound like a great idea but it might be best to leave that decision to the consumer. A suggested approach to this is have an opt-in functionality with auto-mounting. Placing this logic behind a conditional (based on some kind of configuration variable) gives consumers of this Engine the power of auto-mounting without the worry of “magic” they did not ask for.
Next Steps
With the basic structure in place, a new Engine can be built like any other Ruby on Rails application. Controllers and their respective views can be created and placed under the appropriate namespaces and folders. Routes that utilize these controllers can be added as well. Assuming that both the main application and the Engine use the same ORM (or are at least compatible), even models can be created.
The Passages Engine was built as a standalone gem. Making it available was the same process as creating any other gem. A .gemspec
file was created, the gem was cut to a version, and then finally hosted on Rubygems.org. An application that wishes to use the Passages Engine can install it by adding a new line to the Gemfile: gem 'passages'
.
This same pattern can be used to make any stand alone Ruby on Rails Engine. However, it would be a wise decision to choose specific versions of Ruby on Rails to support. For example, the Passages Engine was built to support Ruby on Rails 4.X, it is not compatible with Ruby on Rails 3.X at all.
Built to Last
Going further, the creation of views, initializer files, migrations, and models can all be accomplished the same way one would in a regular Ruby on Rails application.
Not all of the Passages Engine was discussed in this guide, but feel free to read the source to find out more information. Also, I am always looking for eager contributors to either submit issues or pull requests with features they need.