Engines are one of the best ways to share functionality across Rails applications. Whether you're looking to extend your Rails application, modularize your project for better maintainability, or are just curious about the finer details, join us as we explore the intricacies of Rails engines.
This article dives into everything you need to know about Rails engines, from their definition to their types, popular examples in the real world, and even building your own.
Rails engines vs plugins
A Rails engine is notably different than a Rails plugin, but they're similar enough to get mixed up. Plugins extend the functionality of an application, while they get fully integrated into the Rails application.
A Rails engine on the other hand acts as a small Rails applications that interfaces with your main application. Engines can have their own models, views, and controllers, while plugins have code that interfaces with and is called by your main Rails application.
Even the Rails guides call out that plugins and engines are similar. They point out that an engine can be considered a plugin and the other way around while noting that an engine is closer to a full Rails application than a plugin.
Rails engines vs gems
While Rails engines and Ruby gems are closely related concepts, they fulfill different requirements in the Ruby on Rails ecosystem. Understanding the distinction between the two can help you choose the right tool for your needs.
Ruby gems
Ruby gems are packaged code that are shared across Ruby projects. Gems are Ruby's equivalent of Node's NPM packages. You can install a gem any use, from simple functions to complex libraries like those that make up the Rails framework. Ruby gems are the most common way of distributing Ruby libraries, whether you're using Rails or not.
Rails engines
A Rails engine is a miniature Rails application that can be plugged into a host Rails app. Engines are distributed as gems, but are a special type of gem designed specifically for Rails applications. Engines can include anything a Rails app can, like models, views, controllers, routes, and even assets.
While most Rails engines are distributed as Ruby gems, certainly not every Ruby gem is a Rails engine. When deciding between creating a gem or an engine, consider the scope of your project.
If you're building Rails-specific functionality that includes models, views, and controllers, an engine might be the better choice, but you'll be able to make an informed decision after better understanding the types of Rails engines.
Types of Rails engines
Rails engines are generally split into two types, Mountable and Non-mountable. Understanding the differences between these two types is crucial for deciding which approach best suits your project's needs.
Mountable Rails engines
Mountable Rails engines are designed to be isolated from the main Rails application, using an entirely separate namespace. They operate as self-contained units within your main application. Models, routes, and even assets are namespaced to the engine, so there are no conflicts with the host Rails application.
With these sorts of Rails engines, you have to mount the routes in the main Rails application's routes.rb
. This is great for contained features that don't need a lot of coupling with the main application.
Here's a quick example of how you might mount a mountable engine in your main application:
Rails.application.routes.draw do
mount ExampleEngine::Engine, at: "/example"
end
You've probably already come across Rails engines in your routes.rb
file, especially if you're using gems like flipper
or devise
. Mountable engines are the right choice for self-contained features built into gems that benefit from isolation.
Non-mountable Rails engines
Non-mountable Rails engines are less isolated than mountable engines. The engine shares a namespace with the host Rails application and doesn't require the routes to be mounted manually. They blend more seamlessly with the main Rails application, which can be both a feature but also a potential source of problems. Non-mountable engines are far more common because they reduce coupling while increasing modularity and reusability.
Non-mountable engines are particularly useful when you want to extend core functionality of your Rails application in a deeply integrated way. They're often used for things like adding new model concerns, view helpers, or even core business logic that needs to be shared across multiple models or controllers.
When to use Rails engines
Rails engines help developers abstract functionality into a modular and reusable component, like a miniature Rails application outside of a Rails application. Maintaining collections of applications gets easier when functionality that is shared across applications lives in a single Rails engine that can be modified as a single change.
Encapsulating functionality in an engine also simplifies dependency management, letting you upgrade Ruby gems in just one engine, rather than multiple applications. Beyond this, building an engine allows you to move complex features outside of your main Rails application. Let's take a look at a few of the benefits of Rails engines:
Sharing functionality across multiple applications
One of the biggest reasons to make a Rails engine is when you need to share functionality across multiple Rails applications. If you ever find yourself copying and pasting code between different projects, it's a great time to consider making a Ruby gem and maybe even a Rails engine.
For example, imagine you're building multiple internal applications for an enterprise that each have similar authentication logic. Each application likely has similar requirements and logic for authentication, so it could be abstracted out into a Rails engine that is shared across projects. This approach not only saves time but also ensures consistency across your projects, and even makes maintenance easier.
Modularizing big Rails apps
As Rails applications grow, they can become challenging to maintain. Every Rails developer has heard the claim that "Rails doesn't scale!", but Rails projects have a number of metaphorical levers that can be pulled to make them easier to work on as they grow.
Rails engines are an ideal a way to break down a monolithic into modular components. This is a particularly useful strategy for simplifying for large, complex applications with discrete feature sets. Each large feature could be implemented as separate engines, making the codebase easier to navigate and maintain. This modular approach also allows different teams to work on different parts of the application with fewer major conflicts.
Managing team workflows
For software teams with many developers, Rails engines can help separate workflows in an application to better distribute responsibility or even deployments. Different groups or team members could be assigned specific engines rather than just parts of a larger repository, which can help avoid conflicts.
By structuring large applications as separate engines within the same application, teams can work independently on these scalable applications while still maintaining a cohesive overall structure and reducing the need for microservices.
Examples of Rails engines
One of the most popular real-world Rails engines is the devise
gem which helps developers add authentication to their applications.
It's distributed as a Ruby gem and can let you quickly add user registration, login, sessions, password resets, lockouts, and more. After installing the gem, you'll then need to run an install script that generates models, migrations, views, and routes.
It doesn't fit into the mold of mountable or non-mountable exactly. It provides its own routes and controllers, but it doesn't live in a separate namespace as it's designed to be integrated directly into your application as opposed to as a standalone feature.
Another popular Rails engine is the activeadmin
gem, which gives you the ability to easily add administrative controls to a Rails application without building them from scratch. Like the devise
gem, it's added to an application by installing a Ruby gem and running an install script. It's a mountable engine, as you will mount it under its own namespace.
How to build your own Rails engine
To demonstrate the power of building modular features for Rails applications as engines, we'll build a new engine! We'll build a Rails engine that acts like a simple administrative view, allowing you to view records of models in your application after mounting it in your host application. You can view the source of the final product on GitHub.
First, generate the plugin with the following command:
rails plugin new god_mode --mountable
This generates a new mountable engine with the name god_mode
. Next, change into the newly created directory:
cd god_mode
Next, declare a root route for your engine in config/routes.rb
:
GodMode::Engine.routes.draw do
root to: "dashboard#index"
end
Then, create a new controller for the dashboard called app/controllers/god_mode/dashboard_controller.rb
. In that file, we'll declare a new controller with an index function that will set an instance variable for all the models in an application.
module GodMode
class DashboardController < ApplicationController
def index
@models = ::ApplicationRecord.descendants
end
end
end
Next, create a new view for the dashboard in app/views/god_mode/dashboard/index.html.erb
. In that file, we'll use the list of models from the instance variable in the controller to create links to pages for the models themselves:
<h1>Careful, You Are In God Mode</h1>
<ul>
<% @models.each do |model| %>
<li><%= link_to model.name, "/god-mode/models/#{model.name.underscore}" %></li>
<% end %>
</ul>
Next, we'll add routes that will dynamically generate for each model so that these links lead somewhere. In config/routes.rb
, add:
get "models/:model_name", to: "dashboard#show", as: :model
Next, we'll create a show
action that we will use for the view that gets rendered when the user clicks one of those links. In app/controllers/god_mode/dashboard_controller.rb
, add this show method:
def show
@model_class = params[:model_name].classify.constantize
@records = @model_class.all
rescue NameError
redirect_to root_path, alert: "Model not found."
end
Lastly, create the view for this show action in a new file, app/views/god_mode/dashboard/show.html.erb
. We'll create a table of each record for the given model and their attributes, leaning on the @records
instance variable:
<h1><%= params[:model_name].classify %></h1>
<table>
<thead>
<tr>
<% @model_class.attribute_names.each do |attr| %>
<th><%= attr %></th>
<% end %>
</tr>
</thead>
<tbody>
<% if @records.empty? %>
<tr>
<td>No records found</td>
</tr>
<% else %>
<% @records.each do |record| %>
<tr>
<% record.attributes.each_value do |value| %>
<td><%= value %></td>
<% end %>
</tr>
<% end %>
<% end %>
</tbody>
</table>
Using the engine in a Rails application
Now that we've made a Rails engine, we'll add it to a project to show how it can quickly contribute meaningful functionality!
Before you can publish a Gem or even use it locally, you'll need to update the Gemspec, god_mode.gemspec
to fix any TODO comments.
Then, in any Rails app that you'd like, include your local version of the Gem in the Gemfile with:
gem "god_mode", path: "../god_mode"
Next, install the gem with the following command:
bundle install
Then, you should mount the engine's routes in your config/routes.rb
with the following:
mount GodMode::Engine => "/god-mode"
In the host app's app/assets/config/manifest.js
file, link the CSS file for the engine:
//= link god_mode/application.css
Also, turn on eager loading so that the application loads the models on startup. This happens by default in production, so you'll just need to turn it on in your config/environments/development.rb
by setting config.eager_load
to true
instead of false
:
config.eager_load = true
Finally, run your application with:
rails s
Visiting localhost:3000/god-mode
will show you a list of all the models in your application as links. For the sample application that I mounted the engine in, this is just a Dog
model.
The God mode dashboard showing the Dog model
Clicking the Dog
link takes you to the show page for that model, which shows a table of every record.
The God Mode dashboard showing records for the Dog model
Building better Rails apps with engines
Rails engines represent a powerful tool in the Rails developer's toolkit, offering a path to more modular, maintainable, and scalable applications. By compartmentalizing features into engines, developers can create cleaner, more organized codebases that are easier to understand, test, and evolve over time.
The hands-on experience of building the GodMode
engine in this tutorial highlights the practical aspects of engine development. From setup and configuration to integration with host applications, we've seen how engines can introduce complex functionality with minimal configuration, showcasing Rails' "convention over configuration" principle.
As you build with Rails engines, remember that like any powerful tool, you should understand the tradeoffs. Not every feature needs to be an engine, and over-modularization can lead to its own complexities. The key is to find the right balance for your project's needs.
Whether you're building a complex monolith or a collection of microservices, consider leaning on engines to improve your development process. Start small, perhaps by extracting a shared feature into an engine, and grow your usage as you become more comfortable with the concept.
If you're ready to dive deeper into Rails best practices and more topics like this, subscribe to the Honeybadger newsletter!