How to Build a Ruby on Rails Engine20 Mar 2016
Ruby 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.
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.
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
Since the Passages Engine is also a gem, it has a file named
passages.rb in its
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.
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.
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.
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
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
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.
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
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
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.
Like controllers and views, an Engine’s assets are also nested under a folder bearing the Engine’s name.
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
To enable precompiled assets, a few more lines need to be added in the same
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
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.
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
Rails.application.routes.draw do mount Passages::Engine, at: '/passages' end
'/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
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
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.
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:
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.