How to Build a Ruby on Rails Engine

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.

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.