Ruby on Rails Module 15 – Rails internals and extending Rails
Engines, generators and application templates
Build a mountable engine with its own routes, table and tests, write a generator from NamedBase, and set package boundaries a command can check.
What you will learn
- Build a mountable engine with its own routes, migrations and tests
- Write a generator from NamedBase and an application template a team can run twice
- Set package boundaries with packwerk and read the violation it reports
- Choose between an engine, a package boundary and a separate application
Before you start
What you’ll learn · Before you start
What you will learn
- Build a mountable engine with its own routes, migrations and tests
- Write a generator from NamedBase and an application template a team can run twice
- Set package boundaries with packwerk and read the violation it reports
- Choose between an engine, a package boundary and a separate application
Before you start
On this page
Kaksha’s fees team and its admissions team ship from one repository, and the feature-flag screen both of them wanted grew into four files nobody owned. One command, rails plugin new feature_flags --mountable, gives that screen its own routes file, its own feature_flags_ table prefix and its own test suite without leaving the repository.
Rails has three tools for work of that kind, and they answer different questions. Engines package a slice of an application so that another application can mount it. Generators write the files a team keeps typing by hand. Application templates set a new application up the way the team agreed. By the end you can build one of each and say which boundary a slice of code deserves.
An engine is a Rails app that lives inside another one
One command writes the skeleton. rails plugin new feature_flags --mountable --skip-git --skip-javascript leaves a gemspec, lib/feature_flags/engine.rb, a routes file drawn on the engine rather than on Rails.application, an app/ tree under feature_flags/, and a complete host application in test/dummy. The engine class is five lines, and the line that decides the rest is isolate_namespace: The engine call that looks up the engine's classes inside its module, prefixes its table names and keeps its route helpers in a route set of their own..
That one call turns an engine: A Rails application packaged as a gem, with its own app folder, routes, migrations and tests, loaded inside a host application. into something you can hand to another team. Rails keeps its models, controllers and helpers inside the FeatureFlags module, prefixes its tables with feature_flags_, and keeps its route helpers in a set of their own. Without it both apps’ FlagsController classes are one constant, and both apps’ flags tables are one table.
The run below generates the engine, fills the gemspec’s eight TODO placeholders from this lesson’s copy, generates a model, and asks Rails what it made of that:
require_relative "session"
# What rails plugin new writes, and what isolate_namespace then decides for you.
Session.run(<<~SH, masks: [[/\b20\d{12}/, "…"]])
# An engine is a small Rails app in a gem. One command writes the whole skeleton:
rails plugin new ../feature_flags --mountable --skip-git --skip-javascript --skip-bundle 2>&1 | grep -E "engine.rb|config/routes.rb|gemspec|application_controller|dummy"
cd ../feature_flags
rm -rf .github
# The one line that keeps the engine's names out of the app's:
cat lib/feature_flags/engine.rb
# Its routes are drawn on the engine, never on Rails.application:
cat config/routes.rb
# The gemspec arrives with placeholders; nothing installs the engine until they are filled:
grep -c TODO feature_flags.gemspec
# This lesson's filled gemspec, a route and a controller go in:
cp -R ../lab/engine/. .
bundle install --local 2>&1 | tail -1
# A model, generated inside the engine as you would inside an app:
bin/rails generate model Flag name:string enabled:boolean
grep -A4 create_table db/migrate/*_create_feature_flags_flags.rb
# The engine's own routes, and the app's single line that leads to them:
bin/rails routes -g flags
SH Output
# An engine is a small Rails app in a gem. One command writes the whole skeleton:
$ rails plugin new ../feature_flags --mountable --skip-git --skip-javascript --skip-bundle 2>&1 | grep -E "engine.rb|config/routes.rb|gemspec|application_controller|dummy"
create feature_flags.gemspec
create app/controllers/feature_flags/application_controller.rb
create config/routes.rb
create lib/feature_flags/engine.rb
vendor_app test/dummy
$ cd ../feature_flags
$ rm -rf .github
# The one line that keeps the engine's names out of the app's:
$ cat lib/feature_flags/engine.rb
module FeatureFlags
class Engine < ::Rails::Engine
isolate_namespace FeatureFlags
end
end
# Its routes are drawn on the engine, never on Rails.application:
$ cat config/routes.rb
FeatureFlags::Engine.routes.draw do
end
# The gemspec arrives with placeholders; nothing installs the engine until they are filled:
$ grep -c TODO feature_flags.gemspec
8
# This lesson's filled gemspec, a route and a controller go in:
$ cp -R ../lab/engine/. .
$ bundle install --local 2>&1 | tail -1
Use `bundle info [gemname]` to see where a bundled gem is installed.
# A model, generated inside the engine as you would inside an app:
$ bin/rails generate model Flag name:string enabled:boolean
invoke active_record
create db/migrate/…_create_feature_flags_flags.rb
create app/models/feature_flags/flag.rb
invoke test_unit
create test/models/feature_flags/flag_test.rb
create test/fixtures/feature_flags/flags.yml
$ grep -A4 create_table db/migrate/*_create_feature_flags_flags.rb
create_table :feature_flags_flags do |t|
t.string :name
t.boolean :enabled
t.timestamps
# The engine's own routes, and the app's single line that leads to them:
$ bin/rails routes -g flags
Routes for application:
Prefix Verb URI Pattern Controller#Action
feature_flags /feature_flags FeatureFlags::Engine
Routes for FeatureFlags::Engine:
Prefix Verb URI Pattern Controller#Action
flags GET /flags(.:format) feature_flags/flags#index
flag GET /flags/:id(.:format) feature_flags/flags#show
root GET / feature_flags/flags#index
Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby engine_session.rb
The generated migration: A Ruby file in db/migrate/ that changes the database schema one step, named with the time it was made so that every database runs the steps in the same order. More in “The Rails command line, console and generators” says create_table :feature_flags_flags, and bin/rails routes -g flags prints two tables: the dummy app’s one line for the whole engine, and the engine’s own routes under controllers named feature_flags/flags. Step through where such a request goes:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The request arrives. A reader asks the host app for the flags screen. The path starts with the prefix the app chose, and nothing in it names the engine.
- The app's only line. The host app's routes file holds one line about the engine, and that line says where the engine's paths start. The app knows no path below it.
- The engine's own routes. Below the mount point the engine's routes file decides everything. It draws its paths on the engine, so inside it the screen is simply /flags.
- A controller under its own name. The route points at a controller inside the engine's module, so it can never collide with a controller of the same name in the host app.
- A table with the prefix. The model lives in the engine's module too, so Active Record reads the prefixed table in the host app's own database. One database, two sets of names.
- What the app had to know. The app named a path and ran one migration task. Everything else came with the engine, which is why a second app can mount the same engine at another path.
Key idea
One line, isolate_namespace, moves an engine’s constants, tables and route helpers into its own module, so a host app can mount it at any path without a collision.
Checkpoint
Check: An engine as an app inside an app
Three tools, and the moment each one fits
Each of the three belongs to a different moment: code a second application will run, the fifth time a team writes the same three files by hand, and the next new application.
Six more pieces come up again and again in this work. --mountable gives a mountable engine: An engine generated with --mountable: it isolates its namespace and draws its own routes, so a host application mounts it at a path of the host's choosing. its own namespace and routes, while --full leaves it sharing the host app’s. A dummy app: The small host application under an engine's test/dummy that the engine's own test suite boots, so the engine can be tested without any other application. in test/dummy is the host application an engine’s suite boots. bin/rails feature_flags:install:migrations copies an engine’s migrations into the host app with new time stamps. A custom generator: A generator a team writes itself, usually a subclass of Rails::Generators::NamedBase, so that bin/rails generate writes the files the team's own conventions need. subclassing Rails::Generators::NamedBase turns one name into files written from .tt templates. An application template: A Ruby script of Thor actions that sets an application up, applied by rails new -m or by the app:template task on an application that already exists. is a script of Thor actions that rails new -m or bin/rails app:template applies. A packwerk: A tool that reads the constants each file of a Rails application references and reports every reference that crosses a package boundary the packages did not declare. package is a folder whose package.yml names the packages it may reference.
| Tool | Runs when | What it leaves behind |
|---|---|---|
| Engine | Once, then on every boot | Routes, models and migrations of its own |
| Generator | Whenever a developer asks | Files written from the team’s templates |
| Application template | Once per application | Initialisers, routes and files, from Thor |
| packwerk package | In the build, on every change | A package.yml and a check that fails |
Engines and application templates are decisions a team makes once and lives with; generators are the ones it runs every week. Using the generators Rails already ships is covered in the command line and generators lesson.
Checkpoint
Check: Engine, generator or template
Choosing between an engine, a package and a separate app
A boundary costs something, and these three cost different things. Answer the figure’s questions about the slice you want to separate, and watch options drop out with their reason.
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Boundary | Reuse in a second app | Work to set up | Checked by a command | Released on its own |
|---|---|---|---|---|
| Mountable engine What it separates: code, routes and tables | Good one Gemfile line and one mount line in the second app | Mixed a gemspec, a dummy app and a second test suite to keep green | Mixed the dummy app has no host classes, so a test that uses one fails | Mixed versioned as a gem, but it still runs inside the host process |
| packwerk package What it separates: constants inside one app | Poor the code stays in this app; nothing else can load it | Good one package.yml per folder and a check command in the build | Good packwerk check names the file, the line and the constant | Poor ships with the app, in the same deploy as everything else |
| Separate app What it separates: code, data and deployment | Mixed reusable over HTTP, with a client, timeouts and failures to handle | Poor its own repository, pipeline, database and monitoring | Good the network is the boundary: nothing can reach past it | Good deployed and rolled back without touching the other apps |
How the answers decide
- “Will a second application need this slice, now or in the next few months?” No, one app uses it: rules out Mountable engine. An engine earns its gemspec, dummy app and second test suite when a second app mounts it; for one app it is extra ceremony.
- “Must this part be released and rolled back on its own schedule?” No, one deploy is fine: rules out Separate app. A separate app pays for its own repository, pipeline and database with every change; nothing returns on that unless it releases on its own.
- “Will a second application need this slice, now or in the next few months?” Yes, another app needs it and “Must this part be released and rolled back on its own schedule?” Yes, its own releases: rules out packwerk package. A package ships inside the app's deploy, so it cannot give a slice its own release or let another app load it.
- “How many people work on this slice day to day?” One or two: adds a caveat to Separate app. One or two people carry the pipeline, the monitoring and the on-call of a separate app alongside their feature work.
- “How many people work on this slice day to day?” A team of its own and “Will a second application need this slice, now or in the next few months?” No, one app uses it: adds a caveat to packwerk package. A team of its own can hold a package boundary, but only a check in the build keeps the dependencies honest.
In short: an engine when a second application will mount the code, a package boundary: A rule about which parts of one application may reference which others, written down in configuration so a command can report a reference that crosses it. when one application needs its dependencies kept honest, and a separate application when the slice must be released on its own. The architecture argument sits in organising business logic.
Checkpoint
Check: Choosing where the boundary goes
A mountable engine end to end: model, routes, migrations and tests Pro: opens with a Pro, Premium or Ultimate pass
An engine nobody has mounted is only a folder of plausible code. The two runs here finish the job: first the engine’s own suite against its dummy app, then the same engine mounted in the Kaksha fixture app, with its migrations installed and a request test through the whole Rack stack.
In this part:
- Tests that run against the dummy app
- Mounting it in a host app, migrations and all
4 runnable examples (Ruby) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Custom generators and an application template a team can run twice Pro: opens with a Pro, Premium or Ultimate pass
A generator is worth writing when a team has typed the same files five times and disagreed about them twice. The pieces are small: a class, a source_root, one method per file, and templates that read the name the developer passed.
In this part:
- A generator built on NamedBase, with its own templates
- A template that is safe to run a second time
4 runnable examples (Ruby, TT) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Package boundaries with packwerk, and the violation it reports Pro: opens with a Pro, Premium or Ultimate pass
Some slices of an application should not become engines and cannot become separate applications, and still need a boundary a command can check. packwerk 3.3.1 is that command: it reads the constants each file references, works out which package defines each one, and reports every reference that crosses a boundary the packages did not declare.
In this part:
- What a package declares, and what check reports
- Where the boundary belongs, and what each choice costs
1 runnable example (Ruby) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise on the runs above
Copy a session from the examples folder and change it, predicting each result first.
- In
engine_session.rb, dropisolate_namespace FeatureFlagsbefore the model is generated, and predict the table the migration creates. - In
mount_session.rb, mount the engineat: "/switches"and predict which of the three tests fail. - In
packwerk_session.rb, makeenrolment.rbreferenceInvoiceas well, and predict how many offences the last check reports.
Practice that adapts to you
Practise this lesson
9 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
What does isolate_namespace change in an engine, and what breaks without it?
It moves three things into the engine’s module. Models, controllers and helpers live inside FeatureFlags, so the engine’s FlagsController can never collide with a host app’s class of that name. Table names gain a feature_flags_ prefix, so the engine’s Flag reads feature_flags_flags. Route helpers move to the engine’s own set, reached from the app through the mount’s proxy, feature_flags.flags_path. Without the call the engine shares the host app’s namespace, and the first definition Zeitwerk loads wins, which surfaces as a controller answering with somebody else’s action or a migration failing because the table exists.
Each question below starts from a decision, so settle on yours and write the reason down first.
-
Mid to staff
A fees slice of one Rails app is owned by three people, ships in the app's deploy, and a second app will need it next quarter. It also calls two concerns from app/models/concerns. Choose a mountable engine, a packwerk package or a separate service, defend the choice, and say what you would do in the first week.
-
Mid to senior
A deploy stops with "table feature_flags_flags already exists" after the feature_flags engine was upgraded and
bin/rails feature_flags:install:migrationswas run. The table holds live rows. Diagnose the cause from what the app's db/migrate folder and schema_migrations table would show, fix it without losing data, and prevent it. -
Mid to senior
Design an application template for twelve Rails apps that must also be applied again every quarter as the team's defaults change. Say which Thor actions are safe to repeat and which need a guard, how the template reports what it changed, and how you would test it before it touches twelve apps.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does rails plugin new feature_flags --mountable write?
A gemspec, lib/feature_flags/engine.rb, a routes file drawn on the engine rather than on the application, an app/ tree under feature_flags/, and a complete host application in test/dummy for the engine's tests.
-
What does isolate_namespace move into an engine's own module?
Its models, controllers and helpers, its table names, which gain a prefix such as feature_flags_, and its route helpers. Without it, both apps' FlagsController classes are one constant and their flags tables one table.
-
How does a --mountable engine differ from a --full one?
A mountable engine gets its own namespace and routes; a full engine shares the host app's.
-
How do an engine's migrations reach the host app?
bin/rails feature_flags:install:migrations copies them into the host app with new timestamps.
-
When does a team write a custom generator, and when an application template?
A generator, built on Rails::Generators::NamedBase with .tt templates, for files the team keeps writing by hand. An application template, a script of Thor actions run by rails new -m or bin/rails app:template, sets up each new application the way the team agreed.
-
When does a slice of code become an engine, a package or a separate app?
An engine when a second application will mount the code; a packwerk package, a folder whose package.yml names what it may reference, when one application needs its dependencies kept honest; a separate application when the slice must be released on its own.
Key takeaways
- An engine is a Rails application packaged as a gem, and
isolate_namespacekeeps its constants, tables and route helpers out of the host app’s. - The host app names one path for the whole engine, and every route below that path is the engine’s business.
- A generator is for files a team writes repeatedly; an application template is for the defaults of the next new application.
- Package boundaries suit a slice that stays in one application, where a build command reports any reference across them.
- Which boundary fits follows from three questions: reuse by a second application, an independent release, and who maintains it.
Terms in this lesson
- application template
- A Ruby script of Thor actions that sets an application up, applied by rails new -m or by the app:template task on an application that already exists.
- custom generator
- A generator a team writes itself, usually a subclass of Rails::Generators::NamedBase, so that bin/rails generate writes the files the team's own conventions need.
- dummy app
- The small host application under an engine's test/dummy that the engine's own test suite boots, so the engine can be tested without any other application.
- engine
- A Rails application packaged as a gem, with its own app folder, routes, migrations and tests, loaded inside a host application.
- isolate_namespace
- The engine call that looks up the engine's classes inside its module, prefixes its table names and keeps its route helpers in a route set of their own.
- mountable engine
- An engine generated with --mountable: it isolates its namespace and draws its own routes, so a host application mounts it at a path of the host's choosing.
- package boundary
- A rule about which parts of one application may reference which others, written down in configuration so a command can report a reference that crosses it.
- packwerk
- A tool that reads the constants each file of a Rails application references and reports every reference that crosses a package boundary the packages did not declare.
References
- Getting Started with Engines (Ruby on Rails)
- Active Record Migrations (Ruby on Rails)
- The Basics of Creating Rails Plugins (Ruby on Rails)
- Creating and Customizing Rails Generators and Templates (Ruby on Rails)
- Rails::Engine API (Ruby on Rails)
- Rails::Generators::NamedBase API (Ruby on Rails)
- packwerk USAGE (Shopify)
Report a problem with this lesson
Kept only in this browser. Your Learn progress