Skip to content

Instantly share code, notes, and snippets.

@albertogalca
Created December 25, 2024 19:30
Show Gist options
  • Save albertogalca/019856876952a826fa6701ab28662cc4 to your computer and use it in GitHub Desktop.
Save albertogalca/019856876952a826fa6701ab28662cc4 to your computer and use it in GitHub Desktop.
suistanable rails
i
Sustainable Web Development with Ruby on Rails
#### Practical Tips for Building Web Applications that Last
David Bryant Copeland
This book is copyright©2023 by David Bryant Copeland, All Rights Reserved. All text, code, images, and diagrams
were produced without any assistance from any generative AI. See [http://declare-ai.org/1.0.0/declare.html](http://declare-ai.org/1.0.0/declare.html) for
details.
For more information about this book, visit https://sustainable-rails.com
## Contents
- Acknowledgements Contents
- Changes from Previous Versions
- December, 12,
- January, 21,
- March 15,
- Dec 4,
- 1 Why This Book Exists I Introduction
- 1.1 What is Sustainability?
- 1.2 Why Care About Sustainability?
- 1.3 How to Value Sustainability
- 1.4 Assumptions
- 1.4.1 The Software Has a Clear Purpose
- 1.4.2 The Software Needs To Exist For Years
- 1.4.3 The Software Will Evolve
- 1.4.4 The Team Will Change
- 1.4.5 You Value Sustainability, Consistency, and Quality
- 1.5 Opportunity and Carrying Costs
- 1.6 Why should you trust me?
- 2 The Rails Application Architecture
- 2.1 Boundaries
- 2.2 Views
- 2.3 Models
- 2.4 Everything Else
- 2.5 The Pros and Cons of the Rails Application Architecture
- 3 Following Along in This Book
- 3.1 Typographic Conventions
- 3.2 Software Versions
- 3.3 Sample Code
- 4 Start Your App Off Right
- 4.1 Creating a Rails App
- 4.2 Using The Environment for Runtime Configuration
- 4.3 Configuring Local Development Environment with dotenv
- 4.4 Automating Application Setup withbin/setup.
- 4.5 Running the Application Locally withbin/dev
- 4.6 Putting Tests and Other Quality Checks inbin/ci
- 4.7 Improving Production Logging with lograge
- 5 Business Logic (Does Not Go in Active Records)
- 5.1 Business Logic Makes Your App Special. and Complex
- 5.1.1 Business Logic is a Magnet for Complexity
- 5.1.2 Business Logic Experiences Churn
- 5.2 Bugs in Commonly-Used Classes Have Wide Effects
- 5.3 Business Logic in Active Records Puts Churn and Complexity in Critical Classes
- 5.4 Active Records Were Never Intended to Hold All the Business Logic
- 5.5 Example Design of a Feature
- 6 Routes and URLs II Deep Dive into Rails
- 6.1 Always Use Canonical Routes that Conform to Rails’ Defaults
- 6.2 Never Configure Routes That Aren’t Being Used
- 6.3 Vanity URLs Should Redirect to a Canonical Route
- 6.4 Don’t Create Custom Actions, Create More Resources
- 6.5 Use Nested Routes Strategically
- 6.5.1 Create Sub-Resources Judiciously
- 6.5.2 Namespacing Might (or Might Not) be an Architecture Smell
- 6.6 Nested Routes Can Organize Content Pages
- 7 HTML Templates
- 7.1 Use Semantic HTML
- 7.1.1 Build Views by Applying Meaningful Tags to Content
- 7.1.2 Use<div>and<span>for Styling
- 7.2 Ideally, Expose One Instance Variable Per Action
- 7.2.1 Name the Instance Variable After the Resource
- 7.2.2 Reference Data, Global Context, and UI State are Exceptions
- 7.3 Wrangling Partials for Simple View Re-use
- 7.3.1 Partials Allow Simple Code for Simple Re-use
- 7.3.2 Reference Only Locals in Partials
- 7.3.3 Partials Should Use Strict Locals
- 7.3.4 Use Default Values for Strict Locals to Simplify Partial APIs
- 7.4 Use the View Component Library for Complex UI Logic
- 7.4.1 Creating a View Component
- 7.4.2 Testing Markup from a Unit Test
- 7.4.3 Deciding Between a Partial or a View Component
- 7.5 Just Use ERB
- 8 Helpers
- 8.1 Don’t Conflate Helpers with Your Domain
- 8.2 Helpers are Best at Exposing Global UI State and Generating Markup
- 8.2.1 Global UI Logic and State
- 8.2.2 Small, Inline Components
- 8.3 Configure Rails based on Your Strategy for Helpers
- 8.3.1 Consolidating Helpers in One File
- 8.3.2 Configure Helpers to Be Actually Modular
- 8.3.3 Usehelper_methodto Share Logic Between Views and Controllers
- 8.4 Use Rails’ APIs to Generate Markup
- 8.5 Helpers Should Be Tested and Thus Testable
- 8.6 Tackle Complex View Logic with Better Resource Design or View Components
- 8.6.1 Presenters Obscure Reality and Breed Inconsistency
- 8.6.2 Custom Resources and Active Model Create More Consistent Code
- 8.6.3 View Components can Render Entire Pages When Logic is Complex
- 9 CSS
- 9.1 Adopt a Design System
- 9.2 Adopt a CSS Strategy
- 9.2.1 A CSS Framework
- 9.2.2 Object-Oriented CSS
- 9.2.3 Functional CSS
- 9.3 Create a Living Style Guide to Document Your Design System and CSS Strategy
- 10 Minimize JavaScript
- 10.1 How and Why JavaScript is a Serious Liability
- 10.1.1 You Cannot Control The Runtime Environment
- 10.1.2 JavaScript’s Behavior is Difficult to Observe
- 10.1.3 The Ecosystem Values Highly-Decoupled Modules that Favor Progress over Stability
- 10.2 Embrace Server-Rendered Rails Views
- 10.2.1 Architecture of Rails Server-Rendered Views
- 10.2.2 Architecture of the JAM Stack
- 10.2.3 Server-Rendered Views by Default, JAM Stack Only When Needed
- 10.3 Tweak Turbo to Provide a Slightly Better Experience
- 11 Carefully Manage the JavaScript You Need
- 11.1 Embrace Plain JavaScript for Basic Interactions
- 11.2 Carefully Choose One Framework When You Need It
- 11.3 Ensure System Tests Fail When JavaScript is Broken
- 12 Testing the View
- 12.1 Understand the Value and Cost of Tests
- 12.2 Use:rack_testfor non-JavaScript User Flows
- 12.3 Test Against Default Markup and Content Initially
- 12.4 Cultivate Explicit Diagnostic Tools to Debug Test Failures
- 12.5 Fake The Back-end To Get System Tests Passing
- 12.6 Usedata-testidAttributes to Combat Brittle Tests
- 12.7 Test JavaScript Interactions with a Real Browser
- 12.7.1 Setting Up Headless Chrome
- 12.7.2 Writing a Browser-driven System Test Case
- 12.7.3 Enhancingwith_cluesto Dump Browser Logs
- 13 Models, Part
- 13.1 Active Record is for Database Access
- 13.1.1 Creating Some Example Active Records
- 13.1.2 Model the Database With Active Record’s DSL
- 13.1.3 Class Methods Should Be Used to Re-use Common Database Operations
- 13.1.4Instance Methods Should Implement Domain Concepts Derivable Directly from the Database
- 13.2 Active Model is for Resource Modeling
- 14 The Database
- 14.1 Logical and Physical Data Models
- 14.2 Create a Logical Model to Build Consensus
- 14.3 Planning the Physical Model to Enforce Correctness
- 14.3.1 The Database Should Be Designed for Correctness
- 14.3.2 Use a SQL Schema
- 14.3.3 UseTIMESTAMP WITH TIME ZONEFor Timestamps
- 14.3.4 Planning the Physical Model
- 14.4 Creating Correct Migrations
- 14.4.1 Creating the Migration File and Helper Scripts
- 14.4.2 Iteratively Writing Migration Code to Create the Correct Schema
- 14.5 Writing Tests for Database Constraints
- 15 Business Logic Code is a Seam
- 15.1 Business Logic Code Must Reveal Behavior
- 15.2 Services are Stateless, Explicitly-Named Classes with Explicitly-Named Methods
- 15.2.1 AThingDoerClass With ado_thingMethod is Fine
- 15.2.2 Methods Receive Context and Data on Which to Operate, not Services to Delegate To
- 15.2.3 Return Rich Result Objects, not Booleans or Active Records
- 15.3 Implementation Patterns You Might Want to Avoid
- 15.3.1 Creating Class Methods Closes Doors
- 15.3.2 “Service Objects” UsingcallSolve No Problem and Obscure Behavior
- 15.3.3 Dependency Injection also Obscures Behavior
- 16 Models, Part
- 16.1 Validations Don’t Provide Data Integrity
- 16.1.1 Outside Code Naturally Skips Validations
- 16.1.2 Rails’ Public API Allows Bypassing Validations
- 16.1.3 Some Validations Don’t Technically Work
- 16.2 Validations Are Awesome For User Experience
- 16.3 How to (Barely) Use Callbacks
- 16.4 Scopes are Often Business Logic and Belong Elsewhere
- 16.5 Model Testing Strategy
- 16.5.1 Active Record Tests Should Test Database Constraints
- 16.5.2 Tests For Complex Validations or Callbacks
- 16.5.3 Ensure Anyone Can Create Valid Instances of the Model using Factory Bot
- 17 End-to-End Example
- 17.1 Example Requirements
- 17.2 Building the UI First
- 17.2.1 Setting Up To Build the UI
- 17.2.2 Create Useful Seed Data for Development
- 17.2.3 Sketch the UI using Semantic Tags
- 17.2.4 Provide Basic Polish
- 17.2.5 Style the Form
- 17.2.6 Style Error States
- 17.3 Writing a System Test
- 17.4 Sketch Business Logic and Define the Seam
- 17.5 Fully Implement and Test Business Logic
- 17.6 Finished Implementation
- 18 Controllers
- 18.1 Controller Code is Configuration
- 18.2 Don’t Over-use Callbacks
- 18.3 Controllers Should Convert Parameters to Richer Types
- 18.4 Don’t Over Test
- 18.4.1 Writing a Controller Test
- 18.4.2 Implementing a Basic Confidence-checking System
- 18.4.3 Avoiding Duplicative Tests
- 19 Jobs
- 19.1 Use Jobs To Defer Execution or Increase Fault-Tolerance
- 19.1.1 Web Workers, Worker Pools, Memory, and Compute Power
- 19.1.2 Network Calls and Third Parties are Slow
- 19.1.3 Network Calls and Third Parties are Flaky
- 19.1.4 Use Background Jobs Only When Needed
- 19.2 Understand How Your Job Backend Works
- 19.2.1 Understand Where and How Jobs (and their Arguments) are Queued
- 19.2.2 Understand What Happens When a Job Fails
- 19.2.3 Observe the Behavior of Your Job Backend
- 19.3 Sidekiq is The Best Job Backend for Most Teams
- 19.4 Queue Jobs Directly, and Have Them Defer to Your Business Logic Code
- 19.4.1 Do Not Use Active Job - Use the Job Backend Directly
- 19.4.2 Job Code Should Defer to Your Service Layer
- 19.5 Job Testing Strategies
- 19.6 Jobs Will Get Retried and Must Be Idempotent
- 20 Other Boundary Classes
- 20.1 Mailers
- 20.1.1 Mailers Should Just Format Emails
- 20.1.2 Mailers are Usually Jobs
- 20.1.3 Previewing, Styling, and Checking your Mail
- 20.1.4 Using Mailcatcher to Allow Emails to be Sent in Development
- 20.2 Rake Tasks
- 20.2.1 Rake Tasks Are For Automation
- 20.2.2 One Task Per File, Namespaces Match Directories
- 20.2.3 Rake Tasks Should Not Contain Business Logic
- 20.2.4 Prefer Ruby Command Line Apps for Developer Automation
- 20.3 Mailboxes, Cables, and Active Storage
- 20.3.1 Action Mailbox
- 20.3.2 Action Cable
- 20.3.3 Active Storage
- 21 Authentication and Authorization III Beyond Rails
- 21.1 When in Doubt Use Devise or OmniAuth
- 21.1.1 Use OmniAuth to Authenticate Using a Third Party
- 21.1.2 Building Authentication Into your App with Devise
- 21.2 Authorization and Role-based Access Controls
- 21.2.1 Map Resources and Actions to Job Titles and Departments
- 21.2.2 Use Cancancan to Implement Role-Based Access
- 21.2.3 You Don’t Have to Use All of Cancancan’s Features
- 21.3 Test Access Controls In System Tests
- 22 API Endpoints
- 22.1 Be Clear About What—and Who—Your API is For
- 22.2 Write APIs the Same Way You Write Other Code
- 22.3 Use the Simplest Authentication System You Can
- 22.4 Use the Simplest Content Type You Can
- 22.5 Just Put The Version in the URL
- 22.6 Use.to_jsonto Create JSON
- 22.6.1 How Rails Renders JSON
- 22.6.2 Customizing JSON Serialization
- 22.6.3 Customize JSON in the Models Themselves
- 22.6.4 Always Use a Top Level Key
- 22.7 Test API Endpoints
- 23 Sustainable Process and Workflows
- 23.1 Use Continuous Integration To Deploy
- 23.1.1 What is CI?
- 23.1.2 CI Configuration Should be Explicit and Managed
- 23.1.3 CI Should be Based onbin/setupandbin/ci
- 23.2 Frequent Dependency Updates
- 23.2.1 Update Dependencies Early and Often
- 23.2.2 A Versioning Policy
- 23.2.3 Automate Dependency Updates
- 23.3 Leverage Generators and Sample Repositories over Documentation
- 23.3.1 Create and Configure Rails Generators
- 23.3.2 Use Template Repositories for Ruby Gems and Rails Apps
- 23.4 RubyGems and Railties Can Distribute Configuration
- 24 Operations
- 24.1 Why Observability Matters
- 24.2 Monitor Business Outcomes
- 24.3 Logging is Powerful
- 24.3.1 Include a Request ID in All Logs
- 24.3.2 Log What Something is and Where it Came From
- 24.3.3 UseCurrentto Include User IDs
- 24.4 Manage Unhandled Exceptions
- 24.5 Measure Performance
- 24.6 Managing Secrets, Keys, and Passwords
- A Setting Up Docker for Local Development IV Appendices
- A.1 Installing Docker
- A.2 What is Docker?
- A.3 Overview of the Environment
- A.4 Creating the Image
- A.5 Starting Up the Environment
- A.6 Executing Commands and Doing Development
- A.7 Customizing the Dev Environment
- A.7.1 Installing Software
- A.7.2 Copying Your Dotfiles Into the Image
- B Monoliths, Microservices, and Shared Databases
- B.1 Monoliths Get a Bad Rap
- B.2 Microservices Are Not a Panacea.
- B.3 Sharing a Database Is Viable
- C Technical Leadership is Critical
- C.1 Leadership Is About Shared Values
- C.2 Leaders Can be Held Accountable
- C.3 Accountability Can be Implicit
- Colophon
# Acknowledgements
If there were no such thing as Rails, this book would be, well, pretty strange. So I must acknowledge and deeply
thank DHH and the Rails core team for building and maintaining such a wonderful framework for all of us to use.
I have to thank my wife, Amy, who gave me the space and encouragement to work on this. During a global
pandemic. When both of us were briefly out of work. And we realized our aging parents require more care than
we thought. And when we got two kittens named Carlos Rosario and Zoni. And when we bought a freaking car.
And when I joined a pre-seed startup. It’s been quite a time.
I also want to thank the technical reviewers, Noel Rappin, Chris Gibson, Zach Campbell, Lisa Sheridan, Raul
Murciano, Geoff The, and Sean Miller. Also special thanks to Brigham Johnson for identifying an embarrassing
number of typos.
## Changes from Previous Versions
This book is intended to be somewhat timeless, and able to be used as a reference. Much of what’s in here hasn’t
changed and I wouldn’t expect it to. That said, some things have changed, and this section captures them.
### December, 12, 2020
- Updated for Rails 6.1 to remove deprecated method of setting errors on Active Records
### January, 21, 2021
- No need to disable Ajax form submissions by default, since Rails 6.1 changed the default behavior.
- Use ofadd_check_constraintandadd_indexinstead of SQL wrapped inreversible.
- Fixed color issues with sidebars on some e-readers
### March 15, 2022
- Updated for Rails 7
- Removal of all NodeJS-related stuff, including removal and re-thinking of the value of unit-testing JavaScript.
- Softened language around using React by default given Hotwire’s existence.
- Changed guidance around nested routes to account for content-heavy marketing pages.
- Clarified the use of controller instance variables for managing UI state.
- Links to gems extracted from code based on the book.
### Dec 4, 2023
This is a more substantial update that previous updates. Chapter numbers refer to the PDF or printed book’s
numbering. e-book numbering continues to be a byzantine nightmare.
- General Changes
**-** Updated for Rails 7.1.
**-** Updated for Ruby 3.
**-** Added explicit language in each section about where to find the sample code for that section.
**-** New cover
- Chapter 1
**-** Update my experience, given the passage of time.
- Chapter 4
**-** Remove mention of Spring and Listen, since they aren’t included and haven’t been in a few versions.
**-** Remove mention of having to add the rexml gem, since selenium-webdriver brings it in.
**-** Changebin/runtobin/dev, since this matches what Rails does (sometimes).
**-** Remove mention of having tobundle updateThor.
**-** Added help flags to the variousbin/scripts.
- Chapter 5
**-** Added a new section that references “Patterns of Enterprise Application Architecture”, since this where
the active record pattern originated.
- Chapter 7
**-** Recommend the use of View Components
**-** Recommend strict locals for partials
- Chapter 8
**-** Clarify that helpers _can_ be made to be modular, and discuss configuring Rails to either treat them that
way or to not generate falsely-modular helpers.
**-** More strongly discourage presenter-like libraries, and remove a lot of content around managing them.
**-** In place of presenters, discuss how using Active Model or View Components can manage complexity
instead of gobs of helpers.
- Chapter 9
**-** Clear warning about Tailwind’s lack of built-in design system and what you should consider if adopting
it.
- Chapter 11
**-** Qualify the recommendation for Hotwire given that 37 Signals have made it clear they will change it
however they like whenever they like.
- Chapter 15
**-** Reference “Patterns of Enterprise Application Architecture” and its definition of a _service layer_ , which is
what this chapter describes.
**-** Make it clear that the term “Service Objects” is not a service layer and is actually just another name for
the command pattern (and that you should not use this pattern).
- Chapter 16
**-** Replace use ofbefore_validationcallback with the newnormalizesmacro
**-** Make a stronger case for not using callbacks by clarifying exactly what they do and are for.
- Chapter 17
**-** Replace the re-usable partial with a View Component in the example.
- Chapter 23
**-** Show code to monkey-patch Thor to make it useful for Rails generators.
**-** Discourage the use of app templates in favor of template repositories.
- Chapter 24
**-** UseCurrentAttributesto store information for the log instead of thread local storage.
**-** Discuss the need to revisit security practices, along with an anecdote from a previous job.
- Appendix A
**-** Re-work Docker stuff based on updated learnings and code.
**-** Explainer on getting your own shell aliases or software into the dev container.
### PART
### I
# introduction
## 1
# Why This Book Exists
Rails can scale. But what does that actually mean? And how do we do it? This book is the answer to both of these
questions, but instead of using “scalable”, which many developers equate with “fast performance”, I’m using the
word “sustainable”. This is really what we want out of our software: the ability to sustain that software over time.
Rails itself is an important component in sustainable web development, since it provides common solutions to
common problems and has reached a significant level of maturity. But it’s not the complete picture.
Rails has a lot of features and we may not need them all. Or, we may need to take some care in how we use them.
Rails also leaves gaps in your application’s architecture that you’ll have to fill (which makes sense, since Rails
can’t possibly provide _everything_ your app will need).
This book will help you navigate all of that.
Before we begin, I want to be clear about what _sustainability_ means and why it’s important. I also want to state
the assumptions I’m making in writing this, because there is no such thing as universal advice—there are only
recommendations that apply in a given context.
### 1.1 What is Sustainability?
The literal interpretation of sustainable web development is web development that can be sustained. As silly as
that definition is, I find it an illuminating restatement.
To _sustain_ the development of our software is to ensure that it can continue to meet its needs. A sustainable web
app can easily suffer new requirements, increased demand for its resources, and an increasing (or changing) team
of developers to maintain it.
A system that is hard to change is hard to sustain. A system that can’t avail itself of the resources it needs to
function is hard to sustain. A system that only _some_ developers can work on is hard to sustain.
Thus, a sustainable application is one in which changes we make tomorrow are as easy as changes are today, for
whatever the application might need to do and whoever might be tasked with working on it.
So this defines _sustainability_ , but why is it important?
### 1.2 Why Care About Sustainability?
Most software exists to meet some need, and if that need will persist over time, so must the software. _Needs_ are
subjective and vague, while software must be objective and specific. Thus, building software is often a matter of
continued refinement as the needs are slowly clarified. And, of course, needs have a habit of changing along the
way.
```
Software is expensive, mostly owing to the expertise required to build and maintain it. People who can write
software find their skills to be in high demand, garnering some of the highest wages in the world, even at entry
levels. It stands to reason that if a piece of software requires more effort to enhance and maintain over time, it
will cost more and more and deliver less and less.
```
```
In an economic sense, sustainable software minimizes the cost of the software over time. But there is a human
cost to working on software. Working on sustainable software is, well, more enjoyable. They say employees quit
managers, but I’ve known developers that quit codebases. Working on unsustainable software just plain sucks,
and I think there’s value in having a job that doesn’t suck... at least not all of the time.
```
```
Of course, it’s one thing to care about sustainability in the abstract, but how does that translate into action?
```
### 1.3 How to Value Sustainability
```
Sustainability is like an investment. It necessarily won’t pay off in the short term and, if the investment isn’t sound,
it won’t ever pay off. So it’s really important to understand the value of sustainability to your given situation and
to have access to as much information as possible to know exactly how to invest in it.
```
```
Predicting the future is dangerous for programmers. It can lead to over-engineering, which makes certain classes
of changes more difficult in the future. To combat this urge, developers often look to the tenets of agile software
development, which have many cute aphorisms that boil down to “don’t build software that you don’t know you
need”.
```
```
If you are a hired consultant, this is excellent advice. It gives you a framework to be successful and manage
change when you are in a situation where you have very little access to information. The strategy of “build
for only what you 100% know you need” works great to get software shipped with confidence, but it doesn’t
necessarily lead to a sustainable outcome.
```
```
For example, no business person is going to ask you to write log statements so you can understand your code in
production. No product owner is going to ask you to create a design system to facilitate building user interfaces
more quickly. And no one is going to require that your database have referential integrity.
```
```
The features of the software are merely one input into what software gets built. They are a significant one just not
the only one. To make better technical decisions, you need access to more information than simply what someone
wants the software to do.
```
```
Do you know what economic or behavioral output the software exists to produce? In other words, how does the
software make money for the people paying you to write it? What improvements to the business is it expected to
make? What is the medium or long-term plan for the business? Does it need to grow significantly? Will there
need to be increased traffic? Will there be an influx of engineers? Will they be very senior, very junior, or a mix?
When will they be hired and when will they start?
```
```
The more information you can get access to the better, because all of this feeds into your technical decision-making
and can tell you just how sustainable your app needs to be. If there will be an influx of less experienced developers,
you might make different decisions than if the team is only hiring one or two experienced specialists.
```
Armed with this sort of information, you can make technical decisions as part of an overall _strategy_. For example,
you may want to spend several days setting up a more sustainable development environment. By pointing to
the company’s growth projections and your team’s hiring plans, that work can be easily justified (see the sidebar
“Understanding Growth At Stitch Fix” on the next page for a specific example of this).
If you don’t have the information about the business, the team, or anything other than what some user wants the
software to do, you aren’t set up to do sustainable development. But it doesn’t mean you shouldn’t ask anyway.
People who don’t have experience writing software won’t necessarily intuit that such information is relevant, so
they might not be forthcoming. But you’d be surprised just how much information you can get from someone by
asking.
Whatever the answers are, you can use this as part of an overall technical strategy, of which sustainability is a part.
As you read this book, I’ll talk about the considerations around the various recommendations and techniques.
They might not all apply to your situation, but many of them will.
Which brings us to the set of assumptions that this book is based on. In other words, what _is_ the situation in
which sustainability is important and in which this book’s recommendations apply?
#### Understanding Growth At Stitch Fix
```
During my first few months at Stitch Fix, I was asked to help improve the operations of our warehouse. There
were many different processes and we had a good sense of which ones to start automating. At the time, there was
only one application—called HELLBLAZER—and it served upstitchfix.com.
If I hadn’t been told anything else, the simplest thing to do would’ve been to make a/warehouseroute in
HELLBLAZERand slowly add features for the associates there. But I had been told something else.
Like almost everyone at the company, the engineering team was told—very transparently—what the growth plans
for the business were. It needed to grow in a certain way or the business would fail. It was easy to extrapolate from
there what that would mean for the size of the engineering team, and for the significance of the warehouse’s efficiency.
It was clear that a single codebase everyone worked in would be a nightmare, and migrating away from it later would
be difficult and expensive.
So, we created a new application that sharedHELLBLAZER’s database. It would’ve certainly been faster to add
code toHELLBLAZERdirectly, but we knew doing so would burn us long-term. As the company grew, the developers
working on warehouse software were fairly isolated since they worked in a totally different codebase. We replicated
this pattern and, after six years of growth, it was clearly the right decision, even accounting for problems that happen
when you share a database between apps.
We never could’ve known that without a full understanding of the company’s growth plans, and long-term vision
for the problems we were there to solve.
```
### 1.4 Assumptions
This book is prescriptive, but each prescription comes with an explanation, and _all_ of the book’s recommendations
are based on some key assumptions that I would like to state explicitly. If your situation differs wildly from the
one described below, you might not get that much out of this book. My hope—and belief—is that the assumptions
below are common, and that the situation of writing software that you find yourself in is similar to situations I
have faced. Thus, this book will help you.
In case it’s not, I want to state my assumptions up front, right here in this free chapter.
#### 1.4.1 The Software Has a Clear Purpose
This might seem like nonsense, but there are times when we don’t exactly know what the software is solving for,
yet need to write some software to explore the problem space.
```
Perhaps some venture capitalist has given us some money, but we don’t yet know the exact market for our solution.
Maybe we’re prototyping a potentially complex UI to do user testing. In these cases we need to be nimble and try
to figure out what the software should do.
```
```
The assumption in this book is that that has already happened. We know generally what problem we are solving,
and we aren’t going to have to pivot from selling shoes to providing AI-powered podiatrist back-office enterprise
software.
```
#### 1.4.2 The Software Needs To Exist For Years
```
This book is about how to sustain development over a longer period of time than a few months, so a big assumption
is that the software actually needs to exist that long!
```
```
A lot of software falls into this category. If you are automating a business process, building a customer experience,
or integrating some back-end systems, it’s likely that software will continue to be needed for quite a while.
```
#### 1.4.3 The Software Will Evolve
```
Sometimes we write code that solves a problem and that problem doesn’t change, so the software is stable. That’s
not an assumption I am making here. Instead, I’m assuming that the software will be subject to changes big and
small over the years it will exist.
```
```
I believe this is more common than not. Software is notoriously hard to get right the first time, so it’s common to
change it iteratively over a long period to arrive at optimal functionality. Software that exists for years also tends
to need to change to keep up with the world around it.
```
#### 1.4.4 The Team Will Change
```
The average tenure of a software engineer at any given company is pretty low, so I’m assuming that the software
will outlive the team, and that the group of people charged with the software’s maintenance and enhancement
will change over time. I’m also assuming the experience levels and skill-sets will change over time as well.
```
#### 1.4.5 You Value Sustainability, Consistency, and Quality
```
Values are fundamental beliefs that drive actions. While the other assumptions might hold for you, if you don’t
actually value sustainability, consistency, and quality, this book isn’t going to help you.
```
```
Sustainability
```
```
If you don’t value sustainability as I’ve defined it, you likely didn’t pick up this book or have stopped reading by
now. You’re here because you think sustainability is important, thus you value it.
```
```
Consistency
```
Valuing consistency is hugely important as well. Consistency means that designs, systems, processes, components
(etc.), should not be arbitrarily different. Same problems should have same solutions, and there should not be
many ways to do something. It also means being explicit that personal preferences are not critical inputs to
decision-making.
A team that values consistency is a sustainable team and will produce sustainable software. When code is
consistent, it can be confidently abstracted into shared libraries. When processes are consistent, they can be
confidently automated to make everyone more productive.
When architecture and design are consistent, knowledge can be transferred, and the team, the systems, and even
the business itself can survive potentially radical change (see the sidebar “Our Uneventful Migration to AWS” on
the next page for how Stitch Fix capitalized on consistency to migrate from Heroku to AWS with no downtime or
outages).
**Quality**
Quality is a vague notion, but it’s important to both understand it and to value it. In a sense, valuing quality
means doing things right the first time. But “doing things right” doesn’t mean over-engineering, gold-plating, or
doing something fancy that’s not called for.
Valuing quality is to acknowledge the reality that we aren’t going to be able to go back and clean things up after
they have been shipped. There is this fantasy developers engage in that they can simply “acquire technical debt”
and someday “pay it down”.
I have never seen this happen, at least not in the way developers think it might. It is extremely difficult to make
a business case to modify working software simply to make it “higher quality”. Usually, there must be some
catastrophic failure to get the resources to clean up a previously-made mess. It’s simpler and easier to manage a
process by which messes don’t get made as a matter of course.
Quality should be part of the everyday process. Doing this consistently will result in predictable output, which is
what managers really want to see. On the occasion when a date must be hit, cut scope, not corners. Only the
developers know what scope to cut in order to get meaningfully faster delivery, but this requires having as much
information about the business strategy as possible.
When you value sustainability, consistency, and quality, you will be unlikely to find yourself in a situation where
you must undo a technical decision you made at the cost of shipping more features. Business people may want
software delivered as fast as possible, but they _really_ don’t want to go an extended period without any features so
that the engineering team can “pay down” technical debt.
We know what sustainability is, how to value it, what assumptions I’m making going in, and the values that drive
the tactics and strategy for the rest of the book. But there are two concepts I want to discuss that allow us to
attempt to quantify just how sustainable our decisions are: opportunity costs and carrying costs.
#### Our Uneventful Migration to AWS
```
For several years, Stitch Fix used the platform-as-a-service Heroku. We were consistent in how we used it, as well
as in how our applications were designed. We used one type of relational database, one type of cache, one type of
CDN, etc.
In our run-up to going public, we needed to migrate to AWS, which is very different from Heroku. We had a team
of initially two people and eventually three to do the migration for the 100+ person engineering team. We didn’t
want downtime, outages, or radical changes in the developer experience.
Because everything was so consistent, the migration team was able to quickly build a deployment pipeline and
command-line tool to provide a Heroku-like experience to the developers. Over several months we migrated one app
and one database at a time. Developers barely noticed, and our users and customers had no idea.
The project lead was so confident in the approach and the team that he kept his scheduled camping trip to an
isolated mountain in Colorado, unreachable by the rest of the team as they movedstitchfix.comfrom Heroku to
AWS to complete the migration. Consistency was a big part of making this a non-event.
```
### 1.5 Opportunity and Carrying Costs
An _opportunity cost_ is basically a one-time cost to produce something. By committing to work, you necessarily cut
off other avenues of opportunity. This cost can be a useful lens to compare two different approaches when trying
to perform a cost/benefit analysis. An opportunity cost that we’ll take in a few chapters is writing robust scripts
for setting up our app, running it, and running its tests. It has a higher opportunity cost than simply writing
documentation about how to do those things.
But sometimes an investment is worth making. The way to know if that’s true is to talk about the _carrying cost_. A
carrying cost is a cost you have to pay all the time every time. If it’s difficult to run your app in development,
reading the documentation about how to do so and running all the various commands is a cost you pay frequently.
Carrying costs affect sustainability more than anything. Each line of code is a carrying cost. Each new feature has
a carrying cost. Each thing we have to remember to do is a carrying cost. This is the true value provided by Rails:
it reduces the carrying costs of a lot of pretty common patterns when building a web app.
To sustainably write software requires carefully balancing your carrying costs, and strategically incurring opportu-
nity costs that can reduce, or at least maintain, your carrying costs.
If there are two concepts most useful to engineers, it is these two.
The last bit of information I want to share is about me. This book amounts to my advice based on my experience,
and you need to know about that, because, let’s face it, the field of computer programming is pretty far away
from science, and most of the advice we get is nicely-formatted survivorship bias.
### 1.6 Why should you trust me?
Software engineering is notoriously hard to study and most of what exists about how to write software is anecdotal
evidence or experience reports. This book is no different, but I do believe that if you are facing problems similar
to those I have faced, there is value in here.
So I want to outline what my experience is that has led to me recommend what I do in this book.
The most important thing to know about me is that I’m not a software consultant, nor have I been in a very long
time. For the past fifteen years I have been a product engineer (or part of a project engineering team), working
for companies building one or more products designed to last. I was a rank and file engineer at times, a manager
on occasion, an architect responsible for technical strategy and, most recently, Chief Technology Officer (CTO) at
a venture-backed startup. I’ve written a lot of code and set a lot of technical and product strategy.
What this means is that the experience upon which this book is based comes from actually building software
meant to be sustained. I have actually done—and seen the long-term results of doing—pretty much everything in
this book. I’ve been responsible for sustainable software several times during my career.
- I spent four years at an energy startup that sold enterprise software. I saw the product evolve from almost
nothing to a successful company with many clients and over 100 engineers. While the software was
Java-based, much of what I learned about sustainability applies to the Rails world as well.
- I spent the next year and half at an e-commerce company that had reached what would be the peak of its
success. I joined a team of almost 200 engineers, many of whom were working in a huge Rails monolith that
contained thousands of lines of code, all done “The Rails Way”. The team had experienced massive growth
and this growth was not managed. The primary application we all worked in was wholly unsustainable and
had a massive carrying cost simply existing.
- I then spent the next six and half years at Stitch Fix, where I was the third engineer and helped set the
technical direction for the team. By the time I left, the team was 200 engineers, collectively managing a
microservices-based architecture of over 50 Rails applications, many of which I contributed to. At that time
I was responsible for the overall technical strategy for the team and was able to observe which decisions we
made in 2013 ended up being good (or bad) by 2019.
- I was CTO of a healthcare startup, having written literally the first line of code, navigating the tumultuous
world of finding product/market fit, becoming HIPAA^1 -compliant, and trying to never be a bottleneck for
what the company needed to do.
What I don’t have much experience with is working on short-term greenfield projects, or being dropped into a
mess to help clean it up (so-called “Rails Rescue” projects). There’s nothing wrong with this kind of experience,
but that’s not what this book is about.
What follows is what I tried to take away from the experience above, from the great decisions my colleagues and I
made, to the unfortunate ones as well (I pushed hard for both Coffeescript and Angular 1 and we see how those
turned out).
But, as they say, your mileage may vary, “it depends”, and everything is a trade-off. I will do my best to clarify the
trade-offs.
### Up Next
This chapter should’ve given you a sense of what you’re in for and whether or not this book is for you. I hope it is!
So, let’s move on. Because this book is about Ruby on Rails, I want to give an overview of the application
architecture Rails provides by default, and how those pieces relate to each other. From that basis, we can then
deep dive into each part of Rails and learn how to use it sustainably.
(^1) HIPAA is the Health Insurance Portability and Accountability Act, a curious law in the United States related to how healthcare information
is managed. Like all compliance-related frameworks, it thwarts sustainability, but it’s a fact of life in the U.S.
## 2 The Rails Application Architecture
This book contains guidelines, tips, and recipes for managing the architecture of your Rails application as it
grows over time, so I want to start with a review of the default application architecture you get with Rails. This
architecture is extremely powerful, mostly because it exists right after you runrails newand it provides a solid
way to organize the code in your application.
Rails is often referred to as an “MVC Framework”, MVC standing for “Model, View, Controller”. Rails does, in fact,
have models, views, and controllers, but digging into the history of MVC and trying to sort out how it relates to
Rails can create confusion, since the concepts don’t exactly match up. This is OK, we don’t need them to.
We’ll skip the theory and look at the actual parts of Rails and how they contribute to the overall application you
build with Rails. Although there are quite a few moving parts, each part falls into one of four categories:
- **Boundaries** , which accept input from somewhere and arrange for output to be rendered or sent. Controllers,
Mailers, etc are boundaries.
- **Views** , which present information out, usually in HTML. ERB files, JavaScript, CSS, and even JBuilder files
are all part of the view.
- **Models** , which are the Active Record classes that interact with your database.
- **Everything else**.
Rails doesn’t talk about the parts this way, but we will, since it allows us to group similar parts together when
talking about how they work. The figure “Rails’ Default Application Architecture” on the next page shows all the
parts of Rails and which of the four categories they fall into. The diagram shows that:
- The boundaries of your Rails app are the controllers, jobs, mailers, mailboxes, channels, and rake tasks, as
well as Active Storage.
- The view is comprised of ERB, JavaScript, CSS, Images, Fonts, and other assets like PDFs or binary files.
- The models are, well, your models, and they are what talk to your database (though a model does not _have_
to talk to a database)
- Anything not mentioned, like configuration files or yourGemfile, are in the catch-all “everything else”
bucket.
Let’s now go through each layer and talk about the parts of Rails in that layer and what they are all generally for.
I’ll stay as close as I can to what I believe the intent of the Rails core team is and try not to embellish or assume
too much.
First, we’ll start with _Boundaries_ , which broker input and output.
```
Figure 2.1: Rails’ Default Application Architecture
```
### 2.1 Boundaries
The Rails Guide^1 says that controllers are
... responsible for making sense of the request, and producing the appropriate output.
When you look at Jobs, Channels, Mailers, Mailboxes, Active Storage, and Rake Tasks, they perform similar
functions. In a general sense, no matter what else goes in these areas, they _have_ to:
- examine the input to make some sort of sense of it.
- trigger some business logic
- examine the output of that business logic and provide some sort of output or effect.
Of course, not all use cases require reading explicit input or generating explicit output, but the overall structure of
the innards of any of these classes, at least at a high level, is the same, as shown in the figure below.
This figure shows that:
1. Some input might come in that triggers the Boundary class
2. The Boundary class examines that input to see if it understands it
(^1) https://guides.rubyonrails.org/action_controller_overview.html
```
Figure 2.2: Structure of a Boundary Class
```
3. Some business logic happens
4. The result of that logic is examined
5. Explicit output is possibly sent
```
For now, we’re not going to talk about the business logic, specifically if it should be directly in the boundary classes
or not. The point is that, no matter where the business logic is, these boundary classes are always responsible for
looking at the input, initiating the logic, and assembling the output.
```
We’ll talk about these boundary classes in more detail in “Controllers” on page 281, “Jobs” on page 291, and
“Other Boundary Classes” on page 313.
```
Because Rails is for building web applications, the output of many of our boundary classes is a web view or some
other dynamic output. And creating the view layer of a web application—even if it’s just JSON—can be complex,
which is why a big chunk of Rails is involved in these views.
```
### 2.2 Views
```
Rails support for rendering HTML web views is quite sophisticated and powerful. In particular, the coupling
between Active Model and Rails’ form helpers is very tight (a great example of the power in tightly-coupling
components). Actions performed by boundary classes that result in dynamic output (usually controllers and
mailers) will initiate the rendering of the view from a template, and that template may pull in JavaScript, CSS, or
other templates (partials).
```
Often the templates are HTML, but they can be pretty much anything, including JSON, text, or XML. Templates
also have access to _helpers_ , which are free functions in the global namespace. Rails provides many helpers by
default, and you can make your own.
View code tends to feel messy, because while a particular template can be isolated pretty well, including
decomposing it into re-usable partials, CSS and JavaScript by their nature aren’t organized the same way. Often
CSS and JavaScript are globally available and taking care to keep them isolated can be tricky.
Rails 7 includes what DHH and the core team believe to be the best ways to manage JavaScript, and most of them
boil down to getting JavaScript (and CSS) to the venerable Asset Pipeline.
Rails is also designed for server-rendered views, and this is where the tight-coupling comes into play. Take this
pretty standard ERB for rendering an edit form for a widget:
<% form_for @widget **do** |form| %>
<%= form.label **:name** %>
<%= form.text **:name** %>
<%= form.submit %>
<% **end** %>
To create the same form in an alternate front-end technology (such as React) would require quite a bit more
code, and it would require specific markup in order to be interpreted by the controller this form submits to. Thus,
replacing the Rails view layer with a single page application requires both giving up some of the power of Rails
and providing your own solution to the problems Rails has already solved.
We’ll discuss aspects of the view in “Routes and URLs” on page 65, “HTML Templates” on page 83, “Helpers” on
page 107, “CSS” on page 125, “Minimize JavaScript” on page 141, “Carefully Manage the JavaScript You Need”
on page 151, and “Testing the View” on page 161. Unlike most other parts of Rails, the view brings together a ton
of different technologies, so it requires a more detailed analysis.
The boundaries and views make up most of the plumbing of a Rails application, which leaves us with the models.
### 2.3 Models
Models are almost always about interacting with the database. Any database table you need access to will
assuredly require a model for you to do it, and you likely have one or more database migrations to manage that
table’s schema.
This isn’t to say that everything we call a “model” has to be about a database, but the history of Rails is such
that the two are used synonymously. It wasn’t until Rails 4 that it become straightforward to make a model that
worked with the view layer that was not an Active Record. The result of this historical baggage is that developers
almost always use “model” to mean “thing that accesses the database”.
Even non-database-table-accessing models (powered by Active Model) still bear a similar mark to the Active
Records. They are both essentially data structures whose members are public and can be modified directly. Of
course code likewidget.name = "Stembolt"is actually a method call, but the overall design of Active Records
and Active Models is one in which public data can be manipulated and there is no encapsulation.
```
In addition to providing access to structured data, models also tend to be where all the business logic is placed,
mostly because Rails doesn’t prescribe any other place for it to go. We’ll talk about the problems with this
approach in the chapter “Business Logic (Does Not Go in Active Records)” on page 49.
The model layer also includes the database migrations, which create the schema for the database being used.
These are often the only artifact in a Rails app other than the database schema itself that tells you what attributes
are defined on Active Records, since Rails dynamically creates those attributes based on what it finds in the
database.
We’ll cover models in “Models, Part 1” on page 181, “The Database” on page 191, and “Models, Part 2 on page
```
227. We’ll discuss business logic specifically in “Business Logic (Does Not Go in Active Records)” on page 49 and
“Business Logic Code is a Seam” on page 215.
```
There are a few other bits of your Rails app that you’re less likely to think about, but are still important.
```
### 2.4 Everything Else
```
Although your Rails app in production is going to be running the code in your Boundaries, Views, and Models,
there is other code that is critical to the sustainability of your Rails app, and I want to mention it here because it’s
important and we’ll talk about it later.
```
First are tests, and there are often tests for each class. But there are also both system tests and integration tests,
which test user flows across many classes. We’ll discuss this in “Helpers Should Be Tested and Testable” on page
117, “Ensure System Tests Fail When JavaScript is Broken” on page 159, “Testing the View” on page 161, “Writing
Tests for Database Constraints” on page 212, “Don’t Over Test” in the “Controllers” chapter on page 285, and in
other parts throughout the book.
There are, of course, your application dependencies as declared inGemfileand eitherpackage.jsonor
config/importmap.rb, as well as the Rails configuration files inconfig/that you might need to modify.
There is alsodb/seeds.rb, which contains data that Rails describes both as useful for production but also for
development. We’ll talk about that in more detail later, but I don’t consider it part of the model layer since it’s
more of a thing used for development or operations and isn’t used in production by default.
Lastly, there isbin/setup, which sets up your app. Rails provides a version of this that provides installation of
gems and basic database setup. We’ll talk about this in detail in “Start Your App Off Right” on page 27.
With our tour of Rails done, let’s talk about the pros and cons of what Rails gives you.
### 2.5 The Pros and Cons of the Rails Application Architecture
It’s important to understand just how powerful the Rails Application Architecture is. Working in any other system
(at least one that did not just duplicate Rails) requires a team to make a lot of decisions about the internal
architecture before they really even get going.
In most situations, teams will end up designing something that looks like Rails anyway (see the sidebar “Maintain-
ing the Architecture of a Java Spring App” on the next page for just how much work there is without having Rails
to help).
What this means is that a team working on a Rails app doesn’t have to make a bunch of big up-front decisions in
order to get started and they don’t have to worry about big drifts in the structure of the codebase.
We can also easily work within this architecture to create a sustainable application. We don’t need to abstract our
code from Rails, or create a framework-within-a-framework. We just need to be intentional in how we use Rails,
and fill in a few gaps for cases where Rails doesn’t provide guidance for what we should do.
There are two downsides to the Rails Application Architecture. The first is that it’s designed to build a particular
type of application: a database-backed web application. If you aren’t doing that, Rails isn’t much help. The second
downside is one Rails can’t really do much about. Rails provides no guidance about where business logic should
go. The result is that every Rails developer I’ve ever met has a slightly different take on it, though those same
developers also have had a bad experience with a variety of strategies.
We’ll talk about this specific problem in several chapters, notably “Business Logic (Does Not Go in Active Records)”
on page 49. It’s important to understand that while DHH, the creator of Rails, might put business logic in models,
the Rails documentation doesn’t explicitly say this—developers used to put business logic in their controllers
before the “fat model, skinny controller” aphorism became popular.
#### Maintaining The Architecture of a Java Spring App
```
I was the tech lead for an application to be built with the Java Spring Framework. Like Rails, Spring is incredibly
powerful. Unlike Rails, however, Spring provides little guidance or direction on how to structure your application.
There were many ways to map routes to controllers, you could name your controller methods anything, and you
could use any database layer you wanted (and the most common database layer—Hibernate—also provides no presets
or guidance and has ultimate flexibility).
The team and I set up a basic structure of where files would go, naming conventions, configuration options, etc.
It wasn’t hard, but it did take time and required documentation. I even wrote some shell scripts to generate some
boilerplate code to help everyone follow the conventions.
The entire build of the product required constant vigilance for adherence to the architectural conventions. New
developers would deviate, veteran developers would forget, and it ended up being a constant tax on the productivity
of the team. I’ve never experienced this with a team working on a Rails application.
```
### Where We Go From Here
I strongly believe that software should be developed with a user focus, and that the behavior of the software must
flow from the user. This means that working “outside in” is preferred. If we know the user experience we want to
create, the code we write can then be laser-focused on making that experience happen.
Before we can think about the user, we have to have a working environment first, and we have to have some
semblance of a Rails app in which to work.
The next chapter will outline what you need to follow along in the book. The chapter after that will involve
creating a new Rails app, all set up for sustainable web development.
## 3 Following Along in This Book
To follow along in this book, you’ll need to know a few things about how it’s written as well as to have a working
development environment. This chapter will give you an outline of everything you need.
### 3.1 Typographic Conventions
This book contains both code listings as well as instructions for running commands in a shell.
Code listings will usually be preceded with the filename and either show the entire file or provide enough context
to know where in the file I’m referring to. Changes will be highlighted with arrows. Lines to remove, if not
obvious from context, are called out with an “x”. For example, the following code listing shows a single method of
a Rails controller where we have changed two lines and removed one^1 :
```
# app/controllers/widgets_controller.rb
```
**def** create
@widget **=** Widget.create(widget_params)
**if** @widget.valid?
×# puts "debug: #{widget_params}"
→ Rails.logger.info(@widget.inspect)
→ redirect_to widget_path(@widget)
**else**
render **:new
end
end**
For shell commands, the command you need to type is preceded by a greater-than sign (>), and the output of that
command is shown without any prefix, like so:
> ls app
controllers models views
(^1) For reasons beyond my understanding, the code listings in the book are difficult to copy and paste. You can always download the code if
you don’t want to type it in.
On occasion, the output will be very long or otherwise too verbose to include. In that case, I’ll use guillemets
around a message indicating the output was elided, like so:
> yarn install
«lots of output»
Sometimes the output is useful but is too wide to fit on the page. In _that_ case, the lines will be truncated with an
ellipsis (... ) like so:
> bin/rails test
A very very long line that is not that important for you to see, bu...
Followed by some possibly short lines
And then maybe some much much longer lines that will have to be tru...
Sometimes a command needs to be on more than one line, due to the constraints of the medium. In that case, I’ll
use the standard Unix mechanism for this, which is the backslash character (\):
> bin/rails g model Widget \
name:string \
quantity:int \
description:text
If you are using a UNIX shell, these backslashes will work and you can type the command in just like it is.
Unless otherwise stated, _all_ shell commands are assumed to be running in your development environment.
Sometimes, however, we need to run commands inside the Rails console or inside the database. In those cases, I’ll
show the command to start the console/connect to the database, and then a change in prompt.
Here is how you would start a Rails console and then count the number of Widgets with a quantity greater than 1:
> bin/rails c
console> Widget.where("quantity > 1").count
99
Here is how you’d do that in SQL:
> bin/rails dbconsole
db> select count(*) from widgets where quantity > 1;
+-------+
| count |
|-------|
| 99 |
+-------+
Finally, note that when Rails console or SQL statements require more space than can fit on one line I _won’t_ be
using the backslash notation, because that notation won’t work in those environments. Sometimes the output will
be formatted to fit this medium and won’t match exactly, but hopefully it’ll all make sense.
Next you need to make sure you have the same versions of the software I do.
### 3.2 Software Versions
Most of the code in this book is executed by a script as the book itself is compiled from the original source
Markdown. This means that, hopefully, any issues with it were sorted out by me before they got to you. If you _do_
have problems, the best way to figure them out is if you and I are using the same environment.
Those versions are:
- Ruby 3.2.2, specifically:
```
> ruby --version
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [aarch64-linux]
```
- Ruby on Rails 7.1.1
- Postgres 15
- Redis 6.2.6
- Bundler 2.4.13
- RubyGems 3.4.13
- Debian bookworm, specifically:
```
> lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 12 (bookworm)
Release: 12
Codename: bookworm
```
In Setting Up Docker for Local Development on page 397, I’ll walk you through setting up an environment
identical to mine, but if you already have a setup you prefer, by all means use that. Try to match versions as much
as possible so if you run into any problems, it’ll eliminate at least a few sources of errors.
### 3.3 Sample Code
Most of the code shown in this book is generated by the source code of the book. At the end of each section a
snapshot is taken of the status of the app being built. You can download the code directly from the book’s website
athttps://sustainable-rails.com/assets/sample-code.zip^2
In each section where there is sample code available, there will be a note at the start of the section that indicates
where to find the code, like so:
```
This section’s code is in the folder03-02of the sample code.
```
When you unzipsample-code.zip, you’ll see a bunch of folders with numeric names. The message above says
that the code in the section you are about to read is in the folder named03-02. In almost every case, each folder
contains the folderwidgets/, which contains the entire state or the sample app _after_ that section is completed.
The folders have numeric names that are ordered in the same way the book is. Thus, all changes would be against
the version of the app in03-01in the example above.
If you are reading this as a PDF or a print book, the numbers should match the chapters and sections. If you are
reading an ePub version on an eReader, the chapter numbers are unfortunately out of my control, so please pay
attention to the notes at the start of each section.
Note that some sample code is just shown in the book as an example. The code in the downloadable.zipfile is
only the code for the example app you’ll build in this book. But, that’s a significant portion of the code!
### Up Next
Now that you’re oriented on the book and ready to write code, let’s start where everyone has to start with Rails,
which is setting up a new app. There’s more than just runningrails newif you want to get set up for sustainable
development.
(^2) https://sustainable-rails.com/assets/sample-code.zip
## 4 Start Your App Off Right
```
This section’s code is in the folder04-01/of the sample code.
```
rails newis pretty powerful. It gives you a ready-to-go Rails application you can start building immediately. But
it doesn’t completely set us up for sustainable development.
We know a few things about our app right now:
- Other developers will work on it, and need to be able to set it up, run its tests, and run it locally.
- It will eventually have security vulnerabilities (in our code and in our dependencies).
- It will be deployed into production via a continuous integration pipeline and require operational observability.
Given the assumptions we listed in the first chapter, we are also quite confident that the app will get more complex
over time and more and more developers will work on it.
Before we start writing code, we’re going to take a few minutes to consider how we create our app, how developers
will set it up and work with it, and how we’ll manage it in production. In other words, we need to consider
_developer workflow_ , which starts with setup and ends with maintaining the app in production.
The figure “Developer Workflow” on the next page shows this workflow and the parts of it that we’ll create in this
chapter.
The diagram shows:
- bin/setupwill set up our app after we’ve pulled it down from version control.
- bin/devwill be used to run our app locally, with the dotenv gem providing runtime configuration for
development and testing.
- bin/ciwill run all of our quality checks, suitable for running in CI, which will include both tests and security
analysis via Brakeman andbundle audit.
- In production, we’ll get all runtime configuration from the UNIX environment, and we’ll use the lograge
gem to configure more production-friendly log output.
This won’t take a lot of code or configuration, and we’ll end up with automation, which is far more effective and
easier to maintain than documentation (see the sidebar “Automating Alert Setup” on the next page to learn how
powerful automation can be).
Before any of this, however, we need an app to work in.
```
Figure 4.1: Developer Workflow
```
### 4.1 Creating a Rails App
This book is intended to be easily referred to after you’re done reading it, so we won’t be embarking on a hero’s
journey to build an app together. That said, it’s helpful to have a single running example, so we’ll create that now.
It’ll be called “widgets” because it will manage the sale of widgets. Boring, I know, but I don’t want you getting
distracted by something more fanciful.
I recommend tailoring yourrails newcommand as little as possible. It can be hard to add back parts of Rails you
initially skip, and for the most part, the parts of Rails you don’t use can sit there, inert, not bothering anyone.
Since we’re using Postgres as our database, we can specify that torails newso we have the right gems and
configuration. This gives the following invocation to create our app:
> rails new --database=postgresql widgets
«lots of output»
We aren’t yet ready to run our app or its tests because Rails needs to know how to connect to Postgres. This leads
us nicely to our next topic on managing runtime configuration.
#### Automating Alert Setup
```
When Stitch Fix was deploying to Heroku, we had a battery of monitors and alerts that each application needed to
have. Setting all of these up was critical to understanding the behavior of our apps, but the setup was lengthy and
complex.
Almost everyone that had to do this setup messed up some part of it. Some developers would skip it entirely. But
the documentation was updated, correct, and made a strong case for why the steps had to be followed. It was just too
complex to do well, and too important to leave to documentation alone.
Eventually, we implemented automation in our deployment pipeline that detected an app’s structure and automat-
ically set up all the monitoring and alerting it would need. This “documentation” was always up to date, and was
always followed because we automated it.
```
### 4.2 Using The Environment for Runtime Configuration
```
This section’s code is in the folder04-02/of the sample code.
```
_Runtime configuration_ is information Rails cannot properly determine on its own, but that is critical for your app
to be able to start up and run. This information also tends to be different in development, test, and production.
Database credentials are a great example.
Rails provides three mechanisms that all work together to manage runtime configuration: the UNIX envi-
ronment,config/database.yml, and an encrypted YAML file calledconfig/credentials.yml.enc(encrypted
withconfig/master.key). In my experience, this creates a lot of confusion and makes scripting a consistent
environment difficult. We value consistency, so we want _one_ way to manage runtime configuration, not three.
Managing files in production is becoming both increasingly difficult (due to ephemeral, containerized deployment
systems), and increasingly risky, since runtime configuration is often secret information like credentials and API
keys.
To that end, we’ll follow the architecture of a 12-Factor App^1 and standardize on the UNIX environment. The
UNIX environment is a set of key/value pairs provided by the operating system to the application. In a Ruby
application, you can access it via theENVconstant.
For example, if your API key to your payment processor is “abcdefg1234”, you would arrange to have that value
set in the UNIX environment, under a key, such asPAYMENTS_API_KEY. You can then access it at runtime via
ENV["PAYMENTS_API_KEY"].
Rails already uses this mechanism for database credentials (looking at the keyDATABASE_URL) as well as the
general secret key used for encrypting cookies (under the keySECRET_KEY_BASE).
Because of this, there’s nothing special we need to do in our app about this—we just need to useENVto access
runtime credentials (see the sidebar “Be Careful with ENV” on the next page for how to do this safely). That said,
the existence of the other mechanisms in our app will be confusing, so we should delete those files now:
> rm config/database.yml config/credentials.yml.enc \
config/master.key
When we deploy, we’ll need to make sure that bothDATABASE_URLandSECRET_KEY_BASEhave values in the
production UNIX environment (see the section “Managing Secrets, Keys, and Passwords” on page 393 for some
production and deployment considerations).
This does lead to the question of how to manage this in our local development environment. We don’t want to set
these values in _our_ UNIX environments for two reasons: 1) it is hard to automate across the team, and 2) we may
work on multiple apps which will have different runtime configuration values.
To manage the UNIX environment for our local development, we’ll use a tool called “dotenv”.
(^1) [http://12factor.net](http://12factor.net)
### 4.3 Configuring Local Development Environment with dotenv
```
This section’s code is in the folder04-03/of the sample code.
```
dotenv^2 merges the existing UNIX environment with a set of key/value pairs stored in files. These files are named
for the Rails environment they apply to, so.env.developmentis used to store development environment variables,
and.env.testfor test.
#### Be Careful with ENV
```
Ruby’sENVconstant behaves like aHash, but it’s actually a special object implemented in C. It may only contain
strings (or objects that implementto_str, which is used to store the object insideENV):
```
```
puts ENV.class # => Object
ENV [ "foo" ] = true
## => TypeError (no implicit conversion of true into String)
```
```
This means when you access it, you need to coerce the string value to whatever type you need. A very common
error developers make is assuming the strings"true"and"false"are equivalent to their boolean counterparts. This
leads to code like so:
```
```
if ENV [ "PAYMENTS_DISBLED" ]
give_free_order
end
```
```
The problem is that every non-nilvalue forPAYMENTS_DISBLEDis truthy, including the string"false". Instead,
always use==to compare the value from ENV:
```
```
if ENV [ "PAYMENTS_DISBLED" ] == "true"
give_free_order
end
```
Storing configuration keys and values in files means we avoid having to document what variables a developer
must set and how to get the right value. Using dotenv means that our app can still access its runtime information
fromENV, so our code won’t be littered with checks for the Rails environment.
Since our development and test runtime configuration values aren’t actual secrets, we can safely check them into
version control. We also won’t allow dotenv to run in production, so there’s no chance of files containing secrets
creeping into our app and being used.
(^2) https://github.com/bkeepers/dotenv
This also has the added benefit of pushing more consistency into our developer workflow. There’s really no reason
developers should have different Postgres configurations, and putting the credentials inside files checked into
version control makes being consistent much easier.
First, we’ll install dotenv by addingdotenv-railsit to ourGemfile:
# Gemfile
```
source "https://rubygems.org"
```
ruby "3.2.2"
→# All runtime config comes from the UNIX environment
→# but we use dotenv to store that in files for
→# development and testing
→gem "dotenv-rails", **groups: [:development** , **:test]**
```
# Bundle edge Rails instead: gem "rails", github: "rails/rail...
gem "rails", "~> 7.1.2"
```
Notice how we’ve preceded it with a comment explaining its purpose? This is a good practice to document why
gems are there and what they do. Ruby gems don’t have a great history of self-explanatory naming, so taking a
few seconds to document what a gem is for will help everyone in the future when they need to understand the
app. Rails 7 uses this convention and you should, too.
We can now install dotenv with Bundler:
> bundle install
«lots of output»
When Bundler loads the dotenv-rails gem, the gem activates itself automatically. There’s no further action we
need to take for our app to use it (other than creating the files containing the environment variables). Because
we’ve specified it only in the:developmentand:testgroup, it _won’t_ be used in production.
The last step is to create our initial.env.developmentand.env.testfiles. All they need to specify right now are
the database credentials. If you followed the Docker-based setup on page 397, the Postgres we are using has a
username and password of “postgres”, runs on port 5432, and is available on the host nameddb. We also follow
Rails’ convention for our database names (widgets_developmentandwidgets_test).
Create.env.developmentas follows.
# .env.development
##### DATABASE_URL="
```
postgres://postgres:postgres@db:5432/widgets_development"
```
Now create.env.testsimilarly:
# .env.test
DATABASE_URL=postgres://postgres:postgres@db:5432/widgets_test
Note if you are not using the Docker-based set up described in the Appendix on page 397, you’ll need to use
whatever credentials you used when setting up Postgres yourself. _Also_ note that you don’t need to quote this
value—I’m doing that to avoid a long line extending off the edge of the page.
dotenv recognizes more files than just the two we’ve made. Three of them would be very dangerous to accidentally
check into version control, so we’re going to modify our local.gitignorefile right now to make sure no one ever
adds them.
The first file is named.env, and it’s used in _all_ environments. This leads to a lot of confusion, and in my experience
it is better to have development and testing completely separated, even if that means some duplication in the two
files. The second two files are called.env.development.localand.env.test.local. These two files override
what’s in.env.developmentand.env.test, respectively.
Convention dictates that these two.localfiles are used when you need an actual secret on your development
machine, such as an AWS key to a development S3 bucket. Unlike our local database credentials, you don’t want
to check that into version control since they are actual secrets you want to keep protected.
Although we don’t have any such secrets yet, ignoring.env.development.localand.env.test.local _now_ will
prevent mishaps in the future (and codify our decision to use those files for local secrets when and if needed).
We’ll also follow the convention established in ourGemfileby putting comments in.gitignoreabout why files
are being ignored.
# .gitignore
# Ignore master key for decrypting credentials and more.
/config/master.key
→# The .env file is read for both dev and test
→# and creates more problems than it solves, so
→# we never ever want to use it
→.env
→# .env.*.local files are where we put actual
→# secrets we need for dev and test, so
→# we really don't want them in version control
→.env.*.local
With that done, our Rails app should be able to start up, however any attempt to use it will generate an error
because we have not set up our database. We could do that withbin/rails db:setup, but this would then require
documenting for future developers and we’d rather maintain automation than documentation.
The place to do this is inbin/setup.
### 4.4 Automating Application Setup withbin/setup.
```
This section’s code is in the folder04-04/of the sample code.
```
Rails provides abin/setupscript that is decent, but not perfect. We want ourbin/setupto be a bit more user
friendly, but we also want it to be idempotent, meaning it has the exact same effect every time it’s run. Right now,
that means it must blow away and recreate the database.
Many developers infrequently reset their local database. The problem with this is that your local database
builds up cruft, which can inadvertently create dependencies with tests or local workflows, and this can lead to
complicated and fragile setups just to get the app working locally.
Worse, you might use a copy of the production database to seed local development databases. This is a particularly
unsustainable solution, since it puts potentially personal user information on your computer and becomes slower
and slower to copy over time as the database size increases.
Instead we want to create a culture where the local development database is blown away regularly. This, becomes
a forcing function to a) not depend on particular data in our database to do work, and b) motivate us to script
any such data we _do_ need in thedb/seeds.rbfile so that everyone can have the same setup.
The situation we want to create is that developers new to the app can pull it down from version control, set up
Postgres, runbin/setup, and be good to go. We also want existing developers to get into the habit of doing this
frequently. As the app gets more and more complex to set up, this script can automate all of that, and we don’t
need to worry about documentation going out of date.
Let’s replace the Rails-providedbin/setupwith one of our own. Remember, this script runs before any gems
are installed, so we have to write it with only the Ruby standard library. This script also won’t be something
developers work on frequently, so our best approach is to make it explicit and procedural, free of clever DSLs or
other complicated constructs.
We’ll create a main method calledsetupthat performs the actual setup steps. That will go at the top of the script.
We’ll also need to add the shebang line to indicate this is a Ruby script. We’ll also require Ruby’sOptionParser
library, which we’ll use to allow-hand--helpto trigger a help message. Here’s how this should look:
# bin/setup
**#!/usr/bin/env ruby**
require "optparse"
**def** setup
log "Installing gems"
# Only do bundle install if the much-faster
# bundle check indicates we need to
system! "bundle check || bundle install"
```
log "Dropping & recreating the development database"
# Note that the very first time this runs, db:reset
# will fail, but this failure is fixed by
# doing a db:migrate
system! "bin/rails db:reset || bin/rails db:migrate"
```
```
log "Dropping & recreating the test database"
# Setting the RAILS_ENV explicitly to be sure
# we actually reset the test database
system!( { "RAILS_ENV" => "test" } , "bin/rails db:reset")
```
log "All set up."
log ""
log "To see commonly-needed commands, run:"
log ""
log " bin/setup help"
log ""
**end**
logandsystem!are not in the standard library, and we’ll define them in a moment.system!executes a shell
command (similar to the built-insystemmethod) andlogprints output (similar toputs).
Note how we’ve written this script. Because it’s not something developers will edit frequently, we’ve written
comments about why and how each command works so that if someone needs to go into it, they can quickly
understand what’s going on. And since these comments explain _why_ and not _what_ , they are unlikely to go out of
date.
Comments like this are particularly useful for complicated scripting and setup. The fact thatbin/rails db:reset
will fail the first time it’s run isn’t obvious, and there’s no sense forcing someone to search the web in a moment
of stress as they navigate unfamiliar code.
Before we definelogandsystem!, let’s create a method calledhelpthat will print out help text (note that in
Ruby,$0contains the name of the script being executed).
# bin/setup
```
log " bin/setup help"
```
log ""
**end**
→ **def** help
→ puts "Usage: #{$0}"
→ puts ""
→ puts "Installs gems, recreates dev database, and generally"
→ puts "prepares the app to be run locally"
→ puts ""
→ puts "Other useful commands:"
→ puts ""
→ puts " bin/dev"
→ puts " # run app locally"
→ puts ""
→ puts " bin/ci"
→ puts " # runs all tests and checks as CI would"
→ puts ""
→ puts " bin/rails test"
→ puts " # run non-system tests"
→ puts ""
→ puts " bin/rails test:system"
→ puts " # run system tests"
→ puts ""
→ puts " bin/setup help"
→ puts " # show this help"
→ puts ""
→ **end**
→# start of helpers
We’ll definebin/devandbin/ciin the next section. We’ve documentedbin/rails testandbin/rails
test:systemhere to be helpful to new or inexperienced developers. They might not realize thatbin/rails
-Twill produce a documented list of all rake tasks, and even if they did, it might not be clear which ones run the
tests.
Next, let’s create our two helper methods. First islog, which wrapsputsbut prepends a message to the user that
bin/setupis where the message originated. This can be helpful when interpreting a lot of terminal output.
# bin/setup
```
end
```
# start of helpers
→# It's helpful to know what messages came from this
→# script, so we'll use log instead of`puts`
→ **def** log(message)
→ puts "[ bin/setup ] #{message}"
→ **end**
→# end of helpers
Next,system!will defer toKernel#system^3 , but handle checking the return value and aborting if anything goes
wrong. It will also log what it’s doing explicitly.
# bin/setup
```
end
```
# start of helpers
→# We don't want the setup method to have to do all this error
→# checking, and we also want to explicitly log what we are
→# executing. Thus, we use this method instead of Kernel#system
→ **def** system!( ***** args)
→ log "Executing #{args}"
→ **if** system( ***** args)
→ log "#{args} succeeded"
→ **else**
→ log "#{args} failed"
→ abort
→ **end**
→ **end**
```
# It's helpful to know what messages came from this
# script, so we'll use log instead of`puts`
```
The last part ofbin/setupis to actually call eithersetuporhelp, depending on what the user has asked for. We
want our script to respond to-hand--help, as these are somewhat standard ways to ask a program what it does
without doing anything. Ideally, our script will also produce an error if the user provides other flags that aren’t
known. This can be achieved with Ruby’sOptionParser.
Lastly, we’ll also respectbin/setup helpas a way to get help, as this is often expected to work. We can check
ARGV[0]to see if the user specified that. Here’s how it all looks:
(^3) https://ruby-doc.org/core-3.1.0/Kernel.html
# bin/setup
```
end
```
# end of helpers
→OptionParser.new **do |** parser **|**
→ parser.on("-h", "--help") **do**
→ help
→ exit
→ **end**
→ **end** .parse!
→ **if** ARGV **[** 0 **] ==** "help"
→ help
→ **elsif!** ARGV **[** 0 **]** .nil?
→ puts "Unknown argument:'#{ARGV **[** 0 **]** }'"
→ exit 1
→ **else**
→ setup
→ **end**
With that done, we want to make sure the file is executable (it should be, since Rails created it that way, but if
you deleted the file before editing, it won’t be):
> chmod +x bin/setup
And _now_ we can run it to complete our setup:
> bin/setup
[ bin/setup ] Installing gems
[ bin/setup ] Executing ["bundle check || bundle install"]
The Gemfile's dependencies are satisfied
[ bin/setup ] ["bundle check || bundle install"] succeeded
[ bin/setup ] Dropping & recreating the development database
[ bin/setup ] Executing ["bin/rails db:reset || bin/rails db...
/root/widgets/db/schema.rb doesn't exist yet. Run`bin/rails...
Dropped database'widgets_development'
Created database'widgets_development'
[ bin/setup ] ["bin/rails db:reset || bin/rails db:migrate"]...
[ bin/setup ] Dropping & recreating the test database
[ bin/setup ] Executing [{"RAILS_ENV"=>"test"}, "bin/rails d...
Dropped database'widgets_test'
Created database'widgets_test'
[ bin/setup ] [{"RAILS_ENV"=>"test"}, "bin/rails db:reset"]...
[ bin/setup ] All set up.
[ bin/setup ]
[ bin/setup ] To see commonly-needed commands, run:
[ bin/setup ]
[ bin/setup ] bin/setup help
[ bin/setup ]
We can also see thatbin/setup --helpproduces some useful help:
> bin/setup --help
Usage: bin/setup
Installs gems, recreates dev database, and generally
prepares the app to be run locally
Other useful commands:
```
bin/dev
# run app locally
```
```
bin/ci
# runs all tests and checks as CI would
```
```
bin/rails test
# run non-system tests
```
```
bin/rails test:system
# run system tests
```
```
bin/setup help
# show this help
```
This file will stand in for any documentation about setting up the app. To keep it always working and up to date,
it will also be used to set up the continuous integration environment. That way, if it breaks, we’ll have to fix it.
Before that, we need to run the app locally.
### 4.5 Running the Application Locally withbin/dev
```
This section’s code is in the folder04-05/of the sample code.
```
Currently, we can run our Rails app like so:
> bin/rails server --binding=0.0.0.0
While this is easy enough to remember, our app will one day require more complex commands to run it locally.
Following our pattern of using scripts instead of documentation, we’ll createbin/devto wrapbin/rails server.
We’re calling itbin/dev(instead of, say,bin/run) for two reasons. First, Rails has somewhat standardized on
bin/devfor situations where you have to run more than one process to run your app locally (and we’ll need to
do that in when we learn about background jobs on page 296). Secondly, _running_ is something the app does in
production as well, and this script _is not_ for doing that. Calling itbin/devmakes it clear it’s just for our local dev
environment.
This will be a Bash script since it currently just needs to run one command. The first line indicates this to the
operating system. We’ll then callset -eto make sure the script fails if any command it calls fails. We’ll also add
some code to check for-h,--help, andhelpto show a brief help message. After that, we callbin/rails server.
# bin/dev
#!/usr/bin/env bash
set -e
**if** [ "${1}" = -h ] **||** \
[ "${1}" = --help ] **||** \
[ "${1}" = help ] **; then**
echo "Usage: ${0}"
echo
echo "Runs app for local development"
exit
**else
if** [! -z "${1}" ] **; then**
echo "Unknown argument:'${1}'"
exit 1
**fi
fi**
# We must bind to 0.0.0.0 inside a
# Docker container or the port won't forward
bin/rails server --binding=0.0.0.0
Bash is weird. You are reading this right that the way to end anifstatement is to use the wordifspelled
backward:fi. And yes, if we created acasestatement, we would end it inesac. I wish I were making that up.
bin/devwill need to be executable:
> chmod +x bin/dev
Let’s try it out:
> bin/dev
=> Booting Puma
=> Rails 7.1.1 application starting in development
=> Run`bin/rails server --help`for more startup options
Puma starting in single mode...
* Puma version: 6.4.0 (ruby 3.2.2-p53) ("The Eagle of Durango")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 782
* Listening on [http://0.0.0.0:3000](http://0.0.0.0:3000)
Use Ctrl-C to stop
Now, if you visithttp://localhost:9999(this is where the app will be available if you followed the Docker-based
setup), you should see your app as shown in the screenshot below.
If you can keepbin/setupandbin/devmaintained, you have a shot at a sustainable developer workflow, and
this will be a boon to the team. Nothing demoralizes developers more than having a constantly broken dev
environment that no one seems capable of fixing. And the bigger the team gets and the more important the app
becomes, the harder it will be to justify taking precious developer time away to fix the development environment.
This leaves two things left: scripting all the app’s quality checks and creating a production-ready logging
configuration.
### 4.6 Putting Tests and Other Quality Checks inbin/ci
```
This section’s code is in the folder04-06/of the sample code.
```
In the output ofbin/setup help, you saw a reference tobin/ci, which is what we’ll create now. This script runs
whatever tests and quality checks the app might need and is namedcifor “continuous integration”. Once this
script is created, you should be able to configure your CI environment to usebin/setupandbin/cias your entire
check. This is also where you can runbin/setuptwice in a row to make sure it’s idempotent. This is the key to
ensuring yourbin/setupstays working, even if developers don’t use it every day.
bin/setup # perform the actual setup
bin/setup # ensure setup is idempotent
bin/ci # perform all checks
We already havebin/rails testandbin/rails test:systemto run our application’s tests. Beyond these, we
want to automate some security vulnerability checks as well. Since we have not written any code yet, we should
not have any security issues.
By setting up an automated check now, we make it much easier to avoid introducing known issues into the
codebase in the future. This sort of policy-as-automation can be hugely impactful for keeping a team consistent in
their approach to best-practices.
Figure 4.2: App Running
Brakeman^4 can perform audits on the code we write, and Bundler can audit our dependencies, though it requires
the bundler-audit gem. Let’s install that and Brakeman now.
# Gemfile
# but we use dotenv to store that in files for
# development and testing
gem "dotenv-rails", **groups: [:development** , **:test]**
→# Brakeman analyzes our code for security vulnerabilities
→gem "brakeman"
→# bundler-audit checks our dependencies for vulnerabilities
→gem "bundler-audit"
```
# Bundle edge Rails instead: gem "rails", github: "rails/rail...
gem "rails", "~> 7.1.2"
```
We’ll install this viabundle install:
> bundle install
«lots of output»
Brakeman includes thebrakemancommand line app. bundler-audit allows us to runbundle audit check --update
which will refresh the database of known vulnerabilities and then analyze ourGemfile.lockto see if we are
running any vulnerable versions. Note that this only works ifbundle-auditis installed in your system gems, but
since we have installed it in the app’sGemfile, we have to usebundle exec bundle audit check --update. I
know.
We’ll put all this, plus our test invocations, intobin/ci. The order matters, however. We want the checks to be
ordered based on how useful their feedback is to local development. There’s no sense in analyzing our code for
security issues using Brakeman if the code doesn’t pass its tests.
Here’s whatbin/cilooks like (note the inclusion of similar logic frombin/devto provide help on the command
line):
# bin/ci
#!/usr/bin/env bash
set -e
(^4) https://brakemanscanner.org
**if** [ "${1}" = -h ] **||** \
[ "${1}" = --help ] **||** \
[ "${1}" = help ] **; then**
echo "Usage: ${0}"
echo
echo "Runs all tests, quality, and security checks"
exit
**else
if** [! -z "${1}" ] **; then**
echo "Unknown argument:'${1}'"
exit 1
**fi
fi**
echo "[ bin/ci ] Running unit tests"
bin/rails test
echo "[ bin/ci ] Running system tests"
bin/rails test:system
echo "[ bin/ci ] Analyzing code for security vulnerabilities."
echo "[ bin/ci ] Output will be in tmp/brakeman.html, which"
echo "[ bin/ci ] can be opened in your browser."
bundle exec brakeman -q -o tmp/brakeman.html
echo "[ bin/ci ] Analyzing Ruby gems for"
echo "[ bin/ci ] security vulnerabilities"
bundle exec bundle audit check --update
echo "[ bin/ci ] Done"
Note again that we print a message for each step of the process and prepend those messages with[ bin/ci ]
so that it’s obvious where the messages came from. These messages also serve as documentation for why the
commands exist.
We’ll need to make this executable:
> chmod +x bin/ci
And, since we just created our app and have written no code, all the checks should pass:
> bin/ci
[ bin/ci ] Running unit tests
Running 0 tests in a single process (parallelization thresho...
Run options: --seed 57185
# Running:
Finished in 0.000210s, 0.0000 runs/s, 0.0000 assertions/s.
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips
[ bin/ci ] Running system tests
Run options: --seed 19317
# Running:
Finished in 0.001059s, 0.0000 runs/s, 0.0000 assertions/s.
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips
[ bin/ci ] Analyzing code for security vulnerabilities.
[ bin/ci ] Output will be in tmp/brakeman.html, which
[ bin/ci ] can be opened in your browser.
[ bin/ci ] Analyzing Ruby gems for
[ bin/ci ] security vulnerabilities
Download ruby-advisory-db ...
Cloning into'/root/.local/share/ruby-advisory-db'...
ruby-advisory-db:
advisories: 827 advisories
last updated: 2023-11-30 12:36:04 -0800
commit: d821bf162550302abd1fa1fe15007f3012b76f32
No vulnerabilities found
[ bin/ci ] Done
Note that the extremely verbose lecture from git above is a factor of my development environment and the way
bundler-audit works (it does agit cloneto get the latest security vulnerabilities). Like much of git’s UI, this
information is useless, confusing, and can be ignored.
The last thing is to get ready for production by changing how Rails does logging
### 4.7 Improving Production Logging with lograge
```
This section’s code is in the folder04-07/of the sample code.
```
Rails’ application logs have colored text and appear on multiple lines. This might be nice for local development,
but wreaks havoc with most log aggregation tools we may use in production to examine our application logs.
Even if we download the files andgrepthem, we need each logged event to be on a single line on its own.
lograge^5 is a gem that provides this exact feature. It requires only a short initializer inconfig/initializersas
configuration.
Let’s install the gem first:
# Gemfile
# bundler-audit checks our dependencies for vulnerabilities
gem "bundler-audit"
→# lograge changes Rails'logging to a more
→# traditional one-line-per-event format
→gem "lograge"
```
# Bundle edge Rails instead: gem "rails", github: "rails/rail...
gem "rails", "~> 7.1.2"
```
Install it:
> bundle install
«lots of output»
To enable lograge, we must setconfig.lograge.enabledtotrueinside aRails.application.configureblock.
Most of the time, we only want lograge’s formatting for production, but sometimes we might want it for local
development. To make this work, we’ll enable lograge if we _aren’t_ in the Rails development environment _or_ if the
environment variableLOGRAGE_IN_DEVELOPMENTis set to"true".
This can all be done inconfig/initializers/lograge.rb, like so:
# config/initializers/lograge.rb
Rails.application.configure **do
if!** Rails.env.development? **||**
ENV **[** "LOGRAGE_IN_DEVELOPMENT" **] ==** "true"
config.lograge.enabled **=** true
**else**
config.lograge.enabled **=** false
**end
end**
(^5) https://github.com/roidrage/lograge
We should document this inbin/setup:
# bin/setup
puts ""
puts " bin/dev"
puts " # run app locally"
→ puts ""
→ puts " LOGRAGE_IN_DEVELOPMENT=true bin/dev"
→ puts " # run app locally using"
→ puts " # production-like logging"
→ puts ""
puts ""
puts " bin/ci"
puts " # runs all tests and checks as CI would"
Now, if you restart your app settingLOGRAGE_IN_DEVELOPMENTtotrue, then go tolocalhost:9999, you should see
the log message on one line (note it’s truncated in this medium):
=> Booting Puma
=> Rails 7.1.1 application starting in development
=> Run`bin/rails server --help`for more startup options
Puma starting in single mode...
* Puma version: 6.4.0 (ruby 3.2.2-p53) ("The Eagle of Durango")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 782
* Listening on [http://0.0.0.0:3000](http://0.0.0.0:3000)
Use Ctrl-C to stop
→ method=GET path=/ format=html controller=Rails::WelcomeController...
Before we finish, we should update the app’s README so it’s consistent with everything we just did. Replace
README.mdwith the following:
<!-- README.md -->
# Widgets - The App For Widgets
## Setup
1. Pull down the app from version control
2. Make sure you have Postgres running
3.` **bin/setup** `
## Running The App
1.` **bin/dev** `
## Tests and CI
1.` **bin/ci** ` contains all the tests and checks for the app
2.` **tmp/test.log** `will use the production logging format
*not* the development one.
## Production
* All runtime configuration should be supplied
in the UNIX environment
* Rails logging uses lograge. ` **bin/setup help** `
can tell you how to see this locally
This minimal README won’t go out of date, because we now have three scripts that automate setup, running,
and CI. Because we’ll be using these scripts every day, they will _have_ to be kept up to date, since when they break,
we can’t do our work.
If you can get your app into a production-like environment now, you should try to do so before writing too much
code. You should also actually configure continuous integration to make sure all this automation is working for
you. See the section “Continuous Integration” on page 363 for some tips and tricks on how to do this if you don’t
have much flexibility in your CI environment.
### Up Next
That might’ve felt like a lot of steps, but it didn’t take _too_ long and this minor investment now will pay dividends
later. Instead of an out-of-date README, we have scripts that we can keep up to date and can automate the
setup and execution of our development environment. It works the same way for everyone (as well as in the CI
environment), so it’s one less thing to go wrong, break, or have to be maintained.
It’s almost time to dive into the parts of Rails, but before we do that, I want to talk about what makes your app
special: the business logic. In the next chapter I’ll define what I mean by business logic, why it’s critical to manage
properly, and the one strategy you need to manage it: don’t put it in your Active Records.
## 5 Business Logic (Does Not Go in Active Records)
Much of this book contains strategies and tactics for managing each part of Rails in a sustainable way. But there is
one part of every app that Rails doesn’t have a clear answer for: the _business logic_.
Business logic is the term I’m going to use to refer to the core logic of your app that is specific to whatever your
app needs to do. If your app needs to send an email every time someone buys a product, but only if that product
ships to Vermont, unless it ships from Kansas in which case you send a text message... this is business logic.
The biggest question Rails developers often ask is: where does the code for this sort of logic go? Rails doesn’t
have an explicit answer. There is noActiveBusinessLogic::Baseclass to inherit from nor is there abin/rails
generate business-logiccommand to invoke.
This chapter outlines a simple strategy to answer this question: do not put business logic in Active Records.
Instead, put each bit of logic in its own class, and put all those classes somewhere insideapp/likeapp/services
orapp/businesslogic.
The reasons don’t have to do with moral purity or adherence to some object-oriented design principles. They
instead relate directly to sustainability by minimizing the impact of bugs found in business logic. That said, Martin
Fowler—who popularized the active record pattern upon which Active Record is based—does not recommend
putting all business logic in active records, either.
We’ll learn that business logic code is both more complex and less stable than other parts of the codebase. We’ll
then talk about _fan-in_ which is a rough measure of the inter-relations between modules in our system. We’ll bring
those concepts together to understand how bugs in code used broadly in the app—such as Active Records—can
have a more serious impact than bugs in isolated code.
So, let’s jump in. What’s so special about business logic?
### 5.1 Business Logic Makes Your App Special. and Complex
Rails is optimized for so-called _CRUD_ , which stands for “Create, Read, Update, and Delete”. In particular, this
refers to the database: we create database records, read them back out, update them, and sometimes delete them.
Of course, not every operation our app needs to perform can be thought of as manipulating a database table’s
contents. Even when an operation requires making changes to multiple database tables, there is often other logic
that has to happen, such as conditional updates, data formatting and manipulation, or API calls to third parties.
This logic can often be complex, because it must bring together all sorts of operations and conditions to achieve
the result that the domain requires it to achieve.
This sort of complexity is called _necessary complexity_ (or _essential_ complexity) because it can’t be avoided. Our
app has to meet certain requirements, even if they are highly complex. Managing this complexity is one of the
toughest things to do as an app grows.
#### 5.1.1 Business Logic is a Magnet for Complexity
While our code has to implement the necessary complexity, it can often be even more complex due to our
decisions about how the logic gets implemented. For example, we may choose to manage user accounts in another
application and make API calls to it. We didn’t _have_ to do that, and our domain doesn’t require it, but it might be
just the way we ended up building it. This kind of complexity is called _accidental_ or _unnecessary_ complexity.
We can never avoid _all_ accidental complexity, but the distinction to necessary complexity is important, because we
do have at least limited control over accidental complexity. The better we manage that, the better able we are to
manage the code to implement the necessarily complex logic of our app’s domain.
What this means is that the code for our business logic is going to be more complex than other code in our app. It
tends to be a magnet for complexity, because it usually contains the necessarily complex details of the domain as
well as whatever accidentally complexity that goes along with it.
To make matters worse, business logic also tends to change frequently.
#### 5.1.2 Business Logic Experiences Churn
It’s uncommon for us to build an app and then be done with it. At best, the way we build apps tends to be iterative,
where we refine the implementation using feedback cycles to narrow in on the best implementation. Software is
notoriously hard to specify, so this feedback cycle tends to work the best. And that means changes, usually in
the business logic. Changes are often called _churn_ , and areas of the app that require frequent changes have _high
churn_.
Churn doesn’t necessarily stop after we deliver the first version of the app. We might continue to refine it, as we
learn more about the intricacies of the problem domain, or the world around might change, requiring the app to
keep up.
This means that the part of our app that is special to our domain has high complexity and high churn. _That_ means
it’s a haven for bugs.
North Carolina State University researcher Nachiappan Nagappan, along with Microsoft employee Richard Ball
demonstrated this relationship in their paper “Use of Relative Code Churn Measures to Predict System Defect
Density”^1 , in which they concluded:
```
Increase in relative code churn measures is accompanied by an increase in system defect density [number of bugs
per line of code]
```
Hold this thought for a moment while we learn about another concept in software engineering called _fan-in_.
(^1) https://www.st.cs.uni-saarland.de/edu/recommendation-systems/papers/ICSE05Churn.pdf
### 5.2 Bugs in Commonly-Used Classes Have Wide Effects
Let’s talk about the inter-dependence of pieces of code. Some methods are called in only one place in the
application, while others are called in multiple places.
Consider a controller method. In most Rails apps, there is only one way a controller method gets called: when an
HTTP request is issued to a specific resource with a specific method. For example, we might issue an HTTP GET
to the URL/widgets. That will invoke theindexmethod of theWidgetsController.
Now consider the methodfindonUser. _This_ method gets called in _many_ more places. In applications that have
authentication, it’s possible thatUser.findis called on almost every request.
Thus, if there’s a problem withUser.find, most of the app could be affected. On the other hand, a problem in the
indexmethod ofWidgetsControllerwill only affect a small part of the app.
We can also look at this concept at the class level. SupposeUserinstances are part of most pieces of code, but we
have another model calledWidgetFaxOrderthat is used in only a few places. Again, it stands to reason that bugs
inUserwill have wider effects compared to bugs inWidgetFaxOrder.
While there are certain other confounding factors (perhapsWidgetFaxOrderis responsible for most of our revenue),
this lens of class dependencies is a useful one.
The concepts here are called _fan-out_ and _fan-in_. Fan-out is the degree to which one method or class calls into
other methods or classes. Fan-in is what I just described above and is the inverse: the degree to which a method
or class is _called_ by others.
What this means is that bugs in classes or methods with a high fan-in—classes used widely throughout the
system—can have a much broader impact on the overall system than bugs in classes with a low fan-in.
Consider the system diagrammed in the figure below. We can see thatWidgetFaxOrderhas a low fan-in, while
Widgethas a high one.WidgetFaxOrderhas only one incoming “uses” arrow pointing to it. Widgethas two
incoming “uses” arrows, but is also related via Active Record to two other classes.
Consider a bug inWidgetFaxOrder. The figure “Bug Effects of a Low Fan-in Module” on the next page outlines the
effected components. This shows that becauseWidgetFaxOrderhas a bug, it’s possible thatOrdersControlleris
also buggy, since it relies onWidgetFaxOrder. The diagram also shows that it’s highly unlikely that any of the rest
of the system is affected, because those parts don’t call intoWidgetFaxOrderor any class that does. Thus, we are
seeing a worst case scenario for a bug inWidgetFaxOrder.
_Now_ consider if insteadWidgethas a bug. The figure “Bug Effects of a High Fan-in Module” on the next page
shows how a brokenWidgetclass could have serious effects throughout the system in the worst case. Because
it’s used directly by two controllers and possibly indirectly by another through the Active Record relations, the
potential for the Widget class to cause a broad problem is much higher than forWidgetFaxOrder.
It might seem like you could gain a better understanding of this problem by looking at the method level, but in an
even moderately complex system, this is hard to do. The system diagrammed here is vastly simplified.
What this tells me is that the classes that are the most central to the app have the highest potential to cause
serious problems. Thus it is important to make sure those classes are working well to prevent these problems.
A great way to do that is to minimize the complexity of those classes as well as to minimize their churn. Do you
see where I’m going?
```
Figure 5.1: System Diagram to Understand Fan-in
```
### 5.3 Business Logic in Active Records Puts Churn and Complexity in Critical Classes
We know that the code that implements business logic is among the most complex code in the app. We know that
it’s going to have high churn. We know that these two factors mean that business logic code is more likely to have
bugs. And we also know that bugs in classes widely used throughout the app can cause more serious systemic
problems.
So why would we put the code most likely to have bugs in the classes most widely used in the system? Wouldn’t it
be extremely wise to keep the complexity and churn on high fan-in classes—classes used in many places—as low
as possible?
If the classes most commonly used throughout the system were very stable, and not complex, we minimize the
chances of system-wide bugs caused by one class. If we place the most complex and unstable logic in isolated
classes, we minimize the damage that can be done when those classes have bugs, which they surely will.
Let’s revise the system diagram to show business logic functions on the Active Records. This will allow us to
compare two systems: one in which we place all business logic on the Active Records themselves, and another
where that logic is placed on isolated classes.
Suppose that the app shown in the diagram has these features:
```
Figure 5.2: Bug Effects of a Low Fan-in Module
```
- Purchase a widget
- Purchase a widget by fax
- Search for a widget
- Show a widget
- Rate a widget
- Suggest a widget rated similar to another widget you rated highly
I’ve added method names to the Active Records where these might go in the figure “System with Logic on Active
Records” on the next page. You might put these methods on different classes or name them differently, but this
should look pretty reasonable for an architecture that places business logic on the Active Records.
Now consider an alternative. Suppose that each bit of business logic had its own class apart from the Active
Records. These classes accept Active Records as arguments and use the Active Records for database access, but
they have all the logic themselves. They form a _service layer_ between the controllers and the database. We can see
this in the figure below.
Granted, there are more classes, so this diagram has more paths and seems more complex, but look at the fan-in
of our newly-introduced service layer (the classes in 3-D boxes). All of them have low fan-in. This means that a
```
Figure 5.3: Bug Effects of a High Fan-in Module
```
bug in those classes is likely to be contained. And because those classes are the ones with the business logic—by
definition the code likely to contain the most bugs—the effect of those bugs is minimized.
And _this_ is why you should not put business logic in your Active Records. There’s no escaping a system in which a
small number of Active Records are central to the functionality of the app. But we can minimize the damage that
can be caused by making those Active Records stable and simple. And to do that, we simply don’t put logic on
them at all.
There are some nice knock-on effects of this technique as well. The business logic tends to be in isolated classes
that embody a domain concept. In our hypothetical system above, one could imagine thatWidgetPurchaser
encapsulates all the logic about purchasing a widget, whileWidgetRecommenderholds the logic about how we
recommend widgets.
Both useWidgetandUserclasses, which don’t represent any particular domain concept beyond the attributes we
wish to store in the database. And, as the app grows in size and features, as we get more and more domain concepts
which require code, theWidgetandUserclasses won’t grow proportionally. Neither willWidgetRecommendernor
WidgetPurchaser. Instead, we’ll have new classes to represent those concepts.
Figure 5.4: System with Logic on Active Records
```
Figure 5.5: System with Business Logic Separated
```
```
In the end, you’ll have a system where churn is isolated to a small number of classes, depended-upon by a few
number of classes. This makes changes safer, more reliable, and easier to do. That’s sustainable.
But don’t take my word for it. Martin Fowler, the person who coined and first described the active record pattern
that was inspiration for this part of Rails encourages this as well, when your application is complex.
```
### 5.4 Active Records Were Never Intended to Hold All the Business Logic
```
You may think that since Rails includes an implementation of the active record pattern , and that pattern is loosely
defined as an object that adds domain logic to database data, we should follow the pattern the Rails Way and put
our logic on our Active Records.
Let’s set aside that this is an appeal to authority and let’s also set aside that 99% of Active Record’s documentation
and 100% of its API are about database access. Is this actually what Martin Fowler, the author of Patterns of
Enterprise Application Architecture , intended? No.
Early in the book, Fowler talks about business logic:
```
```
Many designers, including me, like to divide “business logic” into two kinds: “domain logic,” having to do purely
with the problem domain (such as strategies for calculating revenue recognition on a contract), and “application
logic,” having to do with application responsibilities... sometimes referred to as “workflow logic”.
```
```
Later, when talking about the active record pattern, he is clear that the logic you’d couple to your database schema
is domain logic only:
```
```
Each Active Record is responsible for saving and loading to the database and also for any domain logic that acts
on the data.
```
“Domain logic that acts on the data” is certainly a subset of your application’s business logic. For one, it doesn’t
include application logic, as defined by Fowler. Secondly, it doesn’t include domain logic that doesn’t “act on
data”. Fowler goes on to clarify this point:
```
Active Record is a good choice for domain logic that isn’t too complex, such as creates, reads, updates, and
deletes. Derivations and validations based on a single record work well in this structure... If your business logic
is complex, you’ll soon want to use your object’s direct relationships, collections, inheritance, and so forth. These
don’t map easily onto Active Record, and adding them piecemeal gets very messy.
```
I have never worked on an application that was so simple it could keep all of its logic in the Active Records.
But I have definitely worked on applications where application logic and database-agnostic domain logic were
crammed into the Active Records. It was not sustainable.
I mention this to really underscore that it’s not just me telling you not to put all your business logic in Active
Records. The guy that came up with it also doesn’t think you should do that.
OK, let’s see an example of some code that doesn’t put business logic in the Active Records.
### 5.5 Example Design of a Feature
Suppose we are building a feature to edit widgets. Here is a rough outline of the requirements around how it
should work:
1. A user views a form where they can edit a widget’s metadata.
2. The user submits the form with a validation error.
3. The form is re-rendered showing their errors.
4. The user corrects the error and submits the edit again.
5. The system then updates the database.
6. When the widget is updated, two things have to happen:
1. Depending on the widget’s manufacturer, we need to notify an admin to approve of the changes
2. If the widget is of a particular type, we must update an inventory table used for reporting.
7. The user sees a result screen.
8. Eventually, an email is sent to the right person.
This is not an uncommon amount of complexity. We will have to write a bit of code to make this work, and it’s
necessarily going to be in several places. A controller will need to receive the HTTP request, a view will need to
render the form, a model must help with validation, a mailer will need to be created for the emails we’ll send and
somewhere in there we have a bit of our own logic.
The figure below shows the classes and files that would be involved in this feature.WidgetEditingServiceis
probably sticking out to you.
Here’s what that class might look like:
**class** WidgetEditingService
**def** edit_widget(widget, widget_params)
widget.update(widget_params)
```
if widget.valid?
```
```
Figure 5.6: Class Design of Feature
```
```
# create the InventoryReport
# check the manufacturer to see who to notify
# trigger the AdminMailer to notify the right person
end
```
widget
**end
end**
The code in the other classes would be more or less idiomatic Rails code you are used to.
Here’sWidgetsController:
**class** WidgetsController **<** ApplicationController
**def** edit
@widget **=** Widget.find(params **[:id]** )
**end**
```
def update
widget = Widget.find(params [:id] )
@widget = WidgetEditingService.new.edit_widget(
widget, widget_params
)
if @widget.valid?
redirect_to widgets_path
else
render :edit , status: :unprocessable_entity
end
end
```
private
**def** widget_params
params.require( **:widget** ).permit( **:name** , **:status** , **:type** )
**end
end**
Widgetwill have a few validations:
**class** Widget **<** ApplicationRecord
validates **:name** , **presence:** true
**end**
InventoryReportis almost nothing:
**class** InventoryReport **<** ApplicationRecord
**end**
AdminMailerhas methods that just render mail:
**class** AdminMailer **<** ApplicationMailer
**def** edited_widget(widget)
@widget **=** widget
**end**
**def** edited_widget_for_supervisor(widget)
@widget **=** widget
**end
end**
Note that just about everything about editing a widget is inWidgetEditingService(which also means that the
test of this class will almost totally specify the business process in one place).widget_paramsand the validations
inWidget _do_ constitute a form of business logic, but to co-locate those inWidgetEditingServicewould be giving
up a _lot_. There’s a huge benefit to using strong parameters and Rails’ validations. So we do!
Let’s see how this survives a somewhat radical change. Suppose that the logic around choosing who to notify
and updating the inventory record are becoming too slow, and we decide to execute that logic in a background
job—the user editing the widget doesn’t really care about this part anyway.
```
Figure 5.7: Design with a Background Job Added
```
The figure below shows the minimal change we’d make. The highlighted classes are all that needs to change.
We might imagine thatWidgetEditingServiceis now made up of two methods, one that’s called from the
controller and now queues a background job and a new, second method that the background job will call that
contains the logic we are backgrounding.
**class** WidgetEditingService
**def** edit_widget(widget, widget_params)
widget.update(widget_params)
```
if widget.valid?
EditedWidgetJob.perform_later(widget.id)
end
```
```
widget
end
```
**def** post_widget_edit(widget)
# create the InventoryReport
# check the manufacturer to see who to notify
# trigger the AdminMailer to notify whoever
# should be notified
**end
end**
TheEditedWidgetJobis just a way to run code in the background:
**class** EditedWidgetJob **<** ApplicationJob
**def** perform(widget_id)
widget **=** Widget.find(widget_id)
WidgetEditingService.new.post_widget_edit(widget)
**end
end**
As you can see, we’re putting only the code in the background job that _has_ to be there. The background job is
given an ID and must trigger logic. And that’s all it’s doing.
I’m not going to claim this is beautiful code. I’m not going to claim this adheres to object-oriented design
principles... whatever those are. I’m also not going to claim this is how DHH would do it.
What I will claim is that this approach allows you to get a _ton_ of value out of Rails, while also allowing you
to consolidate and organize your business logic however you like. And this will keep that logic from getting
intertwined with HTTP requests, email, databases, and anything else that’s provided by Rails. And _this_ will help
greatly with sustainability.
Do note that the “service layer” a) can be called something else, and b) can be designed any way you like yet still
reap these benefits. While I would encourage you to write boring procedural code as I have done (and I’ll make
the case for it in “Business Logic Class Design” on page 215), you can use any design you like.
### Up Next
This will be helpful context about what’s to come. Even when isolating business logic in standalone classes, there’s
still gonna be a fair bit of code elsewhere in the app. A lot of it ends up where we’re about to head: the view. And
the first view of your app that anyone ever sees is the URL, so we’ll begin our deep-dive into Rails with routes.
### PART
### II
# deep dive into rails
## 6
# Routes and URLs
Routes serve two purposes. Their primary purpose is to connect the view to the controller layer. Routes let you
know what code will be triggered when an HTTP request is made to a given URL. The second (and unfortunate)
purpose of routes is as a user interface element. URLs have a tendency to show up directly in social media, search
results, and even newspaper articles. This means that a user will see them. This means they matter.
It can be hard to design routes that serve both purposes. If your routes are designed first around aesthetic
concerns, you will quickly have a sea of inconsistent and confusing URLs, and this will create a carrying cost on
the team every time a new feature has to be added. But you also can’t insist that your app is only available with
conventional Rails routes. Imagine someone reading a podcast ad with a database ID in it!
The marketing department isn’t the only source of complexity with your routes, however. The more routes you
add and the more features your app supports, the harder it can be to keep the routes organized. If routes become
messy, inconsistent, or hard to understand, it adds carrying costs with every new feature you want to implement.
Fortunately, with a bit of discipline and a few simple techniques, you can keep your routes file easy to navigate,
easy to understand, and still provide the necessary human-friendly URLs if they are called for.
The five conventions that will help you are:
- Always use canonical routes that conform to Rails’ defaults.
- Never configure a route inconfig/routes.rbthat is not being used.
- User-friendly URLs should be added _in addition_ to the canonical routes.
- Avoid custom actions in favor of creating new resources that use Rails’ default actions.
- Use nested routes strategically.
Let’s dig into each of these to learn how they help sustainability.
### 6.1 Always Use Canonical Routes that Conform to Rails’ Defaults
```
This section’s code is in the folder06-01/of the sample code.
```
With just a single line of code, Rails sets up eight routes (seven actions) for a given resource.
resources **:widgets**
This simple declaration inconfig/routes.rbis the basis for a consistency that provides a lot of leverage. You
get URL helpers to generate canonical URLs without string-building, you get a clear and easy to understand
connection to your controllers, and there’s some nice documentation available viabin/rails routes.
If the app’s routes are made up entirely of calls toresources, it becomes easy to understand the app at a high
level. Developers can begin each feature by identifying the right resource, and choosing which of the seven
conventional actions need to be supported. It also means that looking at the URL of a browser is all you need to
figure out what code is triggering the view you’re seeing.
Even though it might not seem like a major architectural decision, sticking with Rails conventions for routing can
reduce real friction during development. Let’s make two routes: one will be conventional usingresourcesand
the other will diverge from this standard and useget.
The first route will be for showing the information about a given widget. We’ll add the “widgets” resource to
config/routes.rb:
# config/routes.rb
Rails.application.routes.draw **do**
→ resources **:widgets**
```
# Reveal health status on /up that returns 200 if the app b...
# Can be used by load balancers and uptime monitors to veri...
```
With just this one line, when we runbin/rails routeswe get a glimpse of what Rails gives us:
> bin/rails routes -g widgets
Prefix Verb URI Pattern Controller#Ac...
widgets GET /widgets(.:format) widgets#index
POST /widgets(.:format) widgets#creat...
new_widget GET /widgets/new(.:format) widgets#new
edit_widget GET /widgets/:id/edit(.:format) widgets#edit
widget GET /widgets/:id(.:format) widgets#show
PATCH /widgets/:id(.:format) widgets#updat...
PUT /widgets/:id(.:format) widgets#updat...
DELETE /widgets/:id(.:format) widgets#destr...
This has set up the eight different routes and also created some URL helpers. The value under “Prefix” is what we
use with either_pathor_urlto generate routes without string-building. The helpers that take arguments (such
aswidget_path) can also accept an Active Model instead of an ID. Those helpers will intelligently figure out how
to build the URL for us.
Before we make the second route, let’s fill in the controller and view here just to have something working. Since
we don’t have any database tables, we’ll use the Ruby standard library’s OpenStruct class to make a stand-in
widget. The code below should be inapp/controllers/widgets_controller.rb. Note that theOpenStructused
in theshowmethod creates an object that responds toid,name, andmanufacturer_id.
# app/controllers/widgets_controller.rb
**class** WidgetsController **<** ApplicationController
**def** show
@widget **=** OpenStruct.new( **id:** params **[:id]** ,
**manufacturer_id:** rand(100),
**name:** "Widget #{params **[:id]** }")
**end
end**
The default behavior of ourshowmethod is to render the template inapp/views/widgets/show.html.erb, so we’ll
make a barebones version of that.
<%# app/views/widgets/show.html.erb %>
<h1><%= @widget.name %> **</h1>
<h2>** ID #<%= @widget.id %> **</h2>**
See the screenshot “Initial Widget ‘show’ page” below for what this looks like^1.
Now, let’s create a route for the manufacturer’s page, but usegetinstead ofresources. This will illustrate the
difference in the approaches.
We’ll add the route toconfig/routes.rb:
# config/routes.rb
# Defines the root path route ("/")
# root "posts#index"
→ get "manufacturer/:id", **to:** "manufacturers#show"
**end**
We can already start to smell a problem when we look atbin/rails routes.
(^1) Just don’t forget to nominate me for a Webby.
```
Figure 6.1: Initial Widget ‘show’ page
```
> bin/rails routes -g manufacturers
Prefix Verb URI Pattern Controller#Action
GET /manufacturer/:id(.:format) manufacturers#show
Whereas our widgets resource had helpers defined for us, usinggetdoesn’t do that. This means that if we have to
create a URL for our manufacturer, we either need to create our own implementations ofmanufacturer_pathand
manufacturer_url, or we have to build the URL ourselves, like so:
**<h1>** <%= @widget.name %> **</h1>
<h2>** ID #<%= @widget.id %> **</h2>**
<%= link_to "/manufacturers/#{ @widget.manufacturer_id }" **do** %>
View Manufacturer
<% **end** %>
This might seem like only a minor inconsistency, but it can have a real carrying cost. If your routes file only
has these two lines in it, you’re already sending a message to developers that each new feature requires making
unnecessary decisions about routing:
- Should they use the standardresourcesor should they make a custom route withget,post, etc.?
- Should they build URLs with string interpolation, or should they make their own helper in
app/helpers/application_helper.rb, or should it go inapp/helpers/manufacturer_helper.rb?
- Should they useas:to give the route a name to make the helper, and what should that name be?
```
There’s just no benefit to hand-crafting routes like this. These are the sort of needless decisions Rails is designed
to save us from having to make. And it won’t end here. Rails provides a lot of ways to generate routes, and some
developers, when they see two ways to do something, create a third.
```
```
Of course, usingresourceson its own isn’t perfect. We’ve created inconsistency around our routes file, controllers,
and views. The output ofbin/rails routesshows eight routes that our app supports, but in reality, our app only
responds to one of them.
```
### 6.2 Never Configure Routes That Aren’t Being Used
```
This section’s code is in the folder06-02/of the sample code.
```
```
Runningbin/rails routeson an app is a great way to get a sense of its size, scope, and purpose. If the output of
that command lies—as ours currently does—it’s not helpful. It creates confusion. More than that, it allows you to
use a URL helper that will happily create a route that will never, ever work.
The solution is to use the optionalonly:parameter toresources. This parameter takes an array of actions that
you intend to support.
```
Doing this ensures that if you try to create a route you don’t support using a URL helper, you get a niceNameError
(as opposed to a URL that will generate a 404). I mistype URL helpers all the time, and it’s much nicer to find out
about this mistake locally with a big error screen than to scratch my head wondering why I’m getting a 404 for a
feature I _just_ implemented.
```
A nice side-effect of explicitly listing your actions withonly:is thatbin/rails routesprovides a clean and
accurate overview of your app. It lists out the important nouns related to your app and what it does, and this can
be a nice jumping-off point for building new features or bringing a new developer onto the team.
This might not seem like a big win for a small app, but remember, we’re setting the groundwork for our app
to grow. If you start off usingresourcesand adopt the use ofonly:when your app gets larger, you now have
needless inconsistency and confusion. You create another decision developers have to make when creating routes:
Do I useonly:or not?
```
```
The Rails Guide^2 even tells you to avoid creating non-existent routes if your app has a lot of them:
```
```
If your application has many RESTful routes, using:onlyand:exceptto generate only the routes that you
actually need can cut down on memory use and speed up the routing process.
```
```
The simplest way to solve this problem is to not create it in the first place. Let’s fix our routes file now by changing
the previous call toresourcesinconfig/routes.rbwith this:
```
(^2) https://guides.rubyonrails.org/routing.html
# config/routes.rb
Rails.application.routes.draw **do**
→ resources **:widgets** , **only: [ :show ]**
```
# Reveal health status on /up that returns 200 if the app b...
```
Now,bin/rails routesis accurate.
> bin/rails routes -g widgets
Prefix Verb URI Pattern Controller#Action
widget GET /widgets/:id(.:format) widgets#show
You might also be aware ofexcept:, which does the opposite ofonly:. It tells Rails to create all of the standard
routes _except_ those listed. For example, if we wanted all the standard routes exceptdestroy, we could useexcept:
[ :destroy ]in our call toresources.
This technique certainly achieves the goal of making the routes file accurate, but I find it confusing to have to
work out negative logic in my head to arrive at the proper value. I would advise sticking withonly:because it’s
much simpler to provide the correct value. It also means you only have a single technique for creating routes,
which reduces the overhead needed to work on the app.
The routes in your app are primarily there for developers, and using canonical routes, explicitly listed, creates
a consistency that the developers will benefit from. This works great until the marketing department wants to
plaster a URL on a billboard. Sometimes, we need so-called _vanity URLs_ that are more human-friendly than our
standard Rails routes.
### 6.3 Vanity URLs Should Redirect to a Canonical Route
```
This section’s code is in the folder06-03/of the sample code.
```
Like it or not, URLs are public-facing, and so they are subject to the requirements of people outside the engineering
team. Because they show up in search results, social media posts, and even podcast ads, we really do need a
way to make human-friendly URLs. But, we don’t want to create a ton of inconsistency with the canonical URLs
created byresources.
The way to think about this is that the canonical URLs you create withresourcesare _for developers_ and should
serve the needs of the team and app so that all the various URLs can be created easily and correctly. If user-facing
URLs are needed, those should be created _in addition_ to the canonical URLs and, of course, only if you actually
need them.
Let’s suppose the marketing team is creating a big campaign about our widget collection, all based around the
word “amazing”. They are initially going to buy podcast ads that ask listeners to go toexample.com/amazing. The
marketing team wants that URL to show the list of available widgets.
We don’t have that page yet, but we should _not_ make the route/amazingbe the canonical URL for that page. For
consistency and simplicity, we want a canonical URL, which is/widgets. Because we already have theresources
call for theshowaction, we’ll modify the array we give toonly:to include:index:
# config/routes.rb
Rails.application.routes.draw **do**
→ resources **:widgets** , **only: [ :show** , **:index ]**
```
# Reveal health status on /up that returns 200 if the app b...
```
Just to get something working, we’ll create a basicindexmethod inapp/controllers/widgets_controller.rb
using OpenStruct again:
# app/controllers/widgets_controller.rb
**manufacturer_id:** rand(100),
**name:** "Widget #{params **[:id]** }")
**end**
→ **def** index
→ @widgets **= [**
→ OpenStruct.new( **id:** 1, **name:** "Stembolt"),
→ OpenStruct.new( **id:** 2, **name:** "Flux Capacitor"),
→ **]**
→ **end
end**
Ourapp/views/widgets/index.html.erbcan be pretty simple for now:
<%# app/views/widgets/index.html.erb %>
<h1>Our Widgets</h1>
<ul>
<% @widgets.each **do** |widget| %>
**<li>**
<%= link_to widget.name, widget_path(widget.id) %>
**</li>**
<% **end** %>
**</ul>**
Everything works as expected as shown in the screenshot “Initial Widgets index page” on the next page.
```
Figure 6.2: Initial Widgets index page
```
This route was created for us, the developers. Any time we need to create a link to the widgets index page, we use
widgets_path, which will create the url/widgets. _Now_ we can create our custom URL for the marketing team.
To do that, we’ll use theredirectmethod inconfig/routes.rb. We’ll also use comments to set these new routes
off from the canonical ones.
# config/routes.rb
# Defines the root path route ("/")
# root "posts#index"
→ ####
→ # Custom routes start here
→ #
→ # For each new custom route:
→ #
→ # * Be sure you have the canonical route declared above
→ # * Add the new custom route below the existing ones
→ # * Document why it's needed
→ # * Explain anything else non-standard
→ # Used in podcast ads for the 'amazing'campaign
→ get "/amazing", **to:** redirect("/widgets")
**end**
That’s a lot of code and it’s mostly comments! The first few lines indicate that we are in a special section of the
routes file for vanity URLs, which I’m calling “custom routes” because that’s a bit more inclusive of what we might
need here. Next, we document our policy around creating these routes. It makes more sense to put the policy
right in the file where it applies than hide it in a wiki or other external document.
Then, we use theto: redirect(... )parameter for thegetmethod to implement the redirect, along with a
comment about what it’s for. Unfortunately, we can’t directly usewidgets_pathinside the routes file, so we have
to hard-code the route, but it’s a minor duplication. In reality, our canonical routes aren’t likely to change, so this
should be OK.
If you _do_ need to make a lot of custom routes, you could do something more sophisticated, like use route globbing
to a custom controller that uses the URL helpers, but I would advise against this unless you really need it.
Note thatredirect(...)will use an HTTP 301 to do the redirect. You can provide an additional parameter to
getnamedstatus:that can override this HTTP status to use a 302 for example.
Once this route is set up, you should be able to navigate to/amazingand see your handiwork, just as in the
screenshot below.
You’ll also notice that Rails made a URL helper for the custom route, so you can useamazing_urlin a mailer view
to put the custom route into an email or other external communications.
If, for whatever reason, it’s really important that no redirects happen, you can always usegetin the more
conventional way:
# config/routes.rb
```
# * Explain anything else non-standard
```
# Used in podcast ads for the 'amazing'campaign
→ get "/amazing", **to:** "widgets#index"
```
end
```
If you check that in your browser, you’ll see the vanity URL render the widget index page without any redirects.
The key thing here is that every single route in the application has a canonical route, consistent with Rails’
conventions. Our vanity URLs are created _in addition_ to those routes. This consistency means that each time a
new route is needed, you always useresourcesto create it in the normal Rails way. If you have a need for a
vanity route, you _also_ create that usinggetandredirect(...).
```
Figure 6.3: A Basic Vanity URL
```
Playing this technique forward a year or two from now, the routes file might be large, but it should be relatively
well-organized. It will mostly be made up of a bunch of calls toresources, followed by that big comment block,
and then any custom URLs you may have added over that time (along with up-to-date comments about what they
are for).
Comments often get a bad rap, but the way they are used here is defensible and important. Routes are one
of the most stable parts of the app (they might even outlive the app itself!). This means that comments about
those routes are equally stable, meaning they won’t get out of date. Because of that, we can take advantage of
the proximity of these comments to the code they apply to. Don’t underestimate how helpful it can be when a
comment about a piece of code exists and is accurate.
The comments also serve to call out the inconsistency vanity URLs create. As you scroll through the routes file
and come across a big, fat comment block, your mind will immediately think that something unusual is coming
up. That’s because it is!
Vanity URLs are a design challenge imposed on us by product stakeholders. But we developers can create our
own design challenges with routes. Let’s talk about one of them next, which is what happens when you feel the
need for a custom action.
### 6.4 Don’t Create Custom Actions, Create More Resources
```
This section’s code is in the folder06-04/of the sample code.
```
Suppose we want to allow users to give a widget a rating, say one to five stars. Let’s suppose further that we store
these ratings aggregated on the widget itself, using the fieldscurrent_ratingandnum_ratings^3.
This example is contrived to create the problem whose solution I want to discuss, but I’m sure you’ve encountered
a similar situation where you have a new action to perform on an existing resource and it doesn’t _quite_ fit with
one of the standard actions.
We know what parameters we need—a widget ID and the user’s rating—but we don’t know what route should
receive them because it’s not exactly clear what resource and what action are involved.
We could use theupdateaction on a widget, triggered by a PATCH to the/widgets/1234route. This would be
mostly conventional, since a PATCH is “partial modification” to a resource. The problem arises if we have lots of
different ways to update a widget. Our controller might get complicated since it would need to check what sort of
update is actually happening:
**def** update
**if** params **[:widget][:rating]** .present?
# update the rating
**else**
# do some other sort of update
**end
end**
The more types of updates we have to a widget, the more complicated this becomes. Developers often seek to
solve this problem by avoiding the genericupdateaction and creating a more specific one. For example, we might
implementupdate_ratingin theWidgetsController, with a route like so:
resources **:widgets** , **only: [ :show ] do**
post "update_rating"
**end**
This creates a decent URL _and_ a route helper, but I don’t recommend this approach. In my experience, this leads
to a proliferation of custom actions, where a scant number of resources start to have a growing set of custom
actions in the routes and controllers.
(^3) Yes, you can maintain a correct running average with just these two fields. If you’d like to work out exactly how to do that, the best way
is to apply for some jobs in Silicon Valley where eventually some smug mid-level engineer will make you solve this on a whiteboard, then scoff
at your inability to do so before quickly writing the answer he memorized prior to interviewing you.
When this happens, the process for making a new feature requires deciding on a custom action name for an
existing resource, rather than considering what resource is really involved. It also further diverges the app’s
codebase from Rails’ standards and doesn’t provide much value in return.
Rails works best when you are _resource-focused_ , not action-focused. When you think about common techniques
around software design, many involve starting with a domain model, which is essentially the list of nouns that the
app deals with. Rails intends these to be your resources.
Thus, you should reframe your process to one that is resource-focused, not action-focused. Doing so results in
many different resources that all support the same small number of actions. Because your app is a web app, and
because HTTP is—you guessed it—resource-based supporting a limited number of actions on any given resource,
this creates consistency and transparency in your app’s behavior.
It allows you to mentally translate URLs through routes to the controller without having to do a lot of lookups to
see how things are wired together. As we’ll talk about in the chapter on controllers on page 281, controllers are
the boundary between HTTP and whatever makes your app special. Sticking with a resource-based approach with
standard actions for routes and controllers reinforces that boundary and keeps your app’s complexity out of the
controllers.
So what do we do about our widget ratings problem? If we stop thinking about the action of “rating” and
start thinking about the resource of “a widget’s rating”, the simplest thing to do is create a resource called
widget_rating. When the user rates a widget, that creates a new instance of thewidget_ratingresource.
This is how that looks inconfig/routes.rb:
# config/routes.rb
Rails.application.routes.draw **do**
resources **:widgets** , **only: [ :show** , **:index ]**
→ resources **:widget_ratings** , **only: [ :create ]**
```
# Reveal health status on /up that returns 200 if the app b...
```
This will assume the existence of acreatemethod inWidgetRatingsController, so we can create that in
app/controllers/widget_ratings_controller.rblike so:
# app/controllers/widget_ratings_controller.rb
**class** WidgetRatingsController **<** ApplicationController
**def** create
**if** params **[:widget_id]**
# find the widget
# update its rating
redirect_to widget_path(params **[:widget_id]** ),
**notice:** "Thanks for rating!"
**else**
head **:bad_request
end
end
end**
We don’t need a view for this new action, but let’s add the new flash message to the existing widget view in
app/views/widgets/show.html.erb, along with a form to do the rating, so we can see it all working.
<%# app/views/widgets/show.html.erb %>
<h1><%= @widget.name %> **</h1>
<h2>** ID #<%= @widget.id %> **</h2>**
→<% **if** flash[ **:notice** ].present? %>
→ **<aside>**
→ <%= flash[ **:notice** ] %>
→ **</aside>**
→<% **end** %>
→ **<section>**
→ **<h3>** Rate This Widget **</h3>**
→ **<ol>**
→ <% (1..5).each **do** |rating| %>
→ **<li>**
→ <%= button_to rating,
→ widget_ratings_path,
→ params: { widget_id: @widget.id,
→ rating: rating } %>
→ **</li>**
→ <% **end** %>
→ **</ol>**
→ **</section>**
Notice how all the code still looks very Rails-like? Our controller has a canonical action, our routes file uses the
most basic form ofresources, and our view uses standard-looking Rails helpers. There is huge power in this as
the app (and team) gets larger.
Don’t worry (for now) that “widget ratings” isn’t a database table. We’ll talk about that more in the database
chapter on page 191. Just know for now that this doesn’t create a problem we can’t easily handle.
As we did with custom routes, play this technique forward a few years. You’ll have lots of resources, each an
important name in the domain of your app, and each will have at most seven actions taken on them that map
precisely to the HTTP verbs that trigger those actions.
You’ll be able to go from URL to route to controller easily, even if your app has hundreds of routes! _That’s_
sustainability.
This brings us to the last issue around routing, which is nested routes.
### 6.5 Use Nested Routes Strategically
```
This section’s code is in the folder06-05/of the sample code.
```
The Rails Routing Guide^4 says:
```
Resources should never be nested more than [one] level deep
```
This is for good reason, as it starts to blur the lines about what resource is actually being manipulated _and_
it creates highly complex route helpers likemanufacturer_widget_order_urlthat then take several positional
parameters.
Nested routes do solve some problems, so you don’t want to entirely avoid them. There are three main reasons to
consider a nested route: sub-resource ownership, namespacing, and organizing content pages.
#### 6.5.1 Create Sub-Resources Judiciously
A sub-resource is something properly owned by a parent resource. Using our widget rating example from the
previous section, you might think that a widget “has many” ratings, and thus the proper URL for a widget’s ratings
would be/widget/:id/ratings.
You could create that route like so:
resources **:widgets** , **only: [ :show ] do**
resources **:ratings** , **only: [ :create ]
end**
This design is making a very strong statement about how your domain is modeled. Consider that a route is
creating a URI—Uniform Resource Identifier—for a resource in your system. A route like/widget/:id/ratings
says that to identify a widget rating, you _must_ have a widget. It means that a rating doesn’t have any meaning
outside of a specific widget. This might not be what you mean, and if you create this constraint in your system, it
might be a problem later.
Consider a feature where a user wants to see all the ratings they’ve given to widgets. What would be the route to
retrieve these? You couldn’t use the existing/widgets/:id/ratingsresource, because that requires a widget ID,
and you want all ratings for a _user_.
If you made a new route like/users/:id/widget_ratings, you now have two routes to what sounds like the
same conceptual resource. This will be confusing. Consider the names of the controllers Rails would use for these
(^4) https://guides.rubyonrails.org/routing.html
two routes:RatingsControllerandWidgetRatingsController. Which is the controller for widget ratings? What
is a plain “rating”? This is confusing.
This comes back to routes as URIs and routes being for developers’ use. If a rating can exist, be linked to, or
otherwise used on its own, independent of any given widget, making ratings a sub-resource of widgets is wrong.
This is because a sub-resource is creating an identifier for a rating that requires information (a widget’s ID) that
the domain does not require.
Of course, you might not actually know enough about the domain at the time you have to make your routes.
Because of this lack of knowledge, making ratings its own resource (as we did initially) is the safer bet. While a
URL like/widget_ratings?widget_id=1234might feel gross, it’s much more likely to allow you to meet future
needs without causing confusion than if you prematurely declare that a rating is always a sub-resource of a
widget.
Remember, these URLs are for the developers, and aesthetics is not a primary concern in their design. They should
be chosen for consistency and simplicity. If you really do need a nicer URL to locate a widget’s rating, you can use
the custom URL technique described above to do that. Just be clear about _why_ you’re doing that.
Another use for nested resources is to namespace parts of the application.
#### 6.5.2 Namespacing Might (or Might Not) be an Architecture Smell
_Namespacing_ in the context of routes is a technique to disambiguate resources that have the same name but are
used in completely different contexts.
Perhaps our app needs a customer service interface to view, update, and delete widgets—the same resources
accessed by users—but requires a totally different UI.
While you could complicateWidgetsControllerand its views to check to see if the user is a customer service
agent, it’s often cleaner to create two controllers and two sets of views.
While you could do something likeUserWidgetsController andCustomerServiceWidgetsController, it’s
cleaner to use namespaces. We can assumeWidgetsControlleris our default view for our users, and create
aCustomerServicenamespace so thatCustomerService::WidgetsControllerhandles the view of widgets for
customer service agents.
Thenamespacemethod available inconfig/routes.rbcan set this up, like so:
# config/routes.rb
```
# Defines the root path route ("/")
# root "posts#index"
```
→ namespace **:customer_service do**
→ resources **:widgets** , **only: [ :show** , **:update** , **:destroy ]**
→ **end**
####
# Custom routes start here
#
This will create canonical Rails-like routes, nested under/customer_service:
> bin/rails routes -g customer_service -E
--[ Route 1 ]-----------------------------------------------...
Prefix | customer_service_widget
Verb | GET
URI | /customer_service/widgets/:id(.:format)
Controller#Action | customer_service/widgets#show
Source Location | config/routes.rb:15
--[ Route 2 ]-----------------------------------------------...
Prefix |
Verb | PATCH
URI | /customer_service/widgets/:id(.:format)
Controller#Action | customer_service/widgets#update
Source Location | config/routes.rb:15
--[ Route 3 ]-----------------------------------------------...
Prefix |
Verb | PUT
URI | /customer_service/widgets/:id(.:format)
Controller#Action | customer_service/widgets#update
Source Location | config/routes.rb:15
--[ Route 4 ]-----------------------------------------------...
Prefix |
Verb | DELETE
URI | /customer_service/widgets/:id(.:format)
Controller#Action | customer_service/widgets#destroy
Source Location | config/routes.rb:15
You get nicely named URL helpers as well as a namespaced controller, in this caseCustomerService::WidgetsController.
The views are similarly expected to be inapp/views/customer_service/widgets. As you get more and more
resources undercustomer_service, your code is nicely separated.
If this is the outcome you want, namespacing is the proper technique. It should _not_ be used for aesthetic reasons.
Create custom URLs as previously discussed if you need that.
The only thing to watch out for is overuse. If you find yourself needing a lot of namespaces, this means that you
have many disparate uses for your resources and _this_ could indicate that your app is doing too many things and
might benefit from being broken up. We’ll talk about this exact problem in the appendix “Monoliths, Microservices,
and Shared Databases” on page 405. For now, just keep an eye on your namespaces and if you start to see more
than a couple of them, take a fresh look at your roadmap and architecture to see if you might need to make more
apps that each do fewer things.
The last use for nested routes is similar to namespacing, but it’s when you have a lot of non-interactive content
pages.
### 6.6 Nested Routes Can Organize Content Pages
In addition to the main features of your web app, web sites that are accessible by the general public or a very
wide audience often have non-interactive pages that serve up content. These could be pages like a privacy policy,
a marketing landing page, or documentation.
Where possible, you should try to model these as resources, but doing so can often be awkward. For exam-
ple, you could useresource :privacy_policy, only: [ :show ]to manage you privacy policy, using the
singularresource, since you don’t have many privacy policies. Confusingly, Rails wants this served from the
PrivacyPoliciesController. It’s even more difficult when you have landing pages for marketing that don’t map
naturally to a resource at all.
In these cases, it can be better to create a namespace for such pages and then have non-standard routes used
simply as a way to serve up content. While some organizations might serve such content from a static web server
or content management system, you may not have the ability to do this and might be served well by organizing
these pages away from the core resources that make up your app.
### Up Next
Bet you didn’t think routing was such a deep topic! I want you to reflect on the lessons here, however. If you
follow these guidelines, you really aren’t using anything but the most basic features of the Rails router. That’s a
good thing! It means anyone can easily understand your routes, and even the most inexperienced developer can
begin adding features. This is sustainable over many years.
And with this, let’s move onto the next layer of the view: HTML templates.
## 7 HTML Templates
Now that we’ve learned about some sustainable routing practices let’s move on to what is usually the bulk of the
work in any Rails view: HTML templates.
HTML templates feel messy, even at small scale, and the way CSS and JavaScript interact with the view can be
tricky to manage. And, even though you _can_ de-couple HTML templates and manage their complexity with layouts
and partials, it’s not quite the same as managing Ruby code, so the entire endeavor often feels awkward at best.
This chapter will help you get a hold of this complexity. It boils down to these guidelines:
- Mark up all content and controls using semantic HTML; usedivandspanto solve layout and styling
problems.
- Build templates around the controller’s resource as a single instance variable.
- Extract shared components into partials.
- The View Components gem helps manage complex views far better than partials.
- ERB is fine.
Remember, these are guidelines. It’s OK to “violate” these rules as long as you have a good reason and understand
the reason for their existence.
Let’s start with the HTML itself.
### 7.1 Use Semantic HTML
```
This section’s code is in the folder07-01/of the sample code.
```
HTML5 contains many tags and attributes to mark up whatever UI or content you need. Mozilla’s reference^1 is
something you should have bookmarked. It has everything you need to know about what tags exist and what they
are for.
The process you follow for building a UI should start by marking up all the content and controls with specific
HTML elements appropriate to the purpose of the content or control. _Do not_ choose HTML tags based on their
appearance or other layout characteristics. _After_ you have applied semantic tags, use<div>or<span>elements
only to solve layout and styling problems. This two-step technique will make it much simpler to build views and
also result in sustainable views that are easier to understand and change later.
Let’s start with marking up the view with tags.
(^1) https://developer.mozilla.org/en-US/docs/Web/HTML
#### 7.1.1 Build Views by Applying Meaningful Tags to Content
We have seen this technique in the book already. We created an index page to list all the widgets in the system.
Regardless of how that page is ultimately supposed to appear, it had these elements:
- A header explaining what was on the page. We used an<h1>for this.
- A list of widgets that was not ordered. We used a<ul>for this.
- Each widget has a name and a link. We used an<li>for this as well as an<a>(as provided by Railslink_to
helper).
While we can absolutely create the visual appearance we need with just<div>s, we used tags the way they were
intended to create the initial version of our UI.
Doing this has three advantages:
- HTML code is easier to navigate when it uses tags appropriately. Opening up a view file to a sea ofdivs can
be jarring, and code like that will be hard to understand and change.
- Semantic markup used to tag content and controls tends to be more stable, so your views’ overall structure
is unlikely to change, even in the face of drastic changes to look and feel.
- Assistive devices will provide their users a _much_ better experience when tags are used appropriately.
The first two advantages speak directly to sustainability. When you can open up the code for a view and easily
navigate it to find the parts you need to change or add, your job working on the app is easier. The decision-making
process for dealing with the view is simpler when you begin by using semantic markup.
Semantic tags are also more stable. Our widget index page might go through many redesigns, but none of them
will change the fact that an un-ordered list uses the<ul>tag. That means that tests that involve the UI can rely
on this and thus be more stable.
The third advantage only tangentially helps with sustainability, mostly when someone decides to care about
assistive devices. When that happens, semantically marked-up UIs will be a better experience and thus require
less overall work to bridge any gaps in what you’ve done with what is needed for a great experience with assistive
devices.
Even if no stakeholder decides to explicitly target assistive devices, I still do think it’s important that we make our
UIs work with them where we can. There are more people than you might think that don’t use a traditional web
browser, and if you can be inclusive to their needs with minimal to no effort, you should be.
There is a practical concern about when to use each tag, because not every piece of content or UI element will
map exactly to an existing tag. You may have noticed when we added the flash message to our widget show page
that I used the<aside>tag. That tag’s explanation^2 is as follows:
```
The HTML<aside>element represents a portion of a document whose content is only indirectly related to the
document’s main content.
```
(^2) https://developer.mozilla.org/en-US/docs/Web/HTML/Element/aside
That sounds like a flash message to me, but it might not to you. As you build your app, you should develop a set
of conventions about how to choose the proper tags. Agreeing to not use<div>or<span>for semantic meaning
will go a long way. Ensconcing these decisions in code also helps.
When you identify re-usable components, _that_ is when to have the design discussion about which tags are
appropriate, and the result of that discussion is the re-usable partial that gets extracted. We’ll talk about that in
the next section.
So, if we aren’t using<div>or<span>to convey semantic meaning (since they cannot), what are they for? The
answer is for styling.
#### 7.1.2 Use<div>and<span>for Styling
Once our UI is laid out with semantic tags, thus providing a holder for each element, the next step is to actually
style those views. In a subsequent chapter we’ll talk about CSS, but to make the point about<div>and<span>,
let’s create a design problem we can’t solve by styling the existing semantic tags.
Our widget show page is just semantic markup right now. Suppose our designer wants the rating section to look
like “Rating UI Mockup” below.
```
Figure 7.1: Rating UI Mockup
```
When we try to style the view, we will eventually hit a wall preventing us from completely achieving this design
without adding more tags. Let’s see that in action.
First, since we have a new element, we need to add that using a semantic tag before styling. We’ll use a<p>tag at
the bottom of the existing<section>:
<%# app/views/widgets/show.html.erb %>
</li>
<% **end** %>
**</ol>**
→ **<p>** Your ratings help us be amazing! **</p>
</section>**
To get the<h3>and the rating buttons all on one line, we’ll float everything left. I’m going to use inline styles so
that you can see exactly what styles are being applied (I do not recommend inline styles as a real approach).
First, we’ll float the<h3>as well as adjust the margin and padding so it eventually lines up with the rating buttons.
<%# app/views/widgets/show.html.erb %>
</aside>
<% **end** %>
**<section>**
→ **<h3 style** ="float: left; margin: 0; padding-right: 1rem;" **>**
→ Rate This Widget:
→ **</h3>
<ol>**
<% (1..5).each **do** |rating| %>
**<li>**
Next, we need to remove the default styling from the<ol>
<%# app/views/widgets/show.html.erb %>
<h3 style="float: left; margin: 0; padding-right: 1rem;">
Rate This Widget:
</h3>
→ <ol style="list-style: none; padding: 0; margin: 0">
<% (1..5).each **do** |rating| %>
**<li>**
<%= button_to rating,
Finally, we’ll float the<li>elements left:
<%# app/views/widgets/show.html.erb %>
</h3>
<ol style="list-style: none; padding: 0; margin: 0">
<% (1..5).each **do** |rating| %>
→ **<li style** ="float: left" **>**
<%= button_to rating,
widget_ratings_path,
params: { widget_id: @widget.id,
We can see the problem if we look at the page now, as shown in the screenshot below.
```
Figure 7.2: Uncleared Floats
```
We need to clear the floats before the<p>tag. One way to do this is to use a<br>tag. However, this is not what
the<br>tag is for^3 , since it is designed to help format text that requires line breaks, such as poetry or addresses.
We could put theclear: allstyle on the<p>tag itself, but this creates an odd situation with margin collapsing^4
that will be very confusing when applying other styles to it later^5.
Ideally, we could wrap the floated elements in a tag whose sole purpose is to clear those floats. Since this is a
visual styling concern, there isn’t such a tag. This is what a<div>is for!
A common way to do this is to create a CSS class with a name like “clear-fix” or “clear-floats” and apply that class
to the<div>which we wrap around floated elements.
We can do that by adding this class toapplication.css:
/* app/assets/stylesheets/application.css */
```
*= require_tree.
*= require_self
*/
```
(^3) https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br
(^4) https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing
(^5) Margin collapsing explains _a lot_ about why CSS behaves counter to your intuition.
→.clear-floats **:after** {
→ **content** : "";
→ **display** : table;
→ **clear** : both;
→}
Now, we can surround our code with<div class="clear-floats">. We’ll start the tag right after the<section>:
<%# app/views/widgets/show.html.erb %>
</aside>
<% **end** %>
**<section>**
→ **<div class** ="clear-floats" **>
<h3 style** ="float: left; margin: 0; padding-right: 1rem;" **>**
Rate This Widget:
**</h3>**
We’ll close it after the ordered list:
<%# app/views/widgets/show.html.erb %>
</li>
<% **end** %>
**</ol>**
→ **</div>
<p>** Your ratings help us be amazing! **</p>
</section>**
The problem is now fixed, as shown in the screenshot below.
We could certainly have done this by using a new<section>tag to contain the<h3>and the rating buttons, but
there is no semantic reason to. If we didn’t have the visual styling requirement, there would be no need to add an
additional wrapper.
If you apply this technique broadly, what will happen is that every view you open that contains a<div>(or
<span>), you can know with certainty that those tags are there to make some visual styling work. This is a strong
cue to how the overall view works, which is the first thing you need to know in order to make changes.
It also provides a clear indication for assistive devices that the tag holds no meaning. If we’d used a<section>
tag instead, assistive devices would tell their users that there is a new section, even though there really isn’t.
```
Figure 7.3: Cleared Floats
```
This might feel a bit dense right now, but after the chapter on CSS, I hope everything will fall into place about
how to apply visual styling in a sustainable way.
The main thing to take away here is that your view code should be treated with the same reverence and care as
your Ruby code, even though the view code will be verbose and ugly. If you are disciplined with the HTML in
your view code, it will be easier to work with.
There’s more to say about our HTML templates, so we’ll leave styling for now and talk about how to communicate
data from the controllers to the templates.
### 7.2 Ideally, Expose One Instance Variable Per Action
```
This section’s code is in the folder07-02/of the sample code.
```
The way Rails makes data from controllers available to views is by copying the instance variables of the controller
into the code for the view as instance variables with the same name. I highly suggest being OK with this design.
We’ll talk about object-orientation and controllers more in the chapter on controllers on page 281, but I don’t think
there is high value in circumventing this mechanism with something that feels “cleaner” or “more object-oriented”.
That said, it’s possible to create quite a mess with instance variables, so that’s what I want to talk about here. The
way to get the most of Rails’ design without creating a problem is to adopt two conventions:
- Expose exactly one instance variable from any given action, ideally named for the resource or resources
being manipulated by the route to that action. For example, the widget show page should only expose
@widget.
- There are three exceptions: when a view requires access to reference data, like a list of country codes, when
the view needs access to global context, like the currently logged-in user, or when there is UI state that is
persisted across page refreshes, such as the currently selected tab in a tab navigation control.
If you follow the advice in the chapter “Routes and URLs” on page 65, these conventions are surprisingly easy to
follow, but it does require doing a good job modeling your domain and resources.
The key situation to avoid is exposing multiple instance variables that collectively represent the resource rather
than creating a single instance variable—and perhaps a new model class—to do so.
#### 7.2.1 Name the Instance Variable After the Resource
As a reminder, my suggestion is to create routes based on resources that use the Rails conventional actions. This
results in an application with many resources. Each controller would then expose a single instance variable named
for that resource (for example@widgetor@widgets).
The primary prerequisite of this guideline is that your resources be well-designed. Whatever information is needed
to render a given view, the resource for that view must have access to all of it.
_How_ you do this is a design decision with many subtleties, particularly around the so-called Law of Demeter^6 ,
which warns against coupling domain concepts too tightly. Most developers interpret the Law of Demeter (for
better of for worse) as avoiding nested method calls [email protected].
I would not have a huge problem with the _Guideline_ of Demeter, but as a _Law_ , I find it over-reaches, especially
given how it is often interpreted. In many cases, it’s perfectly fine—and often better—to dig into the object
hierarchy for the data you need.
Let’s add some code to our widget show page to see the exact problem created by the “single instance variable”
approach and the Law of Demeter.
For the purposes of this example, we’ll assume our domain model in the figure on the next page describes our
domain, which is:
- A widget always has a manufacturer.
- A manufacturer can manufacture many widgets.
- A manufacturer always has an address.
- An address always a country.
Let’s updateWidgetsControllerso that ourOpenStruct-based placeholder mimics this domain model.
We can nest OpenStructs for now to create a fake manufacturer. I promise this nastiness will go away when we
create real database tables (though faking out the back-end for the sake of the front-end does have other benefits
as we’ll learn later).
(^6) https://en.wikipedia.org/wiki/Law_of_Demeter
```
Figure 7.4: Widgets and Manufacturers
```
# app/controllers/widgets_controller.rb
**class** WidgetsController **<** ApplicationController
**def** show
→ manufacturer **=** OpenStruct.new(
→ **id:** rand(100),
→ **name:** "Sector 7G",
→ **address:** OpenStruct.new(
→ **id:** rand(100),
→ **country:** "UK"
→ )
→ )
@widget **=** OpenStruct.new( **id:** params **[:id]** ,
**manufacturer_id:** rand(100),
**name:** "Widget #{params **[:id]** }")
We can now use that in theOpenStructwe are returning as@widget:
# app/controllers/widgets_controller.rb
)
)
@widget **=** OpenStruct.new( **id:** params **[:id]** ,
→ **manufacturer_id:** manufacturer.id,
→ **manufacturer:** manufacturer,
**name:** "Widget #{params **[:id]** }")
**end
def** index
Since this is available from the@widgetwe’re exposing, we can add this to the view like so:
<%# app/views/widgets/show.html.erb %>
<%= flash[ **:notice** ] %>
**</aside>**
<% **end** %>
→ **<h3>**
→ Built by <%= @widget.manufacturer.name %>
→ out of <%= @widget.manufacturer.address.country %>
→ **</h3>
<section>
<div class** ="clear-floats" **>
<h3 style** ="float: left; margin: 0; padding-right: 1rem;" **>**
Set aside how gnarly our placeholder code is. When widgets and manufacturers become real models, that code
will go away and be simpler, but the view will still look like this, at least if we do the simplest thing and navigate
the relationships created by Active Record.
The first thing to understand is that the view’s requirements couple the widget to its manufacturer’s name and
country by design. This is not a coupling created by us developers, but one that naturally occurs in the domain
itself.
To me, this makes the code above perfectly fine, and I don’t believe the Law of Demeter applies here.
For the sake of argument, however, let’s say that we don’t like this coupling. If we solve it by creating a new
@manufacturerinstance variable, we create a less sustainable solution. Our view would have code like this in it:
**<h3>**
Built by <%= @manufacturer.name %>
out of <%= @manufacturer.address.country %>
**</h3>**
This view is intended to show the widget’s manufacturer’s name and country. _This_ implementation—that uses a
second instance variable—means we cannot verify that the view is correct just by looking at the view code. We
have to go into the controller to figure out how@manufacturergets its value. Even if we assume widgets and
manufacturers are modeled correctly, we can’t know if the correct manufacturer is being used in this view.
Using a second instance variable also creates a practical problem around consistency. Once code with multiple
instance variables becomes prolific, developers now have to make a decision every single time they build a
controller action: How many instance variables to expose and which ones should they be? This can be a hard
question to answer.
The alternative is to modify the way we’ve modeled our widget. The widget show view’s requirements are a big
input into what a widget fundamentally _is_. So if a widget really is a thing that has a manufacturer name and
country, it would not be unreasonable to model it like so:
@widget **=** OpenStruct.new(
**id:** params **[:id]** ,
**name:** "Widget #{params **[:id]** }",
**manufacturer_name:** "Sector 7G",
**manufacturer_country:** "UK",
)
Which would make our view code:
**<h3>**
Built by <%= @widget.manufacturer_name %>
out of <%= @widget.manufacturer_country %>
**</h3>**
Because the view is using a single instance variable, we know the view is showing the correct data—assuming
the resource has been modeled correctly. We can’t make that assumption with the multiple instance variable
implementation.
This may feel like we’ve overloaded our Active Record with “view concerns”. I would push back on this for three
reasons. First, “view concerns” are a requirement to what your domain should actually be, so they should not be
dismissed simply because they don’t make sense in a relational data model. Second, when your app is made up
of many more resources than database tables, you _won’t_ end up with tons of methods on your small set of core
models.
Lastly, however, the various solutions to the problem of separating so-called view concerns mostly result in
unsustainable code. Two common solutions are to create presenters (or view models)—classes that just encapsulate
whatever the view needs—or to use decorators—classes that proxy what is needed for a view to the real Active
Records.
Both of these approaches can mask over problems with domain modeling, especially given Ruby’s highly dynamic
nature. I’ve seen code that dynamically changes the methods available on a model depending on the context, and
I can’t think of a more confusing way to build an app:
**module** WidgetDecorator
**def** manufacturer_name
manufacturer.name
**end**
**def** manufacturer_country
manufacturer.address.country
**end
end**
## app/controllers/widgets_controller.rb
**def** show
@widget **=** Widget.find(params **[:id]** ).include(WidgetDecorator)
**end**
This adds two methods to theWidgetpassed to the view. Figuring out how this works is not necessarily easy.
The view code will appear to callmanufacturer_nameon aWidget, and figuring out where that method comes
from requires following a circuitous route through the code. I would argue that if the user thinks about a widget
as having a manufacturer name, but we don’t model that explicitly in our code, we have not done a good job
designing.
When controllers sometimes expose Active Records, sometimes mix in concerns, sometimes create presenters, and
sometimes do something else, it becomes more difficult than necessary to design new views and features. Even if
the team diligently documents how to make those decisions, documentation is rarely found or interpreted in the
way intended. This mental overhead makes each new feature harder to deliver.
It’s worth re-iterating that if two domain concepts are tightly coupled by design, having the code tightly couple
them can actually be an advantage. Our original code that navigated from widget to manufacturer to address
mimics the domain.
That being said, I mentioned three exceptions above.
#### 7.2.2 Reference Data, Global Context, and UI State are Exceptions
Almost every Rails app has a method calledcurrent_userthat exposes an object representing who is logged in.
It’s also common to need a list of reference data, such as country codes, in order to build a drop-down menu or
other piece of UI. Lastly, it’s common to need to persist UI state between requests, such as for a tabbed-navigation
control. None of these make sense as part of an existing resource, because you’d end up with every single model
providing access to this data.
These are the exceptions to the “one instance variable per view” guideline. You can certainly provide access to
data like this in helpers, andcurrent_useris a very common one. We’ll talk about helpers in the next chapter, but
too many helpers can create view code that is hard to understand. When a piece of view code _only_ uses instance
variables, it becomes very easy to trace back where those instance variables got their values: the controller.
We don’t have any drop-downs in our app yet, but this is what it would look like to expose a list of country codes
on a hypothetical manufacturer edit page:
**class** ManufacturersController **<** ApplicationController
**def** edit
@manufacturer **=** Manufacturer.find(params **[:id]** )
@country_codes **=** CountryCode.all
**end
end**
Further, we might have a tabbed navigation on the page and need to know which tab is active _and_ make that state
persist by encoding it in the url, like/widgets?tab=advanced. The controller might look like so:
**class** WidgetsController **<** ApplicationController
**def** show
@tab **= if** params **[:tab] ==** "advanced"
**:advanced
else
:basic
end**
@widget **=** Widget.find(params **[:id]** )
**end**
If you end up needing access to country codes or UI state in many places, you can extract the lookup logic at the
controller level. I’d still recommend passing this information to the view as an instance variable, for the reasons
stated above: instance variables pop out and can only come from the controller. Helpers can come from, well,
anywhere.
As your app takes shape, you may start to see patterns of data or markup common to some views. We’ll talk about
that in the next few sections.
### 7.3 Wrangling Partials for Simple View Re-use
```
This section’s code is in the folder07-03/of the sample code.
```
When your app’s views are relatively self-contained and display data in a straightforward way, they are easily
managed with ERB and helpers. There aren’t many apps that don’t have more complex needs that would benefit
from the re-use of markup or logic from the view. Rails provides one way to do this, which is the use of partials.
We’ll talk about partials in this chapter, since they are a lightweight, low-ceremony way to manage complexity.
Partials aren’t great when you have complex view logic. We’ll talk about that in the next section.
#### 7.3.1 Partials Allow Simple Code for Simple Re-use
For sharing markup with little or no logic, partials work great. They are easy to extract, easy to reference, and
easy to manage.
Let’s suppose our markup for rating a widget needs to be used on more than one page. To that end, let’s extract it
as partial so we can talk about the potential pitfalls you can run into.
First, we’ll copy the ERB to its own file inapp/views/widgets/, called_rating.html.erb:
<%# app/views/widgets/_rating.html.erb %>
<section>
<div **class** ="clear-floats">
<h3 style="float: left; margin: 0; padding-right: 1rem;">
Rate This Widget:
</h3>
<ol style="list-style: none; padding: 0; margin: 0">
<% (1..5).each **do** |rating| %>
**<li style** ="float: left" **>**
<%= button_to rating,
widget_ratings_path,
params: {
widget_id: @widget.id,
rating: rating
}
%>
**</li>**
<% **end** %>
**</ol>
</div>
<p>** Your ratings help us be amazing! **</p>
</section>**
We can remove that markup from the widget show page and reference the partial:
<%# app/views/widgets/show.html.erb %>
```
<h1><%= @widget.name %> </h1>
<h2> ID #<%= @widget.id %> </h2>
<% if flash[ :notice ].present? %>
```
```
<aside>
<%= flash[ :notice ] %>
</aside>
<% end %>
```
→<%= render partial: "rating" %>
This works great, under certain conditions. I’m not showing the hypothetical other view that needs this, but it’s
not hard to imagine that that view won’t expose@widgetas an instance variable. If following the guidelines in
the previous section, it definitely won’t be. That means, this partial won’t work, since it would try to callidon
nil(the default value of any instance variable).
We need to make this partial more of a re-usable component.
#### 7.3.2 Reference Only Locals in Partials
Partials can be given _locals_ —variables declared when the partial is referenced that are only available to that
partial. Let’s do that, using a local namedwidget:
<%# app/views/widgets/show.html.erb %>
```
</aside>
<% end %>
```
→<%= render partial: "rating", locals: { widget: @widget } %>
Then, in_rating.html.erb, we usewidgetinstead of@widget:
<%# app/views/widgets/_rating.html.erb %>
<%= button_to rating,
widget_ratings_path,
params: {
→ widget_id: widget.id,
rating: rating
}
%>
This results in a better system. If someone tries to use the partial without settingwidget, they won’t get an error
aboutnil, orNoMethodError, but instead get an error saying thatwidgetis not defined. This is a stronger clue
thatwidgetis required.
But, as of Rails 7.1, we can do even better by using _strict locals_
#### 7.3.3 Partials Should Use Strict Locals
Rails 7.1 introduced a feature to partials called _strict locals_. What this feature allows you to do is declare the
locals that are required by the partial. If someone attempts to use the partial without setting that local, Rails will
produce a very clear error message: “missing local widgets”.
This feature _also_ generates an error for any un-declared local, meaning if someone mis-types the local and uses
widget, Rails will produce an error message that this local is not accepted. This is a huge benefit for sustainable
use of locals.
Let’s do this right now in our_rating.html.erb:
<%# app/views/widgets/_rating.html.erb %>
→<%# locals: (widget:) %>
<section>
<div class="clear-floats">
<h3 style="float: left; margin: 0; padding-right: 1rem;">
You can see this in action by omitting the local inapp/views/widgets/show.html.erband reloading the page.
Note that this feature is implemented as a magic comment parsed with a regular expression. This means if you
mis-type it, Rails will not realize you are intending to use strict locals and treat the syntax error as a comment.
Two steps forward and one step back is still a step forward.
Strict locals provides another benefit when we need logic driven by optional parameters. Suppose we want to
allow users of the new ratings component to omit the “Your ratings help us be amazing” call-to-action. Further
suppose we don’t want any existing users of this component to have to specify this—we want them to default to
the current behavior without changing their call torender.
Strict locals allow default values for any declared local. No more usinglocal_assigns.
#### 7.3.4 Use Default Values for Strict Locals to Simplify Partial APIs
Without using strict locals, to achieve a default value for a local, you had to uselocal_assigns. Suppose our new
local is going to be calledshow_cta, with a default value of true. To make this work, here is what you have to do
if you _aren’t_ using strict locals:
##### <%
**if** !local_assigns.key( **:show_cta** )
show_cta = true
**end**
%>
This is clunky. Strict locals allows setting a default value, like so:
<%# app/views/widgets/_rating.html.erb %>
→<%# locals: (widget:, show_cta: true) %>
<section>
<div class="clear-floats">
<h3 style="float: left; margin: 0; padding-right: 1rem;">
Now, we can add the conditional logic without worrying aboutlocal_assignsor any other error-checking:
<%# app/views/widgets/_rating.html.erb %>
<% **end** %>
**</ol>
</div>**
→ <% **if** show_cta %>
→ **<p>** Your ratings help us be amazing! **</p>**
→ <% **end** %>
**</section>**
Although our new bit of logic is relatively simple, you don’t have to work on web apps very long before you
end up having to build a complex bit of UI that has lots of different possible UI states. In this situation, partials
become unwieldy, both in terms of implementation, but also for testing.
The only way to completely cover a complex bit of UI logic with tests is to use system tests. As we’ll discuss later
in the book, these are slow and brittle. Unfortunately, Rails doesn’t provide anything other than partials and
helpers to manage complex views.
This is why I’d recommend a third-party library for this case: View Components.
### 7.4 Use the View Component Library for Complex UI Logic
```
This section’s code is in the folder07-04/of the sample code.
```
Our widget rating component is pretty simple, and if how it is now is all it needs to be, partials work great. But
suppose our widget rating component became more complex? What if ratings below 3 warranted a different
visual design? What if admins looking at the page shouldn’t see the rating controls? What if are running an A/B
test on a different way of gathering ratings?
All of these hypothetical situations will result in complex logic in the ERB that requires management and testing.
If you’ve ever had to do this, it can be difficult. You don’t have a way to test it outside of a system test, and
complexifstatements inside a markup language creates a lot of friction.
View Component^7 is a library that can solve these problems in a Rails-like way. View Component can manage
server-rendered HTML fragments with Ruby and ERB code together. A fragment of ERB is connected to a Ruby
class, and the two are used to render HTML. View Component was developed at GitHub where it is widely used,
and is otherwise popular and well-maintained. And, it’s a small API that can be learned quickly.
Here’s how you’d render a view component namedWidgetRatingComponentin any ERB file:
<%= render(WidgetRatingComponent.new(widget: @widget)) %>
By default, the fileapp/components/widget_rating_component.html.erbis rendered in the context of an instance
ofWidgetRatingComponent, which is defined inapp/components/widget_rating_component.rb. The result of that
render is inserted into the ERB file, just like with a partial.
Let’s see this in action by converting our widget rating component to use View Component.
#### 7.4.1 Creating a View Component
First, we’ll add the gem to ourGemfile:
# Gemfile
# lograge changes Rails'logging to a more
# traditional one-line-per-event format
gem "lograge"
→# View Component is used to manage
→# and test complex view logic
→gem "view_component"
```
# Bundle edge Rails instead: gem "rails", github: "rails/rail...
gem "rails", "~> 7.1.2"
```
We’ll install it withbundle install:
> bundle install
«lots of output»
View Component comes with a generator to create a scaffold for us viabin/rails generate component. We’ll use
that to create our new component. It takes a required argument for the name of the component, and then any
number of optional arguments that represent the parameters to pass to a constructor. We’ll set those as the locals
we used earlier:widgetandshow_cta:
(^7) https://viewcomponent.org
> bin/rails generate component WidgetRating widget show_cta
create app/components/widget_rating_component.rb
invoke test_unit
create test/components/widget_rating_component_test...
invoke erb
create app/components/widget_rating_component.html....
This created a component Ruby class, an ERB template, and a test. We’ll populate the ERB template first.
It’s located inapp/components/widget_rating_component.html.erband should look pretty similar to the partial
we extracted:
<%# app/components/widget_rating_component.html.erb %>
**<section>
<div class** ="clear-floats" **>
<h3 style** ="float: left; margin: 0; padding-right: 1rem;" **>**
Rate This Widget:
**</h3>
<ol style** ="list-style: none; padding: 0; margin: 0" **>**
<% (1..5).each **do** |rating| %>
**<li style** ="float: left" **>**
<%= button_to rating,
widget_ratings_path,
params: {
widget_id: widget.id,
rating: rating
}
%>
**</li>**
<% **end** %>
**</ol>
</div>**
<% **if** show_cta %>
**<p>**
Your ratings help us be amazing!
**</p>**
<% **end** %>
**</section>**
When using View Component, any method called from the ERB must be available from the component’s
class as an instance method. Thus, we’ll need to define bothwidgetandshow_ctain the component class.
Since we don’t need any other logic for now, we can do that by declaring them both asattr_readers in
app/components/widget_rating_component.rb. We’ll also change the initializer to defaultshow_ctato true:
# app/components/widget_rating_component.rb
```
# frozen_string_literal: true
```
**class** WidgetRatingComponent **<** ViewComponent **::** Base
→ attr_reader **:widget** , **:show_cta**
→ **def** initialize(widget:, **show_cta:** true)
@widget **=** widget
@show_cta **=** show_cta
**end**
Now, we can remove the partial and use the component. First, we’ll deleteapp/views/widgets/_rating.html.erb:
> rm app/views/widgets/_rating.html.erb
Next, we’ll changeapp/views/widgets/show.html.erbto use the component instead. We’ll setshow_ctato false
to demonstrate that it’s using the logic:
<%# app/views/widgets/show.html.erb %>
```
</aside>
<% end %>
```
→<%= render(WidgetRatingComponent.new(widget: @widget,
→ show_cta: false)) %>
If you restart your server, you can see that the page is working (and the CTA is omitted):
Based on the markup alone, this is a moderate improvement over ERB. If the logic for what should be in the view
becomes complex, most of that logic can be managed in the View Component’s class. And, to be fair, without View
Component, you could achieve this by creating an Active Model or other Ruby class that the controller exposes.
Where View Component really shines is testing.
#### 7.4.2 Testing Markup from a Unit Test
A regular Ruby class can be tested by creating a test intest/«whatever». For a bit of re-usable ERB, however, you
can’t just test the logic, you also need to make sure the ERB code itself is working. By default, the only part of
Rails that actually executes and renders the view templates is a system test.
View Component provides the best of both worlds by allow the template logic to be rendered in a unit test that
runs quickly. You can set up your test the same as any other test in Rails, but then assert on the rendered HTML,
```
Figure 7.5: Using a View Component
```
like you would in a system test. This provides a lot of confidence that your view logic is working—confidence that
is difficult to get otherwise.
The scaffold generated an empty test namedwidget_rating_component_test.rbintest/components/. Let’s
modify that to test our ratings component. The component has two states: showing the CTA and hiding it. We’ll
write two tests, each which will instantiate the View Component, callrender_inline, then make assertions that
are similar to those you’d make in any system test.
Here’s how it will look:
# test/components/widget_rating_component_test.rb
require "test_helper"
**class** WidgetRatingComponentTest **<** ViewComponent **::** TestCase
**def** test_show_cta
render_inline(
WidgetRatingComponent.new(
**widget:** OpenStruct.new( **id:** 1234),
**show_cta:** true
)
)
assert_text("Your ratings help us be amazing")
**end
def** test_no_cta
render_inline(
WidgetRatingComponent.new(
**widget:** OpenStruct.new( **id:** 1234),
**show_cta:** false
)
)
refute_text("Your ratings help us be amazing")
**end
end**
Now, we can test this component, and it’ll execute just as fast as a unit test:
> bin/rails test \
test/components/widget_rating_component_test.rb
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 30458
# Running:
Finished in 0.028657s, 69.7916 runs/s, 69.7916 assertions/s.
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
Nice! In my most recent job, I maintained a customer service app that had a lot of complex UI states. View
Component, in particular this method of testing, allowed me to quickly build the UI and have confidence that it
was working.
Why not always use View Component? Why even use partials?
#### 7.4.3 Deciding Between a Partial or a View Component
As you can see from our simple example, a partial just requires moving markup from one file to another, adding
a single line of code to declare the locals, and that’s it. A View Component requires the same sort of file, plus
a class, plus a test. And, as you create more and more View Components, yourapp/componentsfolder will get
messy and require organization.
Although there is virtue in having exactly one way to do something, I think there is value in using both partials
and View Component. Partials work great when you need to re-use markup that has very little or no logic. Using
a View Component for that situation would result in an empty class and an empty test.
And, as we saw, converting a partial to a View Component is relatively straightforward. So, if a partial today
becomes complex, you can quickly make it into a View Component when needed.
Another rule of thumb is if your partial has logic you wish you could test outside of a system test, make it a View
Component. And, if you grow tired of typing out class names in your view templates, you can always create a
helper to callrender.
We’ll talk about helpers next, but before we leave, I want to urge you to avoid alternative templating mechanisms
and stick with ERB.
### 7.5 Just Use ERB
The default templating mechanism in Rails is HTML using ERB (which I’m going to refer to simply as “ERB” even
though ERB is a general templating system that can template anything). Some developers strongly believe ERB to
be problematic and seek to use alternatives like HAML^8 or Slim^9. I don’t believe the benefits ascribed to these
technologies outweigh the downsides, and I want to talk briefly about why.
There are two reasons I believe ERB is the sustainable choice:
- It’s the default in Rails, so its behavior is managed and updated with Rails and thus more stable and reliable.
- It is based on HTML, which is widely understood by almost every web developer, even those unfamiliar with
Rails.
Sticking with Rails’ default choices is a sustainable decision, because you will need to update your version of Rails
over the life of the app. The fewer dependencies your app has, the easier that process is going to be. I’m sure
HAML and Slim are well-updated and maintained, but if incompatibilities exist between these technologies and
Rails, it’s not going to delay a Rails release. Incompatibilities with ERB will. This means that HAML and Slim (like
any dependency) can prevent you from updating to the latest version of Rails.
As to the broad mindshare of HTML, while it’s not hard to learn HAML or Slim, neither technology actually makes
it easier to write HTML. They are both translators, not abstractions, so you still need to think about what HTML is
going to be generated. I don’t enjoy writing code that I must mentally translate as I write it. I find it difficult
to both understand how the dynamic nature of the template affects the resulting markup _while also_ translating
HAML or Slim mentally into HTML.
A non-default templating language is also one more thing to learn in order to be productive (especially since
Slim and HAML use a modified version of embedded Ruby that doesn’t needendstatements). While any single
non-standard thing may not be hard to learn, these tend to add up. Anything you add to your app should provide
a clear benefit to justify its existence. For non-default templating languages, there really isn’t a strong benefit.
Consider also the use of advanced front-end technologies like React or Vue. Those use HTML by default,
too. Adopting HAML or Slim for HTML templates means you either have inconsistency with your JavaScript
components, or you need a JavaScript dependency to change the markup language there, too. While RubyGem
dependencies carry risk, JavaScript dependencies carry a higher risk (as we’ll discuss later).
It’s just not worth it. HAML and Slim simply don’t solve a serious enough problem to justify the cost of their
adoption. Arguments about “cleanliness” are subjective, and I prefer to limit the number of technical decisions
made based on subjective measures. Subjective or aesthetic arguments can be decent tiebreakers, but as the
foundation of a technical decision, I find them wanting^10.
(^8) [http://haml.info](http://haml.info)
(^9) [http://slim-lang.com](http://slim-lang.com)
(^10) I want to point out that I have made no argument related to the whitespace-significance of HAML or Slim. I believe their lack of
appropriateness can be understood on technical merits alone.
### Up Next
We’ve talked about HTML templates and how to manage them. As we work our way into the app, the next view
technology to look at is the helper. Helpers are used to extract logic needed in templates to Ruby code, where
they can be more easily managed and tested. But we can make an awful mess with them.
## 8 Helpers
Ah helpers! So handy, yet also a magnet for mess and unsustainability. I am not going to give you a clean, perfect,
sustainable solution here, but I _can_ help clarify the issues with helpers, explain the best way to use them, and help
ease some of the pain.
Helpers are a way (well, the _only_ way included with Rails) to export methods to be available to a view. Any
method defined inapp/helpers/application_helper.rbwill be included and available to all your views. Helpers
can also be added via thehelpermethod in a controller, which will import methods from a class, module, block
of code, or really anywhere.
The main problem that comes up around helpers is the sheer volume of them. Because they exist in a single global
namespace by default, the more helpers there are, the harder it is to avoid name clashes and the harder it is to
find helpers to reuse. It’s just not feasible to expect engineers to read through tons of helpers to figure out if what
they need exists or not.
An extreme way to deal with this problem is to ban the use of helpers entirely. You could be successful with this
approach, but you’d then need an answer for where code goes that does what a helper would normally do. Those
approaches, usually called _presenters_ , have their own problems, which we’ll talk about.
But even a nuanced approach that clearly defines what code should be in a helper and what shouldn’t still requires
answering questions about where all the code you need should end up. And, of course, helpers generate HTML,
making them a great place to inject security vulnerabilities.
The reality is, there’s going to be a lot of code to handle view logic and formatting. Whether that code is in helpers
or not, it doesn’t change the fact that we have a code management problem, and there’s no perfect solution.
To deal with this reality, we’ll look at the following techniques:
- Reduce the number of helpers you need by properly modeling your domain.
- Concentrate helpers on what they do best: producing inline markup.
- When generating markup (in a helper or not), use Rails APIs to avoid security issues.
- Helpers with logic should be tested, but take care not to over-couple them to the markup being generated.
- Presenters (or other proxies where you might put helper-like methods) aren’t needed given Active Model
and View Components.
We’ll start with the most important technique for managing helpers, which is to make sure you are putting domain
concerns in the domain objects where they belong, not in your helpers.
### 8.1 Don’t Conflate Helpers with Your Domain
```
This section’s code is in the folder08-01/of the sample code.
```
Helpers are often used for so-called _view concerns_ , which is the transformation of canonical data to something
only needed for a view. Rails’number_to_currencyis a great example. Therefore, to understand helpers is to
understand view concerns. What are they?
A common convention for identifying view concerns is to assume any piece of data that doesn’t come from the
database, and is thus aggregated or derived from the database, is a view concern. While easy to follow, this
convention is overly simplistic and ends up pushing too many actual domain concepts out of the domain.
Instead, you should think more deeply about what really is part of the domain. The resource upon which your
view is based isn’t just an aggregation of data from the database but instead is _everything_ that’s part of that domain
concept, _including_ data that might be derived or aggregated from the database.
Let’s suppose our widget IDs are meaningful to users. There are a lot of good reasons for this to be true. In our
imagined domain of widget sales, we can assume we’re migrating some legacy widget database into our own, and
we’ll suppose that users are used to seeing widget IDs in general, and specifically, they are accustomed to seeing
them formatted with the last two digits separated by a dot. So the widget with ID 12345 would be shown as
123.45.
This might seem like a view concern. It’s a formatting of canonical data in our database. But _why_ do we need to
do this? Because it’s meaningful to users. This formatted ID represents a meaningful concept to the users of our
system. That feels fundamentally different than, say, using a monospaced font to render the ID.
I’d argue that something like this is _not_ a view concern and _should_ be part of the domain. That doesn’t mean we
have to store it in our database, but what it _does_ mean is that it’s part of the widget resource and not something
we’d put in a partial template component or helper. See the sidebar “Formatting Item IDs” on page 110 for a
real-world example of this.
We don’t have aWidgetclass yet, but we can still add this derived data to our stand-inOpenStruct. Let’s do that
now inwidgets_controller.rb:
# app/controllers/widgets_controller.rb
**manufacturer_id:** manufacturer.id...
**manufacturer:** manufacturer,
**name:** "Widget #{params **[:id]** }")
→ **def** @widget.widget_id
→ **if** self.id.to_s.length **<** 3
→ self.id.to_s
→ **else**
→ self.id.to_s **[** 0 **..-** 3 **] +** "." **+**
→ self.id.to_s **[-** 2 **..-** 1 **]**
→ **end**
→ **end
end**
```
def index
@widgets = [
```
If you haven’t done this sort of hacky metaprogramming, don’t worry. It’s not a technique you should use often,
but essentially this is defining the methodwidget_idon the@widgetobject itself. Note that this code won’t last
long, as we’ll turnWidgetinto a real class later in the book.
We can use this in the view:
<%# app/views/widgets/show.html.erb %>
<h1><%= @widget.name %> **</h1>**
→ **<h2>** ID #<%= @widget.widget_id %> **</h2>**
<% **if** flash[ **:notice** ].present? %>
**<aside>**
<%= flash[ **:notice** ] %>
This should work great as shown in the screenshot “Formatted Widget ID” below.
> rm -f log/development.log
> cat log/development.log
Started GET "/widgets/12345" for 172.18.0.3 at 2023-12-04 23...
ActiveRecord::SchemaMigration Load (0.4ms) SELECT "schema...
Processing by WidgetsController#show as HTML
Parameters: {"id"=>"12345"}
Rendering layout layouts/application.html.erb
Rendering widgets/show.html.erb within layouts/application
Rendered widgets/show.html.erb within layouts/application...
Rendered layout layouts/application.html.erb (Duration: 33...
Completed 200 OK in 47ms (Views: 38.4ms | ActiveRecord: 0.0m...
When you start to critically examine your domain, and take into account all the inputs to what should define it,
you’ll find that there are many pieces of data that you won’t store in your database. These aren’t view concerns.
Nevertheless, you will still encounter the need to render data or perform logic specific to how data is viewed.
Formatting numbers or currency based on locale is one. Another is UI logic based on global state or context, such
as showing or hiding parts of a view based on what the current logged-in user is authorized to do. This means
we’ll need _some_ code between our resources and our views to manage this. Helpers can do this, and so let’s talk
about what helpers can do, specifically what _only_ helpers can do.
```
Figure 8.1: Formatted Widget ID
```
#### Formatting Item IDs
The Stitch Fix warehouses were organized in a seemingly chaotic, random fashion. This was by design as it helped
the efficiency of the fulfillment process greatly. We initially had 1,000 locations or _bins_ , and we assigned an item’s
location based on the last three digits of its primary key in the database.
When you looked at any app, any tag, or any packing slip, item IDs would render like1234-567, and this would
tell you that bin 567 is where that item should go. The code to format the IDs originally lived in a helper. Of course,
we ended up needing it in a lot of places over the years. The result was a ton of duplicate code spread across the app
(and later, many apps), all because we considered it a view concern.
```
The reality is, this formatted ID was meaningful to everyone, and the fact that it came from the database primary
key was irrelevant. It was part of the domain model that we missed.
```
### 8.2 Helpers are Best at Exposing Global UI State and Generating Markup
```
This section’s code is in the folder08-02/of the sample code.
```
Rails built-in helpers format data for the view, often by generating markup. For situations where little or no
markup is needed, helpers can be a good solution, since they are lighter-weight than a partial or View Component.
Helpers can also provide access to global state without requiring instance variables. As long as there’s not too
much global state, helpers can work well.
#### 8.2.1 Global UI Logic and State
Almost every Rails app has acurrent_usermethod that exposes the logged-in user. It’s common to use this on
many views, either to access user-specific information or to check that the user is authorized to view different
aspects of the UI. Setting a@current_userinstance variable in every controller method would be cumbersome.
Thus, acurrent_userhelper is common and makes sense.
Another example is feature flags. You might be rolling out a new feature to a subset of users and want to
modify the UI for only those users. You may need to check if the user has been granted access to the new
feature from anywhere, and an instance variable might be inconvenient or hard to maintain. You might expose a
feature_enabled?helper to handle this.
Helpers are also a good tool for managing markup generation. You could think of these needs as small inline
components that don’t warrant a partial and definitely don’t warrant a View Component.
#### 8.2.2 Small, Inline Components
Suppose we wish to render our widget ID in a monospace font, and let’s suppose we need to do this everywhere
in the app. While the formatting of our ID using dots is not a view concern but part of our domain, the specific
font we’re using really _is_ a view concern.
If we want a re-usable component for this, we need something to produce this HTML:
**<span style** ="font-family: monospace" **>** 123.45 **</span>**
Note again I’m using inline styles merely to show what styles are being applied. In reality you’d use CSS for
this, but the overall point will stand (we’ll talk about CSS later). _Also_ note the use of<span>. Certainly,<code>
would achieve the look we want, but our widget ID is not a piece of computer code, so using<code>would be
semantically incorrect.
To create this inline component, we’ll create a new helper inapp/helpers/application_helper.rb.
# app/helpers/application_helper.rb
```
module ApplicationHelper
```
##### →
→ **def** styled_widget_id(widget)
→ content_tag( **:span** ,
→ widget.widget_id,
→ **style:** "font-family: monospace")
→ **end
end**
We can use this helper inapp/views/widgets/show.html.erb:
<%# app/views/widgets/show.html.erb %>
<h1><%= @widget.name %> **</h1>**
→ **<h2>** ID #<%= styled_widget_id(@widget) %> **</h2>**
<% **if** flash[ **:notice** ].present? %>
**<aside>**
<%= flash[ **:notice** ] %>
It works, as you can see in the screenshot “Widget ID Component” on the next page.
If we’d used a partial template for this, it would be super cumbersome:
**<h2>**
ID #<%= render partial: "styled_widget_id",
locals: { widget: @widget } %>
**</h2>**
A View Component would be equally clunky.
**<h2>**
ID #<%= render(StyledWidgetIdComponent.new(widget: @widget) %>
**</h2>**
Even with a disciplined approach that minimized helpers to only those that are necessary and beneficial, your app
will end up with a lot of them. Thus, you need a strategy for where they are defined.
```
Figure 8.2: Widget ID Component
```
### 8.3 Configure Rails based on Your Strategy for Helpers
By default, all files inapp/helpersare included in every view. This creates a huge global namespace that gives
the appearance of modularity, but without actually achieving it. There are two ways to deal with this: only use
app/helpers/application_helper.rbor configure Rails to use helpers in a modular way.
#### 8.3.1 Consolidating Helpers in One File
If you are using View Component (whose relation to helpers we’ll discuss later in this chapter), you
can probably survive on a single global namespace for helpers, all of which would be defined in
app/controllers/application_helper.rb. This is what I have done and is a simple pattern to understand and
conform to.
If you want to do that, you should configure Rails to not create per-controller helper files. You can do this by
placing code like so inconfig/application.rb:
# config/application.rb
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
→ config.generators **do |** g **|**
→ # Prevent generators from creating
→ # per-controller helpers
→ g.helper false
→ **end
end
end**
After configuring this, when you do something likebin/rails g resourceRails will not create a helper for that
resource, which codifies your decision to use a single namespace for all helpers.
If you _do_ want modularity, there is a different configuration for this:
#### 8.3.2 Configure Helpers to Be Actually Modular
If you aren’t using View Component, a way to manage view-specific helpers is to make Rails treat the per-controller
helper files as actually separate. By default, if you have the fileapp/helpers/widgets_helper.rb, that would be
included in all views. This behavior can be confusing and error-prone, because you cannot easily manage the
global namespace and avoid name clashes.
If you wanted actual modularity, meaningapp/helpers/widgets_helper.rbwould _only_ be included on views
rendered from theWidgetsController, you can achieve this by configuring Rails like so:
```
# config/application.rb
require_relative "boot"
```
```
require "rails/all"
```
```
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require( * Rails.groups)
```
```
module Widgets
class Application < Rails :: Application
```
```
# existing configuration...
```
→ # Only include controller-specific helpers for that
→ # controller's views.
→ config.action_controller.include_all_helpers **=** false
```
end
end
```
I find the use of View Component for consolidating view-specific logic easier to manage than helpers, but
modularizing the helpers system isn’t a bad solution. It does allow you to keep within vanilla Rails and reduce the
dependencies your project has.
One caveat with this approach is the use of helpers in partials. If a partial using a helper is rendered by a view
from theWidgetsController, but then is re-used from, say, theManufacturersController, the helper won’t be
available. This can be confusing, so ensure you have good test coverage to detect these issues.
Another consideration for defining helpers is when you have logic that must be shared between a controller and a
view.
#### 8.3.3 Usehelper_methodto Share Logic Between Views and Controllers
A common need in applications is to access the currently logged-in user, which is typically available via a method
namedcurrent_user, defined inApplicationController(or a module that it includes). The view often needs
access to this method as well. This can be arranged without duplicating the method by usinghelper_methodin
ApplicationController, like so:
**class** ApplicationController **<** ActionController **::** Base
private
```
def current_user
User.find_by( id: session [:current_user_id] )
end
helper_method :current_user
```
**end**
Any controller can usehelper_methodto expose a method as a helper to the views that are rendered. I would
caution the extensive use of this feature as it can quickly become difficult to know which helper methods truly
are available to a view, and make view refactoring difficult. Imagine extracting a partial or View Component
that relies on a controller-specific helper, and then using that as part of a view that renders a partial that renders
another partial that calls a View Component that relies on that helper. This is not sustainable.
As messy as this all seems, helpers are the only feature of Rails to allow calling a method inside a view. View
Component, which we discussed in the previous chapter on page 99 provide true scoped methods accessible by
ERB.
We’ll talk about more involved use-cases later in this chapter, but next, let’s go a bit deeper on generating markup
without creating security vulnerabilities.
### 8.4 Use Rails’ APIs to Generate Markup
The view is a magnet for security issues, because it’s code that gets executed in the user’s browser and not on your
servers. If you aren’t familiar with the OWASP Top Ten^1 , it’s a list of the ten most problematic security risks for a
web application. Several of these vulnerabilities can be exploited by allowing unsafe content to be sent to a user’s
browser in HTML, CSS, or JavaScript.
When we just use ERB templates and Rails view helpers, Rails does a great job of preventing these problems. If a
user creates a Widget named"<strong>HACKED</strong> Stembolts", Rails would escape those<strong>tags so
the browser doesn’t render them.
Problems can occur when we generate markup in Ruby code, which is often what our helpers need to do.
For example, we could’ve implemented our styled widget ID helper like so:
**def** styled_widget_id(widget)
%{
<span style="font-family: monospace">
#{ widget.widget_id }
</span>
}
**end**
Rails does not consider this string to be HTML safe, so it would escape all of that HTML and the result would be
that the user would see raw un-rendered HTML in their browser.
We can tell Rails that the string _is_ safe to render in the browser by callinghtml_safeon it.
**def** styled_widget_id(widget)
%{
<span style=\"font-family: monospace\">
#{ widget.widget_id }
</span>
→ }.html_safe
**end**
Rails will then skip escaping this string thus allowing the browser to render it. For the<span>tags in this
method, that’s fine. We can easily see that we have not introduced a security vulnerability. But what about
widget.widget_id? Figuring out where that value comes from, and if it could contain markup or JavaScript, is
not easy. We can’t really be sure this implementation won’t introduce a vulnerability.
If instead, our helper absolutely prevents this problem, we don’t have to worry about any of that. We need to
generate HTML-safe markup, but we need to escape anything we can’t trust, such as thewidget_id. While we
(^1) https://owasp.org/www-project-top-ten/
could handle that by callingCGI.escapeHTMLfrom the standard library, it’s much better to use Rails’ APIs like
content_tag.
When our helper code sometimes useshtml_safeand sometimes doesn’t, it creates confusion. Developers will
wonder when they have to use it and when they shouldn’t. They will have to know the nuances of injection
attacks and know when to escape values and when not to. And they will have to do it correctly. This is exceedingly
difficult to manage. I’ve seen very senior developers—myself included—mess this up, even after thinking it
through and getting peer feedback.
Instead, Rails providescontent_tag(along with all the other various form helpers), which will safely build strings
with dynamic content.
Thus, when authoring helpers, _never_ build strings using interpolation or concatenation. Try to _always_ use Rails’
helper methods to create your markup. I would even recommend using our old friend code comments if you have
to usehtml_safe. Explaining in words why you think the string is safe to send to the browser at least captures
your thinking at the time the code was written while sending a warning to others thathtml_safeis not something
to reach for by default.
Helpers, being Ruby code, can be tested, and it can be worth testing them via unit tests.
### 8.5 Helpers Should Be Tested and Thus Testable
```
This section’s code is in the folder08-04/of the sample code.
```
Helpers are Ruby code, and if one of them is broken, the only way to know that is to hope that a system test
catches it. Since helpers are relatively easy to test, there is value in testing them in isolation, at least when they
have nontrivial logic in them. We have to be careful not to overly specify our tests for helpers, however, because
we don’t want our helpers’ tests to fail if we change immaterial things like styling.
The testing strategy I recommend for helpers is:
- If there is no logic, it may be OK to skip writing a test, especially if the helper is called from a view exercised
by a system test.
- If there _is_ logic, a unit test can be beneficial, but you should not over-specify the assertions in case the
markup needs to change.
- Testing for HTML-safety can be useful if you are already writing a test.
Given the waystyled_widget_idis implemented, there isn’t any logic to it, and thus I’m not sure there is a lot of
value in testing it. But, as a demonstration, let’s make it more involved. Suppose that the business rules are such
that if the widget ID is less than 10,000 (which indicates it’s a legacy widget imported from the old system), we
prefix the value with zeros to make it six digits. The widget with ID 1234 would render0012.34, whereas widget
with ID 987654 would render as9876.54.
We don’t want to over-couple our test to the markup in question, so let’s try to assert only on the content, since
that is what will change based on the business rules.
We’ll write two tests, both of which go inapplication_helper_test.rb, located intest/helpers/. The first will
test that a widget with ID 1234 prefixes the value with two zeros. The second will check that widget 987654 does
not.
We’ll do these checks using regular expressions, asserting that the rendered widget id is present. Our regular
expressions will also use\Dto ensure that no additional digits are present around the rendered ID.
Since we’re testing the output, we can also assert that it is HTMl safe. It’s not worth testing just for this, but since
we are testing the output, it’s good to include.
# test/helpers/application_helper_test.rb
require "test_helper"
**class** ApplicationHelperTest **<** ActionView **::** TestCase
test "styled_widget_id < 6 digits, pad with 0's" **do**
widget **=** OpenStruct.new( **widget_id:** "12.34")
rendered_markup **=** styled_widget_id(widget)
```
assert_match /\D0012\.34\D/,rendered_markup
assert rendered_markup.html_safe?
end
```
```
test "styled_widget_id >= 6 digits, no padding" do
widget = OpenStruct.new( widget_id: "9876.54")
rendered_markup = styled_widget_id(widget)
```
assert_match /\D9876\.54\D/,rendered_markup
assert rendered_markup.html_safe?
**end
end**
This test should fail, since we haven’t made the change yet:
> bin/rails test test/helpers/application_helper_test.rb || \
echo Test failed
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 18356
# Running:
F
Failure:
ApplicationHelperTest#test_styled_widget_id_<_6_digits,_pad_...
Expected /\D0012\.34\D/ to match "<span style=\"font-family:...
bin/rails test test/helpers/application_helper_test.rb:4
##### .
Finished in 0.003475s, 575.4705 runs/s, 1438.6763 assertions...
2 runs, 5 assertions, 1 failures, 0 errors, 0 skips
Test failed
Let’s change the implementation to match our test using Ruby’srjustmethod, which does basically what we
want:
# app/helpers/application_helper.rb
**def** styled_widget_id(widget)
content_tag( **:span** ,
→ widget.widget_id.to_s.rjust(7,"0"),
**style:** "font-family: monospace")
**end
end**
Now, the test should pass:
> bin/rails test test/helpers/application_helper_test.rb
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 34155
# Running:
..
Finished in 0.007018s, 284.9781 runs/s, 854.9343 assertions/...
2 runs, 6 assertions, 0 failures, 0 errors, 0 skips
If we change the styling of the component in the future, the test will continue to pass as long as the ID is formatted
properly.
Before we move on, let’s talk about handling situations where you have more complex view logic. By default,
helpers are the _only_ way in Rails to invoke a method directly in a view. Given that helpers are in a global
namespace, it can be come quite hard to manage them over time, and if you have complex view-specific methods
you want to write, you have to put them into the global namespace and hope no one re-uses them.
Presenter frameworks are historically used to manage this, but in my experience, you can handle this with better
resource modeling and/or View Components.
### 8.6 Tackle Complex View Logic with Better Resource Design or View Components
Often, a view requires highly complex logic that you may not want to be implemented in ERB, and is too
use-case-specific to be in a helper. Historically, Rails developers have used presenter frameworks (also called _view
models_ , _proxies_ , or _decorators_ ) to try to wrangle this.
This section is going to make the case for _not_ using such frameworks and instead rely on either new resources and
Active Model or View Components.
#### 8.6.1 Presenters Obscure Reality and Breed Inconsistency
Most presenter frameworks work by creating proxy objects around an existing Active Record. For example, if you
needed to show a flag on the widget show page that indicated that widget’s manufacturer was local to the user’s
locality, you could implement that by creating a wrapper or proxy object like so:
**class** WidgetPresenter
delegate_missing_to **:@widget**
```
def initialize(widget)
@widget = widget
end
```
```
def local_to_user?(user)
widget.manufacturer.address.us_state == user.address.us_state
end
```
**end**
If you’ve never useddelegate_missing_to, it allows the object that calls it to send all missing methods to the
underlying object. In this case, if you callnameon aWidgetPresenter, that method is missing, so it will delegate
it to@widget, effectively [email protected]. It makesWidgetPresenterappear to be aWidgetwith extra
methods.
Even at moderate complexity, this will be confusing for several reasons:
- Do you call the instance variable@widget, thus making it unclear what the actual class is, or do you call it
@widget_presenter, making it diverge from the convention of naming a resource and instance variable the
same?
- Do you always create a presenter? If not, when do you? When don’t you?
- Do re-usable components, partials, or helpers expect aWidgetor aWidgetPresenter? How do you know
which to use when?
There are many ways to decorate one object with additional behavior, but they all create the same problems.
Managing these problems can be difficult, and often requires code review to avoid a convoluted mess.
Because of this, I would caution against the use of presenters or proxies and instead use one of the two techniques
described below, starting with resource modeling.
#### 8.6.2 Custom Resources and Active Model Create More Consistent Code
In the chapter on routes on page 75, we talked about using resources instead of custom routes. Rails allows
you to create any resource you like, even if it’s not an Active Record. Rails also provides you with the module
ActiveModel::Modelthat allows you to create a class that works with the Rails view in the same way an Active
Record does.
When faced with the need to make a complicated view for a resource, it may be helpful to give that view a more
specific name, and then create an Active Model to represent it. For example, we may decide that instead of the
widget show resource, our need to show the locality of a widget will be called aGeographicLocalWidget. Not the
best name, but it will be hard to mis-use.
First, we’d create a canonical Rails route for this:
# config/routes.rb
resources **:geographic_local_widgets** , **only: [ :index** , **:show ]**
This means that the controllerGeographicLocalWidgetsControllerwill have anindexand ashowmethod.index
will expose the instance variable@geographic_local_widgets, andshowwill expose@geographic_local_widget.
What we want is for those instance variables to behave like Active Records. We want to write code like so:
**<ul>**
<% @geographic_local_widgets.each **do** |geographic_local_widget| %>
**<li>**
<%= link_to geographic_local_widget **do** %>
Widget <%= geographic_local_widget.id %>
<% **end** %>
**</li>**
<% **end** %>
**</ul>**
But, of course, we also need to be able to calllocal_to_user?(current_user)on whatever ageographic_local_widget
is. We can achieve both with Active Model. We’ll talk more about Active Model in chapter 13 on page 186, but if
we createGeographicLocalWidgetlike so, it will work:
**class** GeographicLocalWidget
include ActiveModel **::** Model
```
attr_accessor :id , :name , :manufacturer
```
```
def persisted? = true
```
```
def to_key = [ self.id ]
```
**def** local_to_user?(user)
manufacturer.address.us_state **==** user.address.us_state
**end
end**
This can be used in our controller as if it were an Active Record:
**def** show
widget **=** Widget.find(params **[:id]** )
@geographic_local_widget **=** GeographicLocalWidget.new(
**id:** widget.id,
**name:** widget.name,
**manufacturer:** widget.manufacturer
)
**end**
Essentially, this creates a new domain concept based on an existing Active Record, and includes the methods you
need on it. It’s _similar_ to a presenter, but works better with Rails and creates more consistent code. View code
that uses an Active Record will look almost exactly like view code that uses an Active Model. You won’t mistake
what a@geographic_local_widgetis, because it’s a well-defined class just likeWidget.
Sometimes, however, the logic you need is not really a new domain concept or resource. In that case, you can
have a View Component take over the entire page.
#### 8.6.3 View Components can Render Entire Pages When Logic is Complex
Although View Components, which we discussed in chapter 7 on page 99, are primarily used to create re-usable
bits of view logic, they can certainly render the entire page. The View Component class could include all the logic
you might otherwise put into a presenter.
For example, we could createWidgetShowPageComponentand place all ofapp/views/widgets/show.html.erbinto
its ERB, leavingwidgets.show.html.erblooking like so:
**<%=** render(WidgetShowPageComponent.new( **widget:** @widget)) **%>**
Then,WidgetShowPageComponentcould implementlocal_to_user?, like so:
# app/components/widget_show_page_component.rb
**class** WidgetShowPageComponent **<** ViewComponent **::** Base
**def** initialize( **widget:** )
@widget **=** widget
**end**
**def** local_to_user?(current_user)
@widget.manufacturer.address.us_state **==**
current_user.address.us_state
**end
end**
The ERB code can calllocal_to_user?.
Like using an Active Model, this technique is similar to using presenters, however it doesn’t have the issues
presenters have. There is no conflation of objects (WidgetvsWidgetPresenter) and the component is a well-
defined class using a pattern you would already have in your app.
### Up Next
Helpers are problematic, but so are the alternatives. Of course, you could just live with some duplication in your
markup, and this isn’t the worst idea in the world. The “Don’t Repeat Yourself” (DRY) Principle isn’t any more of
a real rule than the Law of Demeter. It’s all trade-offs.
The news is about to get worse. All the problems that exist with helpers are exacerbated by our next topic: CSS.
## 9 CSS
```
This section’s code is in the folder09-01/of the sample code.
```
Like helpers, the problem with CSS is how to manage the volume of code. CSS, by its nature, makes the problem
worse, because of the way CSS can interact with itself and the markup. It’s not unheard of for a single line of CSS
to break an entire website’s visuals.
When CSS is unmanaged, developer productivity can go down, and the app becomes less sustainable. There are
two main factors that lead to this that you must control:
- Some CSS must be written for each new view or change to a view. The more required, the slower
development will be.
- The more CSS that exists, that harder it is to locate re-usable classes, which leads to both duplication _and_
even more CSS. As with helpers, there is a volume at which no developer can reasonably understand all the
CSS to locate re-usable components, and the safest route is to add more CSS.
Therefore, to keep CSS from making your app unsustainable, you must manage the volume. Ideally, the rate of
growth in CSS is lower than the rate of growth of the codebase. Thus, the more re-usable CSS you have, the less
CSS we will need.
To achieve this, you need three things:
- A Design System, which specifies font sizes, spacing, and colors (among other things).
- A CSS Strategy, which implements the design system, but also provides a single mechanism for styling
components and re-using them when needed.
- A Style Guide, which is a living document of your Design System and CSS Strategy.
The absolute biggest boon to any team in wrangling CSS is to adopt a _design system_.
### 9.1 Adopt a Design System
A _design system_ is a set of elemental units of the design of your app. At its base, it is:
- A small set of font-sizes, usually around eight.
- A small set of pre-defined spacings, again usually around eight.
- A color palette of a finite number of colors.
Any design for any part of the app uses these elemental units. For example, any text in the app should be in one
of the eight available sizes.
Many designers create a design system before doing a large project, because it reduces the number of design
decisions they have to make. Most apps can be very well designed without needing an infinite number of font
sizes, spacing, or colors. For example, when a designer is laying out a page, they can literally audition all eight
font sizes and choose the best one.
You can leverage this by replicating the design system in your code. So instead of specifying the font-size directly
in pixels or rems, you specify “font size 3” or “font size 5” (for example).
The design system can also contain reusable components like buttons, form fields, or other complex layouts.
These reusable components might not all be known up front, so some emergent additions to the design system
will appear over time.
If your app is designed based on a design system, this will vastly reduce the amount of CSS you have to write,
and the CSS you _do_ write will be easier to understand and predict. Ideally, entire views can be created using the
design system’s classes without creating any new CSS.
Talk to your design team, if you have one, and ask about the design system. Even if all they have is a set of
font-sizes, that’s something. Encourage them to standardize colors and spacings if they haven’t, and explain to
them (plus whatever manager might be around making decisions) that a stable design system will boost your
team’s productivity. You can always—if you are feeling subversive—implement their designs using a design system
you reverse engineer. Many designers don’t want pixel-perfect designs.
Of course, not everything will conform to the design system. Some designs will require something custom, but
this should be a small percentage of the designs and pages. Writing some CSS is OK, as long most of what you do
conforms to the design system.
If you don’t have a design team, which is common when building so-called “internal” software (for example, a
customer service app), you can use a CSS framework which will be based on its own design system. We’ll talk
about that in the next section.
### 9.2 Adopt a CSS Strategy
A design system is great, but if you don’t have a way to manage your CSS and leverage that system, your CSS will
be a huge mess. Unfortunately, Rails does not provide any guidance on how to manage CSS. Before Rails 7, Rails’
generators create per-controller.cssfiles, creating confusion. These files gave the illusion of modularity, but
those.cssfiles were rolled up into oneapplication.cssand you ended up with a global namespace of classes
spread across many files.
When deciding on a strategy, remember that we are building server-rendered views. We’ll talk about that a bit
more in the next chapter on page 144, but the important thing to understand is that a strategy that doesn’t work
with Rails views is not a viable strategy.
This leaves three main strategies: a framework, Object-Oriented CSS (OOCSS), and Functional CSS.
I do want to be explicit about a strategy you should _not_ use, which is likely the strategy you learned when you
first learned web development: semantic CSS.
There is no value in giving markup aclassthat has some semantic meaning. Users using a web browser won’t
see this class, and assistive technologies rely on ARIA Roles^1 when more meaning is needed for some markup. If
you need to provide a hook for a piece of the DOM for non-presentational purposes,data-attributes are more
effective.
Thus, the front-end engineering ecosystem has largely embraced using classes with presentational meanings, since
the only reason to use a class is to attach CSS to it. For example, here is the markup for a button in the Bootstrap
framework that uses an outline look and a large font:
**<button class** ="btn btn-outline-success btn-lg" **>**
OK
**</button>**
Both OOCSS and Functional CSS take the approach of using classes in markup to have presentational meaning.
They differ in exactly how they do that. Both approaches are ways to manage CSS and thus create your design
system in code. A framework does all this for you, but it’s not always the right choice.
#### 9.2.1 A CSS Framework
A CSS Framework is something like Bootstrap^2 or Bulma^3. These contain a wide variety of pre-styled components,
from font-sizes to complex forms and dialog boxes. For an internally-facing app, a framework is going to make
your team far more productive than hand-styling views, because the design doesn’t matter _as much_ as for a
public-facing app, _and_ , you rarely need highly-branded visual styling for internal apps.
Using something like Bootstrap means you don’t need to create a design system (Bootstrap and other frameworks
have a set of defaults built-in), and without writing any CSS, anyone on the team can design and build UIs that
look pretty good. CSS Frameworks aren’t replacements for real designers or user-experience experts, but if you
have internal apps that can use a framework as its design system, you have fewer decisions to make and will have
an easier time building views.
Also, there will be _far_ less CSS to manage, and you won’t need to write much, if any. This is highly sustainable.
That said, most public-facing apps need more customization, more specialized branding, and have more function-
ality than the simple web forms and info dumps present in an internal app.
In those cases, you will want more control over CSS and you will want to implement and grow the design system
yourself. Thus, you need a single convention on how to use CSS, which comes down to deciding what the classes
should be on your markup.
There are many popular approaches that I’m going to group together as _object-oriented CSS_ , which we’ll discuss
first.
(^1) https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles
(^2) https://getbootstrap.com
(^3) https://bulma.io
#### 9.2.2 Object-Oriented CSS
Object-oriented CSS (OOCSS) is not strictly defined, and it’s a confusing name if you come from object-oriented
programming. In OOCSS, there are no classes, objects, or methods like there are in Ruby. The _object_ being
referred to in the name is what we’ve been calling a component, or might be called a module. It is markup
plus CSS to achieve some particular design. A button with rounded corners and a large font in all caps is an
object/component/module. I’m going to use the word _component_ , since that’s what we’ve been using thus far.
In OOCSS, markup is assigned a name as to what visual component it is supposed to be. OOCSS methodologies
employ naming conventions based on that to attach classes to any part of the component’s markup that needs
styling. There is typically no deep nesting of CSS, no styling directly on elements, and often a delineation between
base styles to achieve a layout and modifiers which tweak it. For example, a button always has rounded corners,
but a dangerous button will additionally have red text.
Two common strategies for OOCSS are Block-Element-Modifier (BEM)^4 and SMACCS^5. If you like the OOCSS
approach, I strongly recommend adopting one of these two, with BEM being slightly easier to understand in my
experience.
For example, suppose we want to enhance the<h1>and<h2>in our widget show page. We want the widget’s
name to be bold and in all-caps, and we want the ID to be in a monospace font. In an OOCSS approach, you
might do something like this:
**<header class** ="widget-title" **>
<h1 class** ="widget-title__name" **>** Stembolt **</h1>
<h2 class** ="widget-title__id" **>** 123.45 **</h2>
</header>**
The classes demarcate each part of the component. The CSS might look like so:
.widget-title__name {
**font-weight** : bold;
**text-transform** : uppercase;
}
.widget-title__id {
**font-weight** : normal;
**font-family** : monospace;
}
Although thewidget-titleclass doesn’t get styling in this example, you can begin to see the theory here.
Components have a class indicating what they are (not semantically, but presentationally), and we use a naming
(^4) https://getbem.com
(^5) [http://smacss.com](http://smacss.com)
convention to create classes as needed for the parts of the component. Note that we _don’t_ prescribe the HTML
tags to use; the CSS is agnostic. This allows us to re-use this component’s styling in a situation where perhaps an
<h3>is more appropriate than an<h1>.
This approach is sustainable, mostly because it provides a clear and simple way to keep CSS isolated. CSS can get
very complicated when there is deep nesting and stacking of styles, and an OOCSS approach instead keeps them
flat
But, it’s not perfect. There are a few downsides:
- Everything you style has to be a component with a name, even if that component is never re-used. This
means that you have to make a _lot_ of naming decisions. It also means that it’s not clear from your CSS what
components are actually intended for re-use.
- When you _do_ identify re-usable components, you need an additional strategy for how to manage that. For
example, if it turns out that componentswidget_title,manufacturer_name, andshipping_locationare
all what should be called atitle_component, you now have to either rename the classes, or configure a
CSS pre-processor to re-use the common styling.
- To predict how a view will render, you must mentally merge the.cssfile and the view. You cannot just look
at the markup to know what styles will be applied.
The result is that you will read and write a lot of CSS. The CSS you write will more or less grow linearly with
your markup and views, and the more of it that exists, the less likely you and your team are to re-use it without
careful grooming and documentation.
Another approach is functional CSS.
#### 9.2.3 Functional CSS
Functional CSS (sometimes called _atomic_ CSS) is a strategy where you have a largely static set of small, single
purpose, highly-presentational classes that you combine to achieve a certain look. For example, there might be a
class namedfwbthat does nothing but setfont-weighttoboldand another calledttuwhich does nothing but
setstext-transformtouppercase. To style some content in bold uppercase, you’d useclass="ttu fwb".
It’s called _functional_ in a nod to mathematical functions, which produce the same output for the given input.
Classes in a functional CSS system have completely predictable and unambiguous behavior. They can feel like
short-hand for using CSS directly in your markup.
Our widget title component would look like so:
**<h1 class** ="fwb ttu" **>** Stembolt **</h1>
<h2 class** ="normal courier" **>** 123.45 **</h2>**
The CSS for this code (which, remember, you don’t have to write) might look like so:
.fwb { **font-weight** : bold }
.normal { **font-weight** : normal }
.ttu { **text-transform** : uppercase }
.courier { **font-family** : Courier, monospace }
These terse classes are based on Tachyons^6. When you use functional CSS, you typically use a library like Tachyons
or Tailwind^7 which provide all the CSS classes you need. There are usually several CSS classes for each CSS
attribute.
For example, there might be 10 classes for 10 common values forfont-weight: 100, 200, 300, 400, 500, 600,
700, 800, 900, and bold. Tachyons uses extremely terse names likefw4orfwb, whereas Tailwind uses more
semantic names likefont-thinorfont-bold.
In my experience, Tachyons becomes easier to use over time, because most of the classes are terse initialisms and
mnemonics. For examplettuis a way to settext-transformtouppercase. In Tailwind, the class to accomplish
this isuppercase, which might read better but has zero connection to the underlying CSS it produces.
**Functional CSS is not the Same as Inline Styles**
Note that this approach is not identical to using inline styles, because you cannot style pseudo elements with
inline styles, nor can you achieve different breakpoints and media queries with inline styles. Inline styles also
have a higher specificity, so using classes for styling allows you to use inline styles if needed to solve a particular
problem.
Functional CSS _is_ highly sustainable, even if it doesn’t seem so at first glance. Consider the markup examples
we’ve seen so far in the book. I used inline styles to demonstrate what styling was being applied without having
to actually discuss CSS. This was merely to keep us focused, but did you notice how you could look at _just_ the
markup and understand the intended visual presentation of the view?
Functional CSS provides this without using inline styles. It means that you can look at _just_ the markup in order to
understand how a page will be styled. It also means you rarely write CSS and thus have almost no CSS to actually
manage.
**Re-use with Functional CSS Leverages your HTML Templates**
Unlike using a framework or OOCSS, functional CSS does not include an obvious way to extract re-usable
components. If we have red bold text, set in the second largest font, all in uppercase with wide letter spacing,
we’d have to write<p class="red fwb f2 ttu tracked">everywhere we wanted to re-use that.
Functional CSS approaches assume that the unit of re-use is not the CSS class, but your templating system. We
discussed in the chapter “HTML Templates” on page 83 how to re-use markup using partials or View Components.
Because that markup has classes that represent all the needed styles, this naturally allows re-use of styling as well.
Thinking of markup as the unit of re-use also provides one way to manage re-usable components, not two. That
said, when you do need to create your own classes, you may want to re-use aspects of your functional CSS
framework. Some frameworks make this easy—by providing CSS custom properties—and some make it more
difficult, requiring a complex toolchain of configuration files and pre-processors.
(^6) [http://tachyons.io](http://tachyons.io)
(^7) https://tailwindcss.com
**Downsidies to Functional CSS**
There are downsides to this approach:
- If your UI must be highly configurable, beyond just sizing, fonts, and colors, functional CSS pretty much
won’t work. This is not a common need, but if it is a real need, OOCSS will work better.
- If you have a split back-end and front-end team, you will need to adopt a workflow to allow both teams to
work, since both teams would do the bulk of their work in the HTML templates. An OOCSS approach allows
the front-end team to work mostly inside.cssfiles.
- Complex styling such as custom form elements can’t easily be done with functional CSS, so you would need
a way to manage that. I would recommend you adopt something like BEM whenever you need to write a lot
of.css.
**Note on Tailwind’s Lack of Design System**
Since I first wrote this book, Tailwind has become quite popular, but it’s worth understanding that Tailwind
_does not_ provide a design system. Tailwind allows you to use a functional style of CSS to achieve almost any
combination of properties and values, then performs a build step on your code to produce the CSS. You can write
a class likefont-[876]and Tailwind will create that class to setfont-weightto 876.
This behavior, while clever, eliminates one of the main advantages of functional CSS and _is not_ sustainable. You
_can_ configure Tailwind to have a design system by crafting your owntailwind.config.js. If you decide to use
Tailwind, I highly suggest you do this, otherwise you will have all the mess of semantic CSS, but spread all
overyour view code instead of in.cssfiles.
Once you have chosen a strategy, you need to use it to build the design system, and the best way to do that is to
create a living style guide.
### 9.3 Create a Living Style Guide to Document Your Design System and CSS Strategy
A living style guide is documentation that both uses your design system and shows developers how to apply
it to the view. Bootstrap’s documentation^8 is an example of this. It shows both the visual appearance of the
components it provides as well as the markup you need to achieve that appearance.
You need this for your app. If you don’t have this, developers will not know what re-usable components exist, nor
will they know how to apply the CSS strategy you have chosen. And then your CSS will be an unsustainable mess.
Let’s create a style guide. We’ll adopt the functional CSS strategy and use Tachyons. To specify our design system,
ideally we’d use some CSS custom properties that our CSS framework knows about. Tachyons doesn’t currently
support this, but there is a SASS^9 port that allows customizing it.
I’ve created the tachyonscss-rails^10 gem that is designed to work with Tachyons, SASS, and Rails 7. We’ll need to
add bothsassc-railsand this gem to ourGemfile:
(^8) https://getbootstrap.com/docs/4.4/getting-started/introduction/
(^9) https://sass-lang.com
(^10) https://github.com/sustainable-rails/tachyonscss-rails
# Gemfile
# lograge changes Rails'logging to a more
# traditional one-line-per-event format
gem "lograge"
→# Tachyons is a functional CSS framework
→# we'll use to style our views
→gem "tachyonscss-rails"
→# tachyonscss-rails embeds a SASS version
→# of Tachyons, so we need to include
→# a SASS compiler
→gem "sassc-rails"
```
# View Component is used to manage
# and test complex view logic
```
Next, we’ll install these two gems:
> bundle install
«lots of output»
To use this, we need to convert ourapplication.cssto be a SASS stylesheet. The easiest way to do that is to
delete the existing file:
> rm app/assets/stylesheets/application.css
And createapplication.scss(note the file extension):
/* app/assets/stylesheets/application.scss */
**@import** "tachyons";
@importis a SASS function that brings in external SASS files. Figuring out the value to give it for an externally-
required gem is not usually possible—the gem maintainer has to tell you. In this case, the gem maintainer is me,
and the README indicates to use"tachyons".
As mentioned above, our design system should have at least a set of font sizes, spacings, and colors. For the sake
of brevity, let’s assume that our design system’s spacing and colors are exactly those provided by Tachyons. Our
font sizes are different. Our designer has chosen these eight sizes (specified inrems):
- 4.8rem
- 3.7rem
- 2.8rem
- 2.2rem
- 1.7rem
- 1.3rem
- 1.0rem
- 0.8rem
The tachyonscss-rails gem embeds the.scssfiles from the Tachyons SASS port. You can see what is included by
looking at the gem’s source on GitHub^11.
At the top are the values for font sizes, like so:
$font-size-headline: 6rem !default;
$font-size-subheadline: 5rem !default;
$font-size-1: 3rem !default;
$font-size-2: 2.25rem !default;
$font-size-3: 1.5rem !default;
$font-size-4: 1.25rem !default;
$font-size-5: 1rem !default;
$font-size-6: .875rem !default;
$font-size-7: .75rem !default;
The!defaultconstruct means that if we don’t set a value for that variable, the value in_variables.scsswill be
used. For example, if we don’t set a value for$font-size-1, the value 3rem will be used. This allows tachyons to
have a default design system if we don’t provide our own.
To override these, we’ll set values for all nine font variables (the two smallest fonts will be the same size since
we only have eight font sizes). It’s important that we leave$font-size-5as1rem, because that is assumed by
Tachyons to be the body font size, which is the size of normal text.
Note that we’ll need to set these values _before_ the call to@importor they won’t take affect. Here’s the change to
app/assets/stylesheets/application.scss:
/* app/assets/stylesheets/application.scss */
→$font-size-headline: 4.8rem;
→$font-size-subheadline: 3.7rem;
→$font-size-1: 2.8rem;
→$font-size-2: 2.2rem;
→$font-size-3: 1.7rem;
(^11) https://github.com/sustainable-rails/tachyonscss-rails/blob/main/app/assets/stylesheets/scss/_variables.scss
→$font-size-4: 1.3rem;
→/* font-size-5 should always be 1rem
→ * as Tachyons expects this to be the
→ * body font. */
→$font-size-5: 1rem;
→$font-size-6: 0.8rem;
→$font-size-7: 0.8rem;
**@import** "tachyons";
With that done, we’ll create our style guide, which is a demonstration of our design system. We’ll create a new
resource calleddesign_system_docsthat has anindexaction.
We’ll first add the route, but only if we are in development (we don’t want our users seeing the style guide):
# config/routes.rb
```
resources :widgets , only: [ :show , :update , :destroy ]
end
```
→ **if** Rails.env.development?
→ resources **:design_system_docs** , **only: [ :index ]**
→ **end**
####
# Custom routes start here
#
We still want to follow the conventions we’ve established about views, so that means our controller methods
should expose an instance variable named@design_system_docs. We’ll useOpenStructagain to create this object.
It’ll have three methods:font_sizes,sizes, andcolors.
Thefont_sizesattribute will be a list of class names to use to achieve those font sizes. Forsizes, since there
are margins and padding, we’ll use the numbers 1–5 and dynamically construct the class names in the view. For
colors, we’ll create a map from the color name to the CSS class that achieves it.
# app/controllers/design_system_docs_controller.rb
**class** DesignSystemDocsController **<** ApplicationController
```
def index
@design_system_docs = OpenStruct.new(
```
**font_sizes: [**
"f-headline",
"f-subheadline",
"f1",
"f2",
"f3",
"f4",
"f5",
"f6",
**]** ,
**sizes: [** 1,2,3,4,5 **]** ,
**colors: {
text:** "near-black",
**green:** "dark-green",
**red:** "dark-red",
**orange:** "orange"
**}**
)
**end
end**
The view is going to be a bit gnarly, because we have to generate markup that uses these styles but also show the
code that achieved that markup. We’ll have three sections and a<nav>at the top, along with a link to Tachyons’
docs.
<%# app/views/design_system_docs/index.html.erb %>
<section **class** ="pa3">
<h1>
Design System Docs
<nav **class** ="f4 di ml3">
<a href="#font-sizes">Font Sizes</a> |
<a href="#sizes">Sizes</a> |
<a href="#colors">Colors</a> |
<a href="https://tachyons.io/docs/">Tachyons Docs</a>
</nav>
</h1>
<h2 id="font-sizes">Font Sizes</h2>
<% @design_system_docs.font_sizes.
each **do** |font_size_css_class| %>
**<p class** ="<%= font_size_css_class %> mt0 mb0" **>**
<%= font_size_css_class %> Font Size
**</p>
<code><pre>**
&lt;p class="<%= font_size_css_class %>"&gt;
<%= font_size_css_class %> Font Size
&lt;/p&gt;
**</pre></code>**
<% **end** %>
**<h2 id** ="sizes" **>** Sizes **</h2>**
<% @design_system_docs.sizes.each **do** |size_number| %>
**<h3>** Size <%= size_number %> **</h3>
<div class** ="pa<%= size_number %> ba
h<%= size_number %>
w<%= size_number %> bg-gray" **>**
&nbsp;
**</div>
<code><pre>**
&lt;div class="pa<%= size_number %>"&gt;
Padding all sides
&lt;/div&gt;
&lt;div class="ma<%= size_number %>"&gt;
Margin all sides
&lt;/div&gt;
**</pre></code>**
<% **end** %>
**<h2 id** ="colors" **>** Colors **</h2>**
<% @design_system_docs.colors.each **do** |name, css_class| %>
**<h3>** <%= name.to_s.humanize %> **</h3>
<div class** ="ma1 pv3 ph2 h4 bg-<%= css_class %> white" **>
<code><pre>**
&lt;div class="bg-<%= css_class %>"&gt;
<%= name %> background
&lt;/div&gt;
**</pre></code>
</div>
<div class** ="ma1 pv3 ph2 h4 ba
b--<%= css_class %>
<%= css_class %> bg-white" **>
<code><pre>**
&lt;div class="<%= css_class %> b--<%= css_class %>"&gt;
<%= name %> border and text
&lt;/div&gt;
**</pre></code>
</div>**
<% **end** %>
**</section>**
Now, if you go to/design_system_docs, you should see it just like the screenshot “Font Size Documentation” on
the next page, “Sizes Documentation” on page 139, and “Color Documentation” on page 140.
You may need more documentation than this, depending on what you are doing. You could also build the page
statically instead of making an object like I did. In any case, this page should provide as much information as
possible about your CSS strategy, the design system, any reusable components, and how to use it all.
Whenever a re-usable component is created, this page should also be updated, and you’ll have to manage that
with code review or pair programming.
If you can manage this, you’ll stick to your CSS Strategy and leverage your design system, and while your CSS
won’t be amazingly perfect, it will be as sustainable as you can make it, and that’s a pretty good result.
### Up Next
CSS is not an easy thing to learn or manage. So it goes with JavaScript.
138 Figure 9.1: Font Size Documentation
Figure 9.2: Sizes Documentation 139
140 Figure 9.3: Color Documentation
## 10 Minimize JavaScript
```
JavaScript and front-end development is a deep topic. I won’t be able to cover it all here and I definitely can’t give
you a guide on sustainably creating highly complex dynamic web applications that run entirely in the browser.
The good news is that you almost certainly don’t need your application to work that way. At best, you’ll need
what Zach Briggs calls “islands of interactivity”^1 : bits of dynamic behavior on some of your pages.
The single best thing you can do to keep your front-end sustainable is to use only what JavaScript you actually
need to deliver value to the app’s users. There are a lot of current realities about client-side JavaScript and web
browsers that make it inherently more difficult to work with than back-end technologies.
```
```
In this chapter, we’ll focus on JavaScript generally: how to think about it and manage it at a high level. The
overall strategy here is:
```
- Understand why JavaScript is a more serious liability than your Ruby code.
- Embrace server-rendered views wherever client-side interactivity isn’t required.
- Tweak Turbo’s defaults to create a stable baseline of front-end behavior.
```
JavaScript solves real problems we face as developers, but it’s not perfect—how could it be? The strategy here
is designed to keep your app sustainable by dealing directly with the realities of JavaScript and the front-end
ecosystem. It’s important to make decisions based on the realities of how our tools work, not on how we wish
they worked.
```
```
To understand this strategy requires being honest about how serious of a liability client-side JavaScript is to your
app, so let’s dive in.
```
### 10.1 How and Why JavaScript is a Serious Liability
```
A liability is something that we are responsible for. Liabilities aren’t good or bad by nature, but the concept is a
useful lens to understand technical decisions.
Your app is a liability. You are responsible for it. You are responsible for building it, maintaining it, operating it,
and explaining its behavior to others. This book is about how to manage that responsibility.
```
But liabilities are relative. Compared to the other code in your app, client-side JavaScript (here on called simply
“JavaScript”) is a more serious liability. It is a large responsibility relative to the back-end, all other things being
equal.
(^1) https://modernweb.com/limit-javascript/
It’s important to understand why this is, so that you can drive your technical architecture decisions based on
realities and not dogma.
There are three contributors to JavaScript as a more serious liability:
- You have no control over the runtime environment.
- Your JavaScript’s behavior is difficult or impossible to observe in production.
- The ecosystem values small decoupled libraries that tolerate breaking changes in order to progress quickly.
Let’s talk about each one of these realities.
#### 10.1.1 You Cannot Control The Runtime Environment
Your JavaScript will run on many different versions of many different brands of browsers on many different
versions of many different brands of operating systems on many different versions of many different brands of
computers connected to many different types of networks.
I can’t think of a more difficult scenario in which to build software.
Your Ruby code, on the other hand, runs on a runtime of a single version of a single operating system on a single
brand of computer using a single type of network connection. Or at least it is possible to arrange this. Certainly
the use of cloud services results in some aspects of our runtime being unknown, but it’s still our choice to cede
that control.
The runtime environment for our JavaScript, being out of our control, means that the behavior of the code
running there is hard to accurately predict. A common strategy for managing code running in unpredictable
environments is to heavily monitor its behavior to find issues and fix them quickly.
But with JavaScript, this is not so easy.
#### 10.1.2 JavaScript’s Behavior is Difficult to Observe
When developing JavaScript, we can run it in a browser on our own computer, thus controlling the runtime
environment during development. But even in this stable environment, actually observing the behavior of the
code is surprisingly difficult.
Pretty much the only mechanisms you have in your development environment are the odd calls toconsole.log
or step through the code in the browser’s debugger. Browsers do provide additional tools for inspecting your code,
but JavaScript’s nature prevents them from being very sophisticated. When you see errors in the console, the
stack traces are often wrong. Most JavaScript runtimes produce unhelpful errors such as “undefined is not a
function”. But at least you can do something in your own browser.
In production, JavaScript is running on the browsers of your app’s users and there is no way by default for you to
observe that behavior on any level. If you’ve ever supported applications for users at the company you work for,
you’ve no doubt asked those users to open the browser console to help debug a problem^2.
What this means is that your code that’s already running on myriad environments you cannot control also cannot
be observed. The most common tool available to try to observe JavaScript’s behavior is to install an error reporting
(^2) The associates working in Stitch Fix’s warehouse called the JavaScript console “The Matrix”, because it was like going behind the scenes
of the real world and hacking the system.
system like Bugsnag. In my experience, tools like this are useful, but they produce a lot of noise and don’t drive a
lot of clarity (see the sidebar “A Year of JS Monitoring and Nothing to Show For It” below for an example of this).
JavaScript libraries you depend on generate spurious error messages and, even with source maps on production,
stack traces are almost always wrong.
#### A Year of JS Monitoring and Nothing to Show For It
```
When building the public website for the healthcare startup where I was CTO, I installed Bugsnag, a popular
error-reporting service. Our website didn’t have a lot of JavaScript, but it did have some and there were a few places
where, if the JavaScript was broken, a potential customer could not proceed and sign up for the service.
During the first year of its existence, this error reporting system captured and reported numerous errors, usually
several a week. Without fail, they were all from marketing pixels we were using to track and analyze our paid ad
performance. In a year, there were exactly two issues with our JavaScript, and these were related to users on Internet
Explorer 11, which was technically below our browser baseline.
We significantly dialed back our use of front-end error reporting to only those pages were it was most crucial.
Those pages were the ones using our credit card processor’s JavaScript to capture card information. To this day, it is
those components that generate all of our front-end errors, and the errors are usually completely useless messages like
“error from stripe.js”.
If you’ve ever wondered how fancy websites from big companies seem to just not work for months on end, this is
probably how. They have no way to know what is truly broken.
```
Compare this to your back-end code. It is possible to get a very fine-grained understanding of how it behaves. By
default, Rails logs requests and responses, which is more than you get with JavaScript. We set up lograge in the
section “Improving Production Logging with lograge” on page 44, which makes those logs even more useful. We
can write our own log messages. We can install tools like DataDog or Honeycomb to tell us how often certain
parts of our app are executed and how long they took. And on and on.
This means that problems in your JavaScript code are harder to predict, harder to detect, and harder to fix once
detected.
But it gets worse, because the ecosystem as it stands moves forward very fast, favoring progress over stability.
#### 10.1.3 The Ecosystem Values Highly-Decoupled Modules that Favor Progress over Stability
We haven’t set up NPM in our project, but my guess is that you have at least one project you’ve worked on that
uses it. Take a peek intonode_modules. On a brand new Rails 6 application there are 770 modules installed (Rails
7 does not require NodeJS). These modules are all needed for the six direct dependencies the Rails 6 app has
on JavaScript modules. Our Rails app has a direct dependency on 16 Ruby Gems, which ultimately require the
installation of 131 RubyGems.
The reason for this disparity is that the JavaScript ecosystem is built on many small de-coupled libraries. For
example, map-obj is a library that contains a single nine-line function. That’s it.
Small, de-coupled libraries aren’t necessarily good or bad, but the way this affects you and your app’s sustainability
is that there are more packages that must interoperate with each other. When you consider that these packages
are all maintained by different people with different road maps and priorities, more packages means higher risk
of one thing breaking another.
If this isn’t bad enough, the JavaScript ecosystem also favors progress over stability. It’s not uncommon for point
releases of a library to contain breaking changes. Libraries also have inter-dependencies on other libraries that
are not explicit. If you’ve seen warnings about “peer dependencies”, this means you have potentially incompatible
versions of two libraries running, but you are on your own to figure out how to fix it. Usually, you can’t without
removing the libraries altogether from your app.
I realize Rails, too, favors progress over stability^3 , but Rails goes to great pains to maintain backwards compatibility,
point out deprecated APIs and provide clear upgrade paths for users. This is not common for JavaScript libraries.
This reality results in a situation where regular updates of your dependencies can cause a cascading effect of
errors that can be difficult and time-consuming to fix. While you can somewhat rely on the Rails core team to
make sure the dependencies that are a part of Rails keep working with Rails, anything you bring in isn’t subject to
that level of care. This is your responsibility.
The single best thing you can do to manage the liabilities that come with JavaScript is to minimize its use to only
where it is needed. By all means, use it when you need it, but don’t use it when you don’t.
A big step toward that goal is to prefer server-rendered views using ERB.
### 10.2 Embrace Server-Rendered Rails Views
Rails server-rendered views work very much like PHP, JSP, or ASP: the server loads an HTML template, populates
that with dynamic data, renders it into HTML, and sends that HTML to the browser as part of the request/response
cycle. This interaction model is easy to understand, instrument, predict, and test.
Outside of Rails, it’s common for developers to send the HTML templates bundled with dynamic data to the
browser and have the browser render the HTML on the client-side. With sufficiently powerful back-end APIs,
developers can build the entire application to run in the browser using JavaScript and markup. This combination
is known as the “JAM Stack”, with “JAM” standing for JavaScript, APIs, and Markup.
Setting aside the risks with JavaScript we just discussed, JAM Stack apps are architecturally more complex. They
have more moving parts that must be carefully coordinated in order to produce a working app. This means that
simple changes in a JAM Stack app can be difficult to make.
The JAM Stack is not a good default choice in most cases. The power it brings is almost never worth the carrying
cost—which is large. The JAM Stack approach should be treated as a surgical tool you use only when you need it,
and not something to use by default.
To understand why, and thus why you should prefer server-rendered views instead, let’s break down both
approaches.
#### 10.2.1 Architecture of Rails Server-Rendered Views
As mentioned above, the architecture of the default view rendering in Rails is for the server to render HTML and
send that to the client as shown in the figure on the next page.
Rails allows the inclusion of JavaScript that is loaded after the page renders to provide interactive elements to the
server-rendered page.
The benefits of this approach are many:
(^3) https://rubyonrails.org/doctrine
```
Figure 10.1: Server-Rendered Views
```
- It is stable and predictable, since HTML rendering happens on the server side in an environment you can
control and observe.
- Because only the interactive parts of the page are using client-side JavaScript, there is minimal client-side
state to manage. Most pages are stateless with no behavior on the client-side after initial rendering by the
browser.
- Any features that don’t manipulate the DOM on the client side can be easily and quickly tested without
firing up a web browser. This makes tests of features using server-rendered views faster and less flaky.
- Click events, network errors, and loading UI are handled by the browser by default without having to do
anything special.
This approach is appropriate for most common needs, such as rendering dynamic content, managing form
submissions, and other basic user interactions. The main downside to this approach is that you need to manage
how JavaScript interacts with the server-rendered HTML. Depending on the technology you choose, this could
result in some complexity.
You may use Hotwire, which is included in Rails 7. This provides a zero-JavaScript solution for common uses
cases, but leaves you on your own for anything else. Or you might choose React, which requires that some of your
HTML be written in JSX, leaving you two ways to write markup. In the next chapter on page 151, we’ll talk a bit
about how to navigate these trade-offs.
Another perceived downside is performance. The theory goes that full page refreshes are always slower than if
content is fetched with Ajax. It is true that server-rendered HTML sends more bytes over the network than an
Ajax request and it is true that re-rendering the entire page is slower than updating part of the existing DOM.
What is not true is that these differences always matter. Optimizing the performance of an application is a tricky
business. Often the source of poor performance isn’t what you think it might be, and it requires careful analysis to
understand both where the problem lies and what the right solution is.
In my experience, most performance problems are caused by the database. If our page requires executing a
database query, and that query isn’t indexed, no front-end rendering optimization in the world is going to fix what
a single line of SQL can.
All this to say that choosing to avoid server-rendered views because of a performance problem that you don’t
know you have and that you don’t know matters is not a sound basis for making technical architecture decisions.
And, of course, using the JAM Stack to boost performance carries a large carrying cost. Let’s see how that works.
#### 10.2.2 Architecture of the JAM Stack
A JAM Stack app is a bundle of JavaScript that contains markup, code to render that markup, code to fetch data
from a remote server, and code to manage the state driving the dynamic contents of the markup. Sometimes this
code is executed on the server to pre-render the markup for a faster startup time in the browser, but the overall
programming model is centered around managing DOM updates in the browser based on browser events and API
calls, as shown in the figure on the next page.
State management is a significant part of a JAM Stack application, as most technologies provide a programming
model where only the part of the DOM affected by state changes is updated when state does change. Thus, a JAM
Stack application, in addition to having HTML templates for rendering HTML, also has a significant bit of wiring
to make sure markup is connected to the correct state.
There are three benefits to this approach:
- Highly interactive UIs are easier to create by consolidating everything into a single bundle of code.
- If you do not control the back-end APIs, you can build a full-featured app with just front-end technologies.
- If the entire app uses the JAM Stack, you have a single view technology.
Carefully consider your problem space against these benefits. There are _many_ downsides to this approach:
- You must carefully map JSON responses to the input of each front-end component and carefully manage the
state of the app’s front-end. There is no one accepted approach, and common tools like Redux are complex.
Managing state in even small apps can be exceedingly difficult to get right.
- You must either replicate Rails’ form helpers to generate the right markup or abandon them altogether,
which can complicate your controller code when processing form submissions.
- You must provide a custom user experience for fault tolerance and progress, because the default for a JAM
Stack application is to silently fail. If you’ve clicked a link in an app and nothing happens, this is why.
- You cannot adequately test this app without heavy use of browser based tests. While you can write unit
tests that simulate the DOM this isn’t the same as testing how the code works when fully integrated.
Browser-based tests are slow and can be flaky, which makes your app’s overall test suite much slower and
flakier than a server-rendered equivalent.
Figure 10.2: JAM Stack Rendering
- If you configure server-side rendering, it becomes harder to write the code, because you must account for it
executing on the server _and_ on a browser.
- JAM Stack apps have more code in the browser, which means more of your app is running in environments
you cannot control or observe.
A JAMStack approach might feel good because it decouples the front-end from the back-end, and we are often
taught that decoupling is good. But Rails is designed to couple key parts of our app together to make common
needs easy to implement.
When working on a Rails app, the developers have control over the entire experience, so the back-end can be built
in concert with the front-end. De-coupling them doesn’t have a strong advantage. It just makes things harder to
build.
That’s not to say you should never use the JAM Stack in your app, but you should use it only when it’s needed,
and only if you are confident that the risks are outweighed by the benefits. This is not common.
#### 10.2.3 Server-Rendered Views by Default, JAM Stack Only When Needed
I have experienced at least four different teams create sustainability problems by using the JAM Stack for features
that did not require it. The strong boundary that was created between front-end and back-end meant that simple
changes required orders of magnitude more work than had they used static HTML or ERB. Even basic copy
changes based on dynamic data would cause a cascade of changes from the API layer to components nested
several layers deep.
If you use Rails server-rendered views by default, you will create a situation in which simple things are simple.
You can still use the JAM Stack in portions of your app when you determine there is a strong need to do so. See
the sidebar “Single Feature JAM Stack Apps at Stitch Fix” on the next page for an example of how this can make
your app successful.
#### Single Feature JAM Stack Apps at Stitch Fix
```
The Stitch Fix warehouse was originally managed by a run-of-the-mill Rails app that we calledSPECTRE. The
warehouse was comprised of different stations and the person working those stations used a custom-built screen in
SPECTREto do their job. For example, one station printed shipping labels, and another located items for a shipment.
Locating items—which we called picking —was by far the most frequent activity in the warehouse. Users would be
given five items at a time to locate. This required at least seven full-page refreshes: one to get started, one for each
item, and one to tell the picker what to do after all five items were picked. The Internet connection in the warehouses
was initially very slow and unreliable, so these page refreshes, driven by server requests, often timed out and caused
pickers to spend too much time picking.
We re-implemented this feature using the hottest front-end framework of 2014: AngularJS. The initial page
load grabbed all the data, and the browser handled all interactivity during the picking process. The only network
connection needed was after picking was complete. The entire picking process could be done without any network
connection at all.
Even though the rest ofSPECTREwas driven by server-rendered views, the picking feature was a JAM Stack app
that solved a real problem for users. While there was friction if you had to switch back and forth while working on
SPECTRE, the result was that easy things were easy, but complex things could be built.
```
All this to say, you will need JavaScript. You might need very small bits of glue code between elements or
full-blown interactive components, but you can’t avoid it entirely. You want it predictable, stable, and small.
### 10.3 Tweak Turbo to Provide a Slightly Better Experience
```
This section’s code is in the folder10-03/of the sample code.
```
In order to effectively manage the behavior of your views, and any JavaScript that is needed, you need a solid
baseline of behavior on which to build. Rails provides this, with one tiny exception: Turbo’s default setting for
showing a progress bar.
Turbo (formerly called Turbolinks) hijacks all clicks and form submissions and replaces them with Ajax calls. It
then replaces the<body>of the page with whatever the<body>is of the returned result. This is ostensibly to make
every page faster, but it often leads to your app feeling broken instead since it will only show a progress bar after
500ms of waiting.
My recommendation is to modify Turbo’s progress timeout.
The reason is that Turbo can make your app feel broken any time a controller fails to respond instantly. A common
rule of thumb in user experience is that if the response to a user’s action takes more than 100ms to happen, the
user will lose the sense of causality between their action and the result. The app will feel broken.
If your controller, along with the network time, takes more than 100ms to respond, and Turbo is enabled, your
app may feel broken, because Turbo prevents the browser from showing any progress UI. Turbo will provide its
own, but only if more than 500ms have elapsed. That’s too long.
Fortunately, we can change the default without much code. Our app is loading Turbo fromapp/javascript/application.js,
but we need access to the returned object in order to make configuration changes. We’ll modify theimport
statement to assign the result to the variableTurbo, which we can then use to callsetProgressBarDelay:
/* app/javascript/application.js */
// Configure your import map in config/importmap.rb. Read mor...
→ **import** { Turbo } **from** "@hotwired/turbo-rails"
→// The default of 500ms is too long and
→// users can lose the causal link between clicking
→// a link and seeing the browser respond
→Turbo.setProgressBarDelay(100)
**import** "controllers"
One thing to note about Turbo is that while the developers have gone to great lengths to make sure it plays well
with the browser and any other JavaScript you may have, it _is_ a layer of indirection between user actions in the
browser and your code. Make sure you understand how any JavaScript that might also hook into the browser
works. In particular, the use ofDOMContentLoadedcould cause unpredictable behavior, since it won’t be triggered
every time a link is clicked (you must use theturbo:loadevent, instead).
### Up Next
These small changes will give you a more predictable base on which to build, along with a more reasonable
default user experience.
Of course, there’s almost no way to avoid JavaScript entirely and so this leads to our next topic, which is how to
manage the JavaScript you _do_ have to write. You want to use whatever JavaScript you actually need to make your
app succeed, but you should carefully manage it, since it is the least stable part of your app.
## 11 Carefully Manage the JavaScript You Need
Despite the above-average carrying cost of JavaScript in your app, you cannot avoid it, and many features of your
app will require some JavaScript. You don’t want to stubbornly avoid JavaScript at all costs, but you _do_ want
carefully manage how you use it.
This chapter will discuss three techniques to maintain control over your JavaScript, but keep in mind these are
scratching the surface. The more JavaScript you have, the more closely you’ll need to manage it—the same as any
code in your app.
The three techniques we’ll discuss here are:
- Embrace plain JavaScript for basic interactions wherever you can.
- Use at most one framework like Hotwire or React, and choose that framework for sustainability.
- Ensure your system tests break when your JavaScript is broken.
Let’s jump into the first one, which is to embrace the power of plain, framework-free JavaScript.
### 11.1 Embrace Plain JavaScript for Basic Interactions
```
This section’s code is in the folder11-01/of the sample code.
```
The more dependencies your app has, the harder it’s going to be to maintain. Fixing bugs, addressing security
issues, and leveraging new features all require updating and managing your dependencies. Further, as we
discussed way back in “Consistency” on page 12, the fewer ways of doing something in the app, the better.
Your app likely doesn’t need many interactive features, especially when it’s young. For any interactivity that you
_do_ need, it can often be simpler to build features that work without JavaScript then add interactivity on top of
that. Modern browsers provide powerful APIs for interacting with your markup, and it can reduce the overall
complexity of your app to use those APIs before reaching for something like React.
Let’s do that in this section. Our existing widget rating system is built in a classic fashion. Although there is no
back-end currently, you might imagine that it will show your rating for any widget where you’ve provided one.
Let’s suppose we want to do that without a page refresh. We want the user to submit a rating and have the page
remove the widget rating form and replace it with a message like “You rated this widget 4”.
Let’s see how to do this with just plain JavaScript. I realize that the Hotwire^1 framework in Rails provides
a zero-code solution to this exact use case. However, if you have not written plain JavaScript in a while, it’s
important to see just how little code is required to do this. The point I’m making in this section is that you can get
quite far without taking on any dependencies.
There are a lot of ways to do it, but the way I’ll show here is one that keeps the number of moving parts to a
minimum. We’ll render all the markup and most of the content we will need for this feature in the ERB file, using
CSS to hide the markup that should not be shown.
When the user clicks on a rating, we’ll run some JavaScript to modify the CSS on various parts of the markup to
remove the form and show the rating, while dynamically inserting that rating into the DOM in the right place.
First, we’ll add a new bit of markup that says “Thanks for rating this”. Semantically, this should be inside a<p>
tag. Since the rating depends on what button the user clicked on, we’ll place a<span>to hold the value, and we’ll
use JavaScript to set it dynamically. The entire thing will need to be surrounded in a<div>.
We’ll then usedata-attributes on each bit of markup so that we can locate them using JavaScript. This is
preferable to using special classes becausedata-elements aren’t commonly used for styling, whereas classes are
almost always used for styling.
<%# app/components/widget_rating_component.html.erb %>
**<section>**
→ **<div class** ="dn" **data-rating-present>**
→ **<p>** Thanks for rating this a
→ **<span data-rating-label></span>**
→ **</p>**
→ **</div>
<div class** ="clear-floats" **>
<h3 style** ="float: left; margin: 0; padding-right: 1rem;" **>**
Rate This Widget:
The existing<div>will get hidden when the user clicks a rating, so that needs adata-attribute as well. We’ll also
replace our hand-madeclear-floatsclass with Tachyons’cfclass that does the same thing.
<%# app/components/widget_rating_component.html.erb %>
**<span data-rating-label></span>
</p>
</div>**
→ **<div class** ="cf" **data-no-rating-present>
<h3 style** ="float: left; margin: 0; padding-right: 1rem;" **>**
(^1) https://hotwired.dev
```
Rate This Widget:
</h3>
```
Next, we’ll make two changes to the button_to call. The first is to make it a remote Ajax call to
WidgetRatingsController. That controller currently does a redirect, but we’ll remove that so that it re-
sponds with an HTTP 204. This will allow us to trigger back-end logic without a page refresh. The second change
is to add adata-attribute to the button so that we can attach a click handler to it.
First, we’ll addremote: trueanddata-ratingto thebutton_tocall:
# app/components/widget_rating_component.html.erb
**<** li style **=** "float: left" **>
<%=** button_to rating,
widget_ratings_path,
→ **remote:** true,
→ **data: {**
→ **rating:** true
→ **}** ,
**params: {
widget_id:** widget.id,
**rating:** rating
Then, we’ll remove the redirect in the controller. I like to add the comment# default renderwhenever there is
branching logic in a controller, since the absence of code in a Rails controller _does_ imply a particular behavior is
going to occur.
# app/controllers/widget_ratings_controller.rb
**def** create
**if** params **[:widget_id]**
# find the widget
# update its rating
×# redirect_to widget_path(params[:widget_id]),
×# notice: "Thanks for rating!"
→ # default render
**else**
head **:bad_request
end**
```
Since there is no template for this controller action, the default behavior is to return an HTTP 204, which is
what we want. If we wanted to render a view or take an action for a non-remote call, we can userespond_toto
differentiate.
Next, we need to write the actual JavaScript. We’ll put that inapp/javascript/widget_ratings/index.jswhich
we’ll later reference via the mainapplication.jsfile. The way this will work is that we’ll create a function named
updateUIWithRatingthat will locate all the DOM elements withdata-rating-presentand show them by adding
Tachyons’dbclass, which stands fordisplay: block(thus showing them).
We’ll then locate all elements withdata-no-rating-presentand adddn, which stands fordisplay: none(thus
hiding them). Finally, we’ll locate the<span>withdata-rating-labeland set its inner text to the chosen rating,
which will make the user see a sentence like “You rated this widget 4”.
We’ll usedocument.querySelectorAll, which allows locating elements via a CSS selector and returning an array of
matching elements. Even though we only have one element for each selector we’re going to use, it’s better to have
our JavaScript not be coupled to that. Instead, it’ll handle any number of those selectors.updateUIWithRating
will accept thedocumentand theratingas parameters.
```
```
/* app/javascript/widget_ratings/index.js */
```
```
const updateUIWithRating = (document, rating) => {
document.querySelectorAll("[data-rating-present]").
forEach( (element) => {
element.classList.add("db")
element.classList.remove("dn")
})
```
```
document.querySelectorAll("[data-no-rating-present]").
forEach( (element) => {
element.classList.add("dn")
})
```
```
document.querySelectorAll("[data-rating-label]").
forEach( (element) => {
element.innerText =`${rating}`
})
}
```
Note that the way we show and hide elements is to use CSS. Because we are using functional CSS as discussed in
“Functional CSS” on page 129, we can use the same techniques here that we’d use in our markup, which a is nice
bit of consistency when it comes to styling the visual appearance of our app.
Now, we want this function to be run whenever a widget rating button is clicked. To do that, we need to create an
onclickevent handler for each button. To do _that_ we have to wait until the DOM has been loaded so the buttons
are there for us to hook into. Since we are using Turbo (as it is configured by default), the way to wait on the
DOM to be loaded is to wait for the eventturbo:load, which is the Turbo equivalent ofDOMContentLoaded.
We’ll wrap all of this into a function namedstart, and we’ll export that function so it can be called in
app/javascript/application.js. Note thatstartwill require thewindowas a parameter. Passing in global
objects likewindowanddocumentkeeps our functions self-contained if we should need to unit test them.
/* app/javascript/widget_ratings/index.js */
element.innerText =`${rating}`
})
}
→ **const** start = (window) **=>** {
→ **const** document = window.document
→ window.addEventListener("turbo:load", () **=>** {
→ document.querySelectorAll(
→ "button[data-rating]"
→ ).forEach( (element) **=>** {
→ element.onclick = (event) **=>** {
→ **const** rating = element.innerText
→ updateUIWithRating(document, rating)
→ }
→ })
→ })
→}
→ **export const** WidgetRatings = {
→ start: start,
→}
The reason we exportedstartis so that the code inwidget_rating/index.jscan be separated from its actual
use. This means that we need to start it up elsewhere. That location isapp/javascript/application.js, which
should be changed like so:
/* app/javascript/application.js */
```
Turbo.setProgressBarDelay(100)
```
**import** "controllers"
→ **import** { WidgetRatings } **from** "widget_ratings"
→WidgetRatings.start(window)
Prior to Rails 7, this code would’ve relied on Webpacker and Webpack and been slightly different. Rails 7
introduced the concept of _import maps_ , provided by importmap-rails^2. Import maps are a modern mechanism for
managing JavaScript without a pre-compiler like Webpack.
Rails won’t automatically detect any new JavaScript file we create, so we must add it to the configuration in
config/importmap.rb. Even though we only have one file inapp/javascript/widget_ratings, we’ll still use
pin_all_fromso that any newly-created files there in the future will be picked up without needing a configuration
change.
# config/importmap.rb
pin "@hotwired/stimulus", **to:** "stimulus.min.js", **preload:** tru...
pin "@hotwired/stimulus-loading", **to:** "stimulus-loading.js",...
pin_all_from "app/javascript/controllers", **under:** "controller...
→pin_all_from "app **/** javascript **/** widget_ratings",
→ under: "widget_ratings"
What this does is to tell the front-end that whenever code likeimport "widget_ratings"is processed, to get
the requested code fromapp/javascript/widget_ratings. The browser does all this for you. Rails will reload
config/import.rbin theory, so if you are running your server, you should not have to restart it.
With this in place, here is the order of events on our page:
1. The page is loaded when someone navigates to the widget show page.
2.start(window)is called in our new JavaScript code. This registers aturbo:loadhandler.
3. Theturbo:loadevent is fired.
4. Our handler is called, which attaches anonclickevent to all five buttons we created withbutton_to.
5. The user clicks a rating
6. Because we did not callpreventDefault, the button will submit the remote form back to the server.
7.This will trigger thecreatemethod of theWidgetRatingsController. Although this doesn’t do anything
now, you could imagine that it would update a rating in the database or something like that.
8.updateUIWithRatingis called with the given rating. This hides the rating buttons and shows the “Thanks
for rating” message, along with the user’s specific rating.
Note that we aren’t waiting for the results of our AJAX call. This may not be the right decision, depending on
what the backend logic is. If there is a chance the user could make a mistake, we want to wait for the back-end
to let us know if the request was successful and update the UI accordingly. In this case, we assume any invalid
request is the result of someone circumventing our UI and so we won’t explicitly handle it.
Putting it all together, you should be able to navigate the widget show page, click a rating and see all this working
as in the screenshot on the next page.
(^2) https://github.com/rails/importmap-rails
```
Figure 11.1: Ajax-based widget rating
```
This might have seemed like a lot of steps, but consider how little code we had to change. We needed to add
some new markup, but the existing markup hardly changed at all. We had to write around 40 lines of JavaScript,
and we didn’t have to make any significant changes to the back-end.
This change feels commensurate with the complexity of the feature we added. If we used something like React,
we would’ve had to rewrite the entire UI first, and then add the feature.
As I said, there are many ways to do this, but the main idea to take away is just how much you can actually do
with plain JavaScript. For interactions like showing or hiding DOM elements, plain JavaScript might be a good
trade-off, because we didn’t need any new dependencies to do this.
As our app ages and grows, this code will remain solid and reliable. As Rails changes front-end approaches,
something it has historically done frequently, plain JavaScript will continue to work.
That said, you may need more. When the interactivity you require exceeds basic Ajax calls and the showing
or hiding of markup, a plain JavaScript approach could turn into a hand-rolled framework. In those cases, an
off-the-shelf framework might be preferable. Adding any dependency to your app introduces a carrying cost, and
a JavaScript framework is one of the largest, so you must choose carefully.
### 11.2 Carefully Choose One Framework When You Need It
While any dependency added to your app should be carefully considered, the front-end framework should be
considered _most_ carefully. As discussed in the previous chapter on page 141, JavaScript is a more serious liability,
and a large framework like React or Ember exacerbates this problem. This means two things: first, you should
try to have exactly one front-end framework in your app to minimize the carrying cost and second, you should
carefully choose the framework for sustainability.
If you have no other constraints, Rails’ default of Hotwire is a good choice, but React is also something to consider.
Let’s take this section to see why and how it relates to sustainability.
As your app evolves and as time goes by, versions of your dependencies—including Rails—will change. Bugs will
be fixed, features will be added, and security vulnerabilities will be addressed. Your app will also gain features,
change developers, change teams, and generally evolve. The more you can rely on your dependencies to weather
these changes, the better.
Thus, when you make decisions for sustainability, you want to favor dependencies that are stable, widely
understood, well-supported, and that easily work with Rails. These are potentially more important than features
and _far_ more important than personal preference.
I would urge you to make a decision aid for each framework you want to consider. Write down these criteria,
along with any other that you feel are important. Here are three different versions for React, Angular, and Hotwire.
I’ve included two subjective criteria: “Org Support”, how well the overall organization supports the framework,
and “Team Appetite”, how excited the team would be to use the framework. We’ll start with one for React.
```
Table 11.1: Decision Aid for React as a Front-end Framework
Criteria Rating Notes
Mind-share High Based on State of JS Survey
Stability High Good backwards compatibility
Rails Support Medium js-bundling or react-rails
Org Support No guidance
Team Appetite High
```
Here’s how I might fill this out for Angular.
```
Table 11.2: Decision Aid for Angular as a Front-end Framework
```
```
Criteria Rating Notes
Mind-share Medium Trends enterprisey
Stability Low Frequent breaking changes
Rails Support Medium js-bundling
Org Support General bad experiences
Team Appetite Low
```
And finally, here’s one for Hotwire.
```
Table 11.3: Decision Aid for Hotwire as a Front-end Framework
Criteria Rating Notes
Mind-share Low Subset of Rails developers at best
Stability Medium Used by 37Signals in production
Rails Support High Developed by 37Signals
Org Support No guidance
Team Appetite Medium
```
```
The point is to make an informed decision as objectively as you can. Mind-share, stability, and Rails support
heavily contribute to the sustainability of your app. Do not ignore them. You’ll also note that I haven’t put features
or any other technical considerations. These are extremely hard to quantify and even harder to value. Does the
fact that Ember renders slightly faster than React actually matter? That’s hard to answer.
```
If you have clearly defined technical requirements, _do_ add them to your decision aid, but make sure you know
how to measure them and how to value them. At a high level, these frameworks all tend to be equally capable
of whatever it is you need to do, and none are likely to have a fatal flaw that will require excising from your
codebase later.
Right now, all things being equal, React is likely the safest, best, most sustainable choice, but I don’t think Hotwire
is a bad choice either, mostly because it’s going to be more and more integrated with Rails since the core team
(and 37Signals, in particular) use and maintain it. That being said, 37Signals insist they will change things
whenever and however they like, so if Hotwire stops working for them, it may be dropped from Rails in a future
release or undergo radical breaking changes.
```
Whatever you do, don’t add multiple frameworks. This will create a sustainability problem as your app matures.
You will have more libraries to deal with keeping updated and will be more affected by the instability in the
JavaScript ecosystem.
```
```
If your chosen framework isn’t working out as well as you hoped, I recommend you scope a project to migrate to
a new framework so that you can quickly transition and avoid the carrying cost of multiple frameworks.
```
```
The last technique to discuss is testing.
```
### 11.3 Ensure System Tests Fail When JavaScript is Broken
```
This section’s code is in the folder11-03/of the sample code.
```
```
In the next chapter on page 161, we’ll talk about the deeper value and purpose of testing, but to briefly preview it,
testing is a way to mitigate the risk of shipping broken code to production.
Because of JavaScript’s unique attributes as discussed in the previous chapter on page 141, it may seem that there
is greater value in unit testing JavaScript that is already covered by system tests if that JavaScript is complex.
```
```
That said, unit testing JavaScript is not easy. There is no all-in-one testing framework and setting up JavaScript-
based tests requires a lot of decisions, plumbing, and dependencies. It also requires having Node installed in your
development environment, which results in testing your JavaScript in an environment that is not the same as that
where it runs: Node is a server-side platform, whereas your JavaScript runs in a web browser.
```
To make matters worse, common tools for JS testing fall out of favor quickly, as you can see in the State of JS
Survey results^3.
The previous edition of this book had us set up Jest^4 and write a unit test of the code we wrote in the last
section. I no longer believe it is worth doing based on the code we have and the approach we have taken thus far.
This chapter of the book broke every time there was a change in the underlying tooling, and I think there is a
sustainability lesson here.
Your system tests (discussed in the next chapter) should fail if your JavaScript is broken. While a unit test might
be a faster way to know this, the carrying cost of Node, NPM, and the modules required for even a basic testing
toolchain may be too high for the value they bring, especially if you are really minimizing JavaScript. In particular,
if you are using Hotwire and related technologies, you may not even _have_ that much JavaScript to test.
What I would recommend instead is to include unit testing as part of your technology decision aide. For example,
Hotwire provides zero-code solutions to common use cases, thus no unit testing would be required, since there is
no code. React _may_ require unit tests, and setting this up is relatively supported by the community. Arbitrary
testing of plain JavaScript is less-supported, though possible.
My recommendation is to add JavaScript unit testing if you truly believe the value it brings exceeds the carrying
cost of the toolchain required, and to choose your toolchain based on community support and stability. I would
also strongly recommend that you not rely entirely on Node-based unit tests to ensure the proper functioning of
your JavaScript, because your JavaScript will be run in a browser.
### Up Next
As mentioned above, your view should be tested and those tests should fail if the JavaScript is broken. That’s
what we’ll cover next.
(^3) https://2020.stateofjs.com/en-US/technologies/testing/
(^4) https://jestjs.io
## 12 Testing the View
We wrote tests for our helpers way back in “Helpers Should Be Tested and Thus Testable” on page 117, but
generally avoided talking about an overarching testing strategy. That’s what we’re going to talk about here.
Testing can be a boon to sustainability, but it can also work against you. If tests are too brittle, duplicative, slow,
or focused on the wrong things, the test suite will drag the team down.
This chapter will introduce a basic testing strategy and then discuss some useful tactics for implementing that
strategy around the view code we’ve been writing. This strategy and its tactics are based on certain values as it
relates to software quality, so let’s state those first.
### 12.1 Understand the Value and Cost of Tests
Kent Beck, who, among other things, is a major proponent of Test-Driven Design, said^1 :
```
I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level
of confidence.
```
This is a great clarifying statement about the purpose of tests, in particular given who said it.
Tests give confidence that our code is working. We can get that confidence in other ways, such as manually
checking the code, pair programming, code reviews, or monitoring the app in production. These mechanisms
have different costs and different levels of effectiveness.
Another way to put this is that tests are a tool to mitigate risk: the risk of code failing in production. They have a
cost, primarily a carrying cost. And that cost has to justify the value the tests bring, otherwise we are not using
our time and resources wisely, and our app will become less and less sustainable.
To make sure tests mitigate the right risks and provide the maximum value, they must be user-focused.
A user-focused test is one that exercises a part of the software the way a user would use it. In a Rails app, that
means a system test.
System tests are expensive. They have a high carrying cost, but if we approach them in the right way, they can
bring immense value. The key is to avoid over-testing.
The strategy I recommend is to have a system test for every major user flow, use unit tests to get coverage of
anything else that is important, and closely monitor production for failures.
(^1) https://stackoverflow.com/questions/153234/how-deep-are-your-unit-tests/153565
A “major” flow is one that is critical to the problem the app exists to solve. It’s something that, if broken, would
severely impact the efficacy of the app. Authentication is a great example. An FAQ page would not be a good
example (in most cases).
The point is, you have to decide what is and is not a major user flow. Most of your app’s features ought to be
major flows, because hopefully you are only building features that matter. But however many it is, they should
have system tests.
To keep system tests manageable, we’ll talk through the following tactics:
- Do not use a real browser for features that don’t require JavaScript.
- Test against markup and content by default.
- If markup becomes unstable, usedata-testidto locate elements needed for a test.
- Cultivate diagnostic tools to debug test failures.
- Fake out the back-end to get the test of the front-end passing, then use that test to drive the back-end
implementation.
- Use a real browser for any feature that _does_ require JavaScript.
Let’s start with the basics.
### 12.2 Use:rack_testfor non-JavaScript User Flows
```
This section’s code is in the folder12-02/of the sample code.
```
Because we’re only using JavaScript where we need it, and because we are favoring Rails’ server-rendered views,
most of our features should work without requiring JavaScript^2. One of the benefits to this approach is that we
can test these features without using a real web browser.
Rails system tests use Chrome by default. We’ll set that up later, but for now, let’s codify our architectural decisions
around server-rendered views by making the default test driver for system tests the:rack_testdriver.
We can do this intest/application_system_test_case.rb.
# test/application_system_test_case.rb
```
require "test_helper"
```
**class** ApplicationSystemTestCase **<** ActionDispatch **::** SystemTestC...
→ driven_by **:rack_test
end**
There is currently an issue with Rails 7 that causes system tests to run in the wrong environment when using the
dotenv-rails gem, which we are using. To work around that, we’re going to add some code toRakefile, like so:
(^2) This doesn’t mean there isn’t any JavaScript for these features, just that the features can be exercised without JavaScript executing at all.
# Rakefile
```
# Add your own tasks in files placed in lib/tasks ending in ....
# for example lib/tasks/capistrano.rake, and they will automa...
```
→# This works around an issue with Rails 7 and dotenv-rails where
→# tests are run in the wrong Rails environment
→ **if** Rake.application.top_level_tasks.grep(
→ /ˆ(default$|test(:|$))/
→ ).any?
→ ENV **[** "RAILS_ENV" **] ||= if** Rake.application.options.show_tasks
→ "development"
→ **else**
→ "test"
→ **end**
→ **end**
require_relative "config/application"
```
Rails.application.load_tasks
```
We have a major user flow where the user sees a list of widgets, clicks one, and sees more information
about that widget. It does not require JavaScript, so we can write a test for it now. We’ll do that in
test/system/view_widget_test.rb:
# test/system/view_widget_test.rb
require "application_system_test_case"
**class** ViewWidgetTest **<** ApplicationSystemTestCase
test "we can see a list of widgets and view one" **do**
# test goes here
**end
end**
What we want to check here is that:
1. When we navigate to the widgets path, we see a list of widgets.
2. When we click one of those widgets, we are taken to that widget’s page.
3. That widget’s page shows some basic information about the widget.
This leads to some open questions:
- What does a list of widgets actually mean?
- What is being clicked on when we want to view a particular widget’s page?
- What constitutes “basic information” about a widget?
Answering questions like these requires understanding why the feature exists and is important. You should _not_
assert every piece of content and markup on the page. Instead, find the minimum indicators that the feature is
providing the value it’s supposed to provide.
For this widget flow, let’s assume that if we see two widgets on the index page and the show page shows the
chosen widget’s name and formatted ID, we are confident the flow is working.
Because our only indicators of this are the presence of content and markup, we _will_ have to assert against that, so
let’s do the simplest thing we can, which is to assert against the markup and content that’s there.
### 12.3 Test Against Default Markup and Content Initially
```
This section’s code is in the folder12-03/of the sample code.
```
We’ll use the DOM to locate content that allows us to confidently assert the page is working. As a first pass, we’ll
use the DOM as it is. That means we’ll expect two<li>s in a<ul>that have our widget names in them. We’ll click
an<a>inside one, and expect to see the widget’s name in an<h1>with its formatted ID in an<h2>.
We’ll assert on regular expressions instead of exact content, so that trivial changes in copy won’t break our test.
Also note that we’re using case-insensitive regular expressions (they end with/i) to further insulate our tests
from trivial content changes.
# test/system/view_widget_test.rb
**class** ViewWidgetTest **<** ApplicationSystemTestCase
test "we can see a list of widgets and view one" **do**
→ visit widgets_path
→ widget_name **=** "stembolt"
→ widget_name_regexp **=** /#{widget_name}/i
→ assert_selector "ul li", **text:** /flux capacitor/i
→ assert_selector "ul li", **text:** widget_name_regexp
→ find("ul li", **text:** widget_name_regexp).find("a").click
→ # remember, 1234 is formatted as 12.34
→ formatted_widget_id_regexp **=** /12\.34/
##### →
→ assert_selector "h1", **text:** widget_name_regexp
→ assert_selector "h2", **text:** formatted_widget_id_regexp
**end
end**
This test is hopefully easy to understand because it maps clearly to the existing page’s markup and asserts based
on the content we expect to be there.
Let’s run this test:
> bin/rails test test/system/view_widget_test.rb || echo \
Test Failed
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 43669
# Running:
F
Failure:
ViewWidgetTest#test_we_can_see_a_list_of_widgets_and_view_on...
expected to find visible css "h1" with text /stembolt/i but...
bin/rails test test/system/view_widget_test.rb:4
Finished in 0.605165s, 1.6524 runs/s, 4.9573 assertions/s.
1 runs, 3 assertions, 1 failures, 0 errors, 0 skips
Test Failed
The error message is not very helpful. It tells us what assertion failed, but it doesn’t tell us why. To figure this out
often requires some trial and error.
A common tactic is to add something likeputs page.htmlright before the failing assertion, but let’s make a better
version of that concept that we can use as a surgical diagnostic tool.
### 12.4 Cultivate Explicit Diagnostic Tools to Debug Test Failures
```
This section’s code is in the folder12-04/of the sample code.
```
A big part of the carrying cost of system tests is the time it takes to diagnose why they are failing when we don’t
believe the feature being tested is actually broken. The assertions available to Rails provide only rudimentary
assistance. Your team will eventually learn to useputs page.htmlas a diagnostic tool, but let’s take time now to
make one that works a bit better.
Let’s wrapputs page.htmlin a method calledwith_clues.with_clueswill take a block of code and, if there is
any exception, produce some diagnostic information (currently the page’s HTML) then re-raise the exception.
This will be a foothold for adding more useful diagnostic information later.
Let’s put this in a separate file and module, then include that intoApplicationSystemTestCase. As we build up a
library of useful diagnostic tools, we don’t want ourtest/application_system_test_case.rbfile getting out of
control.
We’ll put this intest/support/with_clues.rb:
# test/support/with_clues.rb
**module** TestSupport
**module** WithClues
# Wrap any assertion with this method to get more
# useful context and diagnostics when a test is
# unexpectedly failing
**def** with_clues( **&** block)
block**.** ()
**rescue** Exception **=>** ex
puts "[ with_clues ] Test failed: #{ex.message}"
puts "[ with_clues ] HTML {"
puts
puts page.html
puts
puts "[ with_clues ] } END HTML"
raise ex
**end
end
end**
Now, we’ll include this module intoApplicationSystemTestCaseso that all of our tests have access to the method.
We’ll need torequirethe file first:
# test/application_system_test_case.rb
require "test_helper"
→require "support/with_clues"
```
class ApplicationSystemTestCase < ActionDispatch :: SystemTestC...
driven_by :rack_test
```
Now we can use the module:
# test/application_system_test_case.rb
```
require "support/with_clues"
```
**class** ApplicationSystemTestCase **<** ActionDispatch **::** SystemTestC...
→ include TestSupport **::** WithClues
driven_by **:rack_test
end**
Note that we’ve prepended messages from this method with[ with_clues ]so it’s clear what is generating these
messages. There’s nothing more difficult than debugging code that produces output whose source you cannot
identify.
If we wrap the assertion like so:
# test/system/view_widget_test.rb
```
# remember, 1234 is formatted as 12.34
formatted_widget_id_regexp = /12\.34/
```
→ with_clues **{** assert_selector "h1", **text:** widget_name_regexp **}**
assert_selector "h2", **text:** formatted_widget_id_regexp
**end
end**
When we run the test, we’ll see the HTML of the page:
> bin/rails test test/system/view_widget_test.rb || echo \
Test Failed
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 2051
# Running:
[ with_clues ] Test failed: expected to find visible css "h1...
[ with_clues ] HTML {
<!DOCTYPE html>
<html>
<head>
```
<title>Widgets</title>
<meta name="viewport" content="width=device-width,initia...
```
<link rel="stylesheet" href="/assets/application-45c6610...
<script type="importmap" data-turbo-track="reload">{
"imports": {
"application": "/assets/application-859894971255110068d2...
"@hotwired/turbo-rails": "/assets/turbo.min-dfd93b3092d1...
"@hotwired/stimulus": "/assets/stimulus.min-dd364f16ec95...
"@hotwired/stimulus-loading": "/assets/stimulus-loading-...
"controllers/application": "/assets/controllers/applicat...
"controllers/hello_controller": "/assets/controllers/hel...
"controllers": "/assets/controllers/index-2db729dddcc5b9...
"widget_ratings": "/assets/widget_ratings/index-b6eb9ad1...
}
}</script>
<link rel="modulepreload" href="/assets/application-85989497...
<link rel="modulepreload" href="/assets/turbo.min-dfd93b3092...
<link rel="modulepreload" href="/assets/stimulus.min-dd364f1...
<link rel="modulepreload" href="/assets/stimulus-loading-357...
<script src="/assets/es-module-shims.min-4ca9b3dd5e434131e3b...
<script type="module">import "application"</script>
</head>
<body>
<h1>Widget 1</h1>
<h2>ID #<span style="font-family: monospace">0000001</span><...
<section>
<div class="dn" data-rating-present>
<p>Thanks for rating this a
<span data-rating-label></span>
</p>
</div>
<div class="cf" data-no-rating-present>
<h3 style="float: left; margin: 0; padding-right: 1rem;"...
Rate This Widget:
</h3>
<ol style="list-style: none; padding: 0; margin: 0">
<li style="float: left">
<form class="button_to" method="post" action="/wid...
</li>
<li style="float: left">
<form class="button_to" method="post" action="/wid...
</li>
<li style="float: left">
<form class="button_to" method="post" action="/wid...
</li>
<li style="float: left">
<form class="button_to" method="post" action="/wid...
</li>
<li style="float: left">
<form class="button_to" method="post" action="/wid...
</li>
</ol>
</div>
</section>
</body>
</html>
[ with_clues ] } END HTML
F
Failure:
ViewWidgetTest#test_we_can_see_a_list_of_widgets_and_view_on...
expected to find visible css "h1" with text /stembolt/i but...
bin/rails test test/system/view_widget_test.rb:4
Finished in 0.261213s, 3.8283 runs/s, 11.4849 assertions/s.
1 runs, 3 assertions, 1 failures, 0 errors, 0 skips
Test Failed
We can see that the problem is that our faked-out data isn’t consistent. The fake widgets in the index view are not
the same as those in the show view. We’ll fix that in a minute.
Note thatwith_cluesis a form of executable documentation.with_cluesis the answer to “How do I figure out
why my system test failed?”. As your team learns more about how to diagnose these problems, they can enhance
with_cluesfor everyone on the team, including future team members. This reduces the carrying cost of these
tests.
While this implementation is perfectly fine, it’s really only a demonstration of the concept of creating a diagnostic
tool. If you’d like to usewith_cluesin your app, you can use the gem with_clues^3 that was extracted from
codebases where this concept was developed.
OK, to fix our test, we should make our faked-out back-end more consistent.
(^3) https://github.com/sustainable-rails/with_clues
### 12.5 Fake The Back-end To Get System Tests Passing
```
This section’s code is in the folder12-05/of the sample code.
```
System tests are hard to write in a pure test-driven style. You often need to start with a view that actually renders
the way it’s intended, and then write your test to assert behavior based on that.
If you are _also_ trying to make the back-end work at the same time, it can be difficult to get everything functioning
at once. It’s often easier to take it one step at a time, and since we are working outside in, that means faking the
back-end so we can get the view working.
Once you have the view working, you don’t actually need a real back-end to write your system test. If you write
your system test against a fake back-end, you can then drive your back-end work with that system test. This
leaves you where you want to be: an end-to-end test of the actual functionality. It’s just easier to get there by
starting off with a fake back-end.
Let’s do that now. We need the hard-coded Stembolt to have an ID of 1234, and we need our show page to detect
item 1234 and use the name “Stembolt” instead of “Widget 1234”. We can do this inWidgetsController:
# app/controllers/widgets_controller.rb
**end
def** index
@widgets **= [**
→ OpenStruct.new( **id:** 1234, **name:** "Stembolt"),
OpenStruct.new( **id:** 2, **name:** "Flux Capacitor"),
**]
end**
Next, we need theshowmethod to use the name “Stembolt” if the id is 1234:
We’ll create a variable calledwidget_name:
# app/controllers/widgets_controller.rb
**country:** "UK"
)
)
→ widget_name **= if** params **[:id]** .to_i **==** 1234
→ "Stembolt"
→ **else**
→ "Widget #{params **[:id]** }"
→ **end**
@widget **=** OpenStruct.new( **id:** params **[:id]** ,
```
manufacturer_id: manufacturer.id...
manufacturer: manufacturer,
```
And we’ll use that for thename:value in ourOpenStruct:
# app/controllers/widgets_controller.rb
@widget **=** OpenStruct.new( **id:** params **[:id]** ,
**manufacturer_id:** manufacturer.id...
**manufacturer:** manufacturer,
→ **name:** widget_name)
**def** @widget.widget_id
**if** self.id.to_s.length **<** 3
self.id.to_s
Now that our faked-out back-end is more consistent with itself, our test should pass:
> bin/rails test test/system/view_widget_test.rb
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 2421
# Running:
.
Finished in 0.260428s, 3.8398 runs/s, 15.3593 assertions/s.
1 runs, 4 assertions, 0 failures, 0 errors, 0 skips
With this test passing, we should _remove_ our diagnostic call towith_clues, because we really don’t want it littered
all over the codebase.
# test/system/view_widget_test.rb
```
# remember, 1234 is formatted as 12.34
formatted_widget_id_regexp = /12\.34/
```
→ assert_selector "h1", **text:** widget_name_regexp
assert_selector "h2", **text:** formatted_widget_id_regexp
**end
end**
What if our view’s markup changes in a way that causes our tests to fail but doesn’t affect the app’s functionality?
For example, we may change the<h1>to an<h2>to address an issue with accessibility. This will cause our test to
fail even though its functionality is still working, since this is not a test what tags were used in the view. This sort
of test failure can create drag on the team and reduce sustainability. Chasing the markup can be an unpleasant
carrying cost, so let’s talk about a simple technique to reduce this cost next.
### 12.6 Usedata-testidAttributes to Combat Brittle Tests
```
This section’s code is in the folder12-06/of the sample code.
```
The tags used in our view are currently semantically correct, and thus our tests can safely rely on that. However,
these semantics might change without affecting the way the page actually works. Suppose our designer wants a
new message, “Widget Information”, on the page as the most important thing on the page.
That means our widget name should no longer be an<h1>, but instead an<h2>.
Here’s the change to update the view:
<%# app/views/widgets/show.html.erb %>
→<h1>Widget Information</h1>
→<h2><%= @widget.name %> **</h2>
<h2>** ID #<%= styled_widget_id(@widget) %> **</h2>**
<% **if** flash[ **:notice** ].present? %>
**<aside>**
This change will break our tests even though the change didn’t affect the functionality of the feature:
> bin/rails test test/system/view_widget_test.rb || echo Test \
Failed
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 40241
# Running:
F
Failure:
ViewWidgetTest#test_we_can_see_a_list_of_widgets_and_view_on...
expected to find visible css "h1" with text /stembolt/i but...
bin/rails test test/system/view_widget_test.rb:4
Finished in 0.268304s, 3.7271 runs/s, 11.1813 assertions/s.
1 runs, 3 assertions, 1 failures, 0 errors, 0 skips
Test Failed
We can see what’s broken, but it’s not clear the best way to fix it. If we change the tag name used in
assert_selectorthat might fix it now, but this same sort of change could break it again, and we’d have to
fix this test again. This can be a serious carrying cost with system tests and we need to nip it in the bud now that
it’s broken the first time.
We’ll assume that the widget name can be in _any_ element that has the attributedata-testidset to"widget-name":
# test/system/view_widget_test.rb
```
# remember, 1234 is formatted as 12.34
formatted_widget_id_regexp = /12\.34/
```
→ assert_selector "[data-testid='widget-name']",
→ **text:** widget_name_regexp
assert_selector "h2", **text:** formatted_widget_id_regexp
**end
end**
Our tests will still fail, but now when we fix them, we can fix them for hopefully the last time. We can add the
data-testidattribute to the<h2>:
<%# app/views/widgets/show.html.erb %>
<h1>Widget Information</h1>
→<h2 data-testid="widget-name"><%= @widget.name %> **</h2>
<h2>** ID #<%= styled_widget_id(@widget) %> **</h2>**
<% **if** flash[ **:notice** ].present? %>
**<aside>**
And _now_ our test should pass:
> bin/rails test test/system/view_widget_test.rb
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 35741
# Running:
Finished in 0.258705s, 3.8654 runs/s, 15.4616 assertions/s.
1 runs, 4 assertions, 0 failures, 0 errors, 0 skips
If this view changes a third time, we just need to make suredata-testid="widget-name"is attached to whatever
DOM node holds the widget’s name.
Why didn’t we do this from the start? Why didn’t we tag everything withdata-testid?
Having to tag every single DOM element withdata-testidis friction. It represents an opportunity cost with each
feature, and it gets harder over time because you must choose names for these tags. It means that even for parts
of the view that never change, we’re creating an extra burden.
So, to balance the desire to test against a semantic DOM, but also not have to constantly change tests, we adopt a
simple convention: the first time a test must be changed to accommodate DOM changes, stop using the DOM for
that assertion and start usingdata-testid. This is much more sustainable than having to constantly change tests
or always usedata-testid.
The reason to usedata-testidand not, for example a more semantic CSS class likeclass="widget-name"is to
make it very clear what this seemingly extraneous markup is for. There can be no doubt thatdata-testidis for a
test. Something likeclass="widget-name"might seem meaningless and perhaps could be accidentally removed
in the future, thus breaking tests.
Up to now, we’ve talked about testing a view rendered entirely server-side with no client-side interactivity. Since
our app will certainly have at least _some_ dynamic client-side behavior, we can’t test that using:rack_test. Our
widget rating feature, for example, can’t be tested without using a real browser. Let’s set that up next.
### 12.7 Test JavaScript Interactions with a Real Browser
```
This section’s code is in the folder12-07/of the sample code.
```
If we have features that require JavaScript, or that won’t work if our JavaScript is broken, we need to test them
in a real browser. While unit tests could help, they won’t give complete confidence because we need to see the
JavaScript executing in context.
Since we’ve set our system tests to use:rack_test, that means they won’t use a real browser and JavaScript won’t
be executed. We need to allow a subset of our tests to actually use a real browser (which is what Rails’ system
tests do by default).
To that end, we’ll create a subclass of our existingApplicationSystemTestCasethat will be for browser-driven
tests. We’ll call itBrowserSystemTestCaseand it will configure Chrome to run the tests^4.
The default configuration for Rails is to use a real Chrome browser that pops up and runs tests while you watch.
This is flaky, annoying, and difficult to get working in a continuous integration environment.
Fortunately, it’s unnecessary as Chrome has a headless mode that works exactly the same way as normal Chrome,
but does everything offline without actually drawing to the screen^5. Practically speaking, Chrome won’t work in
(^4) If you are using RSpec, this is something you’d implement with tags, as that is a more natural fit for RSpec.
(^5) This is what I’ve been using to create the screenshots for this book.
our Docker-based setup anyway.
#### 12.7.1 Setting Up Headless Chrome
It helps to know a little bit about what’s going on between Rails, your OS, and Chrome. Since automated browser
testing became common, the way to make it work has changed a lot over the years, and I expect it to keep
changing. Ultimately, our test suite needs to make a network connection to a running browser in order to tell it to
do things, as well as to make assertions about what’s happening on the page.
Our tests use Capybara which communicates with the browser via Selenium^6. Selenium has a component called
_WebDriver_ that provides an abstraction over many browsers to drive them. Each browser has an adapter and for
Chrome, it’s called Chromedriver^7. Chromedriver manages starting up Chrome to allow Selenium’s WebDriver to
talk to it, which is what allows our Capybara-based tests to test in a real browser.
The way we’ll do this is to register what Selenium calls a _driver_ , which basically represents a web browser. Instead
of calling our driver “chrome”, let’s call itroot_headless_chrometo signify that it’s headless (no graphical UI will
run) _and_ that it’s set up to run as root inside a Docker container.
Here’s how we do that, at the top ofapplication_system_test_case.rb:
# test/application_system_test_case.rb
require "test_helper"
→Capybara.register_driver **:root_headless_chrome do |** app **|**
→ options **=** Selenium **::** WebDriver **::** Options.chrome(
→ **args: [**
→ "headless",
→ "disable-gpu",
→ "no-sandbox",
→ "disable-dev-shm-usage",
→ "whitelisted-ips"
→ **]** ,
→ **logging_prefs: { browser:** "ALL" **}** ,
→ )
→ Capybara **::** Selenium **::** Driver.new(
→ app,
→ **browser: :chrome** ,
→ **options:** options
→ )
→ **end** # register_driver
require "support/with_clues"
```
class ApplicationSystemTestCase < ActionDispatch :: SystemTestC...
```
(^6) https://www.selenium.dev
(^7) https://chromedriver.chromium.org/downloads
I won’t claim to have a deep understanding of what all of those strings given toargsactually do, but suffice it to
say, they were needed to make this work inside a Docker container. Of note is thelogging_prefsoption. We’ll
see a bit later how we can print the messages sent to the JavaScript console, and by default, Selenium only allows
access to errors and warnings. By using{ browser: "ALL" }, we can get all the messages.
There’s one more step I need to do that you may not. Since this book was first published, Apple has started selling
computers with Apple Silicon chips. They are not compatible with the previous generation of Intel chips. Although
macOS provides an emulation layer, when running an Intel-based Docker container on an Apple Silicon-based
computer, not everything is properly emulated. As luck would have it, Chrome requires some of those features, so
basically doesn’t work inside the Docker environment used by this book.
To make matters worse, there is no version of Chrome that runs on Linux running on Apple Silicon, there is only a
macOS version. But, Chromium, upon which Chrome is based, _does_ run on Linux on Apple Silicon. It will need to
be installed and we’ll need to specify the path to the version of chromedriver installed with Chromium. Gotta love
web development.
Installing Chromium highly depends on your OS and, if you are running on macOS or an Intel-based Linux or
Windows computer, you don’t need to do this. The Docker-based setup uses Debian linux and does this to install
Chromium:
apt-get -y install chromium chromium-driver
To tell Selenium where chromedriver is, we’ll need to setdriver_pathonSelenium::WebDriver::Chrome::Service,
which we should do right at the top of ourapplication_system_test_case.rbfile:
# test/application_system_test_case.rb
require "test_helper"
→Selenium **::** WebDriver **::** Chrome **::** Service.driver_path **=**
→ "/usr/bin/chromedriver"
```
Capybara.register_driver :root_headless_chrome do | app |
options = Selenium :: WebDriver :: Options.chrome(
```
Note that the right path for your environment might be different, and if you can use Chrome, that will make this
much easier. This API is not documented by Selenium, so it may change. All part of the fun.
Now, let’s createBrowserSystemTestCasewhich will use the newly-registered driver and extendApplicationSystemTestCase.
Since our existing tests (and any new ones) will include it, we’ll put it intest/application_system_test_case.rb:
# test/application_system_test_case.rb
include TestSupport **::** WithClues
driven_by **:rack_test
end**
→# Base test class for system tests requiring JavaScript
→ **class** BrowserSystemTestCase **<** ApplicationSystemTestCase
→ driven_by **:root_headless_chrome** , **screen_size: [** 1400, 1400 **]**
→ **end**
While we are setting up our system tests, let’s configure Capybara to recognizedata-testid(which we adopted
earlier in this chapter on page 172) whenever we use helpers likeclick_on. This will go intest/test_helper.rb:
# test/test_helper.rb
ENV **[** "RAILS_ENV" **] ||=** "test"
require_relative "../config/environment"
require "rails/test_help"
→Capybara.configure **do |** config **|**
→ # This allows helpers like click_on to locate
→ # any object by data-testid in addition to
→ # built-in selector-like values
→ config.test_id **=** "data-testid"
→ **end**
```
module ActiveSupport
class TestCase
```
Now, let’s write our first browser-driven test case.
#### 12.7.2 Writing a Browser-driven System Test Case
We’ll write a test case of the widget rating feature, which will look very much like the one we wrote before.
To test the widget rating feature, we need to:
1. Navigate to a widget page.
2. Click a rating button.
3. Check that the DOM reflects our rating.
We’ll create this test in test/system/rate_widget_test.rb and it will look for an element matching
[data-rating-present]that has text content including the rating the test will choose.
Even though this content is not initially visible and some of it (the rating itself) isn’t even in the DOM, Capybara
will wait a small amount of time for the matching markup and content to appear:
# test/system/rate_widget_test.rb
require "application_system_test_case"
**class** RateWidgetsTest **<** BrowserSystemTestCase
test "rating a widget shows our rating inline" **do**
visit widget_path(1234)
```
click_on "2"
```
assert_selector "[data-rating-present]",
**text:** /thanks for rating.*2/i
**end
end**
The test should pass:
> bin/rails test test/system/rate_widget_test.rb
Rack::Handler is deprecated and replaced by Rackup::Handler
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 61296
# Running:
Capybara starting Puma...
* Version 6.4.0 , codename: The Eagle of Durango
* Min threads: 0, max threads: 4
* Listening on [http://127.0.0.1:42029](http://127.0.0.1:42029)
.
Finished in 1.361959s, 0.7342 runs/s, 0.7342 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
If you change the test to inherit from ourApplicationSystemTestCase, you will see that the test fails, because
JavaScript is not executed. More importantly, if you break the JavaScript, the test will also fail. I highly recommend
doing this to make sure your test is testing what you think it’s testing. When doing test driven development, you
typically watch for your test to fail in just the right way. For system tests, as discussed, this is not always ideal. So,
it’s good to undo or break your code to make sure the test also breaks.
One other thing to note about why this test works is that Capybara waits for DOM content to become available, to
account for changes in the DOM that JavaScript makes. This means that you must make sure that changes you
make to the DOM can be unambiguously detected.data-testidcan be used to help do this if you can’t otherwise
write markup that can be relied upon.
Now that we’ve added browser-based tests, it may be useful to see the browser’s logs whenever we usewith_clues.
Let’s add that ability as a demonstration of the power of built-in diagnostics we discussed earlier in the chapter on
page 165
#### 12.7.3 Enhancingwith_cluesto Dump Browser Logs
As a diagnostic tool,with_cluesneeds to be pretty fault-tolerant. It’s only ever called when a test fails, so we don’t
want it masking a test failure if it itself fails. Sincewith_clueswill be used for both browser and non-browser
tests, we need to take extra care when trying to print out the browser’s logs. Prepare for someifstatements.
# test/support/with_clues.rb
block**.** ()
**rescue** Exception **=>** ex
puts "[ with_clues ] Test failed: #{ex.message}"
→ **if** page.driver.respond_to?( **:browser** )
→ **if** page.driver.browser.respond_to?( **:logs** )
→ logs **=** page.driver.browser.logs
→ browser_logs **=** logs.get( **:browser** )
→ browser_logs.each **do |** log **|**
→ puts log.message
→ **end**
→ puts "[ with_clues ] } END Browser Logs"
→ **else**
→ puts "[ with_clues ] NO BROWSER LOGS: " **+**
→ "page.driver.browser" **+**
→ "#{page.driver.browser.class} " **+**
→ "does not respond to #logs"
→ **end**
→ **else**
→ puts "[ with_clues ] NO BROWSER LOGS: page.driver " **+**
→ "#{page.driver.class} does not respond to #browser"
→ **end**
→ puts
puts "[ with_clues ] HTML {"
puts
puts page.html
Whew! The reason we didn’t usetryis because we want to give a specific message about why the logs aren’t being
output. If someone adds a third driver later—say Firefox—and it doesn’t provide log access in this way, these
error messages will help future developers figure out how to address it. It certainly helped me when Selenium
changed this API since the last version of this book was published!
Note that if you’d like to usewith_clues, I extracted it to a gem called with_clues^8 that provides all this and a bit
more, including explicit support for RSpec.
### Up Next
This covers system tests and hopefully has provided some high level strategies and lower-level tactics on how to
get the most out of system tests and keep them sustainable. We’ll discuss unit tests later as we delve into the
back-end of Rails. In fact, that’s up next since we have now completed our tour of the view layer.
(^8) https://github.com/sustainable-rails/with_clues
## 13
# Models, Part 1
Although Rails is a Model-View-Controller framework, the model layer in Rails is really a collection of record
definitions. Models in Rails are classes that expose attributes that can be manipulated. Traditionally, those
attributes come from the database and can be saved back, though you can use Active Model to create models that
aren’t based on database tables.
No matter what else goes into a model class, it mostly exists to expose attributes for manipulation, like a record or
struct does in other languages. As outlined in “Business Logic (Does Not Go in Active Records)” on page 49, that’s
all the logic that should go in these classes. I find it helpful to think of Active Records as _models_ of a _database
table_ , which is what they are (and they are darn good at it!).
When you follow that guidance, the classes inapp/models—the model layer—become a library of the data that
powers your app. Some of that data comes directly from a database and some doesn’t, but your model layer can
and should define the _data model_ of your app. This data model represents all the data coming in and going out of
your app. The service layer discussed in the business logic chapter deals in these models.
This chapter will cover the basics around managing that. We’ll talk about Active Records and their unique place in
the Rails Architecture, followed by Active Model, which is a powerful way to create Active Record-like objects that
work great in your view.
There are other aspects of models that we won’t get to until Models, Part 2 on page 227, since we need to learn
about the database and business logic implementation first.
Let’s start with accessing the data in our database using Active Record.
### 13.1 Active Record is for Database Access
```
This section’s code is in the folder13-01/of the sample code.
```
With two lines of code, an Active Record can provide sophisticated access to a database table, in the form of class
methods for querying and a record-like object for data manipulation. It’s one of the core features of Rails that
makes developers feel so productive.
In my experience, when you place business logic elsewhere, you don’t end up needing much code in your Active
Records. Those few lines of code you do need are often enough to enable access to all the data your app needs.
That said, there are times when we need to add code to Active Records. The three main types of code are:
- additional configuration such asbelongs_toorvalidates.
- class methods that query the database and are needed by multiple other classes to reduce duplication.
- instance methods that define core domain attributes whose values can be directly derived from the database,
without the application of business logic.
Let’s dig into each of these a bit, but first we need some Active Records to work with.
#### 13.1.1 Creating Some Example Active Records
First, we’ll create theManufacturermodel. A manufacturer has a name as well as an address which I’ll put directly
on the table for now (this might not be ideal, but we’ll worry about that in a future chapter).
Note that we’re using thetexttype for all of our string-based fields. There is no reason to usevarchartypes in
Postgres. Hubert Lubaczewski wrote a blog post^1 that has a pretty good overview about why.
> bin/rails g model manufacturer name:text address:text \
city:text post_code:text
invoke active_record
create db/migrate/20231204235350_create_manufacture...
create app/models/manufacturer.rb
invoke test_unit
create test/models/manufacturer_test.rb
create test/fixtures/manufacturers.yml
Next, we’ll create theWidgetmodel which has a name, a status, and a reference to a manufacturer:
> bin/rails g model widget name:text status:text \
manufacturer:references
invoke active_record
create db/migrate/20231204235351_create_widgets.rb
create app/models/widget.rb
invoke test_unit
create test/models/widget_test.rb
create test/fixtures/widgets.yml
This should’ve created two classes inapp/modelsas well as the database migrations. Let’s run those now.
> bin/rails db:migrate
== 20231204235350 CreateManufacturers: migrating ===========...
-- create_table(:manufacturers)
-> 0.0071s
== 20231204235350 CreateManufacturers: migrated (0.0071s) ==...
== 20231204235351 CreateWidgets: migrating =================...
-- create_table(:widgets)
-> 0.0051s
== 20231204235351 CreateWidgets: migrated (0.0051s) ========...
(^1) https://www.depesz.com/2010/03/02/charx-vs-varcharx-vs-varchar-vs-text/
With these created, let’s now talk about Active Record’s configuration DSL.
#### 13.1.2 Model the Database With Active Record’s DSL
Because we createdWidgetwithmanufacturer:references, Rails was able to automatically set that relationship
up for us:
> cat app/models/widget.rb
class Widget < ApplicationRecord
belongs_to :manufacturer
end
Rails _could’ve_ modifiedapp/models/manufacturer.rbto create the inverse relationship, but it doesn’t know if the
relationship is a to-many or a to-one, and Rails doesn’t want to presume we actually want it modeled either way.
The question is: should we model it now?
You’re creating Active Records when you create database tables, so this is the time to codify the meaning of the
relationships in your database. By adding a call tohas_many, you are explicitly documenting that this model has a
to-many relationship. If it has a to-one relationship, you would usehas_one. If you do nothing, no one will know
the intention.
The relationship here is a to-many, so we’ll add a call tohas_manytoapp/models/manufacturer.rb:
# app/models/manufacturer.rb
**class** Manufacturer **<** ApplicationRecord
→ has_many **:widgets
end**
On rare occasions you don’t want to allow this relationship to exist in code. If this applies to you, add a code
comment explaining why, so a future developer doesn’t inadvertently add it.
Regarding additional configuration such as validations, I would recommend you add only what configuration you
actually need. Think about it this way: if there is no code path in your app to set the name of a widget, what
purpose could a presence validation on that field possibly serve?
Next, let’s talk about the class methods you might add to your Active Record.
#### 13.1.3 Class Methods Should Be Used to Re-use Common Database Operations
If you look at the class methods that are provided by Rails (excluding the DSL methods previously discussed),
they all center around providing ways of accessing the underlying database. This is a good guide for the types of
methods _you_ should add. But, I would recommend you only add methods to facilitate re-use.
Said another way, add class methods to your Active Record only if both of these criteria hold:
- There is a need for the method’s logic in more than one place.
- The method’s logic is related to database manipulation only and not coupled to business logic.
Let’s see an example. Suppose widgets can have one of three statuses: “fresh”, “approved”, and “archived”. Fresh
widgets require manual approval, so we might write some code like this in a background job that emails our
admin team for each fresh widget they should approve:
**class** SendWidgetApprovalEmailJob
**def** perform
Widget.where( **status:** "fresh").find_each **do |** widget **|**
AdminMailer.widget_approval(widget).deliver_later
**end
end
end**
There’s no particular reason thatwhere(status: "fresh")should be wrapped in a class method onWidget.
Widget’s public API includes the methodwhere, and the purpose ofWidgetis facilitate database access. Thus,
callingwhereis a normal, expected, acceptable thing to do.
That said, we may need this query in more than one place. For example, manufacturers might want to see what
widgets are still fresh, perhaps in aManufacturer::WidgetsController:
**def** index
@widgets **=** Widget.where( **status:** "fresh")
**end**
Using this in two places creates duplication we may want to avoid, particularly because the string"fresh"is a
specific value from the database.
```
class Widget < ApplicationRecord
belongs_to :manufacturer
```
→ **def** self.fresh **=** self.where( **status:** "fresh")
**end**
Now, anyone needing fresh widgets doesn’t have to worry about what string is used in the database to represent
this.
Let’s see a subtly different example where this would not be the right solution.
Suppose our manufacturers need to see a list of recently approved widgets. Suppose that “recently” is defined as
approved in the last 10 days. We might write this code:
```
def index
@widgets = Widget.where( status: "approved").
where( updated_at: 10.days.ago .. )
end
```
The10.days.agois certainly business logic, as is the combination of it with the “approved” status. The concept
of “recently approved” might change, and it might be different depending on context. This should _not_ go into
theWidgetclass. We’ll talk about the ramifications of putting business logic in controllers in “Controllers” on
page 281, but if we need to re-use this logic, the place to put it is in the service layer (which we’ll talk about in
“Business Logic Class Design” on page 215).
```
Lastly, let’s talk about instance methods.
```
#### 13.1.4Instance Methods Should Implement Domain Concepts Derivable Directly from the Database
```
Pretty much all of the same guidance I gave in the previous section applies here. Further, the chapter on business
logic on page 49 outlines why you shouldn’t put instance methods on Active Records that implement that logic.
Outside of business logic, the most common area of trouble for instance methods on an Active Record has to
do with derived data—data whose value is based on the data in the database. Sometimes this derived data is
presentational and use-case specific, but other times it represents a true domain concept that is core to the models’
existence.
As discussed in the many View chapters, including “Don’t Conflate Helpers with Your Domain” on page 108, you
need to be careful about how you model the data inside the application. This requires a solid understanding of
your domain and carefully naming your attributes.
The convention I’m suggesting here is to make instance methods on your Active Records only when you have a
strongly-defined domain concept whose value can be directly derived from the database, without any real logic
applied.
Previously, we created the methodwidget_idto hold the formatted ID of a widget, since that was part of our
domain. Digging deeper, the reasoning for this is that users use this as an identifier. They write it down, paste it
into emails, and discuss it verbally.
Since it’s based on the actual database primary key and not a separate field, this could be a good candidate for
an instance method, though the namewidget_idleaves a lot to be desired. Let’s call ituser_facing_identifier
instead, and we’ll add it to theWidgetclass.
```
```
# app/models/widget.rb
```
```
class Widget < ApplicationRecord
belongs_to :manufacturer
→ def user_facing_identifier
```
→ id_as_string **=** self.id.to_s
→ **if** id_as_string.length **<** 3
→ **return** id_as_string
→ **end**
→ "%{first}.%{last_two}" **% {**
→ **first:** id_as_string **[** 0 **..-** 3 **]** ,
→ **last_two:** id_as_string **[-** 2 **..-** 1 **]**
→ **}**
→ **end
end**
If the _only_ methods we add toWidgetare for clearly defined concepts derivable from data, we can start to
understand our domain better by looking at the Active Records. Instead of seeing a mishmash of command
methods that invoke logic, presentational attributes, and use-case-specific values, we see only the few additional
domain concepts that we need but aren’t in the database.
Note that this method deserves a test, but we’re not going to talk about testing models until “Models, Part 2” on
page 227.
As a contrast touser_facing_identifier, suppose we need to show the first letter of the status on the widget
show page. Suppose further that this is for aesthetic reasons and that the “short form” of a status isn’t part of the
domain—users don’t think about it.
In this case, we should _not_ create a method onWidgetwith this logic. Instead, we should put this logic in the
view, or even make a helper. If our needs were even greater, such as deriving new fields of a widget based on the
application of complex logic, we should make an entirely new class.
For that, we should use Active Model.
### 13.2 Active Model is for Resource Modeling
```
This section’s code is in the folder13-02/of the sample code.
```
Suppose we need to produce a report about the shipping zone to a given user, for each widget, from its
manufacturer. A shipping zone is a rough approximation about how long it takes to mail something from one
place to another, and we can calculate it based on two post codes: the user’s and the manufacturer’s.
We discover that our users refer to this as a “user shipping estimate”, and that a list of widget names, ids, and
zone numbers can be fed into many downstream systems that already exist. Our job is to produce these values.
Because we use resources for our routing, we’ll have a route like/user_shipping_estimatesthat, when given a
destination postal code, will render a list of estimates based on our current database of widgets. Ideally, we could
use objects that behave like Active Records and thus could be used with Rails form and URL helpers.
This is what Active Model does. Let’s create ourUserShippingEstimateresource. We need to include
ActiveModel::Modeland define our attributes withattr_accessor. Just these two bits of code will enable
several handy features of our class. It will give us a constructor that accepts attributes as aHash, and will enable
assign_attributesfor bulk assignment.
# app/models/user_shipping_estimate.rb
**class** UserShippingEstimate
include ActiveModel **::** Model
attr_accessor **:widget_name** ,
**:widget_user_facing_id** ,
**:shipping_zone** ,
**:destination_post_code
end**
To make our model work with some of Rails’ form and URL helpers, we need to tell Rails what fields uniquely
identify an instance of our model. For Active Records, that is simply theidfield, and this is what Active
Model will use by default. But Rails defines the methodto_key(inActiveModel::Conversions, included by
ActiveModel::Model) to allow us to override it.
In our case,user_facing_identifierisn’t sufficient to uniquely identify aUserShippingEstimatebecause
the estimate changes based on thedestination_post_code. By combining bothuser_facing_identifierand
destination_post_code, we _can_ uniquely identify a shipping estimate.
Thus, if we implementto_key, we can use our model in Rails views the same as we could an instance of an Active
Record. We also need to tell Rails that our object actually has an identifier, which requires that we implement
persisted?to return true.to_keyshould return an array of the values comprising the unique identifier, like so:
# app/models/user_shipping_estimate.rb
**:widget_user_facing_id** ,
**:shipping_zone** ,
**:destination_post_code**
→ **def** persisted?
→ true
→ **end**
→ **def** to_key
→ **[** self.widget_user_facing_id,
→ self.destination_post_code **]**
→ **end
end**
That’s it! We now have an Active Record-like object:
> bin/rails c
rails> user_shipping_estimate = UserShippingEstimate.new(
widget_name: "Stembolt",
widget_user_facing_id: "123.45",
shipping_zone: 4,
destination_post_code: "90210")
rails> Rails.application.routes.draw do
rails* resources :user_shipping_estimates
rails> end
rails> app.user_shipping_estimate_path(user_shipping_estimate)
=> "/user_shipping_estimates/123.45-90210"
As a class inapp/models, this adds to our growing library of data definitions. While the class alone can’t completely
explain what a “user shipping estimate” is, the few lines of code in the class tell quite a bit: it has four attributes,
two of which uniquely identify it. This is surprisingly powerful, especially when everything inapp/modelsis
designed the way we’ve described.
It’s important to note that Rails didn’t always provide Active Model. Even today, the model generator produces an
Active Record. This has led to countless libraries that allow you to define record-like objects, wrap Active Records,
or create delegates to simulate a class that works in Rails view helpers. You don’t need them.
The Rails team has gone to great lengths to extract the parts of Active Record that don’t depend on the database
into modules that make up Active Model. This gives us powerful tools to create objects that work the way we
want, work with Rails view helpers, and don’t require a third party library. Today, you should not have much need
for third party gems to create record-like classes.
### Up Next
We can start to see some larger architectural principles taking shape. See the figure “Consistency Across Layers”
below for how we can trace names and concepts from the URLs all the way to the model layer, and that it doesn’t
matter if data is stored in the database or not. This architectural consistency helps greatly with sustainability.
We haven’t finished with models, yet. In particular, we still need to discuss validations, callbacks, and testing.
We’ll get to that, but first we need to learn about structuring our business logic and database design. The database
is next.
Figure 13.1: Consistency Across Layers
## 14 The Database
For most apps, the data in its database is more important than the app itself. If a cosmic entity swooped in
and removed your app’s source code from all of existence, you could likely recreate it, since you’d still have
the underlying data it exists to manage. If that same entity instead removed your _data_... this would be an
extinction-level event for your app.
What this thought experiment tells me is that the way data is managed and stored requires a bit more care and
rigor than is typically applied to code. This “care and rigor” amounts to spending more time modeling the data
and using everything available in your database to keep the data correct, precise, and consistent.
This contradicts Rails’ overly simplistic view of the database. By only following Rails’ defaults, and designing your
database when you write migrations, you will eventually have inconsistent or incorrect data, and likely a fair bit
of unnecessary complexity in your code to deal with it.
That said, there are some realities about using a database we have to account for:
- Databases provide much simpler types and validations than our code.
- Large or high-traffic databases can be hard to change.
- Databases are often consumed by more than just your Rails app.
To navigate this, we’ll talk about the logical model of the data—the one the users talk about and understand—as
distinct from the physical model—what tables, columns, indexes, and constraints are actually in the database.
With regard to the physical model, we’ll break that down into two distinct steps for development. We’ll learn how
to decide what database structures you want first, and then how to write a proper Rails migration to create them.
First, let’s define logical and physical models.
### 14.1 Logical and Physical Data Models
When you runbin/rails g migrationto create a database migration, you are manipulating the _physical_ data
model: the actual schema in the database. The _logical_ model is the data model as understood by users and other
interested parties. For simple domains, these models are often very similar, but it’s important to understand the
differences.
The logical model is a tool to get alignment between the developers who build the app and the users or other
stakeholders who know what problems the app needs to solve. Users won’t usually care about physical elements
such as indexes, join tables, or reference data lookup tables when discussing how the app should behave.
The logical model is in the language of the users, at the level of abstraction they understand. This is often
insufficient for properly managing the data, but you can’t make a database without an understanding of the
domain.
For example, a user will think that a widget has a status, or a manufacturer has an address. This doesn’t mean
that the widget _table_ must have a status _column_ or that the manufacturer _table_ has columns for each part of an
address. You may not want to (or be able to) model it that way in the database.
See the figure “Example Logical and Physical Models” on the next page for an example of a logical and physical
model for a hypothetical widget and manufacturer relationship.
It stands to reason, then, that you should create a logical model to build alignment before you start thinking
about the physical model.
### 14.2 Create a Logical Model to Build Consensus
The logical model is a tool to build consensus with the developers who must write the software and anyone else
that understands what the software must do or what problems it must solve. The logical model is where you can
identify requirements for the data to be stored without worrying (yet) about how to store it.
I recommend that the developers either lead this process or have final approval, since this model is input into
their work. While non-developers can do a good job of drafting logical models, there are often some fine details
they miss that a developer will need to know in order to move forward.
I don’t want you to think of the logical model as some grandiose document created by a formalized process. Often
a single spreadsheet is sufficient. No matter how you do it, I highly recommend writing it down and being explicit.
It’s usually sufficient to capture:
- The names of all entities or “things” to be managed
- For each attribute of those entities:
**-** The name of it
**-** What type of data it is
**-** Is it a required value?
**-** What other requirements are there, such as allowed values, uniqueness, etc.
- For each entity, what uniquely identifies it? Can two entities have the exact same values for all attributes
and, if so, what does that mean?
For example:
```
Table 14.1: Example logical model as a spreadsheet
```
```
Entity Attribute Type Req? Other Requirements
Widget name String Y unique to manufacturer
Widget status String Y “Fresh”, “Approved”, or “Archived”
Widget price Money Y Not negative, <= than $10,000
Widget created Date Y
Manufacturer name String Y unique
Manufacturer address Address Y street and zip is fine
```
```
Entity Attribute Type Req? Other Requirements
```
However you draft this logical model, make sure you have a good sense of the allowed values for each attribute.
If the user uses attribute types like “Address”, define a new entity called “Address” and identify its requirements.
For more general types like “String” or “Date”, try to get clarity on what values are allowed. There are a lot of
strings in the world and probably not all of them are a valid widget status.
As to the uniqueness questions, getting these right can greatly reduce confusion in the actual data. Often there are
several sets of values that represent uniqueness. For example, the widget ID we’ve discussed previously sounds
like a unique value. But you also may want widget _names_ to be unique. It’s fine to have multiple unique identifiers
for entities, but it’s important to understand all of them.
The less familiar you are with the domain, or the newer it is, the more time you should spend exploring it before
you start coding. Mistakes in data modeling are difficult to undo later and can create large carrying costs in
navigating the problems created by insufficient modeling.
You don’t have to know everything, but even knowing how data _might_ be used is useful. You don’t have to handle
those “someday, maybe” requirements, but knowing how stable certain requirements are can help you properly
translate them to the physical model. Stable requirements can be enforced in the database; unstable requirements
might need to be enforced in code so they can be more easily changed.
Once you have alignment, you can build the physical model, which you should do in two steps: plan it, then
create it.
### 14.3 Planning the Physical Model to Enforce Correctness
```
This section’s code is in the folder14-03/of the sample code.
```
Translating the logical model to the physical model requires making several design decisions, especially as the app
becomes more complex and needs to manage more types of data.
This should be done in two discrete steps. This section discusses the first, which is to plan exactly how you are
going to store the data in the database. The next section discusses how to write a Rails migration to implement
this plan.
Whereas the logical model was for building alignment and discovering business rules, the physical model is for
accurately managing data that conforms to those rules. This means that correctness, precision, and accuracy are
paramount.
The design decisions you’ll make amount to how and where you will enforce the correctness of the data. Your
database is an incredibly powerful tool to do this, and it’s where most of your constraints around correctness
should go.
#### 14.3.1 The Database Should Be Designed for Correctness
Rails’ view of the database is that it’s more or less a dumb store and Rails—via validations and other mechanisms—
will keep the data correct. This is unrealistic, even in simple circumstances. Active Record provides a public API to
Figure 14.1: Example Logical and Physical Models
bypass validations, and the reality of most systems is that Things That Aren’t Rails will be accessing the database
directly.
For example, it’s common to connect business and financial reporting systems directly to the app’s database. It’s
often much more economical and flexible to allow business users to query the data however they like than to get
developers to build custom views for them. Tools like Looker^1 or Heroku Dataclips^2 provide ways of turning SQL
into reports. Common data warehousing techniques usually involve dumping the entire operational database into
another system where it can be analyzed.
If these systems have to deal with incorrect or ambiguous data, in the best case, they will be complex and difficult
to maintain. More realistically, the reports will simply be wrong. If, on the other hand, these systems can rely on
the data in the database being correct and unambiguous, the reports are more valuable and can lead to better
decisions.
For simple to moderate requirements, you can use the database to absolutely ensure the data is correct and
precise. For complex requirements, you may need to use code in addition to the database. Unstable requirements
benefit from being implemented in code, because the database will become harder to change as time goes on.
Stable or critical requirements, however, benefit greatly from being enforced in the database.
No matter what, we’re going to use database-specific features. That requires using a SQL schema instead of a
Ruby-based one.
#### 14.3.2 Use a SQL Schema
It’s rare to create an app that must connect to many different types of databases. It’s also rare to migrate from one
database type to another. Thus, we should not be shy about using database-specific features whenever it helps
us meet our users’ needs. Rails’ API for managing the database doesn’t provide access to all of these features,
however.
This matters because Rails uses a schema file to maintain the test database, as well as to initialize a development
database in a fresh environment. We need that schema to match production, so we cannot usedb/schema.rb, and
instead must use SQL.
Fortunately, this is a one-line configuration change inconfig/application.rb
# config/application.rb
# per-controller helpers
g.helper false
**end**
→ # We want to be able to use any feature of our database,
→ # and the SQL format makes that possible
→ config.active_record.schema_format **= :sql
end
end**
(^1) https://looker.com
(^2) https://devcenter.heroku.com/articles/dataclips
Note that we added a comment as to why we made this change. It’s important that all deviations from Rails’
defaults are understood by current and future team members. Comments are an easy way to make that happen.
Git commit messages are not.
We should also deletedb/schema.rb, since that will no longer be used. Rails will store the SQL schema in
db/structure.sql.
> rm db/schema.rb
I recommend this change for all database types, because it costs nothing and provides a lot of benefit.
For Postgres specifically, we need to make another change, which is to useTIMESTAMP WITH TIME ZONEfor
timestamps.
#### 14.3.3 UseTIMESTAMP WITH TIME ZONEFor Timestamps
The SQL standard provides for theTIMESTAMPfields to store... timestamps. A timestamp is a number of millisec-
onds since a reference timestamp, usually midnight on January 1, 1970 in UTC.
TheTIMESTAMPdata type does not store a time zone, however. Most databases store timestamps in UTC and
provide an automatic translation based on... well, it’s complicated.
By default, the computer your database is running on is configured with a system time zone. This can be hard to
inspect or control. The connection to the database itself can override this. The code that makes a connection to
the database can override this as well. Rails can override this. Your code can override Rails.
This means that your timestamps will be translated using a reference time zone that might not be obvious. And if
the wrong reference is used when reading those timestamps out, the reader can interpret the timestamp differently.
Even though Rails defaults to using UTC, some other process might be configured differently. This is extremely
confusing.
Postgres provides the data typeTIMESTAMPTZ(also known asTIMESTAMP WITH TIME ZONE) that avoids this problem.
It stores the reference time zone with the timestamp so it’s impossible to misinterpret the value. Postgres expert
Dave Wheeler wrote a blog post^3 that can provide you more details.
We can make Rails use this type by default. The classPostgreSQLAdapter(which is in theActiveRecord::ConnectionAdapters
namespace) has an attribute nameddatetime_typethat allows overriding the default SQL type used whenever a
migration has adatetimein it.
We can set this to:timestamptzand all of our migrations will useTIMESTAMPTZinstead ofTIMESTAMP. This can be
done anywhere as long it loads when Rails does. Best place to do that is inconfig/initializers/postgres.rb:
# config/initializers/postgres.rb
require "active_record/connection_adapters/postgresql_adapter"
ActiveRecord **::** ConnectionAdapters **::** PostgreSQLAdapter.datetime_type **=
:timestamptz**
(^3) https://justatheory.com/2012/04/postgres-use-timestamptz/
Now, when we write code liket.timestampsort.datetime, Rails will useTIMESTAMP WITH TIME ZONEand all of
our timestamps will be stored without ambiguity or implicit dependence on the system time zone.
With this base, we can start planning the physical model.
#### 14.3.4 Planning the Physical Model
A formal way to model a database is called _normalization_ , and it’s a dense topic full of equations, confusing
terms, and mathematical proofs. Instead, I’m going to outline a simpler approach that might lack the precision of
theoretical computer science, but is hopefully more approachable.
Here’s how to go about it:
1. Create a table for each entity in the logical model.
2. Add columns to associate related models using foreign keys.
3.For each attribute, decide how you will enforce its requirements and create the needed columns, constraints,
and associated tables.
4. Create indexes to enforce all uniqueness constraints.
5. Create indexes for any queries you plan to run.
To do this, it’s immensely helpful if you understand SQL. In addition to knowing how to model your data, knowing
SQL allows you to understand the runtime performance of your app, which will further help you with data
modeling. Of all the programming languages you will ever learn, SQL is likely to remain useful for your entire
career. Execute Program^4 has a course that will help.
Outside of learning SQL, the hardest part of the planning process is step 3: deciding how to enforce the
requirements of each attribute.
You will bring together some or all of the following techniques:
- Choosing the right column type
- Using database constraints
- Creating lookup tables
- Writing code in your app
Let’s dive into each one of these.
**Choosing the Right Column Type**
Each column in the database must have a type, but databases have few types to choose from. Usually there are
strings, dates, timestamps, numbers, and booleans. That said, familiarize yourself with the types of _your_ database.
Unless you are writing code that has to work against _any_ SQL database (which is rare), you should not be bound
by Rails’ least-common-denominator set of types.
The type you choose should allow you to store the exact values you need. It should also make it difficult or
impossible to store incorrect values. Here are some tips for each of the common types.
(^4) https://www.executeprogram.com/courses/sql/lessons/basic-tables
**Strings** In the olden days, choosing the size of your string mattered. Today, this is not universally true. Consult
your database’s documentation and use the largest size type you can. For example, in Postgres, you can use
aTEXTfield, since it carries no performance or memory burden overVARCHAR. It’s important to get this right
because changing column types later when you need bigger strings can be difficult.
**Rational Numbers** AvoidFLOATif possible. Databases storeFLOATvalues using the IEE 754^5 format, which _does
not store precise values_. Either convert the rational to a base unit (for example, store money in cents as
an integer), or use theDECIMALtype, which _does_ store precise values. Note that neither type can store all
rational numbers. One-third, for example, cannot be stored in either type. To store precise fractional values
might require storing the numerator and denominator separately.
**Booleans** Use thebooleantype. Do not store, for example,"y"or"n"as a string. There’s no benefit to doing
this and it’s confusing. And yes, people do this and I don’t understand why.
**Dates** Remember that a date is not a timestamp. A date is a day of the month in a certain year. There is no time
component. TheDATEdatatype can store this, and allow date arithmetic on it. Don’t store a timestamp set
at midnight on the date in question. Time zones and daylight savings time will wreak havoc upon you, I
promise.
**Timestamps** As opposed to a date, a timestamp is a precise moment in time, usually a number of milliseconds
since a reference timestamp. As discussed above, useTIMESTAMP WITH TIME ZONEif using Postgres. If you
aren’t using Postgres, be _very explicit_ in setting the reference timezone in all your systems. Do not rely on the
operating system to provide this value. Also, _do not_ store timestamps as numbers of seconds or milliseconds.
TheTIMESTAMP WITH TIME ZONEandTIMESTAMPtypes are there for a reason.
**Enumerated Types** Many databases allow you to create custom enumerated types, which are a set of allowed
values for a text-based field. If the set of allowed values is stable and unlikely to change, anENUMcan be a
good choice to enforce correctness. If the values might change, a lookup table might work better (we’ll talk
about that below).
No matter what other techniques you use, you will always need to choose the appropriate column type. Next,
decide how to use database constraints.
**Using Database Constraints**
All SQL databases provide the ability to preventNULLvalues. In a Rails migration, this is whatnull: falseis
doing. This tells the database to preventNULLvalues from being inserted. Any required value should have this set,
and most of your values should be required.
Many databases provide additional constraint mechanisms, usually called _check constraints_. Check constraints are
extremely powerful for enforcing correctness. For example, a widget’s price must be positive and less than or
equal to $10,000. With a check constraint this could be enforced:
##### ALTER TABLE
widgets
**ADD CONSTRAINT**
(^5) https://en.wikipedia.org/wiki/IEEE_754
price_positive_and_not_too_big
**CHECK** (
price_cents > 0 **AND**
price_cents <= 1000000
)
If you try to insert a widget with a price of -$100 or $300,000, the database will refuse. Thus, you can be
absolutely sure the price is valid. Check constraints can do all sorts of things. If you want all widget names to be
lowercase, you can do that, too:
##### CHECK (
lower(name) = name
)
Modifying these constraints becomes more difficult as the database gets larger, because these sorts of changes
can create locks on the table that prevent access or modification or both. This can create downtime for your app.
There are strategies to deal with this that are beyond the scope of this book, but the strong migrations gem^6 is a
great place to start with understanding them. Note, however, that it’s entirely likely that you will _never_ reach the
size of database where this would be a problem.
Here are the guidelines I find most useful:
- Any stable requirement should be implemented as a check constraint.
- Any critical requirement should be implemented as a check constraint.
- Unstable requirements on tables expected to grow might be better implemented in code, so you can change
them frequently, but it still might be better to use a check constraint and wait for the table to actually get
large enough to be a problem.
The next technique for enforcing correctness is the use of lookup tables.
**Using Lookup Tables**
When a column’s value should be one value from a static list of possible values, anENUMcan work as we discussed
above. If the possible values are likely to change, or if users are modifying those values, _or_ if you need additional
metadata to go along with the values, anENUMwon’t work. In these cases, you need a lookup table.
In the data model above on page 194, you can see an example of this for the widget’s status. Suppose we had
three widgets in the database, two of which have the status “Fresh” and the other “Approved”. Here’s how that
would look in the database using a lookup table:
(^6) https://github.com/ankane/strong_migrations
```
Table 14.2: Examplewidgetstable referencing a lookup table
```
```
id name widget_status_id
10 Stembolt 1
11 Thrombic Modulator 1
12 Tachyon Generator 2
```
```
Table 14.3: Examplewidget_statuseslookup table
```
```
id name
1 Fresh
2 Approved
3 Archived
```
Note a key difference between the physical and logical model. The logical model simply states that a widget
has a status attribute. To enforce correctness and deal with a potentially unstable list of possible values, we are
modeling it with a new table. In our code, a widget willbelong_toa status (which willhas_manywidgets).
When using lookup tables, you must create a _foreign key constraint_. This tells the database that the value for
widget_status_id _must_ match anidin the referencedwidget_statusestable. This prevents widgets from having
invalid or unknown statuses, sincewidget_statusescontains all known valid statuses.
A lookup table also allows modeling metadata on the referenced value. For example, if only “Approved” widgets
can be sold, we might model that with abooleancolumn on thewidget_statusestable:
```
Table 14.4: Examplewidget_statuseslookup table with metadata
```
```
id name allows_sale
1 Fresh false
2 Approved true
3 Archived false
```
The last tool available to enforce correctness is your app.
**Enforcing Correctness in App Code**
Some requirements are too difficult to enforce at the database layer, either because of necessary complexity or
because of a lack of stability. In these cases, your app can enforce correctness by refusing to write data that
violates the requirements.
Rails validations are quite powerful at doing this, and this is the mechanism you should use if you must validate
correctness in code. Just be aware that Active Record’s public API allows circumventing the validations. Anything
your database can possibly store, you can put into it using Active Record, no matter what validations you have
created.
That said, some requirements are so complex that using validations becomes quite difficult and you’ll need to
write a bunch of code to prevent bad data from getting written.
For example, if only supervisors can change a widget’s status to “Approved” for manufacturers created before
July 10, 1998, except for the manufacturer “Cyberdyne Systems”, this is going to be a convoluted and hard-
to-understand validation. It would be simpler as code (and relatively straightforward to implement if you’ve
followed the previous guidance and avoided putting business logic in your Active Records).
Once you have decided how you are going to model everything, it’s time to make your migrations.
### 14.4 Creating Correct Migrations
```
This section’s code is in the folder14-04/of the sample code.
```
Writing migrations is how we programmatically modify the database to conform to the physical schema we want
to use. Because Rails’ API for doing this is not SQL, it’s important that we take some time to make sure the
migrations we write result in the schema we need. Rails’ API is powerful and will save us time and make the work
easier, but it lacks a few useful defaults.
In the previous chapter, we created models so we could talk about some model basics. Rather than edit those
models and the schema it created, let’s start over (you can’t do this in real life, but it’ll make this chapter simpler
if we do).
If we delete the migrations and fixtures created bybin/rails g modeland re-runbin/setup, we should be good
to go.
> rm db/migrate/* test/fixtures/*.* && bin/setup
«lots of output»
The figure “Example Logical and Physical Models” on page 194 outlines what we’re going to do, but to re-iterate:
- A Widget has a name, price, status, and manufacturer, all of which are required.
- A Manufacturer has a name and an address, both of which are required.
- An address is a street and a zip code (both required).
- Widget names must be unique within a manufacturer.
- Manufacturer names must be unique globally.
- We’ll use lookup tables for addresses and widget statuses.
- We’ll use a database constraint to enforce a price’s lower-bound, but code for the upper-bound.
It’s important that changes that logically relate to each other go in a single migration file. Some databases,
including Postgres, run migrations in a transaction, which allows us to achieve an all-or-nothing result. Either our
entire change is applied successfully, or none of it is.
While we still want to end up with one migration, I find it easier to built it iteratively. Write some of the migration,
apply it and check it, then rollback and continue until everything is correct.
The figure “Authoring Migrations” on the next page outlines this basic process:
1. Create your migration file.
2. Add some code to it.
3. Apply the migrations and check the database to see if it had the desired effect.
4. If anything is wrong, or you aren’t yet done, roll back the changes.
5. Repeat until you have correctly modeled the physical changes.
This allows you to take each change step-by-step, but still end up with only one migration file that makes the
cohesive change you’re making. In our case, we want a single migration that creates the needed tables.
#### 14.4.1 Creating the Migration File and Helper Scripts
Before we create the migration file, we need three scripts to help this process. I find thatbin/rails db:migrate
andbin/rails db:rollbackdon’t consistently modify both the development and test schema. This can result in
a test schema that is not the same as what’s described in the migration file, which can cause some confusing test
behavior. Rather than document this problem, let’s make two scripts to handle applying migrations and rolling
them back.
Here’s the script to migrate all databases (note again the duplicated checks for-hand friends):
# bin/db-migrate
#!/bin/sh
set -e
**if** [ "${1}" = -h ] **||** \
[ "${1}" = --help ] **||** \
[ "${1}" = help ] **; then**
echo "Usage: ${0}"
echo
echo "Applies outstanding migrations to dev and test databases"
exit
**else
if** [! -z "${1}" ] **; then**
echo "Unknown argument:'${1}'"
exit 1
**fi
fi**
echo "[ bin/db-migrate ] migrating development schema"
bin/rails db:migrate
echo "[ bin/db-migrate ] migrating test schema"
bin/rails db:migrate RAILS_ENV=test
Figure 14.2: Authoring Migrations 203
Here’s the one we’ll use to roll back all databases:
# bin/db-rollback
#!/bin/sh
set -e
**if** [ "${1}" = -h ] **||** \
[ "${1}" = --help ] **||** \
[ "${1}" = help ] **; then**
echo "Usage: ${0}"
echo
echo "Rolls back the current migration from dev and test databases"
exit
**else
if** [! -z "${1}" ] **; then**
echo "Unknown argument:'${1}'"
exit 1
**fi
fi**
echo "[ bin/db-rollback ] rolling back development schema"
bin/rails db:rollback
echo "[ bin/db-rollback ] rolling back test schema"
bin/rails db:rollback RAILS_ENV=test
Let’s also make a script calledbin/psqlthat connects to our development database. I realize thatbin/rails
dbconsoledoes this, but a) it requires us to type a password each time, and b) it’s incredibly slow to start up
because it must load Rails first, only to delegate to thepsqlcommand-line client.
# bin/psql
#!/bin/sh
set -e
**if** [ "${1}" = -h ] **||** \
[ "${1}" = --help ] **||** \
[ "${1}" = help ] **; then**
echo "Usage: ${0}"
echo
echo "Uses psql to connect to dev database directly"
exit
**else
if** [! -z "${1}" ] **; then**
echo "Unknown argument:'${1}'"
exit 1
**fi
fi**
echo "[ bin/psql ] Connecting to widgets_development"
PGPASSWORD=postgres psql -U postgres \
-h db \
-p 5432 \
widgets_development
Note that because we have consolidated all dev-environment configuration, we can safely rely on the database
connection information to be consistent for all developers, and thus hard-code it into this script.
We’ll need to make them executable:
> chmod +x bin/db-migrate bin/db-rollback bin/psql
It’s also a good idea to add these tobin/setup help. I’ll leave that as an exercise for the reader.
Now, let’s create our migration file:
> bin/rails g migration make_widget_and_manufacturers
invoke active_record
create db/migrate/20231204235403_make_widget_and_ma...
For the sake of repeatability when writing this book, I’m going to rename the migration file to a name that’s not
based on the current date and time. You don’t need to do this.
> mv db/migrate/*make_widget_and_manufacturers.rb \
db/migrate/20210101000000_make_widget_and_manufacturers.rb
With that set up, we can now iteratively put code in this file to generate the correct schema we want.
#### 14.4.2 Iteratively Writing Migration Code to Create the Correct Schema
We’ll need to work a bit backward. We can’t createwidgetsfirst, because it must referencewidget_statusesand
manufacturers.manufacturersmust referenceaddresses. So, we’ll start withwidget_statuses.
By default, Rails creates nullable fields. We don’t want that. Fields with required values should not allow null.
We’ll usenull: falsefor these fields (even for nullable fields I like to usenull: trueto make it clear that I’ve
thought through the nullability).
I also like to document tables and columns usingcomment:. This puts the comments in the database itself to be
viewed later. Even for something that seems obvious, I will write a comment because I’ve learned that things are
never as obvious as they might seem.
# db/migrate/20210101000000_make_widget_and_manufacturers.rb
**class** MakeWidgetAndManufacturers **<** ActiveRecord **::** Migration **[** 7**.**...
**def** change
→ create_table **:widget_statuses** ,
→ **comment:** "List of definitive widget statuses" **do |** t **|**
→ t.text **:name** , **null:** false,
→ **comment:** "The name of the status"
→ t.timestamps **null:** false
→ **end**
→ add_index **:widget_statuses** , **:name** , **unique:** true,
→ **comment:** "No two widget statuses should have the same name"
**end
end**
Note that I’ve created a unique index on the:namefield. Although database indexes are mostly for allowing fast
querying of certain fields, they are also the mechanism by which databases enforce uniqueness. Thus, to prevent
having more than one status with the same name, we create this index, specifyingindex: { unique: true }.
This will create a case-sensitive constraint, meaning the statuses"Fresh"and"fresh"are both allowed in the
table at the same time. Currently, the developers control the contents of this table, so a unique index is fine—we
won’t create a duplicate status in a different letter case. If the contents of this field were user-editable, I might
create a case-insensitive constraint instead. Sean Huber wrote a short blog post^7 about how you could do this if
you are interested.
Next, let’s create theaddressestable. Our user’s documentation said “street and zip is fine”, so we’ll create the
table with just those two fields for now.
# db/migrate/20210101000000_make_widget_and_manufacturers.rb
add_index **:widget_statuses** , **:name** , **unique:** true,
**comment:** "No two widget statuses should have the same n...
→ create_table :addresses,
→ comment: "Addresses **for** manufacturers" do |t|
(^7) [http://shuber.io/case-insensitive-unique-constraints-in-postgres/](http://shuber.io/case-insensitive-unique-constraints-in-postgres/)
→ t.text :street, null: false,
→ comment: "Street part of the address"
→ t.text :zip, null: false,
→ comment: "Postal **or** zip code of this address"
→ t.timestamps null: false
→ end
end
end
Again, liberal use ofcomment:will help future team members. At this point, I like to run the migrations to make
sure everything’s working before proceeding.
> bin/db-migrate
[ bin/db-migrate ] migrating development schema
== 20210101000000 MakeWidgetAndManufacturers: migrating ====...
-- create_table(:widget_statuses, {:comment=>"List of defini...
-> 0.0069s
-- add_index(:widget_statuses, :name, {:unique=>true, :comme...
-> 0.0013s
-- create_table(:addresses, {:comment=>"Addresses for manufa...
-> 0.0024s
== 20210101000000 MakeWidgetAndManufacturers: migrated (0.01...
[ bin/db-migrate ] migrating test schema
== 20210101000000 MakeWidgetAndManufacturers: migrating ====...
-- create_table(:widget_statuses, {:comment=>"List of defini...
-> 0.0039s
-- add_index(:widget_statuses, :name, {:unique=>true, :comme...
-> 0.0010s
-- create_table(:addresses, {:comment=>"Addresses for manufa...
-> 0.0021s
== 20210101000000 MakeWidgetAndManufacturers: migrated (0.00...
I also like to connect to the database and describe the tables to see if it looks correct. It may seem silly, but looking
at the same information in a different way can often uncover mistakes.
With Postgres, you can use thebin/psqlscript we made and type\d+ widget_statusesor\d+ addressesto
display the schema. If anything looks wrong—including a spelling error in a comment—usebin/db-rollback, fix
it, and move on.
Of course, we aren’t done yet, so we’llbin/db-rollbackanyway.
> bin/db-rollback
[ bin/db-rollback ] rolling back development schema
== 20210101000000 MakeWidgetAndManufacturers: reverting ====...
-- drop_table(:addresses, {:comment=>"Addresses for manufact...
-> 0.0012s
-- remove_index(:widget_statuses, :name, {:unique=>true, :co...
-> 0.0014s
-- drop_table(:widget_statuses, {:comment=>"List of definiti...
-> 0.0005s
== 20210101000000 MakeWidgetAndManufacturers: reverted (0.00...
[ bin/db-rollback ] rolling back test schema
== 20210101000000 MakeWidgetAndManufacturers: reverting ====...
-- drop_table(:addresses, {:comment=>"Addresses for manufact...
-> 0.0006s
-- remove_index(:widget_statuses, :name, {:unique=>true, :co...
-> 0.0011s
-- drop_table(:widget_statuses, {:comment=>"List of definiti...
-> 0.0003s
== 20210101000000 MakeWidgetAndManufacturers: reverted (0.00...
Becausewidgetsmust refer tomanufacturers, we need to makemanufacturersnext. We’ll usereferencesto
create the foreign key frommanufacturerstoaddresses. Rails’ default is to skip creating a foreign key constraint.
This is not a good default, because there’s no benefit to skipping foreign key constraints.
We’ll useforeign_key: trueto make sure the constraint gets created. We cannot have manufacturers referencing
non-existent addresses. We’ll also add an index to the reference because we’ll definitely be navigating these
foreign keys and an index will ensure that navigation performs well.
# db/migrate/20210101000000_make_widget_and_manufacturers.rb
```
t.timestamps null: false
end
```
→ create_table **:manufacturers** ,
→ **comment:** "Makers of the widgets we sell" **do |** t **|**
→ t.text **:name** , **null:** false,
→ **comment:** "Name of this manufacturer"
→ t.references **:address** , **null:** false,
→ **index:** true,
→ **foreign_key:** true,
→ **comment:** "The address of this manufacturer"
→ t.timestamps **null:** false
→ **end**
##### →
→ add_index **:manufacturers** , **:name** , **unique:** true
**end
end**
And now, finally, we can make thewidgetstable:
# db/migrate/20210101000000_make_widget_and_manufacturers.rb
```
add_index :manufacturers , :name , unique: true
```
→ create_table **:widgets** ,
→ **comment:** "The stuff we sell" **do |** t **|**
→ t.text **:name** , **null:** false,
→ **comment:** "Name of this widget"
→ t.integer **:price_cents** , **null:** false,
→ **comment:** "Price of this widget in cents"
→ t.references **:widget_status** , **null:** false,
→ **index:** true,
→ **foreign_key:** true,
→ **comment:** "The current status of this widget"
→ t.references **:manufacturer** , **null:** false,
→ **index:** true,
→ **foreign_key:** true,
→ **comment:** "The maker of this widget"
→ t.timestamps **null:** false
→ **end**
**end
end**
We have only two steps left. We must enforce the uniqueness of widget names amongst manufacturers, and
enforce the widget’s price allowed values. We’ll tackle the uniqueness requirement next.
To enforce the widget name/manufacturer uniqueness requirement, we can create our own index on both fields
usingadd_index:
# db/migrate/20210101000000_make_widget_and_manufacturers.rb
```
t.timestamps null: false
end
```
→ add_index **:widgets** , **[ :name** , **:manufacturer_id ]** ,
→ **unique:** true,
→ **comment:** "No manufacturer can have two widgets with " **+**
→ "the same name"
**end
end**
This allows many widgets to have the same name, as long as they don’t also have the same manufacturer.
To create the constraint on price, we can use theadd_check_constraintmethod. Prior to Rails 6.1, you needed
to usereversibleandexecuteto put raw SQL in your migration. No longer!
We’ll add this to the migration file:
# db/migrate/20210101000000_make_widget_and_manufacturers.rb
```
comment: "No manufacturer can have two widgets with " +...
"the same name"
```
→ add_check_constraint(
→ **:widgets** ,
→ "price_cents > 0",
→ **name:** "price_must_be_positive"
→ )
**end
end**
If you don’t know SQL or it’s still new to you, this syntax for what goes into the second argument of
add_check_constraintcan seem daunting and hard to derive. Your database’s documentation is a great place to
start and you _can_ piece it together from that. A little bit of trial-and-error also helps, and since you can easily
apply and rollback your migration, a combination of reading docs and trying things out will allow you to arrive at
the right syntax. That’s how I did it!
Also note that we used the optional:nameparameter to give the constraint a name. Like adding comments to our
tables and columns, giving constraints a descriptive name can be useful. If the constraint is violated, the name will
appear in the error message and it can be helpful to use that to start figuring out what might have gone wrong.
Lastly, you’ll notice that we didn’t need to use any raw SQL, but we are still using a SQL-based schema. A
SQL-based schema is always a better option from the start, because they you don’t have to remember to change it
later if you _do_ need to use SQL in your migrations.
Let’s apply it:
> bin/db-migrate
«lots of output»
We aren’t _quite_ done, because we have not modeled the upper-limit on price. We planned to do that in code, so
we need to make sure all of our model classes are created and correct, following the guidelines we learned about
in “Active Record is for Database Access” on page 181.
First up isWidgetStatus. Since there is a to-many relationship with widgets, we’ll usehas_many :widgets. Note
that this file will not already exist and you must create it.
# app/models/widget_status.rb
**class** WidgetStatus **<** ApplicationRecord
has_many **:widgets
end**
Next isAddress. It has a to-many relationship with manufacturers, since multiple manufacturers can exist at the
same address. Also note that this file won’t already exist.
# app/models/address.rb
**class** Address **<** ApplicationRecord
has_many **:manufacturers
end**
We’ll add the other end of the relationship toManufacturer:
# app/models/manufacturer.rb
**class** Manufacturer **<** ApplicationRecord
has_many **:widgets**
→ belongs_to **:address
end**
```
Finally we’ll modelWidget. Because we did not model the price’s upper-end in the database, we should add it to
the code now as a validation. Even though we have no use-case that would trigger this validation, since it’s part
of the logical data model that we couldn’t model in the database, we have to put it here.
Note that we aren’t putting any other validations in these models. The database will enforce correctness and
prevent bad data from being written. We only need redundant checks if there’s a specific reason. We’ll discuss this
more in “Validations Don’t Provide Data Integrity” on page 227.
```
```
# app/models/widget.rb
```
```
last_two: id_as_string [- 2 ..- 1 ]
}
end
→ belongs_to :widget_status
→ validates :price_cents ,
→ numericality: { less_than_or_equal_to: 10_000_00 }
end
```
```
If you aren’t used to database constraints, it might feel like we’ve put business logic in our database. In a way,
we have, and we really should consider testing some of it. The check constraint, in particular, seems hard to be
confident in without a test.
Let’s see what a test looks like for our database constraints.
```
### 14.5 Writing Tests for Database Constraints
```
This section’s code is in the folder14-05/of the sample code.
```
Like all tests, tests for the correctness of the data model have a carrying cost. I don’t see a lot of value in testing
null: false, orunique: true, because these tend to be easy to get right. Check constraints are more like real
code and thus easier to mess up. I usually write tests for them.
Let’s write a test for the constraint around the widget’s price. We’ll need two tests: one that successfully sets the
widget’s price to a correct value, and another that fails in an attempt to set it to a negative value.
Because this is testing the database and not the code inapp/models, our tests will useupdate_column, which
skips validations and callbacks, writing directly to the database. If we usedupdate!instead, and we later added
validations to theWidgetclass, our test would fail to write the database at all. Usingupdate_columnensures we
are testing the database itself.
To do that, we’ll set up a valid widget in thesetupmethod, which requires a widget status and a manufacturer
(which requires an address).
```
# test/models/widget_test.rb
```
require "test_helper"
**class** WidgetTest **<** ActiveSupport **::** TestCase
setup **do**
widget_status **=** WidgetStatus.create!( **name:** "fresh")
manufacturer **=** Manufacturer.create!(
**name:** "Cyberdyne Systems",
**address:** Address.create!(
**street:** "742 Evergreen Terrace",
**zip:** "90210"
)
)
@widget **=** Widget.create!(
**name:** "Stembolt",
**manufacturer:** manufacturer,
**widget_status:** widget_status,
**price_cents:** 10_00
)
**end**
test "valid prices do not trigger the DB constraint" **do**
assert_nothing_raised **do**
@widget.update_column(
**:price_cents** , 45_00
)
**end
end**
test "negative prices do trigger the DB constraint" **do**
ex **=** assert_raises **do**
@widget.update_column(
**:price_cents** , **-** 45_00
)
**end**
assert_match(/price_must_be_positive/i,ex.message)
**end
end**
Note the way we are checking that we violated the constraint. We check that the message in the assertion
references the constraint name we used in the migration:price_must_be_positive. This means our test should
hopefully _only_ pass if we violated that constraint, but fail if we get some other exception.
Now, let’s run the test.
> bin/rails test test/models/widget_test.rb
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 53004
# Running:
..
Finished in 0.046877s, 42.6650 runs/s, 85.3301 assertions/s.
2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
This should pass. While we could write a test for the validation, I find those sorts of tests less valuable since the
code is straightforward with no real logic.
### Up Next
Data modeling is not easy and it can take a lot of experience to get comfortable with it. Hopefully, I’ve stressed
how important it is to create your database in a way that favors correctness and precision at the database layer, as
well as some helpful techniques to get there.
In the chapter after next, we’ll finish talking about models, but to do that, we need to revisit business logic. While
our database schema implements some of our business rules, most of the logic that makes our app special will be
in code, so let’s talk about that next.
## 15 Business Logic Code is a Seam
Way back at the start of the book, I outlined a core part of sustainable Rails architecture, which is to not put
business logic in the Active Records. In particular, the section “Business Logic in Active Records Puts Churn and
Complexity in Critical Classes” on page 52 outlines why. The chapter was light on details about how to structure
the classes that _do_ contain business logic. That’s what we’ll discuss here.
As mentioned in that chapter, the key thing to do is isolate your business logic from your Active Records and other
Rails-managed classes. How your business logic is structured is less important. But it’s not unimportant.
The way to think about the API of your business logic class is as a _seam_. On one side of this seam is code managed
by Rails inside a controller, job, or rake task. On the other side is logic specific to your domain and a particular
use-case that might use Rails, but isn’t managed by it (see the figure on the next page).
I like to refer to this as your app’s _service layer_. This term appears in Martin Fowler’s _Patterns of Enterprise
Application Architecture_ , which was the basis for creating Active Record. Fowler defines the service layer as
follows:
```
A Service Layer defines an application’s boundary and its set of available operations from the perspective of
interfacing client layers. It encapsulates the application’s business logic, controlling transactions and coordinating
responses in the implementation of its operations.
```
This is precisely what I am recommending you do, and this chapter is about that, and what it may look like.
To understand this, we need to first be clear about what’s important—and not very important—about the code
that implements business logic. We’ll then talk about the seam itself, which has three parts: a class, a method,
and a return value. The strategy I will advocate is to have a stateless class named for the specific process or use
case it implements, a single method that accepts the parameters needed to perform the logic, and an optional
richly-defined result object describing what happened. This forms a base on which future complexity can be most
easily managed and requires the fewest design decisions to get a working implementation.
Let’s first talk about important considerations regarding the code implementing the business logic, namely that its
behavior is as transparent as possible.
### 15.1 Business Logic Code Must Reveal Behavior
The code implementing business logic is the most critical in your app, since it delivers the results your app exists
to deliver. It is also the least stable, since it is implemented iteratively and must be responsive to change. It stands
to reason that this code, apart from working, must be easy to understand, since understanding code is required to
change it.
```
Figure 15.1: Seam Overview
```
And _this_ means that the code must be _behavior-revealing_ (as opposed to _intention-revealing_ ). It must be as easy as
possible to understand what the code _actually does_. Do not lose sight of this, and be wary of making changes for
other reasons.
In particular, it does not matter if
- the code is “object-oriented” (whatever that means).
- you use functional programming.
- the code can be re-used.
- the implementation is “elegant” or “clean” (again, whatever they mean).
- some code metrics have been satisfied.
- you have used design patterns.
- you have used idiomatic Ruby or Rails (whatever they... well, you get the point).
I mention this because I have seen time and time again developers write code to serve one or more of the above
purposes at the cost of clarity in behavior. Refactoring code to be “more OO” is a specious activity. In particular,
the so-called SOLID Principles can wreak havoc on a codebase when applied broadly^1. I’ve been guilty of this
many times in my career. Some of the most elegant, compact, object-oriented code I’ve ever written was the most
difficult to understand and change later^2.
(^1) I even wrote a short book about it: https://solid-is-not-solid.com
(^2) If you are thinking maybe I just wasn’t doing it right, well, maybe I wasn’t. But that’s still the point. I don’t claim to be the best developer
in the world, but I’m at least average. And if, after 20 years of working in object-oriented languages, I’m not able to “do it right”, I think
maybe, just maybe, the problem isn’t entirely me.
This isn’t to say there is no value in the list above. Design patterns, object-oriented programming, and Ruby
idioms do serve a purpose, but it should be directed toward the larger goal, which is to write code that can be
easily changed... by being behavior-revealing.
The technique I have had the most success with—and seen others succeed with as well—is to create a single class
and method from which a given bit of business logic is initiated. That class and method (as well as the object the
method returns) represent a _seam_ or dividing line between the generic world of Rails-managed code, and my
own. The internals of that class can then be freely structured as needed.
### 15.2 Services are Stateless, Explicitly-Named Classes with Explicitly-Named Methods
When implementing the business logic, there are a lot of design decisions that need to be made. The architecture
of our app serves to—in part—tell us how to make some of those decisions. Not putting our business logic in an
Active Record is a start. We can eliminate even more design decisions by creating conventions around this seam
between our logic and the Rails-managed outside world.
What is the absolute simplest thing we can do (besides putting our code directly inObject)? If we had no Rails,
no framework, no libraries, we’d need to make a class with a method on it, and call that method. Suppose _this_ is
our strategy for business logic? Suppose we always put new code in a new class and/or a new method? This
would eliminate a lot of design decisions.
It turns out this strategy has further advantages beyond eliminating design decisions. First, it doesn’t require
changing any existing code, which reduces the chances of us breaking something. Second, it provides a ton of
flexibility to respond to change in the future. It’s much easier to combine disparate bits of code that turn out to be
related than it is to excise unrelated code inside a large, rich class.
Classes like this are often called _services_ , and I would encourage the use of this term. It’s specific enough to avoid
conflating with models, databases, data structures, controllers, or mailers, but general enough to allow the code
to meet whatever needs it may have.
So what do we call these services?
#### 15.2.1 AThingDoerClass With ado_thingMethod is Fine
Barring extenuating circumstances, I will choose a noun for the class name, and make it as specific and explicit as
possible to what I’m implementing, in the context of the domain and app at that time. This means that early on,
the names are broad, likeWidgetsCreator. Later, when our domain and app are more complex, we may need
more explicit names likePromotionalWidgetsCreator.
The method name is a verb representing whatever process or use-case is being implemented, which will create
some redundancy. For example,create_widget. You might be feeling a bit uncomfortable right now, because you
are no-doubt envisioning “enterprisey” code like this:
WidgetsCreator.new.create_widget( **...** )
What I’m suggesting will definitely result in code like this. I won’t claim this code is elegant, but it does have the
virtue of being pretty hard to misinterpret. It also closes the fewest doors to changes in the future.
Now, you might think “We _have_ aWidgetclass and it _has_ acreatemethod. Isn’t _that_ where widget creation
should go?”. I understand this line of thinking, but remember,Widgetis a class to manipulate a database table
that holds one particular representation of a real-life widget. And thecreatemethod is one way (out of many) to
insert rows into that table. This isn’t my opinion—this is what Rails provides. It’s the very essence of theWidget
class. And there is no reason to conflate inserting database rows with the business process of widget creation.
And, what if we require another way to create a widget?WidgetsCreatorcan grow a new method, or we can
make a whole new class to encapsulate that process. We can couple these implementations only as tightly as the
underlying process in the real world is coupled. Our code can reflect reality. Wrapping it around the insertion of a
row in a database divorces our code from reality.
You might be thinking we should not have to callnewor perhapscreate_widgetshould be named in a more
generic way, likecall. We’ll get to that, but let’s talk about input to this method first.
#### 15.2.2 Methods Receive Context and Data on Which to Operate, not Services to Delegate To
There are typically three types of objects you need access to in order to implement your business logic in a Rails
app:
- Rails-managed classes like your Active Record classes, Jobs, or Mailers
- Data-holding objects (Active Records or Active Models), which are typically what is being operated on or a
context in which an operation must occur
- Other services needed by your service to which you delegate some responsibility
A significant design decision—after naming your class and method—is how your method’s code will get access to
these objects.
**Rails-managed Classes** In the vein of facing reality and treating things as they are—not how we might like
them to be—we are writing a Rails app. Rails provides jobs, mailers, and Active Records. Using them
directly—thus creating a hard dependency—is fine. We are likely not (or shouldn’t be) writing code to work
in any Ruby web framework. Further, unless our code needs to be agnostic of mailer, model, or job, there’s
no value in abstracting the actual implementation. The class needs what it needs and we should be explicit
about that.
**Data-holding Objects** Your method exists to operate on data or perform a process in the context of data, and
this data should be passed to the method directly. This information is not specific to the logic, but is an
input to that logic. For example, if Pat edits a widget, the logic is the same as if Chris edited a different
widget. So we’d pass an instance ofUserand an instance ofWidgetto our method.
**Other Services** Other services, be they services you create, or third party classes you’ve added to your app,
should either be referred to directly—if callers should not configure them or specify them—or passed into
the constructor—if the caller _must_ configure or specify them. Note the distinction. If the logic requires a
specific implementation, it should be strongly dependent on that. If it’s not, it shouldn’t be. Making all
dependencies generic and injectable belies the way the logic will actually work.
When you follow these guidelines, your code will communicate clearly how it works and what its requirements
are. For example:
**class** WidgetsCreator
**def** initialize( **notifier:** )
@notifier **=** notifier
**end**
```
def create_widget(widget_params)
widget = Widget.create(widget_params)
if widget.valid?
@notifier.notify( :widget , widget.id)
sales_tax_api.charge_tax(widget)
end
```
```
end
```
private
**def** sales_tax_api
@sales_tax_api **||=** ThirdParty **::** Tax.new
**end
end**
This code has a:
- dependency on some sort ofnotifier.
- hard dependency onThirdParty::Taxas well asWidget
- per-method-call dependency onwidget_params.
That tells you a lot about the runtime behavior of this code. IfWidgetandThirdParty::Taxwere also passed into
the constructor, you’d have more sleuthing to do in order to figure out what this routine did. _And_ you’d know less
about how coupled this routine is to the various objects it needs to do its work.
This code reflects reality: it wasn’t built to function on a generic Active Record or a generic tax service. Thus, we
can more easily understand its behavior. This means it’ll be easier to change and more sustainable to maintain.
You may have thoughts about this, but let’s wait one more section, because the last bit of our seam requires a
return value. For that, I recommend using rich result objects.
#### 15.2.3 Return Rich Result Objects, not Booleans or Active Records
A caller often needs to know what happened in the call they made. Not always, but often. Typical reasons are to
report errors back to the user, or to feed into logic it needs to execute. As part of the seam between the outside
world and our business logic, a boolean value—true if the call “succeeded”, false otherwise—is not very useful
and can be hard to manage^3.
(^3) If you’ve ever experienced a website or app giving you a generic message like “The operation could not be completed”, you can be sure
there is a boolean return value somewhere that has made it difficult or impossible to provide a useful error message.
If, instead, you return a richer object that exposes details the caller needs, not only will your code and tests be
more readable, but your seam can now grow more easily if needs change.
A rich result doesn’t have to be fancy. I like creating them as inner classes of the service’s class as a pretty basic
Ruby class, like so:
**class** WidgetsCreator
**def** create_widget(widget_params)
**if ...**
Result.new( **created:** true, **widget:** widget)
**else**
Result.new( **created:** false, **widget:** widget)
**end
end**
```
class Result
attr_reader :widget
def initialize(created:, widget: nil)
@created = created
@widget = widget
end
```
**def** created?
@created
**end
end
end**
Note how we used a specific past-tense verb—created?—and not something generic likesucceeded?. Also note
that we are including more than just an indicator of success. In this case, we’re returning the widget we attempted
to create, because the caller will need access to the validation errors. But we could include any other things that
are relevant _and_ we can enhance this class over time without having to touch any Active Records.
The caller’s code will then read as more specific and explicit:
result **=** WidgetsCreator.new.create_widget(widget_params)
**if** result.created?
redirect_to widget_path(result.widget)
**else**
@widget **=** result.widget
render "new"
**end**
Result objects should not be generic. Over time, you may see that related concepts and logic have related result
classes, and you can certainly extract duplication then, but by default, don’t make a generic result class library.
Take the 20 seconds required to type out what initially might amount to wrapping a boolean value.
Rich results shine in two places as you later change code. First, if your needs change, you have a return object
that you control and can change. Perhaps the results of widget creation aren’t just “did it get created or not”:
result **=** WidgetsCreator.new.create_widget(widget_params)
**if** result.created?
redirect_to widget_path(result.widget),
**info:** "Widget created"
→ **elsif** result.existing_widget_updated?
→ redirect_to widget_path(result.widget),
→ **info:** "Widget updated"
**else**
@widget **=** result.widget
render "new"
**end**
If we’d started off with a boolean return value, this change would be significant. A result object can also
wrap sophisticated errors (or, more commonly, refer to relevant Active Records/Models that themselves expose
validation errors).
The other benefit to rich result objects is with testing. They can make tests more clear, certainly, but they can also
cause your tests to fail in an obvious way if you change the contract of the seam.
For example, here is how we might mock our service using RSpec’s mocking library^4 :
mocked_widgets_creator **=** instance_double(WidgetsCreator)
allow(mocked_widgets_creator).to
receive( **:create_widget** ).and_return(
WidgetsCreator **::** Result.new( **created:** false)
)
Compare this toreceive(:create_widget).and_return(false). The rich result is more explicit. Now if we
changeWidgetsCreatorand modify theResultto require additional constructor parameters, _this_ test will fail
with an error related to that new required parameter. This will be a strong indicator that the class we are testing
is now mis-usingWidgetsCreatorand could break in production.
Do not use an Active Record for this purpose. Active Records are for database access and, even though they also
contain a powerful validation API, the entire purpose of the rich result object is that you can control it as part of
the seam you are building.
(^4) RSpec’s mocking system _is_ superior to minitest’s. It’s more powerful and easier to predict what it’s doing if you don’t already know RSpec.
Note that you should not create any sort of return value if one isn’t needed. If the caller of your service doesn’t
need to know what happened, don’t return anything. You can always add a return value later.
Bringing it all together, the figure “Business Logic Seam with Rich Result” on page 222 shows the various pieces.
```
Figure 15.2: Business Logic Seam with Rich Result
```
I want to talk through a few patterns I see around this topic and why you should be wary adopting them. They
aren’t wrong, so I’m not calling them anti-patterns, but there are trade-offs to consider.
### 15.3 Implementation Patterns You Might Want to Avoid
There are three patterns I have seen frequently that I don’t think deliver the value developers often think they will.
I’m not saying you should never use these patterns. I’m saying you need to be honest about the problem you are
solving by applying them, how serious that problem is, and how well they actually do solve it. The patterns are:
- Creating class methods instead of instance methods.
- “Service Objects”, which are typically classes that have a single parameterless method namedcall
- Using dependency injection.
#### 15.3.1 Creating Class Methods Closes Doors
Developers often bristle at having to call.newor putting a method in a class that has no state. They think it’s
more clean/compact/expedient/correct to declare this lack of state by making a class method:
**class** WidgetsCreator
**def** self.create_widget(widget_params)
# ...
**end
end**
## to use:
WidgetsCreator.create_widget
This approach might save a few keystrokes, but it prevents you from encapsulating state later, if you should need
to.
Some developers will try to split the difference and use the Singleton Pattern^5 :
**class** WidgetsCreator
```
def self.create_widget(widget_params)
self.instance.create_widget
end
```
```
def create_widget(widget_params)
# ...
end
```
private
(^5) https://en.wikipedia.org/wiki/Singleton_pattern
**def** self.instance
@instance **||=** self.new
**end
end**
This is better, but still unnecessary. It saves callers from typing four characters at the cost of maintaining a lot of
code to manage the singleton instance or—worse—the use of a gem that does it for you. It will also require you
to think through multi-threading issues at some point, and those are notoriously hard to get right.
#### 15.3.2 “Service Objects” UsingcallSolve No Problem and Obscure Behavior
Many Rails developers are theoretically onboard with the concept of a service layer, but some implement it using
the Gang of Four’s command pattern^6 , and call it a “service object”.
These classes accept all parameters on the constructor, then have a method calledcallthat takes no parameters
and performs the operation, like so:
**class** WidgetsCreator
**def** initialize(widget_params)
@widget_params **=** widget_params
**end**
**def** call
@widget_params **....
end
end**
## to use:
WidgetsCreator.new(widget_params).call
There are even gems and libraries that wrap this into a DSL to, in theory, make it easier to manage these classes.
There is almost no reason to create classes like this in a Rails app.
Outside of Rails, a class designed this way is used when you wish to execute some code at a different time or
location than when that code’s input parameters were available. This situation arises frequently in Rails apps, but
in those cases you would use a background job, not a so-called “service object”. Rails background jobs _are_ the
Rails implementation of the command pattern.
There is little benefit to adding a second set of classes that use the command pattern, however there are several
downsides:
(^6) https://en.wikipedia.org/wiki/Command_pattern
- Having all your core logic be invoked with the same method name—call—can be incredibly confusing, as
compared to methods that say what they do.
- You cannot share code using private methods because your class may only have one public method. Code
re-use through private methods is extremely powerful, and this pattern makes that difficult or impossible to
do.
- Collecting parameters in one method (the constructor) and using them in another (call) splits up core
logic for no benefit. It also can make complex routines more difficult to understand since parameters are
initialized far from where they are used.
This isn’t to say that creating a specialized set of classes that respond to the same interface is always bad. But, as
a default way of designing your core business logic, “service objects”—AKA the command pattern—is not a good
one.
#### 15.3.3 Dependency Injection also Obscures Behavior
_Dependency Injection_ involves passing _all_ needed dependent objects to the class that needs them. This means that
your business logic code will never call.new(since this creates objects, and those should be injected) and never
refer to a class directly (since, even though a class is an object, the object should be injected, not pulled out of the
air).
OurWidgetsCreatormight look like this, if it were implemented using dependency injection:
**class** WidgetsCreator
**def** initialize(notifier:,
sales_tax_api:,
widget_repository:)
```
@notifier = notifier
@sales_tax_api = sales_tax_api
@widget_repository = widget_repository
```
```
end
```
```
def create_widget(widget_params)
widget = widget_repository.create(widget_params)
if widget.valid?
notifier.notify( :widget , widget.id)
sales_tax_api.charge_tax(widget)
end
end
```
private
```
attr_reader :notifier , :sales_tax_api , :widget_repository
```
**end**
This might seem nice—we’ve removed hard dependencies and deferred configuring this object to somewhere else,
allowing this object to focus only on the logic it exists to implement. But this has obscured reality.
The reality is that this logic _is coupled_ toWidgetandThirdParty::Tax. There was no requirement for alternative
implementations of these classes. Thus, the class has behavior that is not required or needed. This means that all
callers must now encode this truth about the system, _or_ we must introduce a new set of classes to manage the
construction of objects of this class.
In a language like Java, where mocking dependencies is quite difficult, you have to design your code this way to
avoid complicated tests. In Ruby, there is no need—we can mock whatever we like. Dependency injection ends up
creating classes that are either more flexible than they need to be, or appear to be more flexible, but actually
aren’t.
That said, sometimes a class _does need_ to be flexible. Some classes are designed to make use of an object that
conforms to some well-known interface. In that case, dependency injection is a great pattern. You just don’t need
to use it by default. Flexibility leads to complexity, and a key way to achieve sustainability is to avoid _unneeded_
complexity.
### Up Next
This chapter was a lot of theory and rhetoric and light on useful examples. If you can bear with me, the impact
of the guidelines outlined here will be more apparent with an end-to-end example (which will also afford us to
talk about testing). We’ll get to that after the following chapter. We must return to models and see how stuff like
callbacks, validations, and other model-related features fit into all this. That’s what’s next.
## 16
# Models, Part 2
```
This section’s code is in the folder16-01/of the sample code.
```
Now that we’ve had an intro to models, a full discussion of business logic, and a journey through database design,
I want to cap off the models discussion by talking about validations, callbacks, scopes, and testing. Then, in the
next chapter, we can see an end-to-end example of how this all fits together, which I think will paint a complete
picture of the sustainable approach to business logic.
I’ve made the point several times to keep business logic out of Active Records, but I’ve also heavily implied that
we should be using validations, which are a form of business logic. We also talked briefly about managing queries,
along with a handful of references to avoid callbacks. This chapter will cover all of these topics.
Let’s start with validations, which are great at user experience management and not so great at data integrity.
### 16.1 Validations Don’t Provide Data Integrity
When we discussed database modeling in “The Database” on page 191, we spent a fair bit of time talking about
how to enforce the types of data that get stored, in particular ensuring that only valid values could be stored in
the database.
This is ostensibly what Rails validations exist to do, and we even used a validation for this purpose in that chapter.
The reality is that Rails validations absolutely cannot ensure data integrity. If you design your system as if they do,
you will become confused about how invalid data ends up in the database. The only tool that _can_ ensure data
integrity is the database itself.
Let’s go over _why_ Rails validations can’t provide data integrity, as this is not often obvious to developers. There
are three reasons.
- Any code that accesses the database outside your Rails app won’t use your validations.
- Rails provides a public API on each Active Record to allow bypassing validations.
- Some validations don’t actually work due to race conditions.
The biggest reason for me is the first one: someone else might access the database.
#### 16.1.1 Outside Code Naturally Skips Validations
Although we’d like to think that the database is a private, encapsulated service only available to our Rails app, this
is not often the case. Developers or system administrators occasionally need to connect to the database directly to
address production issues. We may have one-off batch jobs that simply _have_ to run outside our Rails app (or that
we may want to). We might even allow other apps to write to our database as a means of application integration.
You might think these types of scenarios are process or system architecture failures. I assure you, they are very
real and often the result of carefully-managed trade-offs to deliver value at low cost. To put it another way, if your
app architecture falls apart when an external process accesses its database, you will either have to live with bad
data, or pay a constant _political_ carrying cost keeping those external processes away from your database. See the
sidebar “Machine Learning Integration in Postgres” below for an example.
#### Machine Learning Integration in Postgres
```
In the early days at Stitch Fix, there was a small engineering team and a very small data science team: one person
named Bhaskar. Bhaskar produced the Stitch Fix styling algorithm, which was the proprietary process by which our
inventory was personally matched to each customer.
The output of Bhaskar’s algorithm was a list of every piece of clothing we sold, cross-referenced against every
customer to produce a “match score” that told us how likely that customer was to buy that piece of clothing, according
to the algorithm. The way this was integrated into the website was a database table. Bhaskar and the engineering
team agreed that this one table would be read-only to us, and write-only to him.
If we had instead insisted on some sort of architectural purity by which writing to the database was forbidden, it
would’ve created tons of work for everyone, delay the delivery of value to the business, and result in a carrying cost
we didn’t need to bear. At the size Stitch Fix is now, preventing direct database integration is a great idea that helps
teams manage their respective apps. At that early stage, however, it would’ve been a terrible decision. Integrating at
the database was the right call.
```
Of course, it doesn’t require an outside system to circumvent Rails validations. Rails will happily let _you_ do it!
#### 16.1.2 Rails’ Public API Allows Bypassing Validations
All Active Records have the methodupdate_column, which updates the database directly, skipping validations.
The existence of this method (and others that allow it likesave(validate: false)) implies that there are times
when your validations may not apply. If that’s not actually true—if your validations should always apply—there’s
no way to achieve that with Active Record.
And _this_ means that no matter how well-factored your code is, it can end up writing data that violates the domain,
either due to a misunderstanding by a developer, a bug, or a mistake made in a production Rails console.
The database, on the other hand, does not allow such circumvention, so when you encode a domain rule in the
database, misunderstandings, bugs, and mistakes will generate errors, but they won’t result in bad data being in
the database.
Of course, even ifupdate_columndidn’t exist, not all validations actually work.
#### 16.1.3 Some Validations Don’t Technically Work
I’m hard-pressed to meet a Rails developer that has not run afoul ofvalidates_uniqueness_of, which is a
validation that seeks to ensure a given value is unique. The documentation for this method^1 spends a good
amount of space outlining why this validation doesn’t really work:
```
Using this validation method in conjunction withActiveRecord::Base#savedoes not guarantee the absence of
duplicate record insertions, because uniqueness checks on the application level are inherently prone to race
conditions.
```
The implementation ofvalidates_uniqueness_ofis to query the database for the value that’s about to be saved.
If that value isn’t found, the record is considered valid and thus saved. But, if another record with the same value
is saved during that time, both records are saved, thus violating our rules about uniqueness.
This isn’t to say thatvalidates_uniqueness_ofisn’t useful, it’s just not able to guarantee uniqueness. The only
way to do that is what we did previously: create a database index.
This leads nicely to the next section, because while Rails validations cannot provide data integrity, they are an
amazing tool for managing the user experience around data validation.
### 16.2 Validations Are Awesome For User Experience
In the previous chapter on writing migrations on page 205, we created a validation to constrain the maximum
value of a widget’s price. We didn’t use the database because we decided this particular domain rule wasn’t stable
and we wanted flexibility that comes with code changes to be able to easily change it later. This won’t ensure the
database contains only valid values, but it was a trade-off we made.
But validations _really_ shine at something else: managing the user experience. If we were to create a form to
add a widget, and a user provided a blank value, they would get an exception from the app. That’s not a great
experience. By adding a presence validation to the widget, we can then access structured error information to
present to the user in a friendly and helpful way.
This coupling of validations, errors, and views is a big reason why working with Rails feels productive. When
we call.valid?on an Active Record (or Active Model), it will populate theerrorsattribute with a rich data
structure allowing us to give the user a detailed breakdown of all the validation errors.
Of course, these kinds of validations are technically business logic, which I went through great pains to convince
you _not_ to put in an Active Record. When people say that programming is all trade-offs, it’s true.
We can either keep all business logic out of our models, which requires throwing out the Rails validation API (and
presumably building our own replacement), _or_ we can let a little bit of our business rules leak into our models
and get access to an extremely powerful API for managing the user experience.
I choose the latter and you should, too. Just know that you are making a trade-off.
Speaking of trade-offs, it might seem that using both validations _and_ database constraints is creating a duplication
of effort. If there is aNOT NULLon the widget name in the database and avalidates :name, presence: trueon
the model, aren’t we creating problematic duplication?
(^1) https://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html
It’s true that if the rules around widget names change, you’ll have to modify the database and the model. You
might have to change a whole bunch of things. That doesn’t mean all of that code is duplicative. The database
constraints prevent bad data from getting into our database. The validations assist the user in providing good
data. Although they are related in what they do and the way they do it, they aren’t the same things.
The only other point to mention about validations is that you can use them on Active Models as well.
ActiveModel::Validationsprovides most of what you get with an Active Record. This means that you can use
validations on your non-database-backed resources. This wasn’t always the case with Rails, so it’s great that the
core team has made it available!
Let’s talk about callbacks next.
### 16.3 How to (Barely) Use Callbacks
Active Record has a detailed set of callbacks^2 available that allow you to run code at various points of a model’s
life-cycle. The use of these callbacks is hotly debated, and their proper intended use is unclear. My suggestion is
to treat them for what they actually are, which is hooks that allow code to run during the lifecycle of various
database-related activities.
For example,before_saveis called only if the mode passes validations, after the database transaction has been
opened, but before the database has been updated. If you need to run code at that exact moment,before_saveis
what you want to use. Practically speaking, you almost never need to run code at this exact moment.
Prior to Rails 7.1,before_validationwas a useful callback to normalize data before validating it and writing it.
For example, you could coerce a blank value tonil:
before_validation **do
if** self.name.blank?
self.name **=** nil
**end
end**
Rails 7.1 introducedActiveRecord::Normalization^3 , which alleviates the need for this by using the new
normalizesmethod:
# app/models/widget.rb
belongs_to **:widget_status**
validates **:price_cents** ,
**numericality: { less_than_or_equal_to:** 10_000_00 **}**
→ normalizes **:name** , **with: ->** (name) **{** name.blank?? nil : name **}
end**
(^2) https://guides.rubyonrails.org/active_record_callbacks.html
(^3) https://api.rubyonrails.org/classes/ActiveRecord/Normalization.html
The only other common use for callbacks I can think of is to collect statistics about the use of certain tables. For
example, if you are trying to deprecate a database table, you may want to add some logging around the use of
that table in your code. You could do this with theafter_commitcallback:
**class** OldStuff **<** ApplicationRecord
after_commit **do**
Rails.logger "#{caller **[** 0 **]** } is using OldStuff"
**end
end**
In general, however, you want to avoid putting business logic in callbacks, and you _especially_ want to avoid
making network calls inside callbacks. Even something seemingly simple like queuing a job after you write a
record can create serious problem. Consider this code that sends an email when a widget with a high price is
created:
**class** Widget **<** ApplicationRecord
after_create **do |** record **|
if** (record.price_cents **>** 10_000)
HighPriceMailer.notify_admins(record).deliver_later
**end
end
end**
Sinceafter_createruns inside a database transaction, this code will hold that transaction open while
deliver_latercompletes. If this is set up to queue a job, and you are using Resque or Sidekiq (the two most
popular job queueing systems for Rails), this means you are making a network call to Redis while holding a
database transaction open.
If there is high activity on theWIDGETStable, or on that specific row, this will create locks in the database. These
locks will cause the application to block and eventually cascade into failures that will seem to have nothing to do
with database transactions or Redis. I have seen this happen first hand at far below the scale you might think
could cause this.
You can avoid this by treating callbacks for what they are: a means to run code during specific phasers of a
database operation. Describing your logic in those terms usually points out the problem. Would anyone design
a system that made network requests to a key/value store while holding open a database transaction? Not
intentionally, they wouldn’t.
Next, let’s talk about scopes, which are another feature of Active Record you won’t end up needing much of.
### 16.4 Scopes are Often Business Logic and Belong Elsewhere
In earlier versions of Rails, scopes were bestowed magical powers not available to regular methods. You could
chain scopes together:
Widget.recent.unapproved.chronological
Nowadays, you can achieve this chaining by declaring class methods on your Active Record—there’s no need to
usescopeat all. This is because methods likewherereturn anActiveRecord::CollectionProxy, which is what
allows the chaining to work.
This means that you don’t even have to declare methods on your Active Record in order to query the database
and chain parts of a query you might be building up. For example:
Widget.where("created_at > ?", 4.weeks.ago)**.**
where("status <>'approved'")**.**
order("created_at asc")
Because this is part of the public API on all your Active Records, you should usewhere,order,limitand friends
as needed to implement your business logic.
Only when you see a pattern of duplication should you consider extracting that duplication somewhere. I prefer
the “rule of three”, which states that a third time you do the same thing, extract it somewhere for re-use.
Note also that you may find it better to extract the query logic to a new service. For example, if we find ourselves
constantly needing “fresh” widgets, but the definition of “fresh” is based on business rules, it might make more
sense to create aFreshWidgetLocator.
Conversely, if we are frequently needing all widgets created in the last day, that’s less about business logic and
more about manipulating data directly. That would be fine as a class method onWidgetlikecreated_in_last_day.
Although we’ve seen a few model tests already, now is a good time to talk about how to think about testing what
little code ends up in your models.
### 16.5 Model Testing Strategy
Models tend to be inputs to (and outputs of) your business logic. In many cases, models are only bags of data,
so they don’t require that much testing themselves. That said, there are three considerations related to model
testing:
- Tests for database constraints, like we wrote in “Writing Tests for Database Constraints” on page 212,
naturally belong in the Active Record whose backing table has the constraint.
- Although simple validations might not benefit from tests, complex validations and callbacks certainly do.
- There should be an easy ability to produce reliable and realistic test instances of the model. I prefer Factory
Bot over Rails’ fixtures.
Let’s go through each of these in a bit more detail.
#### 16.5.1 Active Record Tests Should Test Database Constraints
We already saw an example of this in the previous chapter, but for completeness, the model is the best place to
put tests of the database constraints since the model is backed by the database table.
When writing these tests, be sure to useupdate_columnso you can modify the database directly. You want your
test to continue to function even if the model gets more validations or callbacks.
Also be sure you assert as closely on the error as you can. I like to watch the test fail to see what error the database
produces. I’ll then craft a regular expression that matches as specifically as possible so that the test will only fail if
the constraint is violated.
#### 16.5.2 Tests For Complex Validations or Callbacks
Validations that are a single line of code aren’t usually worth testing, since they are more like configuration than
actual logic. There’s not much value in testing that you typed the value10_000_00in your model file.
If, however, you make use of more complex validations or use custom validators, you should write a test for this.
If you put _any_ code in a callback beyond basic logging, you may benefit from testing that as well, because the test
will allow you to be precise in which public methods you call on your Active Record are intended to trigger the
callback.
#### 16.5.3 Ensure Anyone Can Create Valid Instances of the Model using Factory Bot
Although it’s not a test of your model, creating a model should also involve ensuring there is a way for others to
create valid and reasonable instances of the model for other tests. Rails provides a test fixture facility, but I find
fixtures difficult to manage at even moderate scale, and have not worked with a team that found them superior to
the popular alternative, Factory Bot.
Factory Bot^4 is a library to create _factories_. Factories can be used to create instances of objects more expediently
than usingnew. This is because a factory often sets default values for each field. So, if you want a reasonable
Widgetinstance but don’t care about the values for each attribute, the factory will set them for you. This allows
code like so:
widget **=** FactoryBot.create( **:widget** )
If you need to specify certain values,createacts very much likeneworcreateon an Active Record:
(^4) https://github.com/thoughtbot/factory_bot
widget **=** FactoryBot.create( **:widget** , **name:** "Stembolt")
A factory can also create any needed associated objects, so the above invocations will create (assuming we’ve
written our factories properly) a manufacturer with an address as well as a widget status.
To generate dummy values, I like to use Faker^5. Faker can provide random, fake values for fields of various types.
For example, to create a realistic email address on a known safe-for-testing domain likeexample.com, you can
writeFaker::Internet.safe_email.
While Faker does introduce random behavior to your tests, I view this as a feature. It makes sure your tests don’t
implicitly become dependent on values used for testing. You can always re-run tests using a previous random
seed if you need to debug something.
Let’s set it all up. We’ll use thefactory_bot_railsgem since that sets up internals for a Rails app automatically
as well as brings in thefactory_botgem. They go in the development and test groups.
# Gemfile
```
# gem "image_processing", "~> 1.2"
```
group **:development** , **:test do**
→ # We use Factory Bot in place of fixtures
→ # to generate realistic test data
→ gem "factory_bot_rails"
→ # We use Faker to generate values for attributes
→ # in each factory
→ gem "faker"
# See https://guides.rubyonrails.org/debugging_rails_applic...
gem "debug", **platforms:** %i[ mri windows ]
**end**
> bundle install
«lots of output»
It’s important that our factories produce instances that pass validations and satisfy all database constraints. To
help us manage this, Factory Bot providesFactoryBot.lint, which will create all of the configured factories and
raise an exception if any fail to create due to constraint or validation failures.
I like to wrap a call to this in a test so it runs as part of our test suite. Let’s do that before we actually make any
factories:
(^5) https://github.com/faker-ruby/faker
# test/lint_factories_test.rb
require "test_helper"
**class** LintFactoriesTest **<** ActiveSupport **::** TestCase
test "all factories can be created" **do**
FactoryBot.lint **traits:** true
**end
end**
Now, let’s create a factory for addresses, and we’ll initially create it to produce invalid data (so we can see our lint
test fail).
Factories traditionally go intest/factories(orspec/factoriesif using RSpec). The code itself is revealing of
intent and does what it appears to do, but relies on meta-programming to do it. I’ll explain how it works, but first,
here’s what it looks like:
# test/factories/address_factory.rb
FactoryBot.define **do**
factory **:address do**
street **{** Faker **::** Address.street_address **}
end
end**
You can likely reason that this produces anAddresswhosestreetvalue comes from theFakercall being made.
But I want to explain a bit about how that works. First,factory :addressknows to create an instance ofAddress,
just asfactory :widget_statuswould know to create an instance ofWidgetStatus. Factory Bot is following the
various Rails conventions^6.
Second, the method calls with blocks inside thefactory :addressblock are declaring test values to use for
attributes ofAddress. BecauseAddresshas astreetattribute, the dynamically-created methodstreetis how we
indicate the value to use for it when creating anAddress.
In this case, the block being given is evaluated each time we want an instance in order to get the value. That
value isFaker::Address.street_address, which returns a randomly generated, realistic street address like “742
Evergreen Terrace”.
Any attribute we don’t list will have a value ofnil. Since we omitted zip and since zip is required by the database,
running our lint test should fail:
(^6) I’ve long internalized this sort of thing, but I can’t understand why using:addressis better than using the class name—Addressor
"Address". The latter is super clear, the same amount of typing, and doesn’t require explanation.
> bin/rails test test/lint_factories_test.rb || echo \
Test failed
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 63490
# Running:
E
Error:
LintFactoriesTest#test_all_factories_can_be_created:
FactoryBot::InvalidFactoryError: The following factories are...
* address - PG::NotNullViolation: ERROR: null value in colu...
DETAIL: Failing row contains (3, 67326 Effertz Divide, null...
(ActiveRecord::NotNullViolation)
test/lint_factories_test.rb:5:in `block in <class:LintFa...
bin/rails test test/lint_factories_test.rb:4
Finished in 0.468395s, 2.1350 runs/s, 0.0000 assertions/s.
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
Test failed
Let’s fix the factory so it produces a validAddress:
# test/factories/address_factory.rb
FactoryBot.define **do**
factory **:address do**
street **{** Faker **::** Address.street_address **}**
→ zip **{** Faker **::** Address.zip **}
end
end**
Now, our lint test should pass:
> bin/rails test test/lint_factories_test.rb
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 12333
# Running:
.
Finished in 0.097839s, 10.2208 runs/s, 0.0000 assertions/s.
1 runs, 0 assertions, 0 failures, 0 errors, 0 skips
Let’s make a factory for manufacturer, which requires an address. Factory Bot provides a shorthand for creating
related objects:
# test/factories/manufacturer_factory.rb
FactoryBot.define **do**
factory **:manufacturer do**
name **{** Faker **::** Company.name **}**
address
**end
end**
The call toaddresson its own works because Factory Bot knows this is not a normal attribute, but a reference to
a related object. Since there is a factory for that relation, Factory Bot will use that as the value for address.
One thing that can lead to flaky tests is when randomness ends up producing the same value multiple times in a
row for a field that must be unique. While it doesn’t happen often, it does happen. Faker can manage this by
callinguniqueon any class before calling the data-generating-method. Let’s use this in our widget status factory,
because widget statuses must be unique (we should’ve used that on the Manufacturer name as well).
# test/factories/widget_status_factory.rb
FactoryBot.define **do**
factory **:widget_status do**
name **{** Faker **::** Lorem.unique.word **}
end
end**
Faker::Loremwill use Lorem Ipsum^7 to come up with a fake word. Because we usedunique, noWidgetStatus
instance we create with this factory will ever have the same value.
Note that we did not use one of the known values for widget status. This is a bit of a trade-off. Even though
widget statuses have a set of known valid values, since those values are in the database, our code should generally
not be coupled to them. Thus, a test that needs any old widget status should not care what the value is.
(^7) https://en.wikipedia.org/wiki/Lorem_ipsum
That said, if we _do_ need to create a status from one of the known valid values, we can do that like so:
widget **=** FactoryBot.create(
**:widget** ,
**status:** FactoryBot.create(
**:widget_status** ,
**name:** "Approved")
)
For completeness, let’s create the widget factory.
# test/factories/widget_factory.rb
FactoryBot.define **do**
factory **:widget do**
name **{** Faker **::** Lorem.unique.word **}**
price_cents **{** Faker **::** Number.within( **range:** 1 **..** 10_000_00) **}**
manufacturer
widget_status
**end
end**
Our lint test should still pass:
> bin/rails test test/lint_factories_test.rb
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 38418
# Running:
.
Finished in 0.120416s, 8.3045 runs/s, 0.0000 assertions/s.
1 runs, 0 assertions, 0 failures, 0 errors, 0 skips
As a final step, let’s replace the setup code in our widget test with factories instead.
# test/models/widget_test.rb
```
require "test_helper"
```
**class** WidgetTest **<** ActiveSupport **::** TestCase
setup **do**
×# widget_status = WidgetStatus.create!(name: "fresh")
×# manufacturer = Manufacturer.create!(
×# name: "Cyberdyne Systems",
×# address: Address.create!(
×# street: "742 Evergreen Terrace",
×# zip: "90210"
×# )
×# )
×# @widget = Widget.create!(
×# name: "Stembolt",
×# manufacturer: manufacturer,
×# widget_status: widget_status,
×# price_cents: 10_00
→ @widget **=** FactoryBot.create(
→ **:widget**
)
**end**
test "valid prices do not trigger the DB constraint" **do**
That single line of code will use the widget factory to create the widget, which will in turn create a widget status
and a manufacturer, which itself will in turn create an address. Note that you can callbuildto create in-memory
versions of these objects without touching the database.
This test should pass:
> bin/rails test test/models/widget_test.rb
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 63177
# Running:
..
Finished in 0.120262s, 16.6303 runs/s, 33.2607 assertions/s.
2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
Factory Bot requires understanding a bit of implicit meta-programming, but I find that once you learn how it
works, it’s much simpler to maintain a suite of test data than Rails’ fixtures.
Fixtures require editing YAML files whose dynamic behavior comes from ERB, and I find this clunkier than using
Ruby code inside of Factory Bot’s domain-specific language (DSL). If you disagree and really like fixtures, I would
still encourage you to create valid fixture data for all your models so that you can access model instances easily in
your tests.
### Up Next
What a journey! It’s now time to look at an end-to-end example. I realize we have not discussed controllers, jobs,
mailers, and other stuff like that, but now that we understand the relationship between the view, models, the
database, and business logic, it’s time to see a real example. That’s what we’ll do next.
## 17 End-to-End Example
We haven’t talked about controllers, mailers, jobs, or mailboxes yet, but we’ve gotten far enough in that I think
a more involved is example will help codify what we’ve learned so far. It should crystallize the benefits of the
approach toward managing business logic. What you’ll see is that we avail ourselves of all that Rails has to offer,
but our core business logic code will be much more sustainable than if we’d put everything on our Active Records.
### 17.1 Example Requirements
We’ll build a feature to create widgets. In our hypothetical domain, creating a widget is a complex process. It’s
not just about putting valid data into thewidgetstable.
Here is what has to happen around creating widgets:
- Users must provide a name, manufacturer, and price. These will be validated using the domain rules we’ve
discussed previously: the name must exist and be unique per manufacturer, and the price must be within 1
cent and $10,000.
- Additionally, a widget name must be more than five characters.
- Widgets are created with the status of “Fresh”.
- Widgets for manufacturers created before 2010 may not be priced below $100, for legacy reasons that I’m
sure many of you can imagine some version of from a past project.
- When a widget is created for more than $7,500, email the financial staff.
- When a widget is created for a manufacturer created in the last two months, email the admin staff.
This might seem convoluted, but I have rarely experienced real world requirements that aren’t like this.
In the remainder of the chapter, we’ll write the code to implement these requirements, starting with the UI. We’ll
follow the guidelines laid out already in the book and proceed to write a system test, then implement the business
logic.
### 17.2 Building the UI First
```
This section’s code is in the folder17-02/of the sample code.
```
No matter how the UI must be styled, it needs to allow the user to select a manufacturer, enter a widget name
and price, and see any validation errors related to the data entered. We’ll create the UI using semantic markup
that is connected to the controller, which we’ll leave pretty bare. We’ll freshen up the UI using our design system,
then write a system test. When that system test is done, we can start on the business logic.
Before we create the UI, we’ll need to set up a route and some controller methods. We should also create some
development data indb/seeds.rb.
#### 17.2.1 Setting Up To Build the UI
First, we’ll modify the existing widgets resource inconfig/routes.rbto allow:new, andcreate:
# config/routes.rb
Rails.application.routes.draw **do**
→ resources **:widgets** , **only: [ :show** , **:index** , **:new** , **:create ]**
resources **:widget_ratings** , **only: [ :create ]**
Next, we’ll create some basic controller methods so our views can be rendered. Fornewwe’ll create an empty
Widget, but we’ll also expose the list of manufacturers, since we need that for a drop-down. If you recall from the
section on exposing instance variables on page 89, we ideally expose only one instance variable for the resource
in question, but we can also expose reference data when needed. The list of manufacturers qualifies as reference
data.
# app/controllers/widgets_controller.rb
**class** WidgetsController **<** ApplicationController
→ **def** new
→ @widget **=** Widget.new
→ @manufacturers **=** Manufacturer.all
→ **end**
→ **def** create
→ render **plain:** "Thanks"
→ **end**
**def** show
manufacturer **=** OpenStruct.new(
**id:** rand(100),
We should also create some data to use for development.
#### 17.2.2 Create Useful Seed Data for Development
Rails’ documentation is unclear on the purpose of seed data, but it’s commonly used to seed _development_ data,
and that’s how I view it as well. Because we have set up Factory Bot to create realistic, yet fake data for tests, we
can use that for our seed data, too.
There are a few considerations for seed data. First, it should run only in development, so we’ll need to check for
that. Second, it should ideally be idempotent without requiring a full database reset. We might not be able to
do this entirely in the seed data file when the data model gets more complex, but for now we can, so we’ll use
destroy_allto delete all the data first.
Lastly, we want data that’s useful in building our UI and exercising the app manually. To that end, we want to
make sure a widget exists so that we can exercise trying to use the same name for two widgets belonging to the
same manufacturer.
Because we are using Faker, it could be annoying to have randomly-changing names, so for this particular case,
we’ll give explicit names. You could give explicit names for everything if you like. It depends on what you need
from the development data.
We’ll replacedb/seeds.rbwith the following:
# db/seeds.rb
**if!** Rails.env.development?
puts "[ db/seeds.rb ] Seed data is for development only, " **+**
"not #{Rails.env}"
exit 0
**end**
require "factory_bot"
Widget.destroy_all
Manufacturer.destroy_all
Address.destroy_all
WidgetStatus.destroy_all
puts "[ db/seeds.rb ] Creating development data..."
FactoryBot.create( **:widget_status** , **name:** "Fresh")
10.times **do**
FactoryBot.create( **:manufacturer** )
**end**
cyberdyne **=** FactoryBot.create( **:manufacturer** ,
**name:** "Cyberdyne Systems")
FactoryBot.create( **:widget** , **name:** "Stembolt",
**manufacturer:** cyberdyne)
puts "[ db/seeds.rb ] Done"
Let’s go ahead and run it now to make sure it’s working:
> bin/rails db:seed
[ db/seeds.rb ] Creating development data...
[ db/seeds.rb ] Done
Note that this will be run as part ofdb:reset, so there’s no need to change ourbin/setupscript. It’ll now insert
this data into the database after re-creating it.
_Now_ , let’s build the UI.
#### 17.2.3 Sketch the UI using Semantic Tags
Our UI will live inapp/views/widgets/new.html.erb. We’ll need a form that has fields for name and price, as
well as a select for manufacturer and a submit button.
Here’s the first pass:
<%# app/views/widgets/new.html.erb %>
<section>
<h1>New Widget</h1>
<%= form_with model: @widget **do** |f| %>
<%= f.label **:name** %>
<%= f.text_field **:name** %>
```
<%= f.label :price_cents %>
<%= f.text_field :price_cents %>
```
<%= f.label **:manufacturer_id** %>
<%=
f.select **:manufacturer_id** ,
options_from_collection_for_select(
@manufacturers, "id", "name"
),
{
include_blank: "-- Choose --",
}
%>
<%= f.submit "Create" %>
<% **end** %>
**</section>**
Semantically, this is what is required to make the feature work. Let’s make sure this is working by navigating to
/widgets/newbefore we embark on our styling adventure. It should look amazingly awful, as in the screenshot
below.
```
Figure 17.1: Bare-bones New Widget Page
```
We _could_ create the system test now, but I find it easier to get at least some of the styling done first, just in case
we end up needing some odd markup that could affect the test.
These are the improvements we need to make:
- The form should be better laid out and spaced.
- The manufacturers should be sorted by name.
- We need placeholders and should auto-focus the name field.
- We don’t want the user to know about “cents”, so that field should appear to be just “price”.
Let’s address those next.
#### 17.2.4 Provide Basic Polish
First, we’ll deal with the label forprice_cents. We can do that by editingconfig/locales/en.yml, which is
where Rails will look for labels to use (specifically for English).
# config/locales/en.yml
en **:**
hello **:** "Hello world"
→ activerecord **:**
→ attributes **:**
→ widget **:**
→ price_cents **:** "Price"
```
This incantation is not easy to find if you don’t know that the problem you are solving is one about locale and
internationalization (and that “internationalization” is often abbreviated as “i18n”^1 ). The documentation is in the
Rails Guide for Internationalization^2.
We can address the placeholders and auto-focus like so:
```
```
<%# app/views/widgets/new.html.erb %>
```
```
<h1>New Widget</h1>
<%= form_with model: @widget do |f| %>
<%= f.label :name %>
→ <%= f.text_field :name , autofocus: true,
→ placeholder: "e.g. Stembolt" %>
```
```
<%= f.label :price_cents %>
<%= f.text_field :price_cents %>
```
```
And for the price field:
```
```
<%# app/views/widgets/new.html.erb %>
```
```
placeholder: "e.g. Stembolt" %>
```
```
<%= f.label :price_cents %>
→ <%= f.text_field :price_cents , placeholder: "e.g. 123.45" %>
```
```
<%= f.label :manufacturer_id %>
<%=
```
Note that we aren’t using the placeholder as a label—that’s not what placeholder text is for.
Lastly, let’s sort the manufacturers. We do this in the view, because it is truly a view concern. The controller’s job
(as we’ll discuss later) is to provide data to the view. The view’s job is to make it consumable by the user.
```
<%# app/views/widgets/new.html.erb %>
```
(^1) I use an editor that was created in the 1970’s and I can easily auto-complete the word “internationalization”, but I guess that’s just too
difficult so we have to have the most ridiculous means of abbreviating technical words possible: count the number of letters in the word and
subtract two. Type the first letter of the word, followed by that count (minus two, remember), followed by the last letter of the word. Sigh.
This has brought us i18n, l10n, a11y, o11y, k8s, and Leto knows how many other nonsense gate-keeping terms.
(^2) https://guides.rubyonrails.org/i18n.html
##### <%=
f.select **:manufacturer_id** ,
options_from_collection_for_select(
→ @manufacturers.sort_by(& **:name** ),
→ "id", "name"
),
{
include_blank: "-- Choose --",
That was the easy part. The hard part is making it look semi-decent. In lieu of a wireframe and spec from a
designer we’ll use our judgement and do our best. That will include styling validation errors.
#### 17.2.5 Style the Form
First, let’s see the form without any validation errors. A mockup is shown on the next page. Here’s the code for
the template:
```
Figure 17.2: Create Widget Mockup
```
<%# app/views/widgets/new.html.erb %>
<section **class** ="center w-two-thirds helvetica pa3">
<h1>New Widget</h1>
<%= form_with model: @widget **do** |f| %>
**<div class** ="mb3" **>**
<%= f.text_field **:name** , **class** : "db w-100 pa2 mb1",
autofocus: true, placeholder: "e.g. Stembolt" %>
<%= f.label **:name** , **class** : "fw4 i" %>
**</div>
<div class** ="mb3" **>**
<%= f.text_field **:price_cents** , **class** : "db w-100 pa2 mb1",
placeholder: "e.g. 123.45" %>
<%= f.label **:price_cents** , **class** : "fw4 i" %>
**</div>
<div class** ="mb3" **>**
<%=
f.select **:manufacturer_id** ,
options_from_collection_for_select(
@manufacturers.sort_by(& **:name** ), "id", "name"
),
{
include_blank: "-- Choose --",
},
{
**class** : "db w-100 pa2 mb1"
}
%>
<%= f.label **:manufacturer_id** , **class** : "fw4 i" %>
**</div>
<div class** ="tr" **>**
<%= f.submit "Create",
**class** : "ba br3 ph3 pv2 white bg-dark-blue" %>
**</div>**
<% **end** %>
**</section>**
You can see what it looks like in the screenshot below.
A way to get comfortable with Tachyons while experiencing the value of a design system is to download this code
and play with the classes. In particular, the classes for padding (classes that start with a “p”) or margin (classes
that start with an “m”) are good to play with. Change their values to increase or decrease the spacing between
components. They will all still look nice and line up. This is the power of a design system.
The last thing to do is style the errors.
#### 17.2.6 Style Error States
There are two things to do here. First, we want a top level red box telling the user that there are errors. We then
want each field to indicate the specific errors that happened.
```
Figure 17.3: First Pass at Styling Widget Creation
```
The top level error code looks like so:
<%# app/views/widgets/new.html.erb %>
<section **class** ="center w-two-thirds helvetica pa3">
<h1>New Widget</h1>
→ <% **if** @widget.errors.present? %>
→ **<aside**
→ **class** ="pa3 tc ba br2 b--dark-red dark-red
→ bg-washed-red b mb3" **>**
→ The data you provided is not valid.
→ **</aside>**
→ <% **end** %>
<%= form_with model: @widget **do** |f| %>
**<div class** ="mb3" **>**
<%= f.text_field **:name** , **class** : "db w-100 pa2 mb1",
This might feel like a re-usable component or that the big mess of classes should be extracted to some sort of
error-dialogclass. Resist these feelings. If we need this exact markup again, we can extract it into a re-usable
component by creating a partial or View Component. Since we only have this in one place, there’s no value in
extracting it or making it re-usable.
What we _will_ want to be re-usable is the field-level error styling. Let’s style the error using the label. When there’s
no error, we’ll show the label as normal. When there _is_ an error, we’ll show the error messages as the label. The
messages contain the field name so this should be reasonable.
Because the code will be the same for all three fields, we can extract it to a re-usable component (when I was
developing this, I didn’t plan on making a component, but after the third repetition of the same thing—the “rule
of three”—it seemed like a good idea).
We’ll use a View Component for this and call itLabelWithErrorComponent. It will require the Active Record, the
name of the field it’s labeling, and the Rails form object.
> bin/rails g component LabelWithError record field_name form
create app/components/label_with_error_component.rb
invoke test_unit
create test/components/label_with_error_component_t...
invoke erb
create app/components/label_with_error_component.ht...
Although a View Component’s template can access instance variables, I find it better to expose them as methods.
The Rails controller convention, while worth embracing in that context, is not a great object-oriented design
pattern.
# app/components/label_with_error_component.rb
```
# frozen_string_literal: true
```
**class** LabelWithErrorComponent **<** ViewComponent **::** Base
→ attr_reader **:record** , **:field_name** , **:form**
**def** initialize(record:, field_name:, form:)
@record **=** record
@field_name **=** field_name
The ERB for the component can handle all the logic of checking for an error. It’s inapp/components/label_with_error_component.html.erb.
<%# app/components/label_with_error_component.html.erb %>
<% **if** record.errors[field_name].blank? %>
<%= form.label field_name, **class** : "fw4 i" %>
<% **else** %>
<%= form.label field_name,
record.errors.full_messages_for(field_name).join(", "),
**class** : "i b dark-red" %>
<% **end** %>
With this in place, we replace the label for the name field:
<%# app/views/widgets/new.html.erb %>
<div **class** ="mb3">
<%= f.text_field **:name** , **class** : "db w-100 pa2 mb1",
autofocus: true, placeholder: "e.g. Stembolt" %>
→ <%= render(
→ LabelWithErrorComponent.new(record: @widget,
→ field_name: **:name** ,
→ form: f)
→ ) %>
**</div>
<div class** ="mb3" **>**
<%= f.text_field **:price_cents** , **class** : "db w-100 pa2 mb1...
Repeat for price:
<%# app/views/widgets/new.html.erb %>
<div **class** ="mb3">
<%= f.text_field **:price_cents** , **class** : "db w-100 pa2 mb1...
placeholder: "e.g. 123.45" %>
→ <%= render(
→ LabelWithErrorComponent.new(record: @widget,
→ field_name: :price_cents,
→ form: f)
→ ) %>
</div>
<div class="mb3">
<%=
And lastly for manufacturer:
<%# app/views/widgets/new.html.erb %>
**class** : "db w-100 pa2 mb1"
}
%>
→ <%= render(
→ LabelWithErrorComponent.new(
→ record: @widget,
→ field_name: **:manufacturer_id** ,
→ form: f)
→ ) %>
**</div>
<div class** ="tr" **>**
<%= f.submit "Create",
To reveal this styling, we’ll manually add errors to the widget in the controller:
# app/controllers/widgets_controller.rb
**class** WidgetsController **<** ApplicationController
**def** new
@widget **=** Widget.new
→ @widget.errors.add( **:name** , **:blank** )
→ @widget.errors.add( **:manufacturer_id** , **:blank** )
→ @widget.errors.add( **:price_cents** , **:not_a_number** )
@manufacturers **=** Manufacturer.all
**end**
You can see the complete styling in the screenshot “New Widget Error UI” below.
Before writing the system test, here’s a recap of how we went about this, following the guidelines discussed in
previous chapters.
- We started with semantic HTML.
- We addeddivtags to afford styling.
- We extracted a re-usable component into a View Component, as opposed to extracting only the styling
information as a CSS class.
- We faked out the back-end in order to do the styling we need so we aren’t wrestling with both back-end
logic and front-end styling at the same time.
Next, we should write a system test.
Figure 17.4: New Widget Error UI
### 17.3 Writing a System Test
```
This section’s code is in the folder17-03/of the sample code.
```
In “Fake the Back-end To Get System Test Passing” on page 170, we learned about minimizing the business logic
in play in order to write a system test. Let’s see that in action now.
We want to test major flows, and there are two that I can see: correctly saving a widget and seeing validation
errors. Our system test can’t reasonably test all the back-end business logic, and it doesn’t need to exhaustively
test each possible error case. We really only need to make sure that all fields that could have an error will show
one. Fortunately, we can create a blank widget and this will show validation errors for all three fields.
Since we don’t have JavaScript, our system test can use the standard test case,ApplicationSystemTestCase. Let’s
call the testCreateWidgetTest:
# test/system/create_widget_test.rb
require "application_system_test_case"
**class** CreateWidgetTest **<** ApplicationSystemTestCase
test "we can create a widget" **do
end**
test "we can see validation errors" **do
end
end**
Let’s start with the validation errors, because the back-end is already faked-out to provide errors no matter what.
This test will go to the new widget page, skip filling in any fields, click “Create”, then validate that there are errors
for each field.
# test/system/create_widget_test.rb
```
end
```
test "we can see validation errors" **do**
→ visit new_widget_path
→ click_on("Create")
→ assert_text "The data you provided is not valid"
→ assert_text "Name can't be blank"
→ assert_text "Price is not a number"
→ assert_text "Manufacturer can't be blank"
**end
end**
We need something to happen when we click “Create”, so let’s implementcreateinWidgetsControllerto redirect
back towidgets/new:
# app/controllers/widgets_controller.rb
```
end
```
**def** create
→ redirect_to new_widget_path
**end**
```
def show
```
The test should pass:
> bin/rails test test/system/create_widget_test.rb
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 44550
# Running:
..
Finished in 0.371636s, 5.3816 runs/s, 10.7632 assertions/s.
2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
We are asserting on content, and so this test could be brittle. We need to assert on something, so this is reasonable
enough to get started. As we learned in “Usedata-testidAttributes to Combat Brittle Tests” on page 172, we
can deal with this problem when or if it shows up.
Let’s write the second test for successful widget creation. We’ll know this by landing on the widget show page and
seeing what we entered. This will require some manufacturers to exist in the database, so that the drop-down can
be used. We’ll need some actual validation logic to avoid breaking the test we just wrote.
In other words, we can’t _totally_ fake the back-end. Fortunately, for what we’re testing, we can implement
something without a lot of code. We can have our controller save the widget, add validations toWidget, then
implement this the old-fashioned way.
Let’s write the test first. It should fill in the fields with correct values, hit “Create”, then validate that we’re on the
widget show page.
To do that, we’ll need a widget status and at least two manufacturers. The status can be created in abeforeblock
since it’s needed for pretty much all the tests. For manufacturers, since they are only relevant to the test we are
about to write, we’ll create those at the top of the test.
# test/system/create_widget_test.rb
```
require "application_system_test_case"
```
**class** CreateWidgetTest **<** ApplicationSystemTestCase
→ setup **do**
→ FactoryBot.create( **:widget_status** , **name:** "Fresh")
→ **end**
→ test "we can create a widget" **do**
→ manufacturer **=** FactoryBot.create( **:manufacturer** )
→ other_manufacturer **=** FactoryBot.create( **:manufacturer** )
→ visit new_widget_path
→ fill_in "widget[name]", **with:** "Stembolt"
→ fill_in "widget[price_cents]", **with:** 123
→ select manufacturer.name, **from:** "widget[manufacturer_id]"
→ click_on("Create")
→ assert_selector "[data-testid='widget-name']",
→ **text:** "Stembolt"
**end**
```
test "we can see validation errors" do
```
To make this pass, we have to implementcreate. We’ll do that in the most basic way possible and not worry—yet—
about clean code or reducing duplication or proper use of Rails. Note that when we callrender :newwe must
now passstatus: :unprocessable_entityas well, since Turbo is managing the form submission and requires an
explicit HTTP status.
# app/controllers/widgets_controller.rb
```
end
```
**def** create
→ @widget **=** Widget.create(
→ **name:** params.require( **:widget** ) **[:name]** ,
→ **price_cents:** params.require( **:widget** ) **[:price_cents]** ,
→ **manufacturer_id:** params.require( **:widget** ) **[:manufacturer_id]** ,
→ **widget_status:** WidgetStatus.first)
→ **if** @widget.valid?
→ redirect_to widget_path(@widget)
→ **else**
→ @manufacturers **=** Manufacturer.all
→ render **:new** , **status: :unprocessable_entity**
→ **end
end**
```
def show
```
Remember, this is just to get the system test passing. This is _not_ production-ready code. If we run the test now,
it’ll still fail for two reasons: we aren’t validating all the fields ofWidget, and ourshowmethod still has all that
OpenStructstuff in it, meaning it’s not locating the widget we just created.
First, we’ll add validations toWidget:
# app/models/widget.rb
**}
end**
belongs_to **:widget_status**
→ validates **:name** , **{ presence:** true **}**
→ validates **:manufacturer_id** , **{ presence:** true **}**
validates **:price_cents** ,
**numericality: { less_than_or_equal_to:** 10_000_00 **}**
normalizes **:name** , **with: ->** (name) **{** name.blank?? nil : name...
Stay with me. These aren’t all the validations we might want, but are enough for us to get our system tests
passing. When we move onto the business logic, the system test can serve as a signal that we haven’t broken any
user-facing behavior.
Let’s head back toWidgetsControllerand update theshowmethod to look up theWidgetfrom the database:
# app/controllers/widgets_controller.rb
```
render :new , status: :unprocessable_entity
end
end
```
×# def show
×# manufacturer = OpenStruct.new(
×# id: rand(100),
×# name: "Sector 7G",
×# address: OpenStruct.new(
×# id: rand(100),
×# country: "UK"
×# )
×# )
×# widget_name = if params[:id].to_i == 1234
×# "Stembolt"
×# else
×# "Widget #{params[:id]}"
×# end
×# @widget = OpenStruct.new(id: params[:id],
×# manufacturer_id: manufacturer.id,
×# manufacturer: manufacturer,
×# name: widget_name)
×# def @widget.widget_id
×# if self.id.to_s.length < 3
×# self.id.to_s
×# else
×# self.id.to_s[0..-3] + "." +
×# self.id.to_s[-2..-1]
×# end
×# end
→ **def** show
→ @widget **=** Widget.find(params **[:id]** )
**end
def** index
@widgets **= [**
Note that we removed the monkey-patchedwidget_id. We added this method toWidgetin “Active Record is for
Database Access” on page 181 and called ituser_facing_identifier, so we need to changestyled_widget_id
inapp/helpers/application_helper.erbto use that instead.
# app/helpers/application_helper.rb
**def** styled_widget_id(widget)
content_tag( **:span** ,
→ widget.user_facing_identifier.rjust(7,"0"),
**style:** "font-family: monospace")
```
end
end
```
The test of this helper is still usingOpenStruct, so we’ll need to change that to useFactoryBot. First, we’ll change
the first test:
# test/helpers/application_helper_test.rb
**class** ApplicationHelperTest **<** ActionView **::** TestCase
test "styled_widget_id < 6 digits, pad with 0's" **do**
→ widget **=** FactoryBot.create( **:widget** , **id:** 1234)
rendered_markup **=** styled_widget_id(widget)
```
assert_match /\D0012\.34\D/,rendered_markup
```
Then, the second one:
# test/helpers/application_helper_test.rb
```
end
```
test "styled_widget_id >= 6 digits, no padding" **do**
→ widget **=** FactoryBot.create( **:widget** , **id:** 987654)
rendered_markup **=** styled_widget_id(widget)
```
assert_match /\D9876\.54\D/,rendered_markup
```
One last thing: we should clean up the explicit error-setting we put in thenewmethod.
# app/controllers/widgets_controller.rb
**class** WidgetsController **<** ApplicationController
**def** new
@widget **=** Widget.new
×# @widget.errors.add(:name, :blank)
×# @widget.errors.add(:manufacturer_id,:blank)
×# @widget.errors.add(:price_cents, :not_a_number)
##### →
```
@manufacturers = Manufacturer.all
end
```
_Now_ , the test should pass:
> bin/rails test test/system/create_widget_test.rb
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 65387
# Running:
Finished in 0.406400s, 4.9213 runs/s, 12.3032 assertions/s.
2 runs, 5 assertions, 0 failures, 0 errors, 0 skips
At this point, we have the UI we want, and we have code to make it behave the way we want, at least as far as the
user experience goes. We also have defined the seam between Rails and the code we have yet to write.
Our code will take a name, a price (in cents?), and a manufacturer ID. It should return, among other things, a
Widgetinstance that, if there are validation errors, makes those available as an Active Record would.
Now we can implement our business logic, as well as test it for all the various edge cases we don’t want to test
through the UI.
### 17.4 Sketch Business Logic and Define the Seam
```
This section’s code is in the folder17-04/of the sample code.
```
Let’s create the service class that will hold our business logic. This will codify the contract between our code and
the controller. We should be able to do this without breaking the system test. Once that’s done, we can then start
to build out the real business logic.
We’ll call the serviceWidgetCreator, and it’ll go inapp/services/aswidget_creator.rb. You’ll need to create
theapp/servicesdirectory. We’ll give it one method,create_widget, and it’ll accept aWidgetinstance initialized
with the parameters received from the UI.
# app/services/widget_creator.rb
**class** WidgetCreator
**def** create_widget(widget)
widget.widget_status **=** WidgetStatus.first
```
widget.save
```
```
Result.new( created: widget.valid?, widget: widget)
end
```
```
class Result
attr_reader :widget
def initialize(created:, widget:)
@created = created
@widget = widget
end
```
**def** created?
@created
**end
end
end**
This may seem like a lot of code has been introduced just to callvalid?on an Active Record, but bear with me. It
will make a lot more sense when we put all the actual business logic here.
Next, we modify the controller to use this class.
# app/controllers/widgets_controller.rb
```
@manufacturers = Manufacturer.all
end
```
**def** create
×# @widget = Widget.create(
×# name: params.require(:widget)[:name],
×# price_cents: params.require(:widget)[:price_cents],
×# manufacturer_id: params.require(:widget)[:manufacturer_id],
×# widget_status: WidgetStatus.first)
×# if @widget.valid?
×# redirect_to widget_path(@widget)
×# else
×# @manufacturers = Manufacturer.all
×# render :new, status: :unprocessable_entity
→ widget_params **=** params.require( **:widget** ).permit(
→ **:name** , **:price_cents** , **:manufacturer_id**
→ )
→ result **=** WidgetCreator.new.create_widget(
→ Widget.new(widget_params)
##### → )
##### →
→ **if** result.created?
→ redirect_to widget_path(result.widget)
→ **else**
→ @widget **=** result.widget
→ @manufacturers **=** Manufacturer.all
→ render **:new** , **status: :unprocessable_entity
end
end**
This looks better. The controller now has no knowledge of business logic. The only thing it knows is what the
service wants, and it uses strong parameters to get that. The only logic it has is related to routing the user to the
right UI, which is what controllers are for.
This means that potentially large changes in the business logic—or its implementation—won’t require this
controller method to change. That’s a good thing.
Let’s run our system test, which should still pass:
> bin/rails test test/system/create_widget_test.rb
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 62887
# Running:
..
Finished in 0.404144s, 4.9487 runs/s, 12.3718 assertions/s.
2 runs, 5 assertions, 0 failures, 0 errors, 0 skips
Nice! We’re almost ready to turn our attention to the business logic, but there’s one thing that’s a bit wrong. We
are passing inprice_cents, but we’ve instructed the user to enter dollars in our placeholder text. Even if we
instruct the user to enter cents, they are going to enter dollars, since it’s more natural.
This is a UI concern that our business logic should not have to worry about. If it wants to receive cents, it should
receive cents. It could, alternately, receive dollars instead. Either way, the controller has to do something, because
the value forprice_centsis a string.
If the service wants dollars, we have to convert that string into aBigDecimal(since usingto_fto make it a float
will lose precision as previously discussed). If the service wants cents, the controller has to also multiply it by 100.
There are a lot of ways to solve this, but in all cases, we want the controller to handle it (we’ll talk more about
why this is in Controllers on page 281). The controller is receiving a string containing dollars, and the service
wants cents (as an integer), so the controller should do the conversion. We’ll do that right in the method:
# app/controllers/widgets_controller.rb
```
:name , :price_cents , :manufacturer_id
)
```
→ **if** widget_params **[:price_cents]** .present?
→ widget_params **[:price_cents] =** (
→ BigDecimal(widget_params **[:price_cents]** ) ***** 100
→ ).to_i
→ **end**
result **=** WidgetCreator.new.create_widget(
Widget.new(widget_params)
)
Our test isn’t affected by the price, because the price is currently not shown in the UI at all. Because of this
conversion, it would be a good idea to find a way to test it, so that if this conversion changed, a test somewhere
would fail. Since the price is _not_ in the UI, let’s add an assertion about the data that gets written, so we at least
have some coverage.
# test/system/create_widget_test.rb
assert_selector "[data-testid='widget-name']",
**text:** "Stembolt"
→ assert_equal 123_00, Widget.first.price_cents
**end**
```
test "we can see validation errors" do
```
This test would’ve failed before the conversion, and now it should pass:
> bin/rails test test/system/create_widget_test.rb
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 64538
# Running:
..
Finished in 0.396362s, 5.0459 runs/s, 15.1377 assertions/s.
2 runs, 6 assertions, 0 failures, 0 errors, 0 skips
And _now_ we have defined our seam: aWidgetinstance is passed in, and a result object is returned that tells the
caller exactly what happened. The result also exposes the possibly-savedWidget.
Note that the controller no longer has to intuit that a valid active record means the process it initiated completed
successfully. After all, creating a widget is more than just writing data into a database. By using the rich result
object (as we discussed in “Return Rich Result Objects... ” on page 219), it can be explicit about what it’s checking
for.
With this seam in place, we can implement the business logic, using the system test to make sure we haven’t
broken the user experience.
### 17.5 Fully Implement and Test Business Logic
```
This section’s code is in the folder17-05/of the sample code.
```
With our seam now defined, I find it easier to switch to a test-first workflow. The logic we have to build is pretty
complex, and this will require a lot of tests.
- Create a valid widget for a manufacturer created three months ago. Check that the status is “Fresh” and
that no emails were sent.
- Create a valid widget with a price of $7,500.01 and make sure the finance staff was emailed.
- Create a valid widget with a manufacturer created 59 days ago and make sure the admin staff was emailed.
- Create invalid widgets and check the errors. For these cases, you don’t need to have one test for every single
validation, though each _does_ need testing:
**-** Widgets missing a name, price, and manufacturer.
**-** Widget with a four-character name.
**-** Widget for an old manufacturer with a price of $99.
**-** Widget with a price over $10,000.
**-** Widget with a price of $0.
For the sake of brevity, we won’t implement all of these right now, but we will implement a few that allow us to
see the affect of Rails validations and mailers on our implementation and tests.
Let’s start with the basic happy path.
# test/services/widget_creator_test.rb
require "test_helper"
**class** WidgetCreatorTest **<** ActiveSupport **::** TestCase
setup **do**
@widget_creator **=** WidgetCreator.new
@manufacturer **=** FactoryBot.create( **:manufacturer** ,
**created_at:** 1.year.ago)
FactoryBot.create( **:widget_status** )
```
end
test "widgets have a default status of'Fresh'" do
result = @widget_creator.create_widget(Widget.new(
name: "Stembolt",
price_cents: 1_000_00,
manufacturer_id: @manufacturer.id
))
```
assert result.created?
assert_equal Widget.first, result.widget
assert_equal "Fresh", result.widget.widget_status.name
**end
end**
This test should fail since we’re using whatever status is returned byWidgetStatus.firstand not looking for one
named “Fresh”.
> bin/rails test test/services/widget_creator_test.rb || echo \
Test Failed
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 29535
# Running:
F
Failure:
WidgetCreatorTest#test_widgets_have_a_default_status_of_'Fre...
Expected: "Fresh"
Actual: "hic"
bin/rails test test/services/widget_creator_test.rb:10
Finished in 0.122025s, 8.1951 runs/s, 24.5852 assertions/s.
1 runs, 3 assertions, 1 failures, 0 errors, 0 skips
Test Failed
We could fix this by naming the status we’re creating in thesetupblock, but that won’t work in production. We
need to make sure that the code breaks if it doesn’t choose the proper status. That means we need the “Fresh”
status, but also another one that would be returned byfirst.
# test/services/widget_creator_test.rb
@manufacturer **=** FactoryBot.create( **:manufacturer** ,
**created_at:** 1.year.ago)
FactoryBot.create( **:widget_status** )
→ FactoryBot.create( **:widget_status** , **name:** "Fresh")
**end**
test "widgets have a default status of'Fresh'" **do**
result **=** @widget_creator.create_widget(Widget.new(
Let’s fix the code.
# app/services/widget_creator.rb
**class** WidgetCreator
**def** create_widget(widget)
→ widget.widget_status **=**
→ WidgetStatus.find_by!( **name:** "Fresh")
widget.save
```
Result.new( created: widget.valid?, widget: widget)
```
The test should now pass:
> bin/rails test test/services/widget_creator_test.rb
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 32174
# Running:
.
Finished in 0.137029s, 7.2977 runs/s, 21.8932 assertions/s.
1 runs, 3 assertions, 0 failures, 0 errors, 0 skips
Note the use offind_by!. Our code assumes “Fresh” is in the database, and if it’s not, we want it to raise an
exception, not returnnil, since this is a condition we should not have allowed to go into production. This assumes
we are monitoring for such unexpected exceptions (we’ll talk more about this in Operations on page 381). Also
note that we aren’t thinking about refactoring. We can worry about that later (or maybe never). Right now we
need to get the code working.
Next, let’s write a test of a validation that doesn’t yet exist. Widget names have to be five characters or longer, so
let’s test that.
# test/services/widget_creator_test.rb
assert_equal Widget.first, result.widget
assert_equal "Fresh", result.widget.widget_status.name
**end**
→ test "widget names must be 5 characters or greater" **do**
→ result **=** @widget_creator.create_widget(Widget.new(
→ **name:** "widg",
→ **price_cents:** 1_000_00,
→ **manufacturer_id:** @manufacturer.id
→ ))
→ refute result.created?
→ assert result.widget.invalid?
→ too_short_error **=** result.widget.errors **[:name].**
→ detect **{ |** message **|**
→ message **=~** /is too short/i
→ **}**
→ refute_nil too_short_error,
→ result.widget.errors.full_messages.join(",")
→ **end
end**
Note that we’re checking for the specific error we expect, not just any error. Also note that second parameter to
refute_nilis the summary of all the errors on the object, so if there _is_ an error, but not the one we expect, the
test failure message is actually helpful.
This test should fail at the firstrefute:
> bin/rails test test/services/widget_creator_test.rb || echo \
Test Failed
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 46338
# Running:
.F
Failure:
WidgetCreatorTest#test_widget_names_must_be_5_characters_or_...
Expected true to not be truthy.
bin/rails test test/services/widget_creator_test.rb:23
Finished in 0.125327s, 15.9583 runs/s, 31.9166 assertions/s.
2 runs, 4 assertions, 1 failures, 0 errors, 0 skips
Test Failed
To fix it, we’ll add a validation toWidgetthat the name must be at least 5 characters long by using thelength:
attribute tovalidates.
# app/models/widget.rb
**}
end**
belongs_to **:widget_status**
→ validates **:name** , **{**
→ **presence:** true,
→ **length: { minimum:** 5 **}**
→ **}**
validates **:manufacturer_id** , **{ presence:** true **}**
validates **:price_cents** ,
**numericality: { less_than_or_equal_to:** 10_000_00 **}**
The test should now pass:
> bin/rails test test/services/widget_creator_test.rb
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 39504
# Running:
..
Finished in 0.125747s, 15.9050 runs/s, 47.7149 assertions/s.
2 runs, 6 assertions, 0 failures, 0 errors, 0 skips
OK, so why is theWidgetCreatorTesttesting code onWidget? The reason is thatWidgetCreatorTestis a test of
the _business process_ of creating widgets. As such, it’s a form of integration test. It’s testing the seam between the
outside world and our code. The test isn’t concerned with precisely _how_ the validation is implemented, just that it
happens.
The only reason ourWidgeteven _has_ this validation is because the business process—as implemented by
WidgetCreator—requires it. There is no other reason to have written that code. And, as you recall from the last
chapter, we’re putting this business logic on the Active Record because the validations API is powerful and we
don’t want to throw that out.
And _this_ is how we can safely refactor the actual implementation of widget creation. As long as the API between
our code and the controller (the seam) is stable, and as long as the contract between the UI and the controller is
stable, we can do what we will inside that.
This is extremely powerful. See the sidebar “Return Processing Makeovers” below for a real world example.
#### Return Processing Makeovers
```
The first major feature I built at Stitch Fix was a system to process returned shipments. Stitch Fix’s business model
requires that un-purchased clothes get back into inventory so they can be sent out to a customer who might like what
the first customer didn’t.
The process was complex, requiring data sanitization, purchase reconciliation, and customer service notifications.
The UI was also highly experimental, since it was replacing a spreadsheet.
The implementation was much like the one we’ve seen here. The controller exposed a complex object to render
the UI, and received a different object back that was passed to a single method of a class calledReturnProcessor.
That class returned a rich result that explained what had happened with the return.
The internals ofReturnProcessorwere enhanced and refactored as business needs changed. The UI was later
completely re-imagined by our user experience team, but the seam between it and the logic—ReturnProcessor—was
largely untouched by this process. This told me there was high value in funneling all business logic invocation through
one single method.
```
Let’s add one more test around notifying our financial staff of widgets priced higher than $7,500. This will further
demonstrate the layered nature of this approach.
We can either mock a hypotheticalFinanceMailer, or we can examineActionMailer::Base.deliveriesto see
what was emailed. Both strategies couple us to the use of Rails mailers as the notification mechanism, but the
latter avoids coupling our test to a specific mailer. Let’s take that approach.
# test/services/widget_creator_test.rb
refute_nil too_short_error,
result.widget.errors.full_messages.join(",")
**end**
→ test "finance is notified for widgets priced over $7,500" **do**
→ result **=** @widget_creator.create_widget(Widget.new(
→ **name:** "Stembolt",
→ **price_cents:** 7_500_01,
→ **manufacturer_id:** @manufacturer.id
→ ))
→ assert result.created?
→ assert_equal 1, ActionMailer **::** Base.deliveries.size
→ mail_message **=** ActionMailer **::** Base.deliveries.first
→ assert_equal "[email protected]", mail_message **[** "to" **]** .to_s
→ assert_match /Stembolt/, mail_message.text_part.to_s
→ **end
end**
Sincedeliveriesis not well documented, it’s risky to use it, but it’s been in Rails for many years, so it should be
stable enough to rely on.deliveriesreturns an array ofMail::Message, which is not part of Rails, but part of
the mail^3 gem that is transitively included in all Rails apps.
The approach of examining the mail queue for just enough data to assume everything worked echoes our approach
to system testing. TheWidgetCreatorTestcares that an email was sent, but it tries to care as little as possible so
that when the actual mail view is implemented, it can do what it needs to do without breaking our test. For our
purposes, if an email goes to the finance team’s inbox with the name of the widget, that’s good enough.
When we implement the mailer for real, this test will make sure that the mail properly fits into the larger widget
creation process. That mailer’s test can cover all the specificities of what that email should contain.
Back to the test, we should also make sure no emails were sent in our other test, since the price there is below
$7,500.
# test/services/widget_creator_test.rb
assert result.created?
assert_equal Widget.first, result.widget
assert_equal "Fresh", result.widget.widget_status.name
→ assert_equal 0, ActionMailer **::** Base.deliveries.size
**end**
test "widget names must be 5 characters or greater" **do**
result **=** @widget_creator.create_widget(Widget.new(
We should also make suredeliveriesis clear before each test.
# test/services/widget_creator_test.rb
**class** WidgetCreatorTest **<** ActiveSupport **::** TestCase
setup **do**
→ ActionMailer **::** Base.deliveries **= []**
@widget_creator **=** WidgetCreator.new
(^3) https://www.rubydoc.info/github/mikel/mail/Mail
```
@manufacturer = FactoryBot.create( :manufacturer ,
created_at: 1.year.ago)
```
To make all the tests pass, we’ll need an actual mailer, so let’s create it:
> bin/rails g mailer finance_mailer high_priced_widget
create app/mailers/finance_mailer.rb
invoke erb
create app/views/finance_mailer
create app/views/finance_mailer/high_priced_widget....
create app/views/finance_mailer/high_priced_widget....
invoke test_unit
create test/mailers/finance_mailer_test.rb
create test/mailers/previews/finance_mailer_preview...
We’ll implement the mailer and its views to do just enough to pass our test. Here’s the entire mailer:
# app/mailers/finance_mailer.rb
**class** FinanceMailer **<** ApplicationMailer
**def** high_priced_widget(widget)
@widget **=** widget
mail **to:** "[email protected]"
**end
end**
The views can just show the widget name only for now.
<%# app/views/finance_mailer/high_priced_widget.text.erb %>
<%= @widget.name %>
<%# app/views/finance_mailer/high_priced_widget.html.erb %>
<%= @widget.name %>
The generator created a test forFinanceMailerthat will now be broken. Let’s delete that for now since we aren’t
actually building the realFinanceMailer.
> rm test/mailers/finance_mailer_test.rb
Now, we can call it in our service and get the test passing:
# app/services/widget_creator.rb
widget.widget_status **=**
WidgetStatus.find_by!( **name:** "Fresh")
widget.save
→ **if** widget.price_cents **>** 7_500_00
→ FinanceMailer.high_priced_widget(widget).deliver_now
→ **end**
```
Result.new( created: widget.valid?, widget: widget)
end
```
> bin/rails test test/services/widget_creator_test.rb
Running 3 tests in a single process (parallelization thresho...
Run options: --seed 64930
# Running:
Finished in 0.162661s, 18.4433 runs/s, 73.7732 assertions/s.
3 runs, 12 assertions, 0 failures, 0 errors, 0 skips
Each of the tests we wrote should demonstrate the overall strategy to get to complete coverage. Note again, that
this is a strategy, and you can apply this to RSpec-based tests if you like.
### 17.6 Finished Implementation
```
This section’s code is in the folder17-06/of the sample code.
```
I know it’ll make this section even longer, but let’s quickly go through the remainder of the implementation. Here
are the remaining tests:
# test/services/widget_creator_test.rb
```
assert_equal "[email protected]", mail_message [ "to" ] .to...
assert_match /Stembolt/, mail_message.text_part.to_s
```
**end**
→ test "name, price, and manufacturer are required" **do**
→ result **=** @widget_creator.create_widget(Widget.new)
→ refute result.created?
→ widget **=** result.widget
→ assert widget.invalid?
→ assert widget.errors **[:name]** .any? **{ |** message **|**
→ message **=~** /can't be blank/i
→ **}** , widget.errors.full_messages_for( **:name** )
→ assert widget.errors **[:price_cents]** .any? **{ |** message **|**
→ message **=~** /is not a number/i
→ **}** , widget.errors.full_messages_for( **:price_cents** )
→ assert widget.errors **[:manufacturer]** .any? **{ |** message **|**
→ message **=~** /must exist/i
→ **}** , widget.errors.full_messages_for( **:manufacturer** )
→ **end**
→ test "price cannot be 0" **do**
→ result **=** @widget_creator.create_widget(Widget.new(
→ **name:** "Stembolt",
→ **price_cents:** 0,
→ **manufacturer_id:** @manufacturer.id
→ ))
→ refute result.created?
→ assert result.widget.errors **[:price_cents]** .any? **{ |** message **|**
→ message **=~** /greater than 0/i
→ **}** , result.widget.errors.full_messages_for( **:price_cents** )
→ **end**
→ test "price cannot be more than $10,000" **do**
→ result **=** @widget_creator.create_widget(Widget.new(
→ **name:** "Stembolt",
→ **price_cents:** 10_000_01,
→ **manufacturer_id:** @manufacturer.id
→ ))
→ refute result.created?
→ assert result.widget.errors **[:price_cents]** .any? **{ |** message **|**
→ message **=~** /less than or equal to 1000000/i
→ **}** , result.widget.errors.full_messages_for( **:price_cents** )
→ **end**
→ test "legacy manufacturers cannot have a price under $100" **do**
→ legacy_manufacturer **=** FactoryBot.create( **:manufacturer** ,
→ **created_at:** DateTime.new(2010,1,1) **-** 1.day)
→ result **=** @widget_creator.create_widget(Widget.new(
→ **name:** "Stembolt",
→ **price_cents:** 99_00,
→ **manufacturer_id:** legacy_manufacturer.id
→ ))
→ refute result.created?
→ assert result.widget.errors **[:price_cents]** .any? **{ |** message **|**
→ message **=~** /< \$100.*legacy/i
→ **}** , result.widget.errors.full_messages_for( **:price_cents** )
→ **end**
→ test "email admin staff for widgets on new manufacturers" **do**
→ new_manufacturer **=** FactoryBot.create( **:manufacturer** ,
→ **name:** "Cyberdyne Systems",
→ **created_at:** 59.days.ago)
→ result **=** @widget_creator.create_widget(Widget.new(
→ **name:** "Stembolt",
→ **price_cents:** 100_00,
→ **manufacturer_id:** new_manufacturer.id
→ ))
→ assert result.created?
→ assert_equal 1, ActionMailer **::** Base.deliveries.size
→ mail_message **=** ActionMailer **::** Base.deliveries.first
→ assert_equal "[email protected]", mail_message **[** "to" **]** .to_s
→ assert_match /Stembolt/, mail_message.text_part.to_s
→ assert_match /Cyberdyne Systems/, mail_message.text_part.to_s
→ **end
end**
The first test—that tests for omitting all of the values—fails, but not in the right way. OurWidgetCreatorhas
a bug, in that it assumesprice_centshas a value. We can fix that by early-exiting when we see the widget is
invalid:
# app/services/widget_creator.rb
widget.widget_status **=**
WidgetStatus.find_by!( **name:** "Fresh")
widget.save
→ **if** widget.invalid?
→ **return** Result.new( **created:** false, **widget:** widget)
→ **end
if** widget.price_cents **>** 7_500_00
FinanceMailer.high_priced_widget(widget).deliver_now
**end**
Next, we’ll trigger the mailer to the admin team. We’ll need that mailer:
# app/mailers/admin_mailer.rb
**class** AdminMailer **<** ApplicationMailer
**def** new_widget_from_new_manufacturer(widget)
@widget **=** widget
mail **to:** "[email protected]"
**end
end**
LikeFinanceMailer, the views can be minimal for now:
<%# app/views/admin_mailer/new_widget_from_new_manufacturer.html.erb %>
<%= @widget.name %>
<%= @widget.manufacturer.name %>
<%# app/views/admin_mailer/new_widget_from_new_manufacturer.text.erb %>
<%= @widget.name %>
<%= @widget.manufacturer.name %>
Now, we use this mailer:
# app/services/widget_creator.rb
```
FinanceMailer.high_priced_widget(widget).deliver_now
end
```
→ **if** widget.manufacturer.created_at.after?(60.days.ago)
→ AdminMailer.new_widget_from_new_manufacturer(widget)**.**
→ deliver_now
→ **end**
Result.new( **created:** widget.valid?, **widget:** widget)
**end**
The rest of the changes are on theWidgetclass. We’ll add agreater_thanattribute for validating the price, but
we’ll also add a custom validator,high_enough_for_legacy_manufacturers:
# app/models/widget.rb
**}**
validates **:manufacturer_id** , **{ presence:** true **}**
validates **:price_cents** ,
→ **numericality: {**
→ **less_than_or_equal_to:** 10_000_00,
→ **greater_than:** 0
→ **}** ,
→ **high_enough_for_legacy_manufacturers:** true
normalizes **:name** , **with: ->** (name) **{** name.blank?? nil : name...
**end**
If you haven’t used custom validators before, you can implement them as a class that extendsActiveModel::EachValidator,
like so:
# app/models/widget.rb
**}
end**
belongs_to **:widget_status**
→ **class** HighEnoughForLegacyManufacturersValidator **<**
→ ActiveModel **::** EachValidator
→ **def** validate_each(record, attribute, value)
→ **return if** value.blank?
→ **if** value **<** 100_00 **&&**
→ record.manufacturer.created_at.year **<** 2010
→ record.errors.add(attribute,
→ "must be < $100 for legacy manufacturers")
→ **end**
→ **end**
→ **end**
validates **:name** , **{
presence:** true,
**length: { minimum:** 5 **}**
This demonstrates the power of the Rails end-to-end experience and why we are using its validation system. This
would’ve been difficult to implement another way without also having to have custom view code to manage this
particular validation check.
This validation, however, will potentially break our widget factory, because it doesn’t guarantee a name will be
created with five or more characters. Let’s change it to useFaker::Lorem.words.join(" "), which will create
three words and join them with a space.
# test/factories/widget_factory.rb
FactoryBot.define **do**
factory **:widget do**
→ name **{** Faker **::** Lorem.unique.words.join(" ") **}**
price_cents **{** Faker **::** Number.within( **range:** 1 **..** 10_000_00) **}**
manufacturer
widget_status
The tests should all pass.
> bin/rails test test/lint_factories_test.rb \
test/services/widget_creator_test.rb \
test/system/create_widget_test.rb
Running 11 tests in a single process (parallelization thresh...
Run options: --seed 50873
# Running:
##### ...........
Finished in 0.459940s, 23.9161 runs/s, 78.2710 assertions/s.
11 runs, 36 assertions, 0 failures, 0 errors, 0 skips
Of course, we’ve likely broken the system tests we wrote in earlier chapters. Bothrate_widget_test.rband
view_widget_test.rbexpected faked-out data. Let’s fix them as well, so we have a clean build by the end of all
this.
First,rate_widget_test.rb(intest/system) needs to create a widget usingFactoryBotand not assume there is
one with the id 1234:
# test/system/rate_widget_test.rb
**class** RateWidgetsTest **<** BrowserSystemTestCase
test "rating a widget shows our rating inline" **do**
→ widget **=** FactoryBot.create( **:widget** )
→ visit widget_path(widget)
```
click_on "2"
```
Fortest/system/view_widget_test.rb, it’s a bit trickier. The test is testing both the index and show actions, and
the index action is still faked out! Let’s fix that, first:
# app/controllers/widgets_controller.rb
**def** show
@widget **=** Widget.find(params **[:id]** )
**end
def** index
×# @widgets = [
×# OpenStruct.new(id: 1234, name: "Stembolt"),
×# OpenStruct.new(id: 2, name: "Flux Capacitor"),
×# ]
→ @widgets **=** Widget.all
**end
end**
Now, our test should create some widgets to assert on. Note that we’re hard-coding one of the widgets to have an
ID of 1234 so that we can assert on the id-formatting logic. This could cause a problem if some other widget
actually got that ID, but for now we’ll assume that won’t happen.
# test/system/view_widget_test.rb
**class** ViewWidgetTest **<** ApplicationSystemTestCase
test "we can see a list of widgets and view one" **do**
→ FactoryBot.create( **:widget** , **name:** "Flux Capacitor")
→ stembolt **=** FactoryBot.create( **:widget** , **name:** "Stembolt")
→ stembolt.update!( **id:** 1234)
visit widgets_path
```
widget_name = "stembolt"
```
Let’s now checkbin/cito see if the app is still overall working:
> bin/ci
[ bin/ci ] Running unit tests
Running 16 tests in a single process (parallelization thresh...
Run options: --seed 48425
# Running:
................
Finished in 0.162555s, 98.4283 runs/s, 258.3743 assertions/s...
16 runs, 42 assertions, 0 failures, 0 errors, 0 skips
[ bin/ci ] Running system tests
Running 4 tests in a single process (parallelization thresho...
Run options: --seed 1153
# Running:
..Rack::Handler is deprecated and replaced by Rackup::Handle...
Capybara starting Puma...
* Version 6.4.0 , codename: The Eagle of Durango
* Min threads: 0, max threads: 4
* Listening on [http://127.0.0.1:42441](http://127.0.0.1:42441)
..
Finished in 1.251790s, 3.1954 runs/s, 8.7874 assertions/s.
4 runs, 11 assertions, 0 failures, 0 errors, 0 skips
[ bin/ci ] Analyzing code for security vulnerabilities.
[ bin/ci ] Output will be in tmp/brakeman.html, which
[ bin/ci ] can be opened in your browser.
[ bin/ci ] Analyzing Ruby gems for
[ bin/ci ] security vulnerabilities
Updating ruby-advisory-db ...
From https://github.com/rubysec/ruby-advisory-db
* branch master -> FETCH_HEAD
Already up to date.
Updated ruby-advisory-db
ruby-advisory-db:
advisories: 827 advisories
last updated: 2023-11-30 12:36:04 -0800
commit: d821bf162550302abd1fa1fe15007f3012b76f32
No vulnerabilities found
[ bin/ci ] Done
Everything looks great, and we’re done!
Looking atWidgetCreatornow, I’m fine with the implementation and don’t see a reason to refactor it. Although
the custom validator is covered by our test, I might add a more exhaustive test for it intest/widget_test.rb
since it’s fairly complex compared to the other validations. I’ll leave that as an exercise for you.
### Reflecting on What We’ve Built
Hopefully, this example has demonstrated some of the advantages of consolidating business logic behind a
well-defined seam, as defined by a class, method, and rich return object.
The tests and implementation actually paint a good picture of how everything is structured, and why. We
wrote code inWidgetto make the test ofWidgetCreatorpass because that code only exists inWidgetto satisfy
WidgetCreator’s requirements. TheWidgetCreatortest outlines everything that’s required of widget creation as
we currently understand it.
If we later re-use these validations in another flow, we could certainly consider some re-work of our tests, but the
requirements we have—and the code that implements them—simply don’t justify it.
Also note the layering. Our system test only tests what it cares about—the user experience—and provides
only cursory coverage of the widget creation process. The finer details—as well as behavior that a user cannot
observe—are left to the test of our seam—WidgetCreator. Of course, _it_ provides only cursory coverage of the
behavior ofFinanceMailer. The test for that class would iron out all the details of that email.
The strategic layering keeps our code and tests maintainable. Plus, the use of a service layer means we have
a clear location where business logic is triggered. Imagine having to chase all that down across callbacks and
convoluted DSLs.
Let’s move onto the boundaries of our Rails app: controllers, mailers, rake tasks, and the like.
## 18 Controllers
If you want to respond to an HTTP request in a Rails app, you pretty much need to use a controller. That’s why
they exist. In this sense, only a controller can receive an HTTP request, trigger business logic based on it, then
send a response, be that rendering a view or redirecting to another path.
There are four issues around controllers that can cause sustainability problems:
- Controller code is structured unlike any other code in... well... any system I’ve ever seen. It’s not object-
oriented, functional, or even procedural. Controller code can seem quite alien.
- Over-use of callbacks can create situations where code is unnecessarily spread across several methods,
connected only implicitly.
- Controllers are the perfect place to insulate downstream business logic from the “hashes of strings” API
Rails provides for accessing the HTTP request.
- Unit tests of controllers are often duplicative of tests in other parts of the system.
Let’s start with what controllers actually are: sophisticated configuration.
### 18.1 Controller Code is Configuration
If I told you I was designing a system in which you’d write code that received no parameters, instead plucking
them out of implicit objects available to use, and that your method’s return value would be ignored, instead
requiring that you manipulate implicit state by calling various methods—each of which could only be called
once—you would probably not be excited about working in this system.
If I further told you that you’d not be able to instantiate the class or call the method yourself—even in a test—and
that the only way to pass information to a template was to declare and assign an instance variable, you might
think I was playing a very cruel trick on you.
This is how Rails controllers are designed and yet _they work great_. A Rails controller is a poster child for what is
called an internal domain specific language, or “internal DSL” ( _internal_ because it’s Ruby code and not another
language made just for this purpose). Despite all of its weirdness, it works really well, as long as you treat it as
what it is.
I like to think of it as a very rich configuration language. This prevents me from putting business logic in the
controllers themselves, and helps me understand the purpose of the code in the controllers.
In the vein of treating Rails for what it is—not what you wish it would be—do not try to bend controller code into
more traditional object-oriented structures. Embrace the controller code for what it is. Since you are making
heavy use of resources (as discussed in “Don’t Create Custom Actions, Create More Resources” on page 75), and
since you have put your business logic behind a seam (as discussed frequently, including the previous chapter),
you won’t end up needing much code in your controllers.
By embracing controllers for what they are and how they work, you’ll keep the code in them minimal, and thus
won’t need exhaustive tests for them, and this all reduces carrying costs (the key to sustainability).
That said, our controllers still do need some code in them, so let’s talk about what sort of code that is and how to
manage it. The biggest source of confusion in controller code is what we’ll talk about next: callbacks.
### 18.2 Don’t Over-use Callbacks
Controller callbacks (originally called _filters_ ) allow you to place code in other methods that run before or after
code in controller methods. This is extremely useful for cross-cutting concerns that apply to many or all controller
methods. Rails’ cross-site request forgery (CSRF), for example, is implemented using callbacks.
Callbacks are sometimes abused by developers overzealously trying to remove duplication. Because callbacks
are invoked implicitly (not explicitly like a private method) this can lead to code that, while it does remove
duplication, is hard to understand since you cannot easily trace the chain of events that occur when a controller
method is invoked.
For example:
**class** ManufacturersController **<** ApplicationController
before_action **:set_manufacturer**
```
def edit
end
```
```
def update
if @manufacturer.save
redirect_to manufacturer_path(@manufacturer)
else
render :edit , status: :unprocessable_entity
end
end
```
```
def show
end
```
private
```
def set_manufacturer
@manufacturer = Manufacturer.find(params [:id] )
end
```
**end**
While this code does consolidate the way in which aManufactureris loaded and exposed to the view, it has
created a controller that is unnecessarily complex - the core part of whatshowandedit _do_ has been hidden
behind an implicit invocation.
As more callbacks are added, piecing together exactly what happens in these methods becomes harder, and for
what gain? All to consolidate a small piece of highly stable code. If that code really _did_ need to be extracted to a
single source, a private method would work far better:
**class** ManufacturersController **<<** ApplicationController
**def** edit
@manufacturer **=** load_manufacturer
**end**
```
def update
@manufacturer = load_manufacturer
if @manufacturer.save
redirect_to manufacturer_path(@manufacturer)
else
render :edit , status: :unprocessable_entity
end
end
```
```
def show
@manufacturer = load_manufacturer
end
```
private
```
def load_manufacturer
Manufacturer.find(params [:id] )
end
```
**end**
When callbacks are added to ApplicationController or any module mixed-in to the controller or
ApplicationController, it can become quite difficult to figure out the order in which all the code exe-
cutes. As a mechanism for managing duplication, callbacks just aren’t the right tool: private methods will always
be easier to manage and understand.
Callbacks _are_ a great tool for managing duplicate code that’s both not specific to any given controller method _and_
is needed in many of the app’s controllers. Authorization and authentication is a classic example of this.
Another example is exception handing, using therescue_fromcallback. There are certain types of errors that can’t
be easily handled by the business logic and that require the same user experience when they occur. Authorization
is a great example. If all of your code raises, say, aUserNotAuthorizedexception (that I just invented for this
example), you could userescue_fromto ensure that those users see the same page, without writing any code in
any controller.
Just be wary of using callbacks too often or for code that is small is scope. They will make it harder to understand
how your code will behave.
Let’s talk about a more subtle type of code that ends up in controllers, which is parameter conversion.
### 18.3 Controllers Should Convert Parameters to Richer Types
```
This section’s code is in the folder18-03/of the sample code.
```
As the invokers of business logic, controllers are responsible for converting parameters into properly typed objects:
**def** show
@widget **=** Widget.find(params **[:id]** )
**end**
This code takes a string containing an identifier that we assume identifies a widget, and looks it up in the database,
passing the actual widget to the view.
Because HTTP is a text-based protocol, and because Rails provides us only hashes of strings as an API into it,
controllers are in the unique position to insulate the rest of the codebase from this reality.
This is complicated by the fact that Active Record handles a lot of conversions for us. For example,findknows to
convert the string it was given into a number to do the database lookup. Active Record can also convert dates and
booleans. For example, you can set a date to the string"2020-05-13"and Active Record will convert it when it
saves to the database.
This isn’t always available to us, as we saw the use of dollars in the UI for a widget’s price, but the requirement by
the back-end to receive cents. And, if we use custom resources based on Active Model, we can’t access any of
Active Record’s conversions.
Nevertheless, I still believe the controller should handle getting strings into whatever types they need to be in for
the business logic. Just keep in mind that for Active Records, strings _are_ the type needed. In other words, there is
no value in writing code like this:
# Not needed, since Active Record can convert the string for us
@widget **=** Widget.find(params **[:id]** .to_i)
This will lead to inconsistency in your controllers, but it’s likely worth it. You’ll just need to carefully manage the
conversion code. This doens’t mean such code has to be inlined into the controller, however. The controller just
needs to make sure the conversion happens.
```
For example, we might end up with a lot of dollars-to-cents conversions in our app. You might make a class like
Price:
```
```
## app/models/price.rb
```
```
class Price
attr_reader :cents
def initialize(dollars)
@cents = if dollars
(BigDecimal(dollars) * 100).to_i
end
end
end
```
```
The controller would still be responsible for using this class:
```
```
widget_params [:price_cents] =
Price.new(widget_params [:price_cents] ).cents
```
(Note that you should _not_ do this unless you need to for managing duplication. If the only dollars-to-cents
conversion you ever need is in this controller, you’ll be glad not to have an extra abstraction hanging around.)
In any case, this logic might not be testable from our system test. Thus, it will need a test. But to test something
like this we may end up duplicating tests we already have.
### 18.4 Don’t Over Test
```
This section’s code is in the folder18-04/of the sample code.
```
```
As mentioned in “Understand The Value and Cost of Tests” on page 161, tests aren’t an end unto themselves. They
have a potentially high carrying cost. Thus, we need to be careful that every test we write serves a purpose and
delivers real value.
In the end-to-end example chapter on page 241, we explicitly did not write tests for validations in the model test
because those validations were covered by the test of our service class. That was a strategic decision to reduce the
carrying cost of tests without sacrificing coverage.
This applies to our controller tests, too. Ideally, we would not need controller tests at all, since our system tests
would tell us if our controller code is broken. That said, the more type conversions our controllers have to do, the
more likely we are to need to test them.
In the last chapter, we had to make our system test reach into the database in order to get coverage of the price
conversion logic. That would be better tested in a controller test, so let’s do that now.
```
#### 18.4.1 Writing a Controller Test
There are two approaches we can take. One would be to mockWidgetCreatorand assert it received converted
values. The other would be to _not_ mock anything and assert what ends up in the database.
One approach isn’t more correct than the other—they both boil down to what you want your test to be coupled to.
Because the API for creating widgets withWidgetCreatoris relatively simple, I’m going to avoid mocking and
assert on the database.
Here’s what the test looks like:
# test/controllers/widgets_controller_test.rb
require "test_helper"
**class** WidgetsControllerTest **<** ActionDispatch **::** IntegrationTest
test "converts dollars to cents when creating widgets" **do**
manufacturer **=** FactoryBot.create( **:manufacturer** )
FactoryBot.create( **:widget_status** , **name:** "Fresh")
post widgets_url, **params: {
widget: {
name:** "New Widget",
**price_cents:** "123.45",
**manufacturer_id:** manufacturer.id.to_s,
**}
}**
widget **=** Widget.last
refute_nil widget
assert_redirected_to widget_path(widget)
assert_equal 12345, widget.price_cents
**end
end**
This test should pass:
> bin/rails test test/controllers/widgets_controller_test.rb
Running 1 tests in a single process (parallelization thresho...
Run options: --seed 20250
# Running:
.
Finished in 0.163104s, 6.1311 runs/s, 24.5243 assertions/s.
1 runs, 4 assertions, 0 failures, 0 errors, 0 skips
```
Note that the test ensures the parameters are strings, no matter what. This is critical, and it’s a failure of Rails that
it does not coerce these values to strings for you. This is because the values in production will always be strings!
I know I’ve made the mistake of posting a boolean to a controller in a test, only to find that while the test passed,
the controller was woefully broken in production, since the string"false"is a truthy value.
On thing to note is that while this test exists to test the price conversion logic, we can’t properly test it if widget
creation is broken. Rather than duplicate all ofWidgetCreator’s tests, we do a quick check first:
```
```
refute_nil widget
assert_redirected_to widget_path(widget)
```
These assertions provide no value in terms of quality assurance. We absolutely have this covered by the system
test. They are a carrying cost. But they need to be there in case we run this test and widget creation is broken
(even if the controller logic is still correct).
```
Consider the third assertion in our test, which is the only one that is providing value:
```
```
assert_equal 12345, widget.price_cents
```
```
This is the assertion that tells us if the controller is working or not. The other two assertions don’t tell us that.
Without those other assertions, if widget creation was broken, the test would fail in an odd way. We’d get
something likeNoMethodError: no such method price_cents for NilClass. We’d expect a failure message for
this assertion to be related to the wrong value forprice_cents, not an error.
That’s why I wrote the other two assertions. If widget creation is broken, we’ll get a failure that thewidgetwas
assumed to have been created. If that assertion fails, we have no confidence in our test at all, because logic it
assumes is working is broken—the test itself can’t technically run.
But it’s hard to know that from looking at the code. We need a way to leverage the assertion library but also to
indicate that some tests are just performing confidence checks before the actual test assertions execute.
```
#### 18.4.2 Implementing a Basic Confidence-checking System
```
Sure, we could just throw# CONFIDENCE CHECKbefore these assertions, but I don’t think this sort of code comment
is nearly as useful as actual code. Let’s make a method that we can use that makes it clear which assertions are
checking that we can even run our test and which are the actual test.
We’ll do that by assuming the existence of a method calledconfidence_checkthat takes a block and executes the
code inside that block.
```
```
# test/controllers/widgets_controller_test.rb
```
##### }
##### }
widget **=** Widget.last
×# refute_nil widget
×# assert_redirected_to widget_path(widget)
→ confidence_check **do**
→ refute_nil widget
→ assert_redirected_to widget_path(widget)
→ **end**
assert_equal 12345, widget.price_cents
**end
end**
_Now_ the test makes it clear thatrefute_nilandassert_redirected_toare only there to double-check that the
basics are working before we do the _real_ assertion, which follows.
In addition to demarcating the code, we need to see a helpful error in our test output letting us know that the test
effectively wasn’t even run because of factors outside its own control. We’ll augment the exception raised by the
testing framework to put a message indicating the failure is not a test failure, but a confidence check failure.
Since Ruby doesn’t have a way to modify the message of a thrown exception, we’ll create our own and delegate
all its methods to the exception raised by the failed assertion.
We can put this insupport/confidence_check.rband require it inside our base test case, similar to what we did
withwith_cluesin “Cultivate Explicit Diagnostic Tools to Debug Test Failures” on page 165.
# test/support/confidence_check.rb
**module** TestSupport
**module** ConfidenceCheck
**class** ConfidenceCheckFailed **<** Minitest **::** Assertion
**def** initialize(minitest_assertion)
super("CONFIDENCE CHECK FAILED: #{minitest_assertion.message}")
@minitest_assertion **=** minitest_assertion
**end**
```
delegate :backtrace ,
:error ,
:location ,
:result_code ,
:result_label ,
:backtrace_locations ,
:cause , to: :@minitest_assertion
end
```
# Used to indicate assertions that give confidence that
# the test has been properly set up or that dependent
# functionality is working
**def** confidence_check( **&** block)
block**.** ()
**rescue** Minitest **::** Assertion **=>** ex
raise ConfidenceCheckFailed.new(ex)
**end
end
end**
We’ll thenrequirethis file:
# test/test_helper.rb
```
config.test_id = "data-testid"
end
```
→require "support/confidence_check"
**module** ActiveSupport
**class** TestCase
# Run tests in parallel with specified workers
And then include it in the base test case:
# test/test_helper.rb
**module** ActiveSupport
**class** TestCase
→ include TestSupport **::** ConfidenceCheck
# Run tests in parallel with specified workers
parallelize( **workers: :number_of_processors** )
Now, if widget creation is broken, _this_ test will show “CONFIDENCE CHECK FAILED” to indicate that it can’t even
perform an assertion. Note that you can follow this same approach with RSpec, but you must create your custom
exception (and thus explicitlyrescue),RSpec::Expectations::ExpectationNotMetError.
Since writing this book, I have extracted this to a gem called confidence-check^1 that you can use if you like. It
works with RSpec as well as Rails’ default test framework.
#### 18.4.3 Avoiding Duplicative Tests
Our controller’screatemethod has anotherifstatement in it, related to re-rendering the new page if there is a
problem creating the widget. Our instincts are thatifstatements require tests, but in this case, the codepath is
covered. Do we really need a test?
No. These exact flows are covered by our system test, which would fail if the controller changed its logic. The
only downside is that you need to figure out that the system test is failing because of the controller, not the view.
This trade-off is worth it, since isolated tests for routing and navigation don’t add much value, given the existence
or system tests, as described. You end up with carrying costs that don’t justify their existence.
When _might_ it be worth it? If the routing was based on a more complex set of logic than a simple predicate, an
isolated test would be easier to use for driving changes. But, wouldn’t these be major flows you’d want tested in a
system test?
### Up Next
When you organize code the way I’m suggesting, your controllers end up being pretty basic. That’s a good thing!
Where controllers process web requests, there is another construct most Rails apps need that process requests
asynchronously: jobs.
(^1) https://github.com/sustainable-rails/confidence-check
## 19 Jobs
```
One of the most powerful tools to make your app high-performing and fault-tolerant is the background job.
Background jobs bring some complexity and carrying cost to the system, so you have to be careful not to swap
one sustainability problem for another.
```
This chapter will help you navigate this part of Rails. We’ll start by understanding exactly what problems
background jobs exist to solve. We’ll then learn why you must understand exactly how your chosen job backend
(Sidekiq, Resque, etc.) works. We’ll set up Sidekiq in our example app, since Sidekiq is a great choice if you don’t
have specific requirements otherwise.
```
We’ll then learn how to use, build, and test jobs. After all that we’ll talk about a big source of complexity around
background jobs, which is making them idempotent. Jobs can and will be automatically retried and you don’t
usually want their effects to be repeated. Achieving idempotency is not easy or even possible in every situation.
```
```
Let’s jump into it. What problems do background jobs solve?
```
### 19.1 Use Jobs To Defer Execution or Increase Fault-Tolerance
```
Background jobs allow you to run code outside a web request/response cycle. Sometimes you do this because you
need to run some batch process on a schedule. There are two other reasons we’re going to focus on, since they
lead to the sort of complexity you have to carefully manage. Background jobs can allow moving non-critical code
to outside the request/response cycle as well as encapsulate flaky code that may need several retries in order to
succeed.
```
```
Both of these situations amount to deferring code that might take too long to a background job to run later. The
reason this is important has to do with how your Rails app is set up in production.
```
#### 19.1.1 Web Workers, Worker Pools, Memory, and Compute Power
```
In development, your Rails app uses the Puma^1 web server. This server receives requests and dispatches them
to your Rails app (this is likely how it works in production as well). When a request comes in, Puma allocates
a worker to handle that request. That worker works on only that request until a response is rendered—it can’t
manage more than one request at a time.
When the response is rendered, the worker can work on another request. Puma keeps these workers in a pool ,
and that pool has a finite limit. This is because each worker consumes memory and CPU (even if it’s not doing
anything) and, because memory and CPU are finite resources, there can only be so many workers per server.
```
(^1) https://puma.io
What if all workers are handling requests? What happens to a new request that comes in when there is no worker
to handle it?
It depends. In some configurations, the new request will be denied and the browser will receive an HTTP 503
(resource unavailable). In other configurations that request will be placed in a queue (itself a finite resource) to
be handled whenever a worker becomes available. In this case the request will appear to be handled more slowly
than usual.
```
While you can increase the overall number of workers through complex mechanisms such as load balancers, there
is always going to be a finite amount of resources to process requests. Often this limit is financial, not technical,
since more servers and more infrastructure cost more money and it may not be worth it.
Another solution to the problem of limited workers is to reduce the amount of work those workers have to do. If
your controller initiates a business process that takes 500ms normally, but can be made to defer 250ms of that
process into a background job, you will have doubled your worker capacity^2.
One particular type of code that leads to poor performance—and thus is a good target for moving to a background
job—is code that interacts with third party APIs, such as sending email or processing payments.
```
#### 19.1.2 Network Calls and Third Parties are Slow
```
Although our app doesn’t have the ability to charge users to purchase widgets, you might imagine that it could,
and that means integrating with a payment processor. And this means making a network call over the Internet.
Although network calls within our data center can fail, network calls over the Internet are so likely to fail that you
have to handle that failure as a first-order issue.
```
```
Of course, network calls that fail don’t fail immediately. They often fail after an interminable amount of time. Or
not. Sometimes the network is just slow and a successful result eventually comes back.
Background jobs can help solve this problem. The figure below outlines how this works.
```
```
In the figure, you can see that the initial POST to create an order causes the controller to insert an order into
the database then queue a background job to handle communicating with the payment processor. While that’s
happening, the controller returns the order ID to the browser.
The browser then uses Ajax to poll the controller’s show method to check on the status of the order. The show
method will fetch the order from the database to see if it’s been processed. Meanwhile, the background job
waits for the payment processor until it receives a response. When it does, it updates the order in the database.
Eventually, the browser will ask about the order and receive a response that it’s completed.
This may seem complex, but it allows the web workers (which are executing only the controller code in this
example) to avoid waiting on the slow payment provider.
```
```
This design can also handle transient errors that might happen when communicating with the third party. The job
can be automatically retried without having to change how the front-end works.
```
#### 19.1.3 Network Calls and Third Parties are Flaky
```
Network calls fail. There’s just no way to prevent that. The farther away another server is from your server, the
more likely it is to fail, and even at small scale, network failures happen frequently.
```
(^2) Yes, this is vastly oversimplified, but the point stands.
```
Figure 19.1: Performing Slow Code in Background Jobs
```
In most cases, network failures are transient errors. Retrying the request usually results in a success. But retrying
network requests can take a while, since network requests don’t fail fast. Your background jobs can handle this.
The figure below shows how this might work.
When our job encounters a network error, it can retry itself. During this retry, the front-end is still diligently asking
for an update. In this case it waits a bit longer, but we don’t have to re-architect how the entire feature works.
This might all seem quite complex and, well, it is. The rest of this chapter will identify sources of complexity and
strategies to work around them, but it’s important that you use background jobs only when needed.
#### 19.1.4 Use Background Jobs Only When Needed
At a certain scale, the benefits of background jobs outweigh their complexity, and you’d be wise to use them as
much as possible. You likely aren’t at that scale now, and might never be. Thus, you want to be judicious when
you use background jobs.
The two main problems that happen when you do all processing in the request are over-use of resources and
failures due to network timeouts. Thus, your use of background jobs should be when you cannot tolerate these
```
Figure 19.2: Retrying a Failed Job
```
failures at whatever level you are seeing them.
This can be hard to judge. A guideline that I adopt is to always communicate with third parties in a background
job, because even at tiny scale, those communications will fail.
For all other code, it’s best to monitor its performance, set a limit on how poor the performance is allowed to
get, and use background jobs when performance gets bad (keeping in mind that background jobs aren’t the
only solution to poor performance). For example, you might decide that the 90th percentile of controller action
response times should always be under 500ms.
When you _are_ going to use background jobs, you need to understand how the underlying system actually works to
avoid surprises.
### 19.2 Understand How Your Job Backend Works
Rails includes a library called Active Job that provides an abstraction layer over queueing and implementing jobs.
Since it is not a job queueing system itself, it unfortunately does not save you from having to understand whatever
system—called a _backend_ —you have chosen. Be it Sidekiq, Sucker Punch, Resque, or something else, each job
backend has different behaviors that are critical to understand.
For example, Resque does not automatically retry failed jobs, but Sidekiq does. Que uses the database to store jobs,
but Sidekiq uses Redis (meaning you need to have a Redis database set up to use Sidekiq and also understand
what a Redis database actually is). And, of course, the default queuing system in Rails is nothing, so jobs don’t
run in the background without setting something up.
Here is what you need to know about the job backend you are using:
- How does queueing work?
**-** How are the jobs themselves stored?
**-** Where are they stored?
**-** How are the arguments to the jobs encoded while jobs wait to execute?
- What happens when a job fails?
- How can you observe what’s happening in the job backend?
#### 19.2.1 Understand Where and How Jobs (and their Arguments) are Queued
When you queue a job with Sucker Punch, the job is stored in memory. Another process with access to that
memory will pluck the job out of an internal queue and execute it. If you use Sidekiq, the job goes into Redis. The
job class and the arguments passed to it are converted into JSON before storing, and converted back before the
job runs.
It’s important to know where the jobs are stored so you can accurately predict failure modes. In the case of Sucker
Punch, if your app’s process dies for some reason, any unprocessed job is gone without a trace.
In the case of Sidekiq (or Resque), you may lose jobs if Redis goes down, depending on how Redis is configured. If
you are also using that Redis for caching, you then run the risk of using up all of the storage available on caching
and will be unable to queue jobs at all.
You also need to know the mechanism by which the jobs are stored wherever they are stored. For example, when
you queue a job for Sidekiq, it will store the name of the job class as a string, and all of the arguments as an array.
Each argument will be converted to JSON before being stored. When the job is executed, those JSON blobs will
be parsed into hashes.
This means that if you write code like this:
ChargeMoneyForWidgetJob.perform_async(widget)
The code inChargeMoneyForWidgetJobwill not be given aWidget, but instead be given aHashcontaining whatever
results from callingto_jsonon aWidget. Many developers find this surprising, and this is precisely why you have
to understand how jobs are stored.
You also need to know what happens when jobs fail.
#### 19.2.2 Understand What Happens When a Job Fails
When a job encounters an exception it doesn’t rescue, it fails. Unlike a web request in a similar situation, which
sends an HTTP 500 to the browser, the job has no client to report its failure to. Each job backend handles this
situation differently by default, and has different options for modifying the default behavior.
For example, Sucker Punch does nothing by default, and failed jobs are simply discarded. Sidekiq will automatically
retry them for a period of time before discarding them. Resque will place them into a special failed queue and
hope you notice.
As discussed above, the ability to retry in the face of failures is one of the reasons to place code in a background
job. My advice is to understand how failure is managed and then configure your jobs system and/or jobs to
automatically retry a certain number of times before loudly notifying you of the job failure.
It’s common for job backends to integrate with exception notification services like Bugsnag or Rollbar. You need
to understand exactly how this integration works. For example, Resque will notify you once before placing the job
in the failed queue. Sidekiq will notify you every time the job fails, even if that job is going to be retried.
I can’t give specific advice, because it depends on what you have chosen, but you want to arrange for a situation in
which you are notified when a job that should complete has failed and won’t be retried. You _don’t_ want notification
when a job fails and will be retried, nor do you need to know if a job fails whose failure doesn’t matter.
Failure is a big part of the next thing you need to know, which is how to observe the behavior of the job backend.
#### 19.2.3 Observe the Behavior of Your Job Backend
When a job fails and won’t be retried, you need a way to examine that job. What class was it? What were the
arguments passed to it? What was the reason for failure? You also need to know how much capacity you have
used storing jobs, as well as how many and what type of jobs are waiting to be processed. You may also wish to
know what jobs have failed and _will_ be retried, and when they might get retried.
Many job backends come with a web UI that can tell you this. Some also include programmatic APIs you can use
to inspect the job backend. Familiarize yourself with whatever is provided and make sure you use it. If there is a
web UI, make sure only authorized users can access it, and make sure you understand what it’s showing you.
The more you can connect your job backend’s metrics to a monitoring system, the better. It can be extremely hard
to diagnose problems that result from the job backend failing if you can’t observe its behavior.
I have personally used Que, Resque, Sucker Punch, and Sidekiq. Of those four, Sidekiq is the best choice for most
situations and if you aren’t sure which job backend to use, choose Sidekiq.
We’ll need to write some job code later on, so we need some sort of backend set up. Let’s set up Sidekiq.
### 19.3 Sidekiq is The Best Job Backend for Most Teams
```
This section’s code is in the folder19-03/of the sample code.
```
I’m going to go quickly through this setup. Sidekiq’s documentation is great and can provide you with many
details about how it works. The point of this chapter is to talk about job code, not Sidekiq, but we need something
set up, and I want to use something that is both realistic and substantial. You are likely to encounter Sidekiq in
the real world, and you are very likely to encounter a complex job backend configuration.
First, we’ll add the Sidekiq gem toGemfile:
# Gemfile
# lograge changes Rails'logging to a more
# traditional one-line-per-event format
gem "lograge"
→# Sidekiq handles background jobs
→gem "sidekiq"
```
# Tachyons is a functional CSS framework
# we'll use to style our views
```
Then install it:
> bundle install
«lots of output»
We will also need to create the binstub so we can run it if we need to:
> bundle binstub sidekiq
Sidekiq assumes Redis is running onlocalhostby default. Assuming you are using the Docker-based setup I
recommended from an early chapter of the book on page 25 , our Redis is running on port 6379 of the hostredis,
so we need to tell Sidekiq about that. Remembering what we learned in “Using The Environment for Runtime
Configuration” on page 29, we want this URL configured via the environment. Let’s add that to our two.envfiles.
First, is.env.development:
# .env.development
DATABASE_URL="
postgres://postgres:postgres@db:5432/widgets_development"
→SIDEKIQ_REDIS_URL=redis://redis:6379/1
The valueredisfor the host comes from the key used in thedocker-compose.ymlfile to set up Redis. For the test
environment, we’ll do something similar, but instead of/1we’ll use/2, which is a different logical database inside
the Redis instance.
# .env.test
DATABASE_URL=postgres://postgres:postgres@db:5432/widgets_tes...
→SIDEKIQ_REDIS_URL=redis://redis:6379/2
Note that we put “SIDEKIQ” in the name to indicate the purpose of this Redis. You should not use the same Redis
instances for both job queueing and caching if you can help it. The reason is that it creates a single point of failure
for two unrelated activities. You don’t want a situation where you start aggressively caching and use up your
storage preventing jobs from being queued.
Now, we’ll create an initializer for Sidekiq that uses this new environment variable:
# config/initializers/sidekiq.rb
Sidekiq.configure_server **do |** config **|**
config.redis **= {
url:** ENV.fetch("SIDEKIQ_REDIS_URL")
**}
end**
Sidekiq.configure_client **do |** config **|**
config.redis **= {
url:** ENV.fetch("SIDEKIQ_REDIS_URL")
**}
end**
Note that we usedfetchbecause it will raise an error if the valueSIDEKIQ_REDIS_URLis not found in the
environment. This will alert us if we forget to set this in production.
We don’t need to actually _run_ Sidekiq in this chapter, but we should set it up. This is going to require thatbin/run
start two simultaneous processes: the Rails server we are already using and the Sidekiq worker process. To do
_that_ we’ll use Foreman^3 , which we’ll add to the development and test sections of ourGemfile:
# Gemfile
# We use Factory Bot in place of fixtures
# to generate realistic test data
gem "factory_bot_rails"
(^3) https://ddollar.github.io/foreman/
→ # Foreman runs all processes for local development
→ gem "foreman"
```
# We use Faker to generate values for attributes
# in each factory
```
We can install it:
> bundle install
«lots of output»
We also need to create a binstub inbin/for it:
> bundle binstub foreman
Foreman uses a “Procfile” to know what to run. The Procfile lists out all the processes needed to run our app.
Rather than create this file, I prefer to generate it insidebin/dev. This centralizes the way we run our app to a
single file, which is more mangeable as our app gets more complex. I also prefer to name this fileProcfile.dev
so it’s clear what it’s for (services like Heroku useProcfileto know what to run in production). Let’s replace
bin/runwith the following:
# bin/dev
#!/usr/bin/env bash
set -e
echo "[ bin/dev ] Rebuilding Procfile.dev"
echo "# This is generated by bin/dev. Do not edit" > Procfile.dev
echo "# Use this via bin/dev" >> Procfile.dev
# We must bind to 0.0.0.0 inside a
# Docker container or the port won't forward
echo "web: bin/rails server --binding=0.0.0.0" >> Procfile.dev
echo "sidekiq: bin/sidekiq" >> Procfile.dev
echo "[ bin/dev ] Starting foreman"
bin/foreman start -f Procfile.dev -p 3000
We’ll also addProcfile.devto our.gitignorefile:
# .gitignore
# and creates more problems than it solves, so
# we never ever want to use it
.env
→# Procfile.dev is generated, so should not be checked in
→Procfile.dev
```
# .env.*.local files are where we put actual
# secrets we need for dev and test, so
```
Now, when we run our app withbin/dev, Sidekiq will be started as well and any code that requires background
job processing will work in development.
Let’s talk about how to queue jobs and how to implement them.
### 19.4 Queue Jobs Directly, and Have Them Defer to Your Business Logic Code
```
This section’s code is in the folder19-04/of the sample code.
```
Once you know how your job backend works and when to use a background job, how do you write one and how
do you invoke it?
Let’s talk about invocation first.
#### 19.4.1 Do Not Use Active Job - Use the Job Backend Directly
Active Job was added to Rails in recent years as a single abstraction over background jobs. This provides a way
for library authors to interact with background jobs without having to know about the underlying backend.
Active Job does a great job at this, but since you _aren’t_ writing library code, it creates some complexities that
won’t provide much value in return. Since Active Job doesn’t alleviate you from having to understand your job
backend, there isn’t a strong reason to use it.
The main source of complexity is the way in which arguments to jobs are handled. As discussed above, you need
to know how those arguments are serialized into whatever data store your job system is using. Often, that means
JSON.
This means that you can’t pass an Active Record directly to a job since it won’t serialize/de-serialize properly:
**>** bin **/** rails c
rails **-** console **>** require "pp"
rails **-** console **>** widget **=** Widget.first
rails **-** console **>** pp JSON.parse(widget.to_json) ; nil
**{** "id" **=>** 1,
"name" **=>** "Stembolt",
"price_cents" **=>** 102735,
"widget_status_id" **=>** 2,
"manufacturer_id" **=>** 11,
"created_at" **=>** "2020-05-24T22:02:54.571Z",
"updated_at" **=>** "2020-05-24T22:02:54.571Z" **}
=>** nil
Before Active Job, the solution to this problem was to pass the widget ID to the job, and have the job look up the
Widgetfrom the database. Active Job uses globalid^4 to automate this process for you. But only for Active Records
and only when using Active Job.
That means that when you are writing code to queue a job, you have to think about what you are passing to that
job. You need to know what type of argument is being passed, and whether or not it uses globalid. I don’t like
having to think about things like this while I’m coding and I don’t see a lot of value in return for doing so.
Unless you are using multiple job backends—which will create a sustainability problem for you and your team—
use the API of the job backend you have chosen. That means that your arguments should almost always be basic
types, in particular database identifiers for Active Records.
Let’s see that with our existing widget creation code. We’ll move the logic around emailing finance and admin to
a background job calledPostWidgetCreationJob, which we’ll write in a moment. We’ll use it like so:
# app/services/widget_creator.rb
widget.save
**if** widget.invalid?
**return** Result.new( **created:** false, **widget:** widget)
**end**
×# if widget.price_cents > 7_500_00
×# FinanceMailer.high_priced_widget(widget).deliver_now
×# end
# XXX
×# if widget.manufacturer.created_at.after?(60.days.ago)
×# AdminMailer.new_widget_from_new_manufacturer(widget).
×# deliver_now
×# end
# XXX
×# Result.new(created: widget.valid?, widget: widget)
→ PostWidgetCreationJob.perform_async(widget.id)
→ Result.new( **created:** widget.valid?, **widget:** widget)
**end**
(^4) https://github.com/rails/globalid
```
class Result
```
perform_asyncis Sidekiq’s API, and we have to passwidget.idfor reasons stated above. We’ll talk about where
the code we just removed goes next.
#### 19.4.2 Job Code Should Defer to Your Service Layer
For all the reasons we don’t want business logic in our controllers, we don’t want business logic in our jobs. And
for all the reasons we want to convert the raw data types being passed into richly-typed objects in our controllers,
we want to do that in our jobs, too.
We passed in a widget ID to our job, which means our job should locate the widget. After that, it should defer to
another class that implements the business logic.
Since this is still widget creation and the job is calledPostWidgetCreationJob, we’ll create a new method on
WidgetCreatorcalledpost_widget_creationand have the job trigger that.
Let’s write the job code and then fill in the new method. Since we aren’t using Active Job, we can’t usebin/rails
g job. We also can’t useApplicationJobin its current form, but it is nice to have a base class for all jobs. Let’s
replace the Rails-providedApplicationJobwith one that is specific to Sidekiq.
# app/jobs/application_job.rb
# Do not inherit from ActiveJob. All jobs use Sidekiq
**class** ApplicationJob
include Sidekiq **::** Worker
sidekiq_options **backtrace:** true
**end**
Now, any job we create that extendsApplicationJobwill be set up for Sidekiq and we won’t have to include
Sidekiq::Workerin every single class. We could customize the output ofbin/rails g jobby creating the file
lib/templates/rails/job/job.rb.tt, but we aren’t going to use this generator at all. The reason is that our job
class will be very small and we won’t write a test for it.
Here’s whatPostWidgetCreationJoblooks like:
# app/jobs/post_widget_creation_job.rb
**class** PostWidgetCreationJob **<** ApplicationJob
**def** perform(widget_id)
widget **=** Widget.find(widget_id)
WidgetCreator.new.post_widget_creation_job(widget)
**end
end**
This means we need to create the methodpost_widget_creation_jobinWidgetCreator, which will contain the
code we removed fromcreate_widget:
# app/services/widget_creator.rb
```
Result.new( created: widget.valid?, widget: widget)
end
```
→ **def** post_widget_creation_job(widget)
→ **if** widget.price_cents **>** 7_500_00
→ FinanceMailer.high_priced_widget(widget).deliver_now
→ **end**
→ **if** widget.manufacturer.created_at.after?(60.days.ago)
→ AdminMailer.new_widget_from_new_manufacturer(widget)**.**
→ deliver_now
→ **end**
→ **end**
**class** Result
attr_reader **:widget
def** initialize(created:, widget:)
Our app should still work, but we’ve lost the proof of this via our tests. Let’s talk about that next.
### 19.5 Job Testing Strategies
```
This section’s code is in the folder19-05/of the sample code.
```
In the previous section, I said we wouldn’t be writing a test for our Job. Given the implementation, I find a test
that the job simply calls a method to have low value and high carrying cost. But, we do need coverage that
whatever uses the job is working correctly.
There are three approaches to take regarding testing code that uses jobs, assuming your chosen job backend
supports them. You can run jobs synchronously inline, you can store jobs in an internal data structure, executing
them manually inside a test, or you can allow the jobs to actually go into a real queue to be executed by the real
job system.
Which one to use depends on a few things.
Executing jobs synchronously as they are queued is a good technique when the jobs have simple arguments using
types like strings or numbers _and_ when the job is incidental to the code under test. Our widget creation code falls
under this category. There’s nothing inherent to widget creation that implies the use of jobs.
Queuing jobs to an internal data structure, examining it, and then executing the jobs manually is more appropriate
if the code you are testing is inherently about jobs. In this case, the test serves as a clear set of assertions about
what jobs get queued when. A complex batch process whereby you need to fetch a lot of data, then queue jobs to
handle it, would be a good candidate for this sort of approach.
This approach is also good when your job arguments are somewhat complex. The reason is that queuing the
jobs to an internal structure usually serializes them, so this will allow you to detect bugs in your assumptions
about how arguments are serialized. It is _incredibly_ common to pass in a hash with symbols for keys and then
erroneously expect symbols to come out of the job backend (when, in fact, the keys will likely be strings).
The third option—using the job backend in a production-like mode—is expensive. It requires running a worker to
process the jobs outside of your tests (or having your test trigger that worker somehow) and requires that the job
data storage system be running _and_ be reset on each new test run, just as Rails resets the database for you.
I try to avoid this option if possible unless there is something so specific about the way jobs are queued and
processed that I can only detect it by running the actual job backend itself.
For our code, the first approach works, and Sidekiq provides a way to do that. We will require"sidekiq/testing"
intest/test_helper.rband then callSidekiq::Testing.inline!around our test.
First, however, let’s make sure our test is actually failing:
> bin/rails test test/services/widget_creator_test.rb || echo \
Test Failed
Running 8 tests in a single process (parallelization thresho...
Run options: --seed 38421
# Running:
2023-12-04T23:55:53.753Z pid=9599 tid=803 INFO: Sidekiq 7.2....
F
Failure:
WidgetCreatorTest#test_finance_is_notified_for_widgets_price...
Expected: 1
Actual: 0
bin/rails test test/services/widget_creator_test.rb:44
.F
Failure:
WidgetCreatorTest#test_email_admin_staff_for_widgets_on_new_...
Expected: 1
Actual: 0
bin/rails test test/services/widget_creator_test.rb:126
.....
Finished in 0.173927s, 45.9964 runs/s, 126.4901 assertions/s...
8 runs, 22 assertions, 2 failures, 0 errors, 0 skips
Test Failed
Good. It’s failing in the right ways. You can see that the expected effects of the code we removed aren’t happening
and this causes the test failures. When we set Sidekiq up to run the job we are queuing inline, the tests should
start passing.
Let’s start withtest/test_helper.rb:
# test/test_helper.rb
ENV **[** "RAILS_ENV" **] ||=** "test"
require_relative "../config/environment"
require "rails/test_help"
→# Set up Sidekiq testing modes. See
→# https://github.com/mperham/sidekiq/wiki/Testing
→require "sidekiq/testing"
Capybara.configure **do |** config **|**
# This allows helpers like click_on to locate
# any object by data-testid in addition to
Sidekiq’s default behavior is the second approach of queueing jobs to an internal data structure. To run them inline,
we’ll useSidekiq::Testing.inline!. We’ll add this to thesetupblock intest/services/widget_creator_test.rb:
# test/services/widget_creator_test.rb
**class** WidgetCreatorTest **<** ActiveSupport **::** TestCase
setup **do**
→ Sidekiq **::** Testing.inline!
ActionMailer **::** Base.deliveries **= []**
@widget_creator **=** WidgetCreator.new
@manufacturer **=** FactoryBot.create( **:manufacturer** ,
We need to undo this setting after our tests run in case other tests are relying on the default (which isfake!):
# test/services/widget_creator_test.rb
FactoryBot.create( **:widget_status** )
FactoryBot.create( **:widget_status** , **name:** "Fresh")
**end**
→ teardown **do**
→ Sidekiq **::** Testing.fake! # the default setting
→ **end**
test "widgets have a default status of'Fresh'" **do**
result **=** @widget_creator.create_widget(Widget.new(
**name:** "Stembolt",
Now, our test should pass:
> bin/rails test test/services/widget_creator_test.rb
Running 8 tests in a single process (parallelization thresho...
Run options: --seed 35393
# Running:
2023-12-04T23:55:56.021Z pid=9674 tid=7xy INFO: Sidekiq 7.2....
Finished in 0.175805s, 45.5049 runs/s, 170.6434 assertions/s...
8 runs, 30 assertions, 0 failures, 0 errors, 0 skips
To use the second testing strategy—allowing the jobs to queue and running them manually—consult your job
backend’s documentation. Sidekiq provides methods to do all this for you if you should choose.
Now that we’ve seen how to make our code work using jobs, we have to discuss another painful reality about
background jobs, which is retries and idempotence.
### 19.6 Jobs Will Get Retried and Must Be Idempotent
```
This section’s code is in the folder19-06/of the sample code.
```
One of the reasons we use background jobs is to allow them to be retried automatically when a transient error
occurs. While you could build up a list of transient errors and only retry them, this turns out to be difficult, because
there are a lot of errors that one would consider transient. It is easier to configure your jobs to automatically retry
all errors (or at least retry them several time before finally failing).
This means that code executed from a job must be idempotent: it must not have its effect felt more than once, no
matter how many times it’s executed.
Consider this code that updates a widget’supdated_at^5
**def** touch(widget)
widget.updated_at **=** Time.zone.now
widget.save!
**end**
Each time this is called, the widget’supdated_atwill get a new value. That means this method is not idempotent.
To make it idempotent, we would need to pass in the date:
**def** touch(widget, updated_at)
widget.updated_at **=** updated_at
widget.save!
**end**
Now, no matter how many times we calltouchwith the same arguments, the effect will be the same.
The code initiated by our jobs must work similarly. Consider a job that charges someone money for a purchase. If
there were to be a transient error partway through, and we retried the entire job, the customer could be charged
twice. _And_ we might not even be aware of it unless the customer noticed and complained!
Making code idempotent is not easy. It’s also—you guessed it—a trade-off. Thetouchmethod above probably
won’t cause any problems if it’s not idempotent. But charging someone money will. This means that you have to
understand what might fail in your job, what might happen if it’s retried, how likely that is to happen, and how
serious it is if it does.
This means that your job is going to be idempotent with respect to some failure modes, and not to others. This is
OK if you are aware of it and make the conscious decision to allow certain scenarios to not be idempotent.
Let’s examine the job we created in the last section. It’s calledpost_widget_creation_jobinWidgetCreator,
which looks like so:
1 **def** post_widget_creation_job(widget)
2 **if** widget.price_cents **>** 7_500_00
3 FinanceMailer.high_priced_widget(widget).deliver_now
4 **end**
5
6 **if** widget.manufacturer.created_at.after?(60.days.ago)
7 AdminMailer.new_widget_from_new_manufacturer(widget)**.**
(^5) I realize you would never actually write this, but idempotence is worth explaining via a trivial example as it is not a concept that comes
naturally to most.
8 deliver_now
9 **end**
10 **end**
When thinking about idempotence, I like to go through each line of code and ask myself what would happen if
the method got an error on that line and the entire thing started over. I don’t worry too much initially how likely
that line is to fail or why it might.
For example, if line 2 fails, there’s no problem, because nothing has happened but if line 7 fails—depending on
how—we could end up sending the emails twice.
Another thing I will do is ask myself what might happen if the code is retried a long time later. For example,
suppose line 3 fails and the mail isn’t sent to the finance team. Suppose that the widget’s price is updated before
the failure is retried. If the price is no longer greater than $7,500, the mail will _never_ get sent to the finance team!
How we deal with this greatly depends on how serious it is if the code doesn’t execute or executes many times. It
also can depend on how much control we really have. See the sidebar “Idempotent Credit Card Charging” on the
next page for an example where a third party doesn’t make it easy to create idempotent code.
Let’s turn our attention to two problems with the code. First is that we might not send the emails at all if the
widget is changed between retries. Second is that a failure to send the admin email might cause us to send the
finance email again.
You might think we could move the logic into the mailers and have the mailers use background jobs. I don’t like
having business logic in mailers as we’ll discuss in “Mailers” on page 313, so let’s think of another way.
Let’s use two jobs instead of one. We’ll have one job do the finance check based on only the price and another do
the manufacturer check based on only the creation date.
#### Idempotent Credit Card Charging
```
The code to charge customers at Stitch Fix was originally written to run in the request cycle. It was ported from
Python to Ruby by the early development team and left alone until we all realized it was the source of double-charges
our customer service team identified.
We moved the code to a background job, but knew it had to be idempotent. Our payment processor didn’t
provide any guarantees of idempotency, and would often decline a retried charge that had previously succeeded. We
implemented idempotency ourselves and it was... pretty complex.
Whenever we made a charge, we’d send an idempotency key along with the metadata. This key represented a
single logical charge that we would not want to have happen more than once.
Before making a charge, we would fetch all the charges we’d made to the customer’s credit card. If any charge had
our idempotency key, we’d know that the charge had previously gone through but our job code had failed before it
could update our system. In that case, we’d fetch the charge’s data and update our system.
If we didn’t see that idempotency key, we’d know the charge hadn’t gone through and we’d initiate it. Just
explaining it was difficult, and the code even more so. And the tests! This was hard to test.
```
First, let’s removePostWidgetCreationJob, since we’re going to replace it with the two new jobs:
> rm app/jobs/post_widget_creation_job.rb
We’ll replace our use of that job inWidgetCreatorwith the two new jobs calledHighPricedWidgetCheckJoband
WidgetFromNewManufacturerCheckJob.
# app/services/widget_creator.rb
**end**
# XXX
# XXX
→ HighPricedWidgetCheckJob.perform_async(
→ widget.id, widget.price_cents)
→ WidgetFromNewManufacturerCheckJob.perform_async(
→ widget.id, widget.manufacturer.created_at.to_s)
Result.new( **created:** widget.valid?, **widget:** widget)
**end**
Note that we are callingto_soncreated_at. Sidekiq cannot correctly serialize aDateTimeand will emit a
warning if we don’t serialize it explicitly.
We’ll now replacepost_widget_creationwith two methods that these jobs will call.
# app/services/widget_creator.rb
```
widget.id, widget.manufacturer.created_at.to_s)
Result.new( created: widget.valid?, widget: widget)
end
```
×# def post_widget_creation_job(widget)
×# if widget.price_cents > 7_500_00
×# FinanceMailer.high_priced_widget(widget).deliver_now
×# end
# XXX
×# if widget.manufacturer.created_at.after?(60.days.ago)
×# AdminMailer.new_widget_from_new_manufacturer(widget).
×# deliver_now
×# end
×# end
# XXX
×# class Result
→ **def** high_priced_widget_check(widget_id, original_price_cents)
→ **if** original_price_cents **>** 7_500_00
→ widget **=** Widget.find(widget_id)
→ FinanceMailer.high_priced_widget(widget).deliver_now
→ **end**
→ **end**
→ **def** widget_from_new_manufacturer_check(
→ widget_id, original_manufacturer_created_at)
→ **if** original_manufacturer_created_at.after?(60.days.ago)
→ widget **=** Widget.find(widget_id)
→ AdminMailer.new_widget_from_new_manufacturer(widget)**.**
→ deliver_now
→ **end**
→ **end**
→ **class** Result
attr_reader **:widget
def** initialize(created:, widget:)
@created **=** created
And now, the jobs, starting withHighPricedWidgetCheckJob
# app/jobs/high_priced_widget_check_job.rb
**class** HighPricedWidgetCheckJob **<** ApplicationJob
**def** perform(widget_id, original_price_cents)
WidgetCreator.new.high_priced_widget_check(
widget_id,
original_price_cents)
**end
end**
ForWidgetFromNewManufacturerCheckJob, we have to deal with several issues we discussed above. Remember
that parameters passed to jobs get serialized into JSON and back—at least when using Sidekiq. In our case, we
are now passing in aStringcontaining a timestamp to the job, since JSON has no data type to store a date.
Because our service layer should not be parsing strings (or hashes or whatever) into real data types, but expect to
receive properly typed values, we will convert it in the job itself. Like a controller, the job code is the right place
to do these sorts of conversions. Fortunately,Date.parsewill do the right thing:
# app/jobs/widget_from_new_manufacturer_check_job.rb
**class** WidgetFromNewManufacturerCheckJob **<** ApplicationJob
**def** perform(widget_id, original_manufacturer_created_at)
WidgetCreator.new.widget_from_new_manufacturer_check(
widget_id,
Date.parse(original_manufacturer_created_at))
**end
end**
Our tests should still pass, _and_ give us coverage of the date-parsing we just had to do^6.
> bin/rails test test/services/widget_creator_test.rb
Running 8 tests in a single process (parallelization thresho...
Run options: --seed 59404
# Running:
...2023-12-04T23:56:00.306Z pid=9832 tid=8lw INFO: Sidekiq 7...
.....
Finished in 0.177779s, 44.9998 runs/s, 168.7491 assertions/s...
8 runs, 30 assertions, 0 failures, 0 errors, 0 skips
Wow. This is a huge amount of new complexity. What’s interesting is that it revealed some domain concepts that
we might not have been aware of. If it’s important to know the original price of a widget, we could store that
explicitly. That would save us some trouble around the finance mailer. Similarly, if it’s important to know the
original manufacturer of a widget, that, too, could be stored explicitly.
Perhaps you don’t think that these emails are important enough to warrant this sort of paranoia. Perhaps you can
think of some simpler ways to achieve what we achieved here. Perhaps you are right. Still, the point remains that
if there _is_ some bit of logic that you you need to execute exactly once, making that happen is going to require
complexity.
Make no mistake, this is accidental complexity with a carrying cost. You absolutely have to weigh this against the
carrying cost of doing it differently. I can tell you that when jobs aren’t idempotent, you create a support burden
for your team and customers and _this_ can have a real cost on team morale. No one wants to be interrupted to
deal with support.
This is why design is hard! But it helps to see what it actually looks like to deal with idempotency. I have certainly
refactored code to this degree, seen that it was not the right trade-off and reverted it. Don’t be afraid to revert it
all back to how it was if the end result is going to be less sustainable than the original.
If you want to go deeper on Sidekiq and background jobs, I have written “Ruby on Rails Background Jobs with
Sidekiq”^7 with the Pragmatic Programmers that you might find useful. It includes a more focused sample app that
demonstrates the various issues you can run into with background jobs and Sidekiq, along with techniques for
dealing with them.
(^6) I actually didn’t catch this the first time I wrote this chapter. Later parts of the book compare the manufacturer created date to another
and, even though it was really a string, the tests all seemed to pass, because I was using<to do the comparison. I changed it to usebefore?
after some reader feedback and discovered it was a string. Even after understanding how jobs get queued in detail, and having directly
supported a lot of Resque jobs (which do the same JSON-encoding as Sidekiq) for almost eight years, I still got it wrong. Write tests, people.
(^7) https://pragprog.com/titles/dcsidekiq/ruby-on-rails-background-jobs-with-sidekiq/
### Up Next
We’re just about done with our tour of Rails. I want to spend the next chapter touching on the other _boundary_
classes that we haven’t discussed, such as mailers, rake tasks, and mailboxes.
## 20 Other Boundary Classes
I want to touch briefly on some other parts of Rails that I had termed _boundary_ classes way back in “The Rails
Application Architecture” on page 17. Like controllers and jobs, rake tasks are a mechanism for triggering business
logic. Mailers, like views, render output for a user. Both Rake tasks and Mailers exist at the outside of the app,
interacting with the outside world, just as a controller does.
This chapter will focus on Mailers and Rake tasks. I’ll mention Mailboxes, Action Cable, and Active Storage only
briefly, because I have not used these parts of Rails in production. I don’t want to give you advice on something I
haven’t actually used.
Let’s start with mailers.
### 20.1 Mailers
```
This section’s code is in the folder20-01/of the sample code.
```
Mailers are a bit of an unsung hero in Rails apps. Styling and sending email is not an easy thing to do and yet
Rails has a good system for handling it. It has an API almost identical to rendering web views, it can handle text
and HTML emails, and connecting to any reasonable email provider is possible with a few lines of configuration.
And it can all be tested.
There are three things to consider when writing mailers. First is to understand the purpose of a mailer and thus
not put business logic in it. Second, understand that mailers are really jobs, so the arguments they receive must
be considered carefully. Last, you need a way to actually look at your emails while styling them, as well as while
using the app in development mode.
Let’s start with the purpose of mailers.
#### 20.1.1 Mailers Should Just Format Emails
Like controllers, you want your mailers to avoid having any business logic in them. The purpose of a mailer is to
render an email based on data passed into it. That’s it.
For example, our widget creation code has logic that sends the finance team an email if the widget’s price is above
$7,500. You might think it’s a good idea to encapsulate the check on the widget’s price in the mailer itself. There
is no real advantage to doing this and it will only create sustainability problems later.
First, it requires executing the mailer to test your widget creation logic. Second, it means that if something _else_
needs to happen for a high-priced widget, you have to move the check back intoWidgetCreatoranyway. It’s much
simpler if your mailers simply format and send mail.
Ideally, your mailers have very little logic in them at all. If you end up having complex rendering logic for an
email, it could be an indicator you actually have two emails. In this case, have the business logic trigger the
appropriate email instead of adding logic to the mailer itself.
The next thing to understand is that in most cases, your email is sent from a job.
#### 20.1.2 Mailers are Usually Jobs
When you calldeliver_nowafter calling a mailer, the email is sent right then and there. It’s typically a better
practice to calldeliver_laterso you can offload email-sending to a background job. The reasons for this are
detailed in the previous chapter, “Jobs” on page 291.deliver_laterwill use Active Job to queue the mail for
later delivery using whatever job backend you have chosen.
If you recall, Active Job uses something called globalid to allow you to safely serialize Active Records (and only
Active Records by default) into and out of the job backend. This means that our code as it’s written _will_ work
correctly if the email is sent via a job.
If, on the other hand, you send a non-Active Record to your mailer (including a date!), it may not be serialized
and de-serialized correctly (this is why I recommended using the job backend directly for background jobs).
That said, to send emails using the job backend directly, you’d have to make your own mailer job or jobs and
duplicate what Rails is already doing. My suggestion is to use Rails to send emails with Active Job, and manage
the inconsistency in how arguments are handled via code review.
You could additionally require that mailer arguments are always simple values that convert to and from JSON
correctly. In any case, make sure everyone understands the conventions.
Lastly, you need to understand how annoying and fussy it is to style an email.
#### 20.1.3 Previewing, Styling, and Checking your Mail
Testing mailers works like any other class in Rails. The more difficult part is styling and checking what you’ve
done. This is because there are _many_ different email clients that all have different idiosyncrasies about how they
work, how much CSS they support—if any—and what they do to render emails.
Fortunately, Rails provides the ability to preview emails in your browser. Let’s style the finance email.
When we created this mailer withbin/rails g, it created a preview class for us intest/mailers/previewscalled
finance_mailer_preview.rb.
If you haven’t used mailer previews before, they allow you to create some test data and render an email in your
browser. It’s not exactly like using a real email client, but it works pretty well. Each method of the preview class
causes a route to be enabled that will call that method and render the email it returns.
To create the test data, you can rely on whatever you may have put intodb/seeds.rb, or you can use your
factories. Let’s use this latter approach.
We’ll replace the auto-generated code with code to create a widget and pass it to the mailer. We’ll usebuild
instead ofcreate.buildwon’t save to the database. For the purposes of our mailer preview, this is fine, and,
because we want to use hard-coded names, it makes things a bit easier. If we saved these records to our dev
database, the first time we refreshed the page, it would try to save new records with duplicate names and cause
an error.
# test/mailers/previews/finance_mailer_preview.rb
# Preview this email at [http://localhost:3000/rails/mailers.](http://localhost:3000/rails/mailers.)..
**def** high_priced_widget
→ manufacturer **=** FactoryBot.build( **:manufacturer** ,
→ **name:** "Cyberdyne Systems")
→ widget **=** FactoryBot.build( **:widget** , **id:** 1234,
→ **name:** "Stembolt",
→ **price_cents:** 8100_00,
→ **manufacturer:** manufacturer)
→ FinanceMailer.high_priced_widget(widget)
**end**
```
end
```
We can fire up our app withbin/dev, and navigate to this path against your development server:
/rails/mailers/finance_mailer/high_priced_widget
You should see our very un-exciting email rendered, as in the screenshot below.
```
Figure 20.1: Previewing an Email
```
Since this is an email to our internal finance team, there’s no need for it to be fancy, but it should look at least
halfway decent. Let’s try to create an email that looks like so:
```
Figure 20.2: Finance Email Mockup
```
We want to use our design system (as discussed in “Adopt a Design System” on page 125), but we can’t use CSS
since few email systems support it. This is a good reminder that our design system is a _specification_ , not an
implementation. Our CSS strategy and related code is one possible implementation, but we can also use inline
styles in our mailer views to implement the design system as well. To do that, we need to know the underlying
spacing and font size values.
We know the font sizes already from when set up our style guide. For example, to get the third-largest font size,
we can use a style likefont-size: 2.8rem. For padding and other sizing, we’ll need to look at how our CSS is
implemented to get the specific sizes. In our case, we’ll only need two of the spacings, specifically 0.25rem and
0.5rem.
And, since we can’t rely on floats, flexbox, or other fancy features of CSS, we’ll create the two column layout with
tables... just like the olden days. Other than that, we’ll still use semantic HTML where we can. This all goes in
app/views/finance_mailer/high_priced_widget.html.erb:
<%# app/views/finance_mailer/high_priced_widget.html.erb %>
<article style="padding: 0.5rem;
font-family: helvetica, sans-serif">
<table style="width: 100%;">
<tr>
<td colspan="2" style="border-bottom: solid thin black;">
<p style="padding-left: 0.25rem;">
A new high-priced widget has been created!
</p>
</td>
</tr>
<tr>
<td colspan="2" style="padding: 0.5rem;">
&nbsp;
</td>
</tr>
<tr>
<td>
<div style="font-size: 2.8rem; margin-bottom: 0.5rem;">
<%= @widget.name %>
**<span style** ="font-size: 2.2rem" **>**
#<%= styled_widget_id(@widget) %>
**</span>
</div>
<div style** ="font-size: 1.3rem;" **>**
<%= @widget.manufacturer.name %>
**</div>
</td>
<td style** ="vertical-align: top; text-align: right" **>
<div style** ="font-size: 2.8rem;
margin-bottom: 0.25rem;
font-weight: bold" **>**
<%= number_to_currency(@widget.price_cents / 100) %>
**</div>
</td>
</tr>
</table>
</article>**
In order to usestyled_widget_idhelper, we need to use themailermethod to bring in the methods in
ApplicationHelper:
# app/mailers/finance_mailer.rb
**class** FinanceMailer **<** ApplicationMailer
→ helper **:application
def** high_priced_widget(widget)
@widget **=** widget
mail **to:** "[email protected]"
If you reload your preview, the email now looks like it should, though it certainly feels underwhelming given all
the markup we just wrote. See the screenshot below.
We should make the plain text version work, too. Let’s avoid any ASCII-art and just do something basic.
```
Figure 20.3: Styled HTML Email
```
<%# app/views/finance_mailer/high_priced_widget.text.erb %>
A new high-priced widget has been created!
<%= @widget.name %>
by <%= @widget.manufacturer.name %>
Price: <%= number_to_currency(@widget.price_cents / 100) %>
This can also be previewed and should look like the screenshot below.
Note that you can use partials and View Components to create re-usable components, just as we did with
web views. You may want to place partials somewhere likeapp/views/mailer_componentsor namespace View
Components in amailersdirectory to make it clear they are intended for mail views only.
For helpers, you can use the helpers inApplicationHelperusing thehelpermethod, but you can make your own
mail-specific helpers. I recommend again somewhere obvious likeapp/helpers/mailer_helpers.rb, so no one
mistakenly uses them in web views.
Lastly, if you are going to be creating a lot of emails in your app, you should consider augmenting your style guide
to show both CSS _and_ inline styles so that you can easily apply the design system to your emails.
In addition to previewing emails for styling, you may want to see them delivered in development.
Figure 20.4: Previewing a plain text email
#### 20.1.4 Using Mailcatcher to Allow Emails to be Sent in Development
By default, emails are not sent in development. Actually, by default they are not sent in _any_ environment, but you
usually end up configuring them in production only. You must setconfig.delivery_methodin one of the files in
config/environmentsso that Rails actually sends emails. This requires configuration from your email provider
and is detailed in the Rails guides^1.
If email is a critical part of your user flows, you may want to be able to see the emails during development. For
example, you might want to fire up your server, create a widget, and see that an email was actually sent to the
finance team. But you probably don’t want to actually email anyone for real.
To do this, you can use an app called MailCatcher^2. MailCatcher runs an SMTP server and provides a UI similar
to the Rails mailer previews we saw in the last section. It shows any email that was sent to it. The MailCatcher
website outlines how to set this up in Rails.
One thing to note is that MailCatcher should _not_ be installed in yourGemfile. It should be set up as another app
entirely. If you are using the Docker-based setup, this can be achieved by using an existing Docker image that
runs MailCatcher and setting that up in yourdocker-compose.ymlfile:
services **:**
mailcatcher **:**
image **:** sj26/mailcatcher
ports **:**
**-** "9998:1080"
This YAML snippet shows that MailCatcher will expose its web UI (running on port 1080) to your local machine’s
port 9998. Thus, you can access MailCatcher’s UI athttp://localhost:9998. Your Rails app would need to
connect to an SMTP server running on port 1025 (the default) of the hostmailcatcher(which is derived from
the service name in the YAML file). MailCatcher is nice to have setup for doing end-to-end simulations or demos
in your development environment.
While mailers respond to business logic by sending email, Rake tasks initiate business logic, so let’s talk briefly
about those.
### 20.2 Rake Tasks
```
This section’s code is in the folder20-02/of the sample code.
```
Sometimes you need to initiate some logic without having a web view to trigger it. This is where Rake tasks come
in. There are two problems in managing Rake tasks: naming/organizing, and code. Before that, let’s talk briefly
about what should be in a Rake task.
(^1) https://guides.rubyonrails.org/action_mailer_basics.html
(^2) https://mailcatcher.me
#### 20.2.1 Rake Tasks Are For Automation
If something needs to be automated, a Rake task is what should trigger that automation. Any time something
needs to happen on a routine basis—even if the schedule is irregular—a Rake task is the simplest mechanism to
trigger it.
For routine tasks that happen on a regular schedule, your job back-end may provide something (like sidekiq-
scheduler^3 ), but you still might have tasks that someone must manually perform on an ad-hoc basis. What you
want to avoid is having a lot of documentation that tells developers what code to run in production to perform
some sort of task. New team members will lack context for what they are doing and mistakes will be made. See
the sidebar “When Your User ID is 1” below for an example of this.
#### When Your User ID is 1
```
At Stitch Fix, we used a lot of what we called runbooks to help perform common tasks that would be needed in
response to support requests. For example, changing the internal status of some inventory to account for a mistake
that couldn’t be fixed by a user. These runbooks were Markdown files with instructions in them as well as code that
you would copy, paste, modify, and run in a production Rails console or in a production database.
A common task in these runbooks was to locate an internal user to associate with the actions being taken. This
provided a rudimentary paper trail for who modified some piece of data. The runbooks would instruct you to locate
your internal user via email or ID and use that when performing subsequent actions.
As the creator of the internal user system, my ID was 1. My ID was also the example used in several of the
runbooks. The result was that I was attributed to tons of changes in the internal systems I didn’t make because an
engineer was working quickly to fix a problem, copied my ID and didn’t think twice (this is why I prefer automation to
documentation—even the most conscientious engineers miss things when following written-out steps).
Fortunately, before Stitch Fix went public, all these runbooks were replaced with auditable code that couldn’t be
mis-attributed.
```
Rake tasks are also a good tool for performing one-off actions where you need some sort of auditable “paper trail”.
If you are in a heavily audited environment, such as one that must be Sarbanes-Oxley (SOX) compliant, you may
not be able to simply change production data arbitrarily. But you _will_ need to change production data sometimes
to correct errors. A Rake task checked into your version control system can provide documentation of who did
what, when, even if the Rake task is only ever executed once.
So, how should you organize these tasks?
#### 20.2.2 One Task Per File, Namespaces Match Directories
To invoke a Rake task, you typebin/rails «task_name». Developers often either need to figure out the task name
in order to invoke it, or they may see an invocation configured and need to find the source code. These are both
unnecessarily difficult if you don’t keep the tasks organized.
For example, if you see that you have a task that runs periodically nameddb:updates:prod:countries, you can’t
justgrepfor that task name. You have to find:countriesorcountries:in a file, and then see if the namespace
containing it isdb:updates:prod. The older an app gets, the more tasks it accumulates and the harder it is to
locate code.
The best way I have found to keep Rake tasks organized is as follows:
(^3) https://github.com/moove-it/sidekiq-scheduler
- Create a directory structure inlib/tasksthat matches the namespaces exactly. In the example above, that
meanslib/tasks/db/updates/prod/would be where we’d find thecountriestask.
- Name the actual file using the name of the task, and place only one task in each file. That means
lib/tasks/db/updates/prod/countries.rakewould be where the task is defined.
- Name the task—the last part of the full task name—something explicit and obvious. This example of
countriesis a terrible name. Tryupdate_list_of_countriesinstead.
- Always always always usedescto explain what the task does.
It might seem like overkill, but this will scale very well and no one is going to complain that they can easily figure
out where a task is defined by following a convention. I’ll also point out that your Rails app has no limit on the
number of source files it can contain—there’s plenty to go around^4.
Beyond this, you will need to think about the information architecture of your Rake tasks. This is not easy. My
suggestion is the same one I’ve given many other times in this book, which is to look for a pattern to develop and
form a convention around that.
As an example, here is how thelib/tasksdirectory is structured in an app I’m working on right now (I’m using
thetree^5 command that will make ASCII art of any directory structure):
> tree --charset=ascii -d lib/tasks/
lib/tasks/
|-- alerting
`-- production_data
|-- corrections
|-- role_assignment
`-- test_data
Thealertingnamespace/subdirectory holds tasks that feed into an alerting system to monitor the app.
production_dataholds tasks that manipulate data in production.production_data/correctionsholds tasks that
fix errant production data,production_data/role_assignmentholds tasks to assign roles programmatically since
there is currently no UI, andproduction_data/test_datacreates data in production for the purposes of testing.
This is just an example. Observe the tasks you need and keep them organized as you see patterns.
Aside from figuring out what to name your tasks and where they should go, you also need to know how to
implement them.
#### 20.2.3 Rake Tasks Should Not Contain Business Logic
All the reasons we’ve discussed about why business logic doesn’t go into controllers, jobs, or mailers applies to
Rake tasks, too. It’s just not worth it. You end up having to test the Rake tasks—not an easy prospect—and you
end up with code you may need elsewhere buried in some file inlib/tasks.
Your Rake tasks should ideally be one line of code to trigger some business logic. If the logic is particularly esoteric
to a one-off use-case, it can be hard to figure out where it should go to avoid being mistakenly re-used.
(^4) Yes, I know there _is_ a real limit, but it’s like in the billions. If you have a Rails app with billions of rake tasks, you may want to look into
microservices.
(^5) https://en.wikipedia.org/wiki/Tree_(command)
Let’s make two Rake tasks to demonstrate the subtleties of this guideline. Suppose we have a new status for
widgets called “Legacy”, and we want any widget in “Approved” to be given the status “Legacy” if it’s more than a
year since creation. We’ll run this task daily to automatically update the widgets.
Since this is our first task, let’s not worry about namespaces—we don’t have enough data about our needs to
choose a good one—and put it inlib/tasks. We’ll call the taskchange_approved_widgets_to_legacy. Because
the actual code should _not_ be in the Rake task, our Rake task will be pretty short:
# lib/tasks/change_approved_widgets_to_legacy.rake
desc "Changes all Approved widgets to Legacy that need it"
task **change_approved_widgets_to_legacy: :environment do**
LegacyWidgets.new.change_approved_widgets_to_legacy
**end**
Given the current state of the app, placing this code inWidgetCreatordoesn’t make much sense, so we’ll make a
new class. If our task was to perform some sort of follow-up to created widgets, it might make sense to go in
WidgetCreator, but since this is about old widgets, we’ll make a new class.
This Rake task doesn’t need to be tested. We’ll run it locally to make sure there are no syntax errors, and that
should be sufficient. It’s unlikely to ever change again and there is no value in asserting that we’ve written a line
of code correctly by reproducing that line of code in a test.
Let’s create the new class:
# app/services/legacy_widgets.rb
**class** LegacyWidgets
**def** change_approved_widgets_to_legacy
# Implementation here...
**end
end**
This class is unremarkable. It’s like any other code we’d write, and we can implement it by writing a test, watching
it fail, and writing the code. Or whatever you do. The point is that the Rake task’s implementation is in a normal
Ruby class.
Let’s consider a much different task. Suppose we have added a validation that all widget prices must end in
.95, for example $14.95. We can enforce this for new widgets via validations, but all the existing ones won’t
necessarily have valid prices.
We need to make a one-time change to fix these. Because the way we fix them could be complicated and because
we want to review and audit this change, we won’t make the change in the database directly. We need some code.
Let’s make the rake task. The task we just created is already inlib/tasks, but this new task is different. If we put
our new task alongside it inlib/tasks,it could be confusing, since our new task is intended to run only one
time, whereaschange_approved_widgets_to_legacyis intended to run regularly.
Let’s make that distinction clear by creating a namespace called one_off, meaning our task will go in
lib/tasks/one_off. We’ll call itfix_widget_pricing:
# lib/tasks/one_off/fix_widget_pricing.rake
namespace **:one_off do**
desc "Fixes the widgets created before the switch to 0.95 validation"
task **fix_widget_pricing: :environment do**
# ???
**end
end**
We need the line of code that replaces# ???to be a single invocation of a class we can test, but since this is
one-off, putting it in a class inapp/servicesdoesn’t feel quite right. Just like we made it clear that the task itself
is a one-off, let’s create a namespace inapp/servicesusing the same name—one_off.
# app/services/one_off/widget_pricing.rb
**module** OneOff
**class** WidgetPricing
**def** change_to_95_cents
Widget.find_each **do |** widget **|**
# Whatever logic is needed to update the price
**end
end
end
end**
We can use this in our Rake task:
# lib/tasks/one_off/fix_widget_pricing.rake
namespace **:one_off do**
desc "Fixes the widgets created before the switch to 0.95 v...
task fix_widget_pricing: :environment do
→ OneOff::WidgetPricing.new.change_to_95_cents
```
end
end
```
Why go through the hassle of having our Rake task defer to a class inapp/servicesthat is clearly not designed to
be used more than once? Doesn’t this make things more complicated than they need to be?
It depends. Yes, to accomplish this particular task requires writing six additional lines of code compared to
in-liningchange_to_95_centsin the Rake task itself. The problem with in-lining is that it creates a decision-point
for all Rake tasks. Should the task’s code go intoapp/servicesor directly into the Rake file?
Decisions like this have a carrying cost, and the inconsistency is just not worth it. It’s more sustainable to reduce
this carrying cost by creating an architecture that minimizes the number of decisions that need to be made.
One common use of Rake tasks that you should be wary of, however, is for development environment automation.
#### 20.2.4 Prefer Ruby Command Line Apps for Developer Automation
Rails includes tasks that manage the database, turn local caching on and off, and so forth. These are tasks that
help you manage your development environment. When you need to create such a task, consider creating a
command-line app inbin/instead of Rake task.
Rake tasks have only one advantage over a command-line script: access to your app’s internals via Rails. A Rake
task can use your Active Records, for example. But if you don’t need access to your Rails app’s internals, a Rake
task makes for a pretty terrible developer experience.
Rake task names cannot easily be tab-completed on the command line. Passing arguments to Rake tasks is
fairly difficult, since it doesn’t work like any other command line app. Rake tasks also don’t provide any way to
document command line arguments or flags.
Instead, create a bash or Ruby script inbin/, much as we did in Automating Application Setup withbin/setup
on page 33. Ruby’sOptionParseris a well-documented class from the standard library to allow you to accept
command line flags, switches, and arguments, documenting how each of them works.
Basically, if you don’t need access to your Rails app’s internals, and you are automating something only useful for
the local development environment, create your automation inbin/and document its existence inbin/setupor
your README.
Before we leave this chapter, I want to briefly touch on some of Rails’ other boundary classes.
### 20.3 Mailboxes, Cables, and Active Storage
I have not used Action Mailbox, Action Cable, or Active Storage in production, so I am not qualified to give strong
advice. That said, it might be useful to share my high level thinking about these technologies.
#### 20.3.1 Action Mailbox
Action Mailbox, added in Rails 6, allows your app to receive emails. I have used Action Mailbox just enough to
write the chapter about it in “Agile Web Development With Rails 6”^6 and that’s it. It seems like a great feature,
though.
(^6) https://pragprog.com/titles/rails6/
Action Mailboxes are very similar to controllers, in that they are triggered by an outside request. The way I would
approach writing a mailbox would be the same as writing a controller. I would handle basic type conversions and
confidence-checking, and hand everything off to something in the service layer.
#### 20.3.2 Action Cable
I have never used Action Cable, nor have I met anyone who had used it in production. That said, it’s an underlying
part of Turbo, which is a part of Hotwire, so I expect there to be more Action Cable in production as developers
adopt Rails 7.
Action Cable requires a lot of moving parts to coordinate, including both JavaScript and Ruby code. While it
certainly does work, it is much more complex than other parts of Rails.
On a few occasions when developers I know have discussed using Action Cable directly, they could usually solve
their immediate problem by having the page auto-refresh. If you don’t need high volumes of real-time updates on
your page, you may find Action Cable has a higher carrying cost than the value it delivers.
There’s no doubt in my mind that Action Cable is a great way to integrate Websockets into your app. Just know
that it’s complex and not widely used. That means you won’t have a lot of resources available to help you if you
have trouble.
#### 20.3.3 Active Storage
Active Storage is a feature that abstracts access to cloud storage services like Amazon’s S3. It is a technology I
very much wish had existed years ago, because we wrote our own janky version of this at my last job and it was a
pain to deal with.
I have not used Active Storage in production, and don’t have a lot of deep thoughts about it. My guess is that it
won’t save you from having to understand how the backing store works. But, since it’s part of Rails, it should be
reliable and supported. It also serves a much more common use case than Action Cable, meaning you are likely to
get better support for it if you run into trouble.
### Up Next
This completes our tour of the various parts of Rails and how I believe you can work with them sustainably. The
rest of the book will focus on patterns and techniques that are more broad and cross-cutting. The next chapter
will talk about something that’s not part of Rails but that most Rails apps need: authentication and authorization.
### PART
### III
# beyond rails
## 21
# Authentication and Authorization
One of the most common cross-cutting concerns in any app is the need to authenticate users and authorize the
actions they may take in an app. Rails does not include any facility for managing this, since the way authentication
is handled is far less common than, say, the way code accesses a database.
This gap requires that you do some up-front thinking and design for how you want to handle this important part
of your app. For authentication, there are two common gems that handle most common cases, and we’ll talk
about which situations are appropriate for which. These gems—Devise and OmniAuth—allow you to avoid the
difficult and error-prone task of rolling your own authentication system.
For authorization—controlling who can do what in your app—the situation is more difficult. There just aren’t as
many commonalities across apps related to role-based access control, so you can’t pick a solution and go. We’ll
talk about using the popular Cancancan gem to define and manage roles, but it’ll still be up to you to design a
role-based system that meets your needs.
And, of course, you’ll need to test your authentication and authorization systems. Remember that tests are a tool
for mitigating risk, and they can work well for mitigating the risks of unauthorized access to your app. But they
don’t come for free.
Let’s talk about _authentication_ first, which is the way in which we know who a user accessing our website is. The
two most common gems that provide this are Devise^1 and OmniAuth^2.
### 21.1 When in Doubt Use Devise or OmniAuth
Building an authentication system is not easy. There are many edge cases that allow would-be attackers to have
unauthorized access to your system. Many of them are quite creative and hard to predict in advance, such as
reverse-engineering the algorithm used for generating random numbers on your server and using that to guess
passwords more efficiently.
Security is one of those areas where leaning heavily on expertise and experience will pay off far better than
learning it from first principles. When it comes to user management, I’m almost certain that you, dear reader, are
not the expert that, say, Google’s entire security team is. And that’s OK.
When it comes to user management, you want to ideally allow someone you trust to handle as much of the
authentication as you can, be that the combined 546 contributors to Devise, or the team at Google that manages
their OAuth implementation.
(^1) https://github.com/heartcombo/devise
(^2) https://github.com/omniauth/omniauth
The simplest way that reduces risk—assuming it meets all your requirements—is to allow a third party service
like Google or GitHub to manage authentication. OmniAuth can handle much of the integration for you if you go
this route.
#### 21.1.1 Use OmniAuth to Authenticate Using a Third Party
OmniAuth is a Rails API for doing OAuth^3 -style authentication. It wraps the specifics of many popular services
providing you with a single API. With a few lines of code, you can allow users to log in with, say, Twitter, and not
have to create an authentication system of your own.
It works by redirecting your users to the third party site, having that site do the authentication, and then redirect
back to you. OmniAuth handles the specifics of integrating with each site that you choose to support (you can use
as many different third parties as you want). See the figure “OmniAuth Authentication Flow” on the next page.
```
Figure 21.1: OmniAuth Authentication Flow
```
Note that in step 5, you will need to store some unique identifier passed from the service to associate with the
user in your app. Take care with what you choose to use for this value. For example, users can change their email
or username without necessarily changing their identity in your service.
The key question around using OmniAuth is about your userbase. Do they all have accounts in one or more third
parties that you can trust with authentication?
(^3) https://oauth.net
If your app is used only by employees of your company, and your company requires everyone to use, say, Gmail
on a company-managed account, the answer is “yes”. Everyone must have a Gmail account, and you are trusting
Google with your email, so you could rely on them for authentication as well.
For an app accessible to the general public, the question is harder to answer. For a service aimed at developers, it’s
likely a good assumption most of the userbase has GitHub accounts, but less likely they would all have Facebook
accounts.
The main consequence of using OmniAuth is that you require your users to have an account with a trusted
third-party. It’s important to understand what “trusted” means in this context. A third party I trust for my app,
might not be worthy of your trust for your app.
For example, if you are working on the website for the United States Internal Revenue Service (responsible for
collecting taxes in the US), you probably don’t want to allow a private company to even know who is logging into
your service. It’s not a slight on Google, but the IRS shouldn’t trust Google with this information.
If you either cannot trust the third parties where your users have accounts, or your users don’t have accounts with
third parties you _do_ trust, you’ll need to build authentication into your app. For that, you should use Devise.
#### 21.1.2 Building Authentication Into your App with Devise
Devise is a gem that provides an almost end-to-end experience for managing user accounts, logins, password
resets, password rules, and user auditing. It does this by generating code to use in your app that relies on code in
Devise’s gem.
Devise is highly configurable and has a steep learning curve. But the documentation is great and since it’s widely
used, it’s easy to get help for using it properly. It is worth traversing this learning curve, because authentication is
so critical to most apps.
The value Devise provides is that it’s battle-hardened and actively developed. Unless you are a deep expert in
security, Devise will do a better job than you at managing all parts of the authentication process. Devise centers
around aUserActive Record, backed by theusersdatabase table (these names are configurable).
TheUsermodel can be configured with Devise-provided modules to give your authentication process whatever
features it needs. For example, you can allow users to reset their passwords using theRecoverablemodule. You
can lock accounts after a certain number of failed attempts by using theLockablemodule. There are many more.
Devise also provides a user interface for you. The views it provides are bare-bones, so you’ll likely need to make
use of your design system (as discussed in “Adopt a Design System” on page 125) to make them look good.
I’m not going to walk through setting up Devise as this would be duplicative of the great documentation it already
has. My suggestions for using Devise are to go through the “Getting Started” part of its documentation in your
app. Then, take a look at the configurable modules and bring in those that you need. You can bring others in later.
Note that you can combine both OmniAuth and Devise to allow multiple forms of authentication. This can
complicate your overall authentication strategy and will reduce the security of your site, since each method of
authentication is a potential attack vector. But it’s an option you have if you need it.
Once you have authentication sorted out, you are likely to need some form of authorization to control which users
are allowed to perform which actions in the app.
### 21.2 Authorization and Role-based Access Controls
In most organizations, the authentication mechanism is driven by product and business concerns, and the decision
around what method to use is typically easy to make. Authorization—the mapping of what users can perform
what actions—is often much more complicated.
If you are building software to be used by employees of the company, or a software-as-a-service product intended
for knowledge workers, there will often be myriad features available, some of which control highly-sensitive or
potentially dangerous functions. For example, you might have a feature to grant credit to users, allowing them to
purchase products without using their own money. You may not want anyone at the company to be able to grant
this credit.
What makes authorization tricky is that it’s often difficult to clearly map users onto roles, and also difficult to
know what the roles actually should be. If you make roles too general, you lose the ability to control access the
way you might want. If you make roles overly-specific, you create a confusing list of permissions that can lead to
errors. If you’ve ever worked with AWS, the list of IAM Roles is massive. You simply can’t consult a list of them to
decide which are the right ones for a given task.
To further complicate the task of authorization design, whatever you come up with has to be easily auditable. In
other words, you need to create a system in which you can easily answer the question “What is this user allowed
to do?” and prove that you have implemented this correctly to someone else.
#### 21.2.1 Map Resources and Actions to Job Titles and Departments
If you have designed your app around many different resources that all have the same set of canonical actions (as
discussed in “Don’t Create Custom Actions, Create More Resources” on page 75), you can use your app’s routes as
a definitive list of all actions and data your app has. The ability to generate this list from code is a _gift_ to your
fellow security professionals and compliance team members!
You then need to map each user account to the list of routes/actions that are appropriate for that user. The best
way to do _that_ is to assign each user a role, based on their job title and department, and then configure access to
routes and actions for each job title and department.
The reason to use job title and department is twofold. First, it’s well-known, unambiguous information about
each user. Second, most rules around who can do what tend to relate to job title and department anyway. The
finance team can access financial records, but the marketing team probably shouldn’t. The engineering team can
access deployments, but the customer service team cannot, etc.
Using job title and department also means that, when your authorization code is audited, it will be far easier to
understand. You are mapping a well-known concept—job title and department—to the particularities of your app.
For example, it’s much easier to verify that “all senior customer service managers can create refunds” than it is to
verify that “all senior customer service managers get the ‘refunds’ role, but sometimes other people get this role
as well, but whoever has this role can create refunds”. When roles can be arbitrarily assigned, you then need a
system to manage _that_ and _this_ system must also be audited (and, of course, restricted based on role-base access
controls). If you can avoid it... avoid it.
To manage the actual access restrictions, the Cancancan gem^4 gives you the plumbing you need^5. But be warned:
it includes a lot of implicit and flexible features that will complicate your application if you aren’t careful in how
(^4) https://github.com/CanCanCommunity/cancancan
(^5) This is a fork/continuation of the original cancan gem, which has not been maintained or transitioned to another team.
```
you use them.
```
#### 21.2.2 Use Cancancan to Implement Role-Based Access
Cancancan has two main parts to its API: anAbilityclass that defines what any given user is allowed to do
(including unauthenticated users), and methods to use in controllers or views to check the given user’s access.
```
For example, to allow your entire customer service team to list and view a refund (which would be the Rails
actionsindexandshow), but only allow senior managers to create them, you might write code like this:
```
```
class Ability
include CanCan :: Ability
```
```
def initialize(user)
if user.present?
if user.department == "customer_service"
can [ :index , :show ] , Refund
```
```
if user.job_title == "senior manager"
can [ :create , :new] , Refund
end
end
end
end
end
```
```
This only defines the permissions. You still need to check them. You can useauthorize_resourceto apply a
permissions check to all the standard controller actions:
```
```
class RefundsController < ApplicationController
authorize_resource
end
```
```
authorize_resourcecan determine that the resource isRefundbased on the controller name. It will then set up its
own controller callbacks to compare the user against the abilities you’ve defined, raising aCanCan::AccessDenied
exception if an unauthorized user tries to access a route/action they shouldn’t.
You can userescue_fromto control the user experience when that happens, for example:
```
```
class ApplicationController < ActionController :: Base
rescue_from CanCan :: AccessDenied do
```
```
redirect_to main_app.root_url,
notice: "You cannot access that page"
end
end
```
```
This all works based on the assumption thatcurrent_userreturns an object representing who is logged in. How
this is defined depends on your authentication scheme, but it’s typical to store the user’s ID in the session, and
implementcurrent_userinApplicationControllerto examine the session and fetch the user record:
```
```
class ApplicationController < ActionController :: Base
```
```
def current_user
@current_user ||= User.find_by( id: session [:user_id] )
end
end
```
Note that if you are using OmniAuth, you will need to store some record in your database when the user
successfully authenticates so you can associate them with roles. This would happen in step 5 from the figure
“OmniAuth Authentication Flow” on page 330.
```
Cancancan will also allow you to callauthorize!in a controller method to authorize more explicitly, but you will
find it much simpler to rely onauthorize_resourceand a properly-configuredAbilityclass.
To restrict content in your views based on roles, you can use the methodcan?. While excessive use of this can
create complicated view code, it’s often handy when you want to omit links the user shouldn’t see. For example,
this will show the “Create Refund” link only to a user authorized to create refunds:
```
```
<% if can? :create , Refund %>
<%= link_to "Create Refund", new_refund_path %>
<% end %>
```
```
Cancancan is more flexible than this, but using this flexibility will likely make your authorization system more
confusing.
```
#### 21.2.3 You Don’t Have to Use All of Cancancan’s Features
```
The features outlined above are sufficient to create an authorization system that will work for your needs and
be easily auditable. The remainder of Cancancan’s features will work against those goals and result in a more
complicated and harder-to-understand setup.
Since you aren’t using custom actions, you won’t need to use that feature of Cancancan, and I suggest you avoid
creating custom authorization actions if possible.
```
You also should avoidload_and_authorize_resource, which conflates an access control check with a database
lookup. It will authorize a user for access to a resource, and then assign it to an instance variable after calling
find. Intermixing authorization with data access like this will be confusing and won’t provide strong benefits.
You should also resist the urge to create an internal DSL around yourAbilityclass. Although an app with many
actions and roles will require a large and complexAbilityclass, I would strongly recommend you manage that
class using conventional means like functional decomposition.
Unlike other classes in your system,Abilitywill be modified infrequently but read very frequently, and often by
people outside your team who may not be Rails developers. Thus, it’s a good idea to keep yourAbilityclass free
of dynamic, implicit concepts. Use functional decomposition via private methods to manage the complexity of the
class, but do _not_ create a sophisticated abstraction layer. This will make it harder to understand.
In addition to the design work required to properly set up authentication and authorization, you should test it
using system tests.
### 21.3 Test Access Controls In System Tests
Security incidents are expensive. They derail teams from providing business value, lead to a crisis of confidence
for the company and—in many cases—expose users’ personal information to bad actors. There’s no way to
absolutely prevent such incidents, but ensuring that your access controls are working is a huge help.
The clearest way to do this is to write system tests that exercise the system as different types of users. Depending
on how complex your authorization needs are, you may need a lot of tests. Remember that tests are a mechanism
for risk management. This means that you probably don’t want to test every action against every possible role,
but you _do_ need to strategically test many roles and actions.
I would highly recommend a thorough testing of all authentication flows no matter what. This is particularly
important if you are using Devise, since Devise outputs code you have to maintain yourself.
As for testing authorizations, this can be trickier. It requires a solid understanding of _why_ your authorization
configuration is the way it is. What problems are being solved by restricting access to various parts of the system?
What is the consequence of an unauthorized person gaining access to a feature they aren’t supposed to access? If
that happened, would you know it had happened?
The answers to these questions can help you know where to focus. For example, if you can’t tell who performed a
critical action that is restricted to certain users, you should thoroughly test the access controls to that action.
You also want to make it as easy as possible for developers to test the authorizations around new features or to
test changes to authorizations. There are two things you can do to help. The first is to make sure you have a wide
variety of test users that you can create with a single line of code in a test. The second is to cultivate re-usable test
code to setup for an authorization-related test or verify the results of one (or both).
The way to cultivate both of these is to start writing your system tests and look for patterns. If you followed my
advice in “Models, Part 2” on page 233, you should have a factory to create at least one user. As you write system
tests using different types of users, extract any that you use more than once into a factory. This allows future
developers—yourself included—to quickly create a user with a given role.
You will also notice patterns in how you set up your test or perform assertions. Extract those when you see
them. The mechanism for this depends on your testing framework. For Minitest, you can follow the pattern we
established withwith_cluesandconfidence_check, by creating modules intest/support:
## test/support/authorization_system_test_support.rb
**module** TestSupport
**module** AuthorizationSystem
**def** login_as(user_factory_name)
user **=** FactoryBot.create(user_factory_name)
```
# Whatever else needed to log into your system as this user
end
```
**def** assert_no_access
# assert whatever the UX is
# for users being denied access
**end
end
end**
## test/system/create_manufacturer_test.rb
require "test_helper"
require "support/authorization_system_test_support"
**class** CreateManufacturerTest **<** ApplicationSystemTestCase
include TestSupport **::** AuthorizationSystem
```
test "only admins can create manufacturers" do
login_as( :non_admin )
```
```
# attempt to create a manufacturer
```
assert_no_access
**end
end**
If using Rspec, you can use this pattern for setup code, but you will likely want to make custom matchers for
assertions.
If you do have security or compliance people on your team or at your company, you should use them to help think
through what should and should not be tested. Most security professionals understand the concept of risk and
understand the trade-offs between exhaustively testing everything and being strategic. In fact, they are better at
this than most, since it’s a critical part of their job. Avail yourself of their expertise.
### Up Next
Continuing our discussion of sustainability issues beyond the Rails application architecture, let’s talk about JSON
APIs next.
## 22 API Endpoints
Rails is a great framework for making REST APIs, which are web services intended to be consumed not by a
browser, but by another programmer. Even if your app is not explicitly an API designed for others to consume, you
might end up needing to expose endpoints for your front-end or for another app at your company to consume.
The great thing about APIs in Rails is that they can be built pretty much like regular Rails code. The only difference
is that your APIs render JSON (usually) instead of an HTML template. Still, developers do tend to over-complicate
things when an API is involved, and often miss opportunities to keep things sustainable by leveraging what Rails
gives you.
That’s what this chapter is about. It’s not about designing, building, and maintaining a complex web of
microservices, but instead just about how to think about JSON endpoints you might use for programmatic
communication between systems.
Here’s what we’ll cover:
- Be clear on what you need an API or JSON endpoint for.
- Approach your JSON API the same as any other Rails feature, by being resource-oriented and using canonical
Rails actions.
- Use the simplest mechanisms for authentication, content negotiation, and versioning that you can.
- Use Rails’ default JSON serialization as much as you can.
- Test the API with an integration test and assert on the proper encoding.
As always, we start with what problem we’re trying to solve with our hypothetical API.
### 22.1 Be Clear About What—and Who—Your API is For
There is a big difference in building and maintaining a massive public API used by millions of developers
and creating some JSON endpoints for your front-end code to consume. If you build your handful of front-
end-consuming endpoints with the fit and finish of, say, the GitHub API, you will have incurred both massive
opportunity costs _and_ large carrying costs without benefit.
Before navigating the complex world of strategies around APIs—from authentication to data serialization—you
should be honest about what your API is actually for. Write out the use cases and identify who will be using the
API. It’s OK to suppose some reasonable future uses and consumers, but don’t let flights of fancy carry you away.
Just because you might think it would be cool to have the world’s preeminent Widget API doesn’t mean it will
happen. And if it _did_ happen, the best way to prepare for it is to minimize carrying costs around the features you
_do_ need to build. This is where a keen understanding of your product roadmap and overall problems your app
solves are critical.
For the rest of this chapter I’m going to assume you need an API for something simple, such as consumption by
your own front-end code via Ajax calls, or lightweight app-to-app integration inside your team or organization. A
public-facing API that is part of your product is a different undertaking.
Keep the details about why you are building an API at the top of your mind. Developers will propose a lot of
different solutions in the name of security, scalability, and maintainability. Being able to align on the actual needs
of the API can help drive those conversations productively. For example, Ajax calls within your Rails app really
don’t require JWTs vended by a separate OAuth flow, even if such an architecture might be more scalable.
Once you understand what your API is for, you next need a general strategy for implementing it. The basis of that
strategy is to adopt the same conventions we’ve discussed in this book: working resource-oriented, following
Rails conventions, and embracing Rails for what it is—not what you wish it might be.
### 22.2 Write APIs the Same Way You Write Other Code
```
This section’s code is in the folder22-02/of the sample code.
```
Ideally, a controller that powers an API should look just as plain as any other controller:
**class** WidgetsController **<** ApplicationController
**def** index
widgets **=** Widget.all
render **json: { widgets:** widgets **}
end**
**def** create
widget **=** Widget.create(widget_params)
**if** widget.valid?
render **json: { widget:** widget **}** , **status:** 201
**else**
render **json: { errors:** widget.errors **}** , **status:** 422
**end
end
end**
You may not want _exactly_ this sort of error-handling, but you get the idea. There’s rarely a reason to do anything
different in your API controller methods than in your non-API methods.
You would be well-served to create a separate routing namespace and thus controller namespace for your API
calls. This means that while a browser might navigate to/widgets/1234to get the view for widget 1234, an API
client would access/api/widgets/1234.jsonto access the JSON endpoint.
The reason for this is to build in from the start a notion of separation that you might need later. For example,
if you eventually need to serve your API from another app, your front-end infrastructure can route/apito a
different back-end app. If both a browser and an API client used/widgets/1234, this will be harder to pull apart.
There’s also little advantage in mixing the browser and API code in the same controller. Often there are little
differences, and you don’t always have an API endpoint for each browser-facing feature (or vice-versa). If you
have duplicated code, you can share it with modules or classes.
You should also create a base controller for all your API endpoints. This allows you to centralize configuration like
authentication or content-negotiation without worrying about your web-based endpoints.
Let’s see both of these in action by creating an endpoint for widgets. We’ll skip authentication and versioning for
now—we’ll talk about those in a bit.
First, we’ll create the base controller, calledApiControllerand place it inapp/controllers/api_controller.rb:
# app/controllers/api_controller.rb
**class** ApiController **<** ApplicationController
**end**
Next, we’ll create a route for our API endpoint, and use theapinamespace:
# config/routes.rb
```
resources :design_system_docs , only: [ :index ]
end
```
→ # All API endpoints should go in this namespace.
→ # If you need a custom route to an API endpoint,
→ # add it in the custom routes section, but make
→ # sure the resource-based route is here.
→ namespace **:api do**
→ resources **:widgets** , **only: [ :show ]**
→ **end**
####
# Custom routes start here
#
This has the nice side-effect of creating a readable route helper:api_widget_path.
Now we’ll create our controller inapi/widgets_controller.rb:
# app/controllers/api/widgets_controller.rb
**class** Api **::** WidgetsController **<** ApiController
**def** show
widget **=** Widget.find(params **[:id]** )
render **json: { widget:** widget **}
end
end**
We’ll write a test for this later, but hopefully you can see that your API controllers can—and should—be written
just like any other. You will still defer business logic to the service layer, and still approach your design by
identifying resources. Concerns like authentication, versioning, and serialization formats can all be handled as
controller callbacks or middleware. Let’s talk about those next, because you have to sort these issues out before
building your API. First, we’ll talk about authentication.
### 22.3 Use the Simplest Authentication System You Can
```
This section’s code is in the folder22-03/of the sample code.
```
Many developers, upon hearing “API” and “Authentication” will jump to JSON Web Tokens, or _JWT_. Or they might
think “OAuth”. Be careful here. If your API is simply a JSON endpoint for consumption by your front-end, you can
transparently use the existing cookie-based authentication you already have. Remember, the more authentication
mechanisms you support, the more vulnerable your app is to security issues, because each mechanism is an attack
vector.
If your API is being consumed internally, there are two other mechanisms you should consider before adopting
something complex like JWT or OAuth, especially if your API does not require a sophisticated set of authorizations.
The first is good ole HTTP Basic Auth, which is a name and a password.
Rails provides a methodhttp_basic_authenticate_withthat you can call in your controllers to use basic auth.
Every HTTP client in the known universe supports basic auth, and you can embed your credentials in a url for
easy debugging and local development like so:
https://username:[email protected]/api/widgets.json
For example, in our baseApiController, you could do something like this:
**class** ApiController **<** ApplicationController
→ skip_before_action **:require_login** # or whatever callback was
→ # set up to require login
→ http_basic_authenticate_with **name:** ENV **[** "API_USERNAME" **]**
→ **password:** ENV **[** "API_PASSWORD" **]
end**
You don’t have to use a single set of hard-coded set of credentials, either. See the Rails documentation^1 for
examples of more sophisticated setups that allow multiple credentials.
A second almost-as-simple mechanism is to use the HTTP Authorization header^2. Despite its name, this header
is used for authentication and can encode an API key. Setting HTTP headers is, like Basic Auth, something any
HTTP client library can do, and can be done with any command-line HTTP client, such ascurl. This, too, is
something Rails provides support for^3.
I would recommend these mechanisms if you don’t have specific requirements that preclude their use. _Many_
high-traffic, public APIs use these mechanisms and have for years, so there is no inherent issue with scalability.
They also have the virtue of being easy for any developer of any level of experience to understand quickly.
Let’s set up token-based authentication for our API. Rather than hard-code a single key, let’s create a database
table of keys instead. This way, we can give each known client their own key, which helps with auditing. We’ll
also allow for keys to be de-activated without being deleted.
> bin/rails g migration create_api_keys
invoke active_record
create db/migrate/20231204235629_create_api_keys.rb
For the stability of this book, I’m going to rename the migration file. You don’t have to do this.
> mv db/migrate/*create_api_keys.rb \
db/migrate/20210102000000_create_api_keys.rb
Now, we’ll create the table. It will have a key, a created date, a client name, and a deactivation date.
# db/migrate/20210102000000_create_api_keys.rb
**class** CreateApiKeys **<** ActiveRecord **::** Migration **[** 7.1 **]
def** change
→ create_table **:api_keys** ,
→ **comment:** "Holds all API keys for access to the API" **do |** t **|**
→ t.text **:key** , **null:** false,
→ **comment:** "The actual key clients should use"
→ t.text **:client_name** , **null:** false,
→ **comment:** "Name of the client who was assigned this key"
(^1) https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
(^2) https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
(^3) https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
→ t.datetime **:created_at** , **null:** false,
→ **comment:** "When this key was created"
→ t.datetime **:deactivated_at** , **null:** true,
→ **comment:** "When the key was deactivated. " **+**
→ "When present, this key is not valid."
```
t.timestamps
end
```
We also don’t needupdated_atbecause there should never be an arbitrary update to this table—just a deactivation
by settingdeactivated_at. This is somewhat unusual, so I will deal with this with... comments!
# db/migrate/20210102000000_create_api_keys.rb
```
"When present, this key is not valid."
```
→ # Note: No updated_at because there should be no updates
→ # to rows here other than to deactivate
**end
end
end**
There are a few other things we need, too. First, the API keys should be unique, so we’ll need an index to enforce
that constraint. Second, we don’t want any client to have more than one active API key. We can achieve this with
a Postgres _conditional_ index. This is an index that only applies when the data matches a givenWHEREclause, which
we can specify to rails using thewhere:option ofadd_index.
# db/migrate/20210102000000_create_api_keys.rb
# Note: No updated_at because there should be no update...
# to rows here other than to deactivate
**end**
→ add_index **:api_keys** , **:key** , **unique:** true,
→ **comment:** "API keys have to be unique or we " **+**
→ "don't know who is accessing us"
→ add_index **:api_keys** , **:client_name** ,
→ **unique:** true,
→ **where:** "deactivated_at IS NULL"
**end
end**
We’ll run the migration:
> bin/db-migrate
[ bin/db-migrate ] migrating development schema
== 20210102000000 CreateApiKeys: migrating =================...
-- create_table(:api_keys, {:comment=>"Holds all API keys fo...
-> 0.0081s
-- add_index(:api_keys, :key, {:unique=>true, :comment=>"API...
-> 0.0051s
-- add_index(:api_keys, :client_name, {:unique=>true, :where...
-> 0.0018s
== 20210102000000 CreateApiKeys: migrated (0.0151s) ========...
[ bin/db-migrate ] migrating test schema
== 20210102000000 CreateApiKeys: migrating =================...
-- create_table(:api_keys, {:comment=>"Holds all API keys fo...
-> 0.0042s
-- add_index(:api_keys, :key, {:unique=>true, :comment=>"API...
-> 0.0008s
-- add_index(:api_keys, :client_name, {:unique=>true, :where...
-> 0.0014s
== 20210102000000 CreateApiKeys: migrated (0.0065s) ========...
Let’s create the model and a test for that partial index, since this is somewhat complex and could be a surprising
implementation to developers unfamiliar with Postgres.
First, the model, which is just two lines of code:
# app/models/api_key.rb
**class** ApiKey **<** ApplicationRecord
**end**
Let’s create a factory for it.
# test/factories/api_key_factory.rb
FactoryBot.define **do**
factory **:api_key do**
key **{** SecureRandom.uuid **}**
client_name **{** Faker **::** Company.unique.name **}
end
end**
We can now create the test of the model, which will exercise the partial index.
# test/models/api_key_test.rb
require "test_helper"
**class** ApiKeyTest **<** ActiveSupport **::** TestCase
test "client cannot have more than one active key" **do**
api_key **=** ApiKey.create!(
**key:** SecureRandom.uuid,
**client_name:** "Cyberdyne"
)
```
exception = assert_raises do
ApiKey.create!(
key: SecureRandom.uuid,
client_name: "Cyberdyne"
)
end
```
```
assert_match /duplicate key.*violates unique constraint/i,
exception.message
end
test "client can have more than one key if all " +
"but one is deactivated" do
api_key = ApiKey.create!(
key: SecureRandom.uuid,
client_name: "Cyberdyne",
deactivated_at: 4.days.ago
)
```
```
assert_nothing_raised do
ApiKey.create!(
key: SecureRandom.uuid,
client_name: "Cyberdyne"
)
end
```
**end
end**
This test should pass:
> bin/rails test test/models/api_key_test.rb
Running 2 tests in a single process (parallelization thresho...
Run options: --seed 56027
# Running:
..
Finished in 0.024890s, 80.3550 runs/s, 160.7101 assertions/s...
2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
With that in place, we can now use this table to locate API keys for authentication.
In ourApiController, we’ll create a callback:
# app/controllers/api_controller.rb
**class** ApiController **<** ApplicationController
→ before_action **:authenticate**
→private
→ **def** authenticate
→ authenticate_or_request_with_http_token **do |** token, options **|**
→ ApiKey.find_by( **key:** token, **deactivated_at:** nil).present?
→ **end**
→ **end
end**
We’ll see this in action when we write our test, but you can try it locally by usingcurlto access your endpoint
and see that you get an HTTP 401. If you create a record in theapi_keystable, then use that key withcurl, it
should work. For example:
curl -V -H "Authorization: Token token=\"«api_keys.key you used»\"" \
[http://localhost:9999/api/v1/widgets/1234](http://localhost:9999/api/v1/widgets/1234)
Once you have authentication set up, you’ll need some sort of content negotiation.
### 22.4 Use the Simplest Content Type You Can
```
This section’s code is in the folder22-04/of the sample code.
```
The HTTPAcceptheader allows for a wide variety of configurations for how a client can tell the API what sort of
content type it wants back (theContent-Typeheader is for the server to specify what it’s sending). You can ignore
it altogether and always serve JSON, or you could require the content type to beapplication/json, or you could
create your own custom content type for all your resources, or even make a content type for each resource. The
possibilities—and associated carrying costs—are endless.
I would not recommend ignoring theAcceptheader. It’s not unreasonable to ask clients to set it, it’s not hard for
them to do so, and it allows you to serve other types of content than JSON from your API if you should need it.
I would discourage you from using custom content types unless there is a very specific problem you have that
it solves. When we discuss JSON serialization, I’m going to recommend usingto_jsonand I’m not going to
recommend stuff like JSON Schema, as it is highly complex. Thus, a content type ofapplication/jsonwould be
sufficient.
That said, if you decide you need to use more advanced tooling like JSON Schema, a custom content type could
be beneficial, especially if you have sophisticated tooling to manage it. If you have to hand-enter a lot of custom
types and write custom code to parse out the types, you are probably over-investing.
While you should examine theAcceptheader, there’s no reason to litter your API code withrespond_tocalls that
will only ever respond to JSON. Thus, you can have a single check inApiControllerfor the right content type.
Rails provides therequestmethod that encapsulates the current request. It has a methodformatthat returns
a representation of what was in theAcceptheader. That representation can respond tojson?to tell us if the
request was a JSON request.
We can use this and, if the request is not JSON, return an HTTP 406 (which indicates that the app doesn’t support
the requested format). First, we’ll specify a callback. We want it after the authentication callback since there’s no
sense checking the content of an unauthorized request.
# app/controllers/api_controller.rb
**class** ApiController **<** ApplicationController
before_action **:authenticate**
→ before_action **:require_json**
```
private
```
Now, we’ll implementrequire_json:
# app/controllers/api_controller.rb
ApiKey.find_by( **key:** token, **deactivated_at:** nil).present...
**end
end**
→ **def** require_json
→ **if!** request.format.json?
→ head 406
→ **end**
→ **end
end**
By implementing this as a callback (instead of a middleware), controllers can override this callback if they need
to respond to some other content type. For example, if we need to allow API access to a widget’s datasheet, which
might be in PDF, we could customize just that endpoint:
**class** Api **::** WidgetDatasheetsController **<** ApiController
skip_before_action **:require_json**
before_action **:require_json_or_pdf**
```
def show
respond_to do | format |
format.json do
# ...
end
format.pdf do
# ...
end
end
end
```
private
**def** require_json_or_pdf
**if!** request.format.json? **&&**
!request.format.pdf?
head 406
**end
end
end**
Note that to make code like this work, you’ll need to register the PDF mime type. See the documentation on
Mime::Type^4 for more details.
(^4) https://api.rubyonrails.org/classes/Mime/Type.html
Once you’ve added code for content types, you next need to decide how you will handle versioning, even though
you might never need it.
### 22.5 Just Put The Version in the URL
```
This section’s code is in the folder22-05/of the sample code.
```
Nothing gets a debate going around API design quite like versioning. Versioning is when you decide that you
need to change an existing endpoint, but maintain both the original and the changed implementations.
There are two decisions you have to make around versioning. First is to decide what constitutes a new version.
Second is how to model that in your API.
I would _highly_ recommend you adopt a simplified semantic versioning policy for your APIs. Semantic Versioning^5
states that a version is three numbers separated by dots, for example 1.4.5. The first is the _major_ version and
when this changes, it indicates breaking changes to the underlying API. Code that worked with version 1 should
expect to not work with version 2. Changes to the other two numbers (called _minor_ and _patch_ ) indicate backwards
compatible changes. Code that works with version 1.3.4 should work with 1.4.5.
For your API, don’t track or worry about minor versions and patches—only track major versions. If you make
backwards-compatible changes to an endpoint, leave the current version as it is. _Only_ when you need to make a
backwards-incompatible change should you bump the version number of the API.
I would make a few additional recommendations:
- Try to avoid making breaking changes if you can. Be _really_ clear on what problem you are solving by
changing your API in this way. Try to think through your API design to avoid having to do this.
- Version your endpoints, not your entire API. For example, if you decide you need a new version of the
widgets API, do not also make your manufacturers API version 2. Doing this will create a version explosion
in your API that will be hard to manage.
- Adopt a deprecation policy as well, so you can remove old versions.
Once you’ve adopted a versioning policy, you next need to decide how this gets implemented in your API. There
are three common mechanisms for this:
- Put the version in the URL, for example/api/v1/widgets.
- Require a version in theAccept:header, for exampleAccept: application/json; version=1.
- Use a custom header that has the version, for exampleX-API-Version: 1.
The simplest thing to do is to put the version in the URL. Everyone on your team will understand this and it will
make the most sense overall. Non-engineers will be able to understand it as well, because it’s explicit.
I know that this may not feel correct, because the version should not be considered as part of a resource locator.
While adhering to idealized standards is nice, if it conflicts with sustainability, we have to look out for ourselves
and do what makes our lives easier. See the sidebar “Versioning Confusion at Stitch Fix” on the next page for an
example of how using headers doesn’t create a sustainable environment.
(^5) https://semver.org
Let’s change our fledgling API code to use the version in the URL. First, we’ll change theconfig/routes.rbfile:
# config/routes.rb
# add it in the custom routes section, but make
# sure the resource-based route is here.
namespace **:api do**
→ namespace **:v1 do**
→ resources **:widgets** , **only: [ :show ]**
→ **end
end**
```
####
```
Next, we’ll move our widgets controller to theV1namespace:
> mkdir app/controllers/api/v1 ; mv \
app/controllers/api/widgets_controller.rb \
app/controllers/api/v1
And then we’ll change the name of the controller’s class:
# app/controllers/api/v1/widgets_controller.rb
→ **class** Api **::** V1 **::** WidgetsController **<** ApiController
**def** show
widget **=** Widget.find(params **[:id]** )
render **json: { widget:** widget **}**
Now, our URLs and classes match precisely, and the way versioning works is pretty obvious. These are good
things!
Let’s talk about JSON next.
#### Versioning Confusion at Stitch Fix
```
At Stitch Fix, we put the version of our API in theAcceptheader, and created some custom code to parse that
version out. That code would then route requests to a controller that had the version number in it.
For example, if you requested/api/shipmentsand set theAccept:header to"application/json; version=2",
code in our routes file would direct that request toApi::V2::ShipmentsController. If you used"application/json;
version=1", it would route toApi::V1::ShipmentsController. This felt very clean at the time.
After several years of reflection and real-world use, I don’t think it solved an actual problem. In fact, it created
confusion. First, seeing a controller likeApi::V2::ShipmentsControllerwill cause most Rails developers to assume a
URL ofapi/v2/shipments.But that’s not how this worked.
Developers also had to wrestle with setting the version in theAcceptheader. Granted, this is not that difficult to
do, but it’s unusual enough that it was just confusing.
And, of course, when debugging, you couldn’t just look at a URL and know what code was going to be executed.
You had to examine the headers, and those are not logged automatically by Rails or most HTTP clients. Overall, this
“more correct” approach made life difficult for everyone and didn’t provide any real benefit.
```
### 22.6 Use.to_jsonto Create JSON
```
This section’s code is in the folder22-06/of the sample code.
```
Your data model has been (presumably) carefully designed to ensure correctness, reduce ambiguity, and model
the data that’s important to your business. Your app’s various endpoints are all resourceful, using Active Model to
create any other domain concepts you need that aren’t covered by the Active Records.
It therefore stands to reason that your API’s JSON should mimic these carefully-designed data structures. If your
API must be so different from your domain model or database model that you need a separate set of classes to
create the needed JSON, something may be wrong with your modeling.
This isn’t to say that your JSON payloads won’t need additional metadata, but if a widget in the database has a
name, it will make the most sense to everyone if the JSON representation contains a key called"name"that maps
to the widget’s name, just like it does in the database and code.
Of course, it’s possible as time goes by that there is some drift, but in my experience this is unlikely. Thus, the way
you should form JSON should be to callto_jsonon an Active Record or Active Model, like so:
**class** Api **::** WidgetsController **<** ApiController
```
def show
widget = Widget.find(params [:id] )
# Note that Rails automatically calls to_json for you
render json: { widget: widget }
end
```
**end**
If you find yourself building a custom hash, or creating an object specifically to render JSON in your API, you
should stop and reconsider if what you are doing makes sense. Perhaps you are really in need of a new resource
instead?
That said, you may need your API to add or omit certain fields. For example, you might want to inline a widget’s
manufacturer so that clients don’t have to make another call. You may also wish to omit database keys or sensitive
values.
You can accomplish all of this by using a few methods that Rails uses to render JSON.
#### 22.6.1 How Rails Renders JSON
The standard library’s JSON package adds the methodto_jsonto pretty much every class, but it doesn’t work
quite the way Rails wants, nor the way we want for making an API. Rails changes this in Active Support^6.
Rails does this by creating a protocol for objects to turn themselves into hashes, which Rails then turns into actual
JSON. The method that does this isas_json. All objects return a reasonable value foras_json. For example:
> bin/rails c
console> puts Widget.first.as_json
=> {
"id"=>1,
"name"=>"Stembolt",
"price_cents"=>747894,
"widget_status_id"=>2,
"manufacturer_id"=>11,
"created_at"=>"2020-06-20T20:01:22.687Z",
"updated_at"=>"2020-06-20T20:01:22.687Z"
}
This even works for non-Active Records in the way you’d expect:
console> estimate = UserShippingEstimate.new(
widget_name: "Stembolt", shipping_zone: 2
)
console> puts estimate.as_json
=> {
"widget_name"=>"Stembolt",
"shipping_zone"=>2
}
When you callrenderin a controller like so:
(^6) https://github.com/rails/rails/blob/7-1-stable/activesupport/lib/active_support/core_ext/object/json.rb
render **json: { widget:** widget **}**
You are asking Rails to turn the hash{ widget: widget }into JSON. It will recursively turn the contents into
JSON as well, meaningto_jsonis called onwidget, and the implementation ofto_jsoncallsas_json.
Of course, the JSON Rails produces might not be _exactly_ what you want. Because of theas_jsonprotocol, you
can customize what happens.
#### 22.6.2 Customizing JSON Serialization
Theas_jsonmethod takes an optional argument calledoptions. Every object in your Rails’ app will respect two
options passed toas_json, which are mutually exclusive:
- :excepttakes an array of attribute names (as strings) for attributes to exclude from the JSON.
- :onlytakes an array of attribute names (as strings) for the only attributes to include in the JSON.
For example:
console> estimate = UserShippingEstimate.new(
widget_name: "Stembolt", shipping_zone: 2
)
console> puts estimate.as_json(only: "widget_name")
=> {"widget_name"=>"Stembolt"}
console> Widget.first.as_json(except: [ "id", "manufacturer_id" ])
=> {
"name"=>"Stembolt",
"price_cents"=>747894,
"widget_status_id"=>2,
"created_at"=>"2020-06-20T20:01:22.687Z",
"updated_at"=>"2020-06-20T20:01:22.687Z"
}
Active Records accept additional options:
- :includeis an array of attributes of related models to inline. You’ll notice above that by default we only see
thewidget_status_idand not the status object.:includeallows you to change that behavior.
- :methodsis an array of symbols representing method names that should be called and included in the JSON
output.
For example:
console> Widget.first.as_json(
methods: [ :user_facing_identifier ],
except: [ :widget_status_id ],
include: [ :widget_status ]
)
=> {
"id"=>1,
"name"=>"Stembolt",
"price_cents"=>747894,
"manufacturer_id"=>11,
"created_at"=>"2020-06-20T20:01:22.687Z",
"updated_at"=>"2020-06-20T20:01:22.687Z",
"user_facing_identifier"=>"1",
"widget_status"=>{
"id"=>2,
"name"=>"facere",
"created_at"=>"2020-06-20T20:01:22.677Z",
"updated_at"=>"2020-06-20T20:01:22.677Z"
}
}
Active Models don’t get these extra options by default. To grant them such powers requires mixing in
ActiveModel::Serializers::JSONand implementing the methodattributesto return a hash of all the model’s
attributes and values.
Now that we know how JSON serialization can be customized how _should_ we customize it?
#### 22.6.3 Customize JSON in the Models Themselves
Suppose we wanted our widgets API to use the JSON encoding we showed above. We could certainly achieve this
in our controller like so:
**def** show
widget **=** Widget.find(params **[:id]** )
```
render json: {
widget: widget.as_json(
methods: [ :user_facing_identifier ] ,
except: [ :widget_status_id ] ,
include: [ :widget_status ]
)
}
```
**end**
Of course, if we need to implement theindexmethod, that code would want to use the same options. We could
create a private method inApi::V1::WidgetsControllercalledwidget_json_options, but what if there is a third
place to serialize a widget? For example, if you are using a messaging system, you might encode data in JSON to
send into that system. There’s no reason to use a different encoding, so how do you centralize the way widgets
are encoded in JSON?
The simplest way is to overrideas_jsonin theWidgetclass itself. Doing that would ensure that anyone who
calledto_jsonon a widget would get the single serialization format you’ve designed.
This might feel uncomfortable. Why are we giving our models yet another responsibility? What if we really do
want a different encoding sometimes? Shouldn’t we separate concerns and have serialization live somewhere
else?
These are valid questions, but we must again return to what Rails and Ruby actually are and how they actually
work. Rails provides ato_jsonmethod on all objects. There are several places in Rails where an object is implicitly
turned into JSON using that method. That method is implemented usingas_json, which is also on every single
object.
Given these truths, it makes the most sense to overrideas_jsonto explicitly define the default encoding of an
object to JSON. If you _do_ have need for a second way of encoding—and you should be very careful if you think
you do—you can always callas_jsonwith the right options.
Let’s see how to write anas_jsonimplementation to address all of our needs. We’ll makeoptionsan optional
argument, and for each option _we_ want to set, we’ll only set it if the caller has not.
# app/models/widget.rb
**}** ,
**high_enough_for_legacy_manufacturers:** true
normalizes **:name** , **with: ->** (name) **{** name.blank?? nil : name...
→ **def** as_json(options **={}** )
→ options **[:methods] ||= [ :user_facing_identifier ]**
→ options **[:except] ||= [ :widget_status_id ]**
→ options **[:include] ||= [ :widget_status ]**
→ super(options)
→ **end
end**
You could also only set default options ifoptionsis empty. Either way, adopt one policy and follow that whenever
you overrideas_json. I would also recommend a test for this behavior. I do want to stress the point about
centralizing this in the model itself. This is, like many parts of Rails, a good default. You can override this when
needed, but a good default makes things easier for everyone. It’s easier for the team to get right, easier for others
doing code review, and it matches the way Rails and Ruby actually _are_.
One last thing about JSON encoding is the use of top-level keys.
#### 22.6.4 Always Use a Top Level Key
The example code we’ve seen thus far looks like this:
render **json: { widget:** widget **}**
Why didn’t we write onlyrender json: widget?
Doing that would result in a JSON object like so:
##### {
"id": 1234,
"name": "Stembolt",
"price_cents": 12345
}
There are two minor problems with this as the way your API renders JSON. The first is that you cannot look at
this JSON and know what it is without knowing what produced it. That’s not a major issue, but when debugging
it’s _really_ nice to have more explicit context if it’s not too much hassle to provide.
The second problem is the potential need to include metadata like page numbers, related links, or other stuff
that’s particular to your app and not something that should go into an HTTP header. In that case, you’d need
to merge the object’s keys and values with those of your metadata. This will be confusing and potentially have
conflicts.
A better solution is to include a top-level key for the object that contains the object’s data. Our code does that by
rendering{ widget: widget }, which produces this:
##### {
"widget": {
"id": 1234,
"name": "Stembolt",
"price_cents": 12345
}
}
Now, if you have this JSON you have a good idea what it is. If you also need to include metadata, you can include
that as a sibling to"widget":and keep it separated.
The problem that this solution creates is that you have to remember to set the top level key in your controllers.
I would _not_ recommend doing this inas_json, because you wouldn’t do this for an array. If you had an array of
widgets, you’d want something like this:
##### {
"widgets": [
{
"id": 1234,
"name": "Stembolt",
"price_cents": 12345
},
{
"id": 2345,
"name": "Thrombic Modulator",
"price_cents": 9876
}
]
}
Active Records can do this automatically by settinginclude_root_in_json, but this doesn’t apply to any other
objects, so I would recommend against using it. Doing so requires everyone to have to think about what sort
of object they are serializing and whether or not the top-level key will be there. As we’ve seen in the past,
architectural decisions that are of the form “always do X” are easier to remember and enforce. So, always put a
top-level key in your controllerrendermethod.
That last thing to consider about APIs is tests.
### 22.7 Test API Endpoints
```
This section’s code is in the folder22-07/of the sample code.
```
Just as you’d test a major user flow (discussed in “Understand the Value and Cost of Tests” on page 161), you
should test major flows around your API. At the very least, each endpoint should have one test to make some
assertions about the format of the response. While inadvertent changes to a UI can be annoying for users, such
changes could be catastrophic for APIs. A test can help prevent this.
Your test should also use the authentication mechanism and content negotiation headers. Let’s write a complete
set of tests for all this against our widgets endpoint.
The tests of the API should be integration tests, which means they should be intest/integration. To keep them
separated from any normal integration tests we might write, we’ll use the same namespaces we used for the
routes and controllers, and place our test intest/integration/api/v1/widgets_test.rb.
# test/integration/api/v1/widgets_test.rb
require "test_helper"
**class** Api **::** V1 **::** WidgetsTest **<** ActionDispatch **::** IntegrationTest
# tests go here
**end**
We’ll need to insert an API key into the database, then perform agetpassing that key in the appropriate header,
along with setting theAccept:header. Here’s how that looks.
# test/integration/api/v1/widgets_test.rb
```
require "test_helper"
```
**class** Api **::** V1 **::** WidgetsTest **<** ActionDispatch **::** IntegrationTest
→ test "get a widget" **do**
→ api_key **=** FactoryBot.create( **:api_key** )
→ authorization **=** ActionController **::**
→ HttpAuthentication **::**
→ Token.encode_credentials(api_key.key)
→ widget **=** FactoryBot.create( **:widget** )
→ get api_v1_widget_path(widget),
→ **headers: {**
→ "Accept" **=>** "application/json",
→ "Authorization" **=>** authorization
→ **}**
→ assert_response **:success**
→ parsed_response **=** JSON.parse(response.body)
→ refute_nil parsed_response **[** "widget" **]**
→ assert_equal widget.name, parsed_response.dig("widget",
→ "name")
→ assert_equal widget.price_cents,
→ parsed_response.dig("widget", "price_cents")
→ assert_equal widget.user_facing_identifier,
→ parsed_response.dig("widget",
→ "user_facing_identifier")
→ assert_equal widget.widget_status.name,
→ parsed_response.dig("widget",
→ "widget_status",
→ "name")
→ **end
end**
Whew! One thing to note is that we aren’t testing all the fields that would be in the response as implemented. I
would likely build this API by writing this test first, and then implementas_jsonto match the output.
It also depends on how strict you want to be. For JSON endpoints consumed by a JavaScript front-end in the app
itself, it’s probably OK if the payload has extra stuff in it. The more widely used the endpoint, the more beneficial
it is to have exactly and only what is needed. You need to consider the carrying and opportunity costs to make
sure you aren’t over-investing.
We also need four more tests:
- A request without an API key gets a 401.
- A request with a non-existent API key gets a 401.
- A request with a real API key that’s deactivated gets a 401.
- A request without a content-type gets a 406.
We could put them in the existingwidgets_test.rb, but this would imply that each endpoint would require these
four tests of what is essentially configuration insideApiController. Let’s instead create two more tests, one for
authentication and one for content negotiation.
First, let’s createtest/integration/api/content_negotiation_test.rb:
# test/integration/api/content_negotiation_test.rb
require "test_helper.rb"
**class** Api **::** ContentNegotiationTest **<** ActionDispatch **::** IntegrationTest
test "a non-JSON Accept header gets a 406" **do**
api_key **=** FactoryBot.create( **:api_key** )
authorization **=** ActionController **::**
HttpAuthentication **::**
Token.encode_credentials(api_key.key)
```
widget = FactoryBot.create( :widget )
```
```
get api_v1_widget_path(widget),
headers: {
"Accept" => "text/plain",
"Authorization" => authorization
}
```
```
assert_response 406
end
```
```
test "missing Accept header gets a 406" do
api_key = FactoryBot.create( :api_key )
authorization = ActionController ::
HttpAuthentication ::
Token.encode_credentials(api_key.key)
```
```
widget = FactoryBot.create( :widget )
```
```
get api_v1_widget_path(widget),
headers: {
"Authorization" => authorization
}
```
assert_response 406
**end
end**
If we end up with more nuanced content negotiation, tests for it can go here. Next, we’ll test authentication in
api/authentication_test.rb:
# test/integration/api/authentication_test.rb
require "test_helper.rb"
**class** Api **::** AuthenticationTest **<** ActionDispatch **::** IntegrationTest
test "without an API key, we get a 401" **do**
widget **=** FactoryBot.create( **:widget** )
```
get api_v1_widget_path(widget),
headers: {
"Accept" => "application/json",
}
```
```
assert_response 401
end
```
```
test "with a non-existent API key, we get a 401" do
authorization = ActionController ::
HttpAuthentication ::
Token.encode_credentials("not real")
```
```
widget = FactoryBot.create( :widget )
```
```
get api_v1_widget_path(widget),
```
```
headers: {
"Accept" => "application/json",
"Authorization" => authorization
}
```
```
assert_response 401
end
```
```
test "with a deactivated API key, we get a 401" do
api_key = FactoryBot.create( :api_key ,
deactivated_at: Time.zone.now)
authorization = ActionController ::
HttpAuthentication ::
Token.encode_credentials(api_key.key)
```
```
widget = FactoryBot.create( :widget )
```
```
get api_v1_widget_path(widget),
headers: {
"Accept" => "application/json",
"Authorization" => authorization
}
```
assert_response 401
**end
end**
Again, if we had more complex requirements or use-cases around authentication, it can go there. Note that we’re
using the widgets endpoint in these tests. That’s a convenience since we have the endpoint built. You could create
a special one just for testing, but it’s always better to test code that actually needs to exist for real reasons and not
code that exists only artificially.
These tests should all pass:
> bin/rails test test/integration/api/authentication_test.rb \
test/integration/api/content_negotiation_test.rb \
test/integration/api/v1/widgets_test.rb
Running 6 tests in a single process (parallelization thresho...
Run options: --seed 40882
# Running:
......
Finished in 0.177152s, 33.8692 runs/s, 62.0935 assertions/s.
6 runs, 11 assertions, 0 failures, 0 errors, 0 skips
One issue that will come up if we add more API endpoints is duplication around setting up an API key and setting
all the headers when calling the API from a test. As I’ve suggested in several other places, watch for a pattern and
extract some better tooling. It’s likely you’ll want a baseApiTestthat extendsActionDispatch::IntegrationTest
that all your API tests then extend, but don’t get too eager making abstractions until you see the need.
### Up Next
Next, we’ll move even farther outside your Rails app to talk about some workflows and techniques to help with
sustainability, such as continuous integration and generators.
## 23 Sustainable Process and Workflows
Up to this point, we’ve mostly talked about the code in your Rails app. Way back in “Start Your App Off Right” on
page 27, we created some scripts inbin, likebin/devandbin/ci, which help with working on the app itself. In
this chapter, I want to talk about a few other techniques that can help with sustainability of the team overall.
The techniques here are some I’ve used in earnest on both small and large teams and they should provide you
value as well. Of course, there are many other techniques, workflows, and processes to make your team productive
and development sustainable. Hopefully, learning about these processes can inspire you to prioritize team and
process sustainability.
Let’s start off with one that you might already be doing: continuous integration.
### 23.1 Use Continuous Integration To Deploy
```
This section’s code is in the folder23-01/of the sample code.
```
The risks mitigated by tests only happen if we are paying attention to our tests and fixing the code that’s broken.
Similarly, the checks we put intobin/cifor vulnerabilities in dependent libraries and analysis of the code we
wrote only provide value if we do something about them.
The best way to do all that is to use a system for deployment that won’t deploy code if any of our quality checks
are failing. This creates a virtuous cycle of incentives for us developers. We want our code in production doing
what it was meant to do. If the only way to do that is to make sure the tests are passing and there are no obvious
security vulnerabilities, we’ll address that.
The most common way to set all this up is to set up _continuous integration_ , or _CI_.
#### 23.1.1 What is CI?
The conventional meaning of CI is a system that runs all tests and checks of every branch pushed to a central
repository^1. When the tests and checks pass on some designated main branch, that branch is deployed to
production.
This enables a common workflow as outlined in the figure “Basic CI Workflow” on the next page. This workflow
allows developers to create branches with proposed changes and havebin/ciexecute on the CI server to make
(^1) The original meaning of CI was that all code was frequently integrated into some sort of main trunk of development to avoid too many
diversions and conflicts within the code. The phrase “continuous integration” has somewhat lost this original meaning, with some teams using
the term _trunk-based development_ instead. When I talk about CI, I’m talking about using a central repository to run tests and deploy. _This_ is the
value I’m discussing. I can’t speak to trunk-based development as I’ve never done that in a team-based environment.
sure all tests and checks pass. The team can do code reviews as necessary. When bothbin/ciand code reviews
are good, the change can be merged onto the main branch for deployment.bin/ciis run yet again to make sure
the merged codebase passes all tests and checks and, if it does, the change is deployed to production.
This is a sustainable workflow, and I daresay it’s not terribly new or controversial. What I want to talk about is
how to make sure this process continues to be sustainable.
#### 23.1.2 CI Configuration Should be Explicit and Managed
There are two main problems that happen with using CI. The first is that the test suite becomes so long that
developers start skipping it in order to deploy. The second is that when CI fails even though the code is actually
working properly, it can require an unwelcome diversion to fix the CI configuration to make the tests pass.
Both of these problems can be fixed by having an explicit CI configuration, and a commitment to manage it like
any other part of the app.
Many services that provide continuous integration for developers have slick, zero-configuration on-boarding.
Particularly if you are using Rails, services like Circle CI and CodeShip can automagically set everything up for
you and run your tests without any configuration.
This is not sustainable. Eventually, you will run into a problem with the implicit configuration and have to debug
it. This will be difficult and will happen when you aren’t planning for it. My experience in this situation is that
teams provide a quick-fix solution to unblock themselves and never go back to think deeply about how CI is
configured and set up. This ensures the cycle repeats itself whenever it is least convenient for you and your team.
Fortunately, most CI services allow you to configure exactly what you want to happen, including the version of
your database, the port it’s running on, and anything else you might need. The CI service providers don’t tell you
about this up front as it can feel daunting. But explicit configuration is sustainable.
CI is something you don’t want to have to constantly manage, so it makes sense to spend as much time as you
need up front creating a sustainable, explicit configuration. The reason is that the configuration inevitably breaks,
meaning your app is working properly, but you can’t prove it on CI because of a problem with the CI configuration
itself.
When this happens, one or more developers will have to debug the configuration. If that configuration is
verbose, clear, explicit, and well-documented, developers can quickly get up to speed on learning what might be a
completely new set of tools for the first time.
Said another way, an explicit configuration means that more team members will be able to modify it when needed,
and this contributes to an overall cultural value that maintaining this configuration is important. Make it clear to
the team that this configuration, since it is the automation for production deploys, is just as critical as any feature
of the app. Any work needed around CI should be prioritized and completed quickly.
A great way to address all of this is to use your development environment scripts inbin/as part of the CI
configuration.
#### 23.1.3 CI Should be Based onbin/setupandbin/ci
Your initial CI configuration should basically runbin/setupfollowed bybin/ci. When this is run on some sort
of designated main branch, the CI system should additionally deploy the code to production. By using your
development environment scripts to power your CI configuration, you ensure that they are working, even if
Figure 23.1: Basic CI Workflow 365
developers aren’t running them frequently. Keepingbin/setupworking is a boon to productivity and this is
exactly how you make sure that happens.
Of course, it’s not always possible for the exactbin/setupscript to work in the CI environment. Sometimes,
you can modify your CI environment so that it matches development, even if the defaults for your CI system
don’t initially match. For example, you could configure your CI system’s Postgres to use the same username and
password you use locally. This is ideal, because it means you don’t have to changebin/setup.
If you can’t change CI directly, another way to manage this is to leverage.env.development.localand
.env.test.local. Those files aren’t checked in, but they will override the values in.env.developmentand
.env.test, respectively, if they exist. Thus, you can modifybin/setupto detect if it’s running in CI and, if it is,
dynamically generate those two files with CI-specific settings. Those files will only exist on the CI servers and
won’t necessitate further changes to your setup or test scripts.
For example, suppose that Redis in your CI environment is running on a host namedci-redisand on
port 3456. That’s not how your development environment works, so you can manage this by creating
.env.development.localand.env.test.localinbin/setup. To detect if your script is running locally or on the
CI server, most CI servers set an environment variable calledCI. We’ll assume that is the case here.
Here’s an example of how to makebin/setupwork on both local development and on the CI server:
# bin/setup
```
require "optparse"
```
**def** setup
→ **if** ENV **[** "CI" **] ==** "true"
→ log "Running in CI environment"
→ log "Creating .env.development.local"
→ File.open(".env.development.local","w") **do |** file **|**
→ file.puts "REDIS_URL=redis://ci-redis:3456/1"
→ **end**
→ log "Creating .env.test.local"
→ File.open(".env.test.local","w") **do |** file **|**
→ file.puts "REDIS_URL=redis://ci-redis:3456/2"
→ **end**
→ **elsif** ENV **[** "CI" **]**! **=** nil
→ # Detect if what we believe to be true about the CI env var
→ # is, in fact, still the case.
→ fail "Problem: CI is set to #{ENV **[** 'CI' **]** }, but we expect " **+**
→ "either'true'or nil"
→ **else**
→ log "Assuming we are running in a local development environment"
→ **end**
log "Installing gems"
```
# Only do bundle install if the much-faster
# bundle check indicates we need to
```
```
Because you’ve configured your app with environment variables, this technique can handle most needs to
customize behavior in CI. That said, you are going to be much better off if you can directly configure CI to use
your settings.
```
```
If changing the environment doesn’t fix an issue with inconsistent behavior, you can always use the environment
variable check inbin/setupto do further customizations. Be careful with this as it means that any code you aren’t
running in CI won’t get executed frequently.
```
Another issue with CI that can happen as your app ages is that the test suite becomes longer and it takes longer to
do deploys. _Throughput_ is a key metric for many teams that illustrates how effective they are in delivering value.
In times of stress, teams can “solve” this problem by disabling tests in CI or simply skipping tests entirely. This will
absolutely destroy team morale over time _and_ lead to lower productivity. It can be extremely hard to recover
from. Never do this.
You can certainly try to make your tests faster, but this can be time consuming and not terribly fruitful. Most CI
services allow you to split your tests and checks and run them in parallel. One way to do this is to run system
tests—which are typically quite slow—in parallel to your other tests. In our app, we might want to run system
tests and unit tests in parallel and, in a third workstream, run our JS tests followed by all the security audits
(Brakeman andbundle audit).
```
To do that without duplicating any code, we could break up ourbin/ciscript into sub-scripts. For example,
bin/cimight look like this:
```
```
##!/usr/bin/env bash
```
```
set - e
```
```
bin / unit - tests
bin / system - tests
bin / security - audits
```
```
Each of these new scripts would contain the commands previously inbin/ci:
```
```
> cat bin/unit-tests
##!/usr/bin/env bash
```
```
set -e
```
```
echo "[ bin/ci ] Running unit tests"
bin/rails test
```
> cat bin/system-tests
##!/usr/bin/env bash
set -e
echo "[ bin/ci ] Running system tests"
bin/rails test:system
> cat bin/security-audits
##!/usr/bin/env bash
set -e
echo "[ bin/ci ] Analyzing code for security vulnerabilities."
echo "[ bin/ci ] Output will be in tmp/brakeman.html, which"
echo "[ bin/ci ] can be opened in your browser."
bundle exec brakeman -q -o tmp/brakeman.html
echo "[ bin/ci ] Analyzing Ruby gems for"
echo "[ bin/ci ] security vulnerabilities"
bundle exec bundle audit check --update
Even though a script likebin/system-testsis one line of code, it functions as a protocol we can enhance, just
like all of ourbin/scripts. We can then use these scripts in our CI configuration so that if, say, what is required to
run JavaScript tests changes over time, we only need to change it in one place.
With these scripts broken out, you can then configure your CI system to run them in parallel as described above
and as shown in the figure “Parallel Testing With Scripts” on the next page.
When your CI system runs security audits regularly, you will find that many of your dependencies have security
vulnerabilities and you’ll be updating them frequently. This leads to the next technique, which is to update your
dependencies on a regular basis, regardless of existing security vulnerabilities.
### 23.2 Frequent Dependency Updates
```
This section’s code is in the folder23-02/of the sample code.
```
In September 2018, GitHub posted a blog entry^2 about their 18-month journey to upgrade Rails from a very
out-of-date version to the latest version at the time (5.2). I have observed a similar project on a slightly smaller
scale, and it required the most talented and experienced engineers at the company to be successful.
But I can’t help feeling that GitHub should’ve never been in this position in the first place. If it were me, I would’ve
much rather had the members of that team driving customer value directly than spending _over a year_ upgrading a
piece of technology. While the team did a lot of hard and amazing work, the decisions that lead to needing that
work at all weren’t made in the interest of sustainability.
(^2) https://github.blog/2018-09-28-upgrading-github-from-rails-3-2-to-5-2/
```
Figure 23.2: Parallel Testing With Scripts
```
One way to avoid this is to update dependencies frequently and try to stay up-to-date.
#### 23.2.1 Update Dependencies Early and Often
At Stitch Fix, we decided early on that we would not have this problem. Our solution was to schedule monthly
dependency updates. This meant that one day each month, we’d runbundle updatein our Rails apps, run the
tests, fix what was broken, and then be up to date. This didn’t come for free, but we wanted to be on the latest
stable versions of everything as frequently as we could.
This worked. We never had a team dedicated to upgrading Ruby or Rails. We never had to spend months and
months on a Rails upgrade. Sure, the upgrade to Rails 4.2 wasn’t pleasant, and it certainly took more than a few
days, but I would say it went more or less without incident.
I highly suggest you make this part of your team culture. If you _don’t_ have a culture of always being on the latest
version of the code you use, you will one day be required to stop everything you are doing and perform an update
due to a critical security bug. This will be unpleasant. I had to do this once, and it required rewriting a gem we
used from scratch because it had not been updated for the version of Rails we had to upgrade to.
Being on the latest version of your tools has many other benefits. Potential team members are much more excited
to use the latest versions of tools than have to deal with out-of-date versions. If you have a security team, their
job becomes much easier and you’ll have a much better relationship with them. And, of course, you get access to
new features of the tools you are using relatively quickly.
The hardest part of this process is managing it as the size of the team grows. The reason is that it’s hard to put
incentives in place to prevent teams from skipping these updates. Part of this is because the updates—and fixes
they often require—aren’t free and aren’t always enjoyable work. There’s not a natural short-term incentive for
engineers to do this or for their managers to prioritize it (which is why having it as part of the culture can help).
You can ensconce this cultural value in your tools. Depending on the sophistication of your deployment toolchain,
you can bake minimum required versions into it. For example, at Stitch Fix, our deployment tools would not work
with any version of Ruby other than the most recent two versions. If you fell behind on updates, you couldn’t
deploy. It’s not the most pleasant motivator, but it did work.
Outside of this, it really is a cultural value you have to bake into the team. Frequently explaining the need for
it helps. Empathizing with how unpleasant it can be helps, too, and equitably rotating who’s responsible each
month can create some camaraderie on the team while avoiding the work always falling to the same person.
To help codify this value, you should create a basic versioning policy. Here is one that I recommend and that will
serve you well.
#### 23.2.2 A Versioning Policy
A _policy_ might sound draconian, but trust me, it helps to have agreed-upon conventions written down when they
can’t be baked into code. It also helps to put, in writing, exactly why the team does certain things.
This is what I recommend:
- Use only the latest two minor versions of Ruby. Each December, when Ruby is updated, schedule time in
January to update any apps on what is the third most-recent version. For example, in December of 2021,
Ruby 3.1 was released, and so all apps using 2.7 would’ve been updated to at least 3.0.
- Use this exact same policy for Rails. All apps should be on the latest or second-latest version. Rails releases
are less regular, but teams should budget some time each year to doing an upgrade of a minor version of
Rails.
- Use this exact same policy for NodeJS, if you are using it.
- In yourGemfile, specify a pessimistic version constraint for Rails to keep it on the current minor version.
Running abundle updateand getting a new minor version of Rails is not a great surprise. You want to
control when the Rails version is updated.
- For as many other dependencies as you can, set no version constraint whatsoever. Let Bundler sort out the
version that goes with your version of Rails.
- Note that if you are using NodeJS, there may be dependencies between some gems and some modules
inpackage.json. Because JSON does not allow for comments, write comments inGemfilethat indicate
dependencies between gems and Node modules.
- For any gem you must pin to a particular version, _write a code comment in the_ Gemfileabout why you have
done this, and under what circumstances you should remove the pin. Don’t let Agile Thought Leaders tell
you that comments are bad. Write a novel if you have to to explain what’s going on and how to tell if the
reason for pinning the version still exists.
Once you have your policy, and you’ve set expectations with teams to do updates, there’s just no getting around
the difficulty of doing the actual updates and fixing whatever the break. You can make the process a bit easier by
providing some automation.
#### 23.2.3 Automate Dependency Updates
GitHub provides a feature called _Dependabot_ , which will update your dependencies frequently, then open a pull
request with those changes. This will trigger your CI build to let you know that everything is—or is not—working.
You can even configure things so that these pull requests are automatically merged and deployed if the tests pass.
This is probably a good thing to set up early on, though it can be hard to migrate to for an older app.
Another option is to at least automate how you’d update dependencies in your app, and then commit to running
that automation on a regular basis.
Let’s do that, by creatingbin/update. This will do a few things. First, it will runbundle update. This command
instructs Bundler to find the latest version of all dependencies that satisfy what is inGemfile.
If you’ve followed the policy above, that should give you the latest point release of the minor version of Rails you
are using, and the latest version of all gems that are compatible with that version of Rails.
As a reminder and check that you may still be behind the latest, we’ll then executebundle outdated. This will
tell you if there are newer versions available of any gems you are using, regardless of what versions are in your
Gemfile. This can help if you’ve temporarily pinned gems to get around an issue and could unpin them. The
script will then runbin/cito see if the updates have broken anything.
# bin/update
#!/bin/sh
set -e
echo "[ bin/update ] Updating Ruby gems"
bundle update
# Turning off exit-on-error because the outdated commands
# will usually exit nonzero and we don't want them
# to abort this script
set +e
echo "[ bin/update ] Checking for outdated gems"
bundle outdated
echo "[ bin/update ] If anything is outdated, you may have"
echo "[ bin/update ] overly conservative versions pinned"
echo "[ bin/update ] in your Gemfile"
echo "[ bin/update ] You should remove these pins if possible"
echo "[ bin/update ] and see if the app works with the "
```
echo "[ bin/update ] latest versions"
```
```
echo "[ bin/update ] Running bin/ci"
bin/ci
```
```
We’ll make it executable:
```
```
> chmod +x bin/update
```
Let’s run it. I’m going to include the massive output for this run so you can see what it looks like. All the tools
that are brought together create a real hodge-podge of messy output.bundle outdatedwill say something like
“No vulnerabilities detected” to indicate success. There may also be some odd git-related messages due to how I’m
running this for the book. You may not see those, but if you do, they can be ignored.
```
> bin/update
[ bin/update ] Updating Ruby gems
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
Bundle updated!
[ bin/update ] Checking for outdated gems
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
```
```
Bundle up to date!
[ bin/update ] If anything is outdated, you may have
[ bin/update ] overly conservative versions pinned
[ bin/update ] in your Gemfile
[ bin/update ] You should remove these pins if possible
[ bin/update ] and see if the app works with the
[ bin/update ] latest versions
[ bin/update ] Running bin/ci
[ bin/ci ] Running unit tests
Running 25 tests in a single process (parallelization thresh...
Run options: --seed 40731
```
```
# Running:
```
```
...............2023-12-04T23:56:52.495Z pid=11210 tid=bqm IN...
..........
```
```
Finished in 0.222227s, 112.4978 runs/s, 274.4947 assertions/...
25 runs, 61 assertions, 0 failures, 0 errors, 0 skips
[ bin/ci ] Running system tests
Running 4 tests in a single process (parallelization thresho...
Run options: --seed 64495
```
# Running:
.2023-12-04T23:56:53.516Z pid=11213 tid=bqh INFO: Sidekiq 7....
.Rack::Handler is deprecated and replaced by Rackup::Handler
Capybara starting Puma...
* Version 6.4.0 , codename: The Eagle of Durango
* Min threads: 0, max threads: 4
* Listening on [http://127.0.0.1:33133](http://127.0.0.1:33133)
Finished in 1.248793s, 3.2031 runs/s, 8.8085 assertions/s.
4 runs, 11 assertions, 0 failures, 0 errors, 0 skips
[ bin/ci ] Analyzing code for security vulnerabilities.
[ bin/ci ] Output will be in tmp/brakeman.html, which
[ bin/ci ] can be opened in your browser.
[ bin/ci ] Analyzing Ruby gems for
[ bin/ci ] security vulnerabilities
Updating ruby-advisory-db ...
From https://github.com/rubysec/ruby-advisory-db
* branch master -> FETCH_HEAD
Already up to date.
Updated ruby-advisory-db
ruby-advisory-db:
advisories: 827 advisories
last updated: 2023-11-30 12:36:04 -0800
commit: d821bf162550302abd1fa1fe15007f3012b76f32
No vulnerabilities found
[ bin/ci ] Done
In addition to shell scripts that automate common tasks, there are some other techniques around automation that
I want to talk about next. The first is using templates and generators to create boilerplate code.
### 23.3 Leverage Generators and Sample Repositories over Documentation
The first step to establishing a convention is to write it down. For example, putting business logic inapp/services
might be something a team would document in a README. A team might also write down examples of how to
write a job or a controller.
In addition to basic automation like we did withbin/setupandbin/dev, or automatically generated documen-
tation like we did with our style guide, automatically generating code for common use-cases can be far more
compelling than documentation, especially when the boilerplate is somewhat complicated.
There are three types of code generation you may encounter:
- Creating new files in your Rails app, like we did with View Components
- Creating RubyGems to manage shared code across apps
- Creating entirely new Rails apps
#### 23.3.1 Create and Configure Rails Generators
Many third party Rails gems come with generators. The Rails Guide^3 walks you through how to make one, and
I would highly recommend you use this to codify any architecture decisions you make. On a recent project, I
created a generator for creating a service.
Rails generators work well and can be tested, but exhibit undesirable behavior under certain failure modes. The
primary cause is due to their core API, which is based on Thor^4. The API used by generators is based around
searching and replacing strings in files, either by regular expression or exact matches.
For example, you might write code like so to add arequirestatement toconfig/routes.rb. This code says
to search the fileconfig/routes.rbfor the string"Rails.application.routes.draw do"and insert the string
"require \"sidekiq/web\"\n\n"before it.
insert_into_file "config/routes.rb",
"require \"sidekiq/web\"\n\n",
**before:** "Rails.application.routes.draw do"
The problem is if the string isn’t found in the file, Thor does not consider this an error. The generator will not
report any problem and continue with its operation, leaving you with the impression that the generator worked
when, in reality, it absolutely did not.
You can monkey-patch Thor to get around this issue, and if you make heavy use of generators, I suggest you do
this. You can add this code anywhere before your generators run:
require "thor"
**class** Thor **::** Actions **::** InjectIntoFile
protected
```
# Copied from lib/thor/actions/inject_into_file.rb so I can
# raise if the regexp fails
def replace!(regexp, string, force)
return if pretend?
content = File.read(destination)
if force ||! content.include?(replacement)
# BEGIN CHANGE
result = content.gsub!(regexp, string)
if result.nil?
raise "Regexp didn't match: #{regexp}:\n#{string}"
```
(^3) https://guides.rubyonrails.org/generators.html
(^4) https://github.com/erikhuda/thor
**end**
# END CHANGE
# ORIGINAL CODE
# content.gsub!(regexp, string)
# END ORIGINAL CODE
File.open(destination, "wb") **{ |** file **|** file.write(content) **}
end
end
end**
**module** Thor **::** Actions
# Copied from lib/thor/actions/file_manipulation.rb
**def** gsub_file(path, flag, ***** args, **&** block)
**return unless** behavior **== :invoke**
config **=** args.last.is_a?(Hash)? args.pop : **{}**
```
path = File.expand_path(path, destination_root)
say_status :gsub ,
relative_to_original_destination_root(path),
config.fetch( :verbose , true)
```
**unless** options **[:pretend]**
content **=** File.binread(path)
# BEGIN CHANGE
result **=** content.gsub!(flag, ***** args, **&** block)
**if** result.nil?
raise "Regexp didn't match #{flag}:\n#{content}"
**end**
# END CHANGE
# ORIGINAL CODE: content.gsub!(flag, *args, &block)
File.open(path, "wb") **{ |** file **|** file.write(content) **}
end
end
end**
With this change, any time an attempt to replace code in a file using a regular expression fails, Thor will raise an
error instead of doing nothing.
Despite this issue, generators are superior to documentation, since the execute your architectural and design
decisions.
For Ruby Gems or entirely new Rails apps, you could also use generators, but I would recommend template
repositories instead.
#### 23.3.2 Use Template Repositories for Ruby Gems and Rails Apps
Bundler includes thebundle gemcommand to create a new Ruby Gem.bundle gemis a nice idea, but it doesn’t
provide very much beyond creating a few files for you. On your team, you’ll want gems created in a particular
way, perhaps with your own ancillary gems, version of Ruby, CI scripts, or gemspec.
Whilebundle gemhas improved over the years, and does provide some flexibility, you’ll still need to document
which command line flags your team should use and this tends to eliminate many of the gains you get from code
generation.
Rails provides “app templates” that work similarly to create a new Rails app. You can giverailsthe--template
flag that will expect to contain many calls to Thor’s API to create a Rails app. This suffers all the problems of
using Thor we discussed above, but is exceedingly hard to test, with behavior that’s hard to predict and control.
The solution to both of these issues is to use _template repositories_. This means that you’d usebundle gemorrails
new --templateto create an example gem or app, then manually tweak it how you like it. When a developer
needs to create a new gem or Rails app, they clone that template as a starting point.
Template repositories aren’t an amazing solution, but they offer you more predictability and control than using
bundle gemor Rails app templates. At Stitch Fix, we used a Rails app template for all Rails apps and it was
perpetually in a state of being only kindof working. A template repository would’ve been easier to use and
maintain.
This section is a bit of a warning, but _any_ automation is better than documentation. Documentation gets out of
date quickly and can be extremely hard to follow, even for the most conscientious developer.
Speaking of Ruby Gems and Rails apps, if you do end up using multiple Rails apps (which we’ll discuss in
more detail in “Monoliths, Microservices, and Shared Databases” on page 405), it will be advantageous to share
configuration across those apps. You can do this via RubyGems and Railties.
### 23.4 RubyGems and Railties Can Distribute Configuration
When you have more than one Rails application, there are often libraries you want to share between apps and
those libraries require a common setup. For example, you might use a message bus like RabbitMQ or Apache
Kafka for asynchronous communication. You might have a library that provides simplified access to the system,
along with configuration settings such as network timeouts or error handling behavior.
Or, you might have a convention around using, say, Bugsnag as your exception-handling service, and want to have
a single set of configuration settings for all apps.
A common way to manage this is to provide documentation about what to do. Or, if you’ve been inspired by the
previous section, you could use code generation via a generator or template.
A better solution to this particular problem is to use Railties embedded in Ruby gems. Railties is a core component
of how Rails works and is the API for customizing Rails’ initialization procedure. By putting a Railtie inside a
Ruby gem, we can automatically insert configuration into any Rails app that bundles that gem.
Let’s see how it works by creating an exception-handling gem that configures and sets up Bugsnag, a common
exception-handling service. Exception-handling services like Bugsnag receive reports about any exception that
your app doesn’t explicitly handle. These reports can alert an on-call engineer to investigate what could be a
problem with the app (Airbrake and Rollbar are two other examples you may have heard of).
This example is going to be a bit contrived, because we only have one Rails app in our running example, and in the
real world you would configure Bugsnag in the one and only app you have. But, to demonstrate the point, we’ll
imagine that we have several Rails apps that all use Bugsnag and that we want to have a common configuration.
First, let’s see what this configuration is that we want to share. Let’s suppose in our case, we want to configure:
- the API Key used with the service.
- the Rails environments in which errors are actually reported.
- the Git SHA-1 of the application in which an error occurs.
- some common exceptions we _don’t_ want reported.
Without using our to-be-implemented gem that uses Railties, the configuration would live inconfig/initializers/bugsnag.rb
and look like so (assuming we are hosted on Heroku):
## config/initializers/bugsnag.rb
Bugsnag.configure **do |** config **|**
config.api_key **=** ENV.fetch("BUGSNAG_API_KEY")
config.app_version **=** ENV.fetch("HEROKU_RELEASE_VERSION")
config.notify_release_stages **= [** "production" **]**
config.ignore_classes **<<** ActiveRecord **::** RecordNotFound
**end**
This is the configuration we want to share. Don’t worry too much if you don’t know what’s going on here. The
point is that we don’t want each application to have to duplicate this information or, worse, do something different.
See the sidebar “Every Environment Variable is Precious” below for an example of what happens if you don’t
manage environment variable names.
#### Every Environment Variable Name is Precious
```
At Stitch Fix, there was a point where the team was around 50 developers and we had around 30 Rails apps in
production as part of a microservices architecture. We had a gem that was used for consuming microservices, but the
gem failed to bake in a convention about how to name the environment variable that held the API key.
The result was that some apps would use SHIPPING_SERVICE_PASSWORD, some SHIPPING_API_KEY, some
SHIPPING_SERVICE_KEY, and othersSHIP_SVC_APIKEY. It was a mess. But, microservices did allow this mess to not
affect the team as a whole. Until we needed to rotate all of these keys.
A third party we used had a major security breach and there was a possibility that our keys could’ve been leaked.
Rather than wait around to find out, we decided to rotate every single internal API key. If the environment variables
for these keys were all the same, it would’ve taken a single engineer a few hours to write a script to do the rotation.
Instead, it took six engineers an entire week to first make the variables consistent and then do the rotation.
According to Glassdoor, an entry-level software engineer makes $75,000 a year, which meant this inconsistency cost
us at least $9,000. The six engineers that did this were not entry-level, so you can imagine the true cost.
Inconsistency is not a good thing. The consistency we paid for that week did, at least, have a wonderful return
when we had to tighten our security posture before going public. The platform team was able to leverage our
new-found consistent variable names to script a daily key rotation of all keys in less time and fewer engineers than it
took to make the variable names consistent.
```
I’m not going to show all the steps for making a Ruby gem, but let’s look at the gemspec we would have, as well
as the main source code for the gem to see how it fits together.
First we have the gemspec, which brings in the Bugsnag gem:
## example_com_bugsnag.gemspec
## **NOTE** : this file is not in a rails app!
spec **=** Gem **::** Specification.new **do |** s **|**
s.name **=** 'example_com_bugsnag'
s.version **=** "1.0.0"
s.platform **=** Gem **::** Platform **::** RUBY
s.summary **=** "Provides access and configuration to Bugsnag " **+**
"for Example.Com apps"
s.description **=** "Include this in your Gemfile and you will " **+**
"now have Bugsnag configured"
```
# This assumes you are using Git for version control
s.files = ` git ls-files `.split("\n")
s.test_files =
` git ls-files -- {test,spec,features}/* `.split("\n")
s.require_paths = [ "lib" ]
```
s.add_dependency("bugsnag")
**end**
Since we usedadd_dependencyfor the Bugsnag gem, that means when an app installs _this_ gem, the Bugsnag gem
will be brought in as a transitive dependency. In a sense, this gem we are creating owns the relationship between
our apps and Bugsnag—our apps don’t own that relationship directly.
What we want is to have the above configuration executed automatically just by including theexample_com_bugsnag
gem. We can do this using two different behaviors of a Rails codebase. The first is Bundler, which will auto-require
files for us.
When we put this into ourGemfile:
## Gemfile
gem "example_com_bugsnag"
Bundler will require the file in our gem located at lib/example_com_bugsnag.rb. This is because in
config/application.rbof all Rails apps is this line of code:
## config/application.rb
Bundler.require( ***** Rails.groups)
Bundler.requirewill userequireto bring in all RubyGems in ourGemfile(unless you specifyrequire: false
for that gem in theGemfile).
We could dump all of the above code intolib/example_com_bugsnag.rb, but executing code just by requiring a
file can lead to confusing problems later. We also can’t exactly control when therequirehappens. This leads to
the second piece of the puzzle: Railties.
If we put the following code inlib/example_com_bugsnag.rb, it will tell Rails to run this code as if it were in
config/initailizers.rb:
## lib/example_com_bugsnag.rb
**class** ExampleComBugsnag **<** Rails **::** Railtie
initializer "example_com_bugsnag" **do |** app **|**
Bugsnag.configure **do |** config **|**
config.api_key **=** ENV.fetch("BUGSNAG_API_KEY")
config.app_version **=** ENV.fetch("HEROKU_RELEASE_VERSION")
config.notify_release_stages **= [** "production" **]**
```
config.ignore_classes << ActiveRecord :: RecordNotFound
end
```
**end
end**
This will register the block of code passed toinitializerwith Rails and, whenever Rails loads the files in
config/initializers, it will also execute this block of code, thus configuring Bugsnag. This means that with a
single line of code in theGemfile, any Rails app will have the canonical configuration for using Bugsnag.
_And_ , if this configuration should ever change, you can change it, release a new version of the gem, and then,
because teams are doing frequent dependency updates as discussed on page 368, the configuration update will
naturally be applied to each app as the team does their updates.
This technique allows you to centralize a lot of configuration options across many apps without complex
infrastructure and without a lot of documentation or other manual work. We used this technique at Stitch Fix
to manage shared configuration for over 50 different Rails apps, including rolling out a highly critical database
connection update in a matter of hours.
### Up Next
There are likely many more workflows and techniques for sustainable development than the ones I’ve shared here.
While these specific techniques _do_ work well, your team should explicitly prioritize looking for new techniques
and workflows to automate. The opportunity cost of creating shared gems, scripts, or other automation can really
reduce carrying costs over time. It’s a worthwhile investment.
The next chapter will be about considerations for actually operating your app in production, namely how to
consider things like monitoring, logging, and secrets management.
## 24 Operations
I’ve alluded to the notion that code in production is what’s important, but I want to say that explicitly right now:
if your code is not in production it creates a carrying cost with nothing to offset it—an unsustainable situation.
However, being responsible for code running in production is a much different proposition than writing code
whose tests pass and that you can use in your development environment. Seeing your code actually solve real
users’ problems and actually provide the value it’s meant to provide can be a sometimes harrowing learning
experience about what it means to develop software. Of course, it’s also extremely rewarding.
That’s what this chapter is about. Well, it’s really a paltry overview of what is a deep topic, but it should give you
some areas to think about and dig deeper into, along with a few basic tips for getting started.
Like may aspects of software development, production operations is a matter of a people and priorities: do you
have the right people given the right priorities to make sure the app is operating in production in all the ways you
need? For a small team just starting out, the answer is “no”. Surprisingly, for larger teams, the answer might still
be still “no”! I can’t help you solve that.
What I’m going to try to help with in this chapter is understanding what aspects are important and what techniques
are simplest or cheapest to do to get started. These techniques—like logging and exception management—will
still be needed on even the most sophisticated team, so they’ll serve you well no matter what.
As context, production operations should be driven by _observability_ , which is your ability to understand the
behavior of the system
### 24.1 Why Observability Matters
In “How and Why JavaScript is a Serious Liability” on page 141, I said, among other things, that JavaScript is
difficult or impossible to observe in production, especially as compared to the back-end Rails code. What does
that mean, exactly?
The term _observability_ (as it applies to this conversation) originates in control theory, as explained in the Wikipedia
entry^1 :
```
In control theory, observability is a measure of how well internal states of a system can be inferred from knowledge
of its external outputs.
```
Based on this definition, what I’m saying about JavaScript is that it’s hard to understand what it actually did or is
doing based just on what information gets sent back to our server (or can be examined in our browser). Even for
(^1) https://en.wikipedia.org/wiki/Observability
```
backend code, it’s not clear how to do this. Can you really look at your database and figure out how it got into
that state?
Charity Majors has been largely responsible for applying the term “observability” to software development and I
highly suggest reading in detail how she defines observability in software^2. Her definition sets a very high bar
that few teams—even highly sophisticated ones—operate the way she defines it. That’s OK. As long as you start
somewhere and keep improving, you’ll get value out of your operations efforts.
```
The way I might summarize observability, such that it can drive our decision-making, is that observability is
the degree to which you can explain what the software did in production and why it did that. For example, in
“Understand What Happens When a Job Fails” on page 296, we discussed the notion of background jobs being
automatically retried when they fail. If you notice an hourly job has not updated the database, how will you know
if that job is going to be retried or simply failed?
The more aspects of the system you can directly examine and confirm, the more observable your system is, and
this applies from low levels such as job control to high levels such as user transactions and business metrics. The
more you can observe about your app’s behavior, the better.
The reason is that if there is a problem (even if it’s not with your app), someone will notice and eventually come
calling wanting an explanation. From “the website is slow” to “sales are down 5% this month”, problems _will_ get
noticed and, even if your app is running perfectly, you need to be able to actually _know_ that.
```
For example, if the marketing team sees a dip in signups, and you can say, with certainty, that every single sign up
attempt in the last month was successful, that helps marketing know where to look to explain the problem. If, on
the other hand, you have no idea if your sign up code is working at all, you now have to go through the process
of trying to prove it has been working... or not!
What all this says to me is that production operations and the ability to observe your app in production is as
important—if not more important—than test coverage, perfect software architecture, or good database design. If
you have done the best job anyone could ever do at those things yet be unable to explain the app’s behavior in
production, you are in a very bad place.
```
```
Remember, techniques like software design, testing, and observability are tools to reduce risk. A lack of
observability carries a great risk, just like shipping untested code to production does.
Fortunately, there are a few low-cost, low-effort techniques that can provide a lot of observability for you that just
about any engineer on your team can understand and apply. Before we talk about them, we need to understand
what we need to monitor to know if the app is experiencing a problem. What we need to monitor is not usually
technical. Instead, we want to monitor business outcomes.
```
### 24.2 Monitor Business Outcomes
```
Before considering how to observe the specific behavior of your app, you need to take a moment to not lose sight
of the purpose of your app. Presumably, your app exists to deliver some sort of business value, and if it stops doing
that, it’s a problem—no matter what the CPU load might be. You need to monitor the expected business outcomes.
Suppose our app allows users to sign up for our service. You might think you can keep tabs on this feature
by monitoring the number of HTTP 500 errors from theSessionsController#createaction. This is how new
customers sign up, so if it’s failing, there is a problem with sign up.
```
(^2) https://charity.wtf/2020/03/03/observability-is-a-many-splendored-thing/
Controller actions completing successfully is not a business outcome. No marketing person, executive, investor, or
customer cares about what a controller is or if it’s working. They only care if sign up is functional.
The reality is that there are a lot of reasons that people might not be able to sign-up for your app, and an errant
controller is only one of them. In fact, there could be non-technical reasons you can’t control or observe at all.
At Stitch Fix, a marketing email went out once that pointed to our staging environment. Sign-ups were down
because of a typo in an email—the sign up code was working perfectly.
This is why you should monitor business outcomes and not technical behavior. Technical behavior could help
explain why business outcomes aren’t being achieved, but it’s those outcomes that are what matter and thus what
should be monitored.
Figuring out what these are is a deep topic, and it requires you to understand the core business problems your
app solves and to pick apart the various measurements that indicate to a business owner, executive, or other
non-engineer if the app is serving its ultimate purpose.
Once you know _what_ you need to monitor, the specifics of _how_ to do it depend on the tools you have. And once
you _do_ have monitoring in place, you then will need to know how the parts of the system behave (or behaved) in
order to explain why business outcomes aren’t being achieved.
What all this means is that your perfectly crafted, beautiful, elegant, programmer-happy codebase is going to
become littered with droppings to allow you to properly monitor your app in production. Ruby and Rails allow
you to manage this sort of code in a mostly clean^3 way, but there’s no avoiding it entirely.
To make matters even more complicated, achieving the level of observability that Charity Majors describes in the
blog post linked above requires a significant investment in culture and tooling. You might not be able to go from
zero to a fully-observable system overnight, especially if you have a small team just starting to grow.
Fortunately, there are a few cheap and easy techniques that can get you pretty far. The first one is the venerable
Rails logger.
### 24.3 Logging is Powerful
```
This section’s code is in the folder24-03/of the sample code.
```
Way back at the start of the book, in “Improving Production Logging with lograge” on page 44, we set up lograge
to change the format of our logs. The reason is that almost every tool for examining logs assumes one message
per line, and that’s not how Rails logs by default.
This matters because even the most under-funded production operations system tends to include a way to look at
application logs. It might require usingsshto connect to the production server, then usingtail,grep,sed, and
awkto filter the log file, but usually there is a way to look at the logs.
Often, when there is a problem in production that no one can explain, the solution is to add more logging, deploy
the app, and wait for the problem to happen again so you can get more data. This might be rudimentary, but it’s
still powerful!
(^3) I struggled with what word to use here, because to many, “clean code” is some moralistic nonsense proselytized by members of the agile
software community. That is not what I mean here. What I mean is that when code contains only what it needs to function, it’s clean—free of
dirt, marks, or stains. When we add log statements, metrics tracing, or performance spans, we add code that’s not needed to make the app
work and it gunks up our code. Thus, it’s a bit dirtier than before. Nothing moral about it.
Logging is also an extremely simple way to provide information about what the app is doing and why, and it’s a
concept that almost any developer of any level of experience can understand and use effectively. If only _everything_
in software were like this!
That said, not all log messages are equally effective, so you want to make sure that you and your team are writing
good log messages. Consider this code:
## app/services/widget_creator.rb
**class** WidgetCreator
**def** create_widget(widget)
widget.widget_status **=**
WidgetStatus.find_by!( **name:** "Fresh")
widget.save
**if** widget.invalid?
**return** Result.new( **created:** false, **widget:** widget)
**end**
→ Rails.logger.info "Saved #{widget.id}"
The code might look obvious, but the log message will look like so:
Wed Jun 24 09:02:01 EDT 2020 - Saved 1234
If you came across this log statement, you would have no idea what was saved. If you were searching for
confirmation that widget 1234 was saved, could you be absolutely certain that this log message confirmed that?
What if the code to save manufacturers used a similar log message?
Consider the two primary use-cases of logs.
- Search the logs to figure out what happened during a certain request or operation.
- Figure out what code produced a log message you noticed but weren’t searching for.
There are four techniques you should apply to your log messages to make these two use-cases easy:
- Include a request ID in every single message if you can.
- When logging identifiers, disambiguate them so it’s obvious what they identify.
- Include some indicator of where the log message originated in the code.
- If there is a current authenticated user, include their identifier in the log message.
#### 24.3.1 Include a Request ID in All Logs
Many hosting providers or web servers generate a unique value for each request and set that value in the HTTP
headerX-Request-Id. If that happens, Rails can provide you with that value. Each controller in a Rails app
exposes the methodrequest, which provides access to the HTTP headers. Even better, you can call the method
request_idonrequestto get the value of theX-Request-Idheader or, if there is no value, have Rails generate a
unique request ID for you.
If you include this value in all your log statements, you can use the request ID to correlate all activity around a
given request. For example, if you see that widget 1234 was saved as part of request ID1caebeaf, you can search
the log for that request ID and see all log statements from all code called as part of saving widget 1234. This is
extremely powerful!
The problem is that Rails doesn’t automatically include this value when you callRails.logger.info. The default
logging from Rails controllers _does_ include this value, but lograge removes it, for whatever reason. Let’s add that
back and then discuss how to include the request ID in log messages that aren’t written from your controllers.
First, we’ll modifyApplicationControllerto include the request ID in a hash that lograge will have access to.
We can do that by overriding the methodappend_info_to_payload, which Rails calls to allow inserting custom
information into a special object used for each request.
# app/controllers/application_controller.rb
**class** ApplicationController **<** ActionController **::** Base
→ **def** append_info_to_payload(payload)
→ super
→ payload **[:request_id] =** request.request_id
→ **end
end**
Thispayloadis available to lograge for logging. We can configure this inconfig/initializers/lograge.rb:
# config/initializers/lograge.rb
**else**
config.lograge.enabled **=** false
**end**
→ config.lograge.custom_options **=** lambda **do |** event **|**
→ **{**
→ **request_id:** event.payload **[:request_id]**
→ **}**
→ **end
end**
With this in place, all logs originating from the controller layer will include this request ID. You can fire up the
app yourself and try it out. Don’t forget to useLOGRAGE_IN_DEVELOPMENT, as instructed bybin/setup help.
Logging from anywhere else in the app won’t have access to this value. This is because the request is not available
to, for example, your service layer or Active Records. To make it available, we’ll use a feature of Rails called
_current attributes_. This is wrapper around _thread local storage_ , which is an in-memory hash that can store data
global to the current thread (but, unlike a true global variable, isolated from other threads).
To use current attributes, you define a class, usually inapp/models, that extendsActiveSupport::CurrentAttributes.
We’ll follow the Rails API docs^4 and call itCurrent.
# app/models/current.rb
**class** Current **<** ActiveSupport **::** CurrentAttributes
attribute **:request_id
end**
This will allow us to callCurrent.request_id = «some id»and fetch that value back out viaCurrent.request_id.
The value provided will be the same within the scope of a Thread, which is in the same scope as a request. Setting
request_idis the perfect use case for a controller callback inApplicationController:
# app/controllers/application_controller.rb
**class** ApplicationController **<** ActionController **::** Base
→ before_action **:set_current_request_id**
→ **def** set_current_request_id
→ Current.request_id **=** request.request_id
→ **end**
**def** append_info_to_payload(payload)
super
payload **[:request_id] =** request.request_id
To put this in our logs is... a bit complicated. There is not a handy gem to do this that I have found, and the
Rails logger is not sophisticated enough to allow some configuration to be set that automatically includes it.
Instead, let’s create a small wrapper aroundRails.loggerthat our code will use. This wrapper will assemble a
log message by accessingCurrentto get the request ID and prepending it to our actual log message.
It works like so:
(^4) https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html
log "Saved Widget #{widget.id}"
## => 2020-07-05 11:23:11.123 - request_id:1caebeaf Saved Widget 1234
First, we’ll create a module inlibthat will wrap calls toRails.logger.infoand fetch the request ID:
# lib/logging/logs.rb
**module** Logging
**module** Logs
**def** log(message)
request_id **=** Current.request_id
Rails.logger.info("request_id:#{request_id} #{message}")
**end
end
end**
Because it’s inlib/, we have torequireit explicitly, so, for example, in ourWidgetCreator:
# app/services/widget_creator.rb
→require "logging/logs"
→ **class** WidgetCreator
→ include Logging **::** Logs
**def** create_widget(widget)
widget.widget_status **=**
WidgetStatus.find_by!( **name:** "Fresh")
Now we can add a log message:
# app/services/widget_creator.rb
**end**
# XXX
# XXX
→ log "Widget #{widget.id} is valid. Queueing jobs"
HighPricedWidgetCheckJob.perform_async(
```
widget.id, widget.price_cents)
WidgetFromNewManufacturerCheckJob.perform_async(
```
If you fire up your app now and create a widget, you should see that the Rails controller logs include a request id,
but that same ID is prepended to the log message you just added.
That you have to go through these hoops isn’t ideal. Rails logging is a pretty big mess and I have not found a
good solution. At Stitch Fix we had a custom logging system that handled this, but it was highly dependent on
undocumented Rails internals and tended to break with each new version of Rails. It was also extremely difficult
for most developers to understand and modify, so it created a carrying cost that I wouldn’t incur again.
To make it easy to use this new module in our non-controller code, we could include it inApplicationModel,
ApplicationJob, and other base classes. We might even createApplicationServicefor our service-layer classes
to extend and include this module there. Once we start using it ubiquitously, we can get the end-to-end request
tracing discussed above.
Of course, if you are looking at logs but don’t have a request ID, you will often want to know what code produced
the log message you are seeing. Further, if a log message references a specific object or database row, you need
more than just an ID to know what it means.
#### 24.3.2 Log What Something is and Where it Came From
Logs are often relevant to a specific Active Record. Logging the ID is a great way to know _which_ Active Record or
row in the database, but you need to know what type of thing that ID refers to. Further, you might want to know
_where_ the log message originated so you can dial into what code was acting on what piece of data.
It would be nice if you could get this for free by callinginspectand having the Rails logger figure out what class
called the log method:
log "#{widget.inspect} updated"
## => 2020-07-09 11:34:12 [WidgetCreator] <#Widget id=1234> updated
Unfortunately, this doesn’t work the way we want. First, deriving the class name of the caller isn’t a feature
of the logger. Second, callinginspecton an Active Record will output _all_ of its internal values. This can be
overwhelming when trying to debug, and can expose potentially sensitive data to the log. Most of the time, you
really just need the class name and its ID.
You could have the team try to remember to include all this context, like so:
log "#{self.class.name}: Widget #{widget.id} updated"
The team will not remember to do this consistently and it will be tedious to try to manage with code review.
Instead, let’s enhance our abstraction that wraps the Rails logger. We can make it more useful by printing out the
class name it was included into as well as accepting an optional argument of a record as context.
Let’s modifyLogging::Logsso thatlogaccepts either one or two parameters. If we pass one, it behaves like it
currently does—prepending the request ID to the parameter, which is assumed to be a message. If we pass _two_
parameters, we’ll assume the first is some object whose class and ID we want to include in the message and the
second parameter is the message.
Further, becauseLogging::Logsis a module, we can include the class name of whatever class is including it in
the log message as well.
This means that code like this:
log widget, "updated"
Will produce a message like this:
request_id: 1caebeaf [WidgetCreator] (Widget/1234) updated
Here’s how we can do that. First, we’ll allow two parameters tolog:
# lib/logging/logs.rb
**module** Logging
**module** Logs
→ **def** log(message_or_object,message **=** nil)
request_id **=** Current.request_id
Rails.logger.info("request_id:#{request_id} #{message}"...
**end**
Next, we’ll create the log message with both the class name whereLogswas included as well as the class and
ID of themessage_or_objectifmessageis present. Note that we need to be a bit defensive around the type of
message_or_objectin case it doesn’t respond toid. If it doesn’t, we’ll include its class and its string representation.
# lib/logging/logs.rb
```
module Logs
def log(message_or_object,message = nil)
request_id = Current.request_id
```
→ message **= if** message.nil?
→ message_or_object
→ **else**
→ object **=** message_or_object
→ **if** object.respond_to?( **:id** )
→ "(#{object.class}/#{object.id} #{message}"
→ **else**
→ "(#{object.class}/#{object} #{message}"
→ **end**
→ **end**
→ Rails.logger.info("[#{self.class}] " \
→ "request_id:#{request_id} " \
→ "#{message}")
**end
end
end**
Now, developers can log a ton of context with not very much code. Granted, they have to provide an object as
context and remember to do that, but this will be much easier to both remember and catch in a code review.
Because Ruby is such a dynamic language, you can do _much_ more here to magically include context without
requiring it in the API.
If you like this approach, the log_method gem^5 was extracted from this book as well as several running codebases
and provides even more useful logging features from the same basiclogmethod.
Another bit of context that can be extremely helpful—and sometimes required by company policy—is the user
who is performing or initiating actions in the app.
#### 24.3.3 UseCurrentto Include User IDs
Just as we included the request ID in theCurrentmodel so that we could log it everywhere, we can do the same
with the currently logged-in user’s ID. This allows us to know _who_ initiated an action. Often, in environments
subject to strict compliance (like the aforementioned SOX), being able to see who did what is crucial.
No matter what mechanism you used in “Authentication and Authorization” on page 329 to add authentication,
you will likely have a method inApplicationControllercalledcurrent_user. To include the ID of this user in
all log messages, you can do exactly what we did in “Include a Request ID in All Logs” on page 385. The only
difference is thatcurrent_usermay returnnil, so the code inApplicationControllerwill need to account for
this, as well as the code inLogging::Logsthat pulls it out ofCurrent.
I’ll leave the specifics of the implementation to you.
Another powerful source of information about the behavior—or misbehavior—of your app is unhandled exceptions.
(^5) https://github.com/sustainable-rails/log_method
### 24.4 Manage Unhandled Exceptions
When an exception happens that is not rescued explicitly by your code, it bubbles up a large call stack inside
Rails for some sort of handling. If the code was initiated by a controller, Rails will render a default HTTP 500
error. If the code was started by a Rake task, nothing special will happen. If run from a background job, it might
be retried, or it might not—it depends. In any case, you need to be able to view and examine these unhandled
exceptions because they indicate a problem with your app.
Certainly, unhandled exceptions aren’t business outcomes, but they _are_ a useful bit of telemetry to explain what’s
happening with your app. Often, unhandled exceptions indicate bugs in the app that need to be fixed to avoid
creating confusion later when you have to diagnose a real failure. For example, if you communicate with a third
party API, you will certainly get a handful of network timeouts. As mentioned in “Network Calls and Third Parties
are Flaky” on page 292, your jobs will retry themselves to recover from these transient network errors. You don’t
need to be alerted when this happens.
Tracking unhandled exceptions isn’t something your Rails app can do on its own. While the log will show
exceptions and stacktraces, the log isn’t a great mechanism for notifying you when exceptions occur, or allowing
you to analyze the exceptions that are happening over time. You need an exception handling service.
There are many such services, such as Airbrake, Bugsnag, or Rollbar. They are all more or less equivalent, though
there are subtle differences that might matter to you, so please do your research before choosing one (though the
only wrong choice is not to use one). Most of these services require adding a RubyGem to your app, adding some
configuration, and placing an API key in the UNIX environment.
They tend to work by registering a Rails Middleware that catches all unhandled exceptions and notifies the service
with relevant information. This information can be invaluable, since it can include browser user agents, request
parameters, request IDs, or custom metadata you provide. Often, you can view a specific exception in the service
you’ve configured, find the request ID, then look at all the logs related to the request that lead to the exception.
I can’t give specific guidance, since it will depend on the service you’ve chosen, but here are some tips for getting
the most out of your exception handling service:
- Learn how the service you’ve chosen works. Learn how they intend their service to be used and use it that
way. While the various services are all mostly the same, they differ in subtle ways, and if you try to fight
them, you won’t get a lot of value out of the service.
- Try very hard to not let the “inbox” of unhandled exceptions build up. You want each new exception to be
something you both notice _and_ take action on. This will require an initial period of tuning your configuration
and the service’s settings to get it right, but ideally you want a situation where any new notification from
the service is actionable and important.
- If the service allows it, try to include additional metadata with unhandled exceptions. Often, you can include
the current user’s ID, the request ID we discussed above, or other information that the exception-handling
service can show you to help figure out why the exception happened.
- Intermittent exceptions are particularly annoying because you don’t necessarily need to know about each
one, but if there are “too many”, you do. Consult your service’s documentation for how to best handle
this. You need to be _very_ careful to not create alert fatigue by creating a situation where you are alerted
frequently by exceptions that you can ignore.
In addition to having access to view and manage unhandled exceptions, it’s helpful to be able to measure the
performance of your app.
### 24.5 Measure Performance
Donald Knuth, Turing Award winner and author of the never-ending “Art of Computer Programming” book series,
is famous for this quote about performance:
```
The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places
and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.
```
This is often quoted when developers modify code to perform better but have not taken the necessary step of
understanding the current performance and demonstrating why the current level of performance is insufficient.
This implies that you must measure performance before you can improve it.
Measuring the performance of your app can also help direct any conversation or complaint about the app being
slow. This is because the cause of app slowness is not always what you think, and if you aren’t measuring _every_
aspect of the apps’ behavior, you may end up optimizing the wrong parts of the app without making it perform
better. See the sidebar “The App is Only as Fast as Wi-Fi” on the next page for an example of how performance
measurement can lead to the right area of focus.
You need to be careful not to over-measure at first, because the code you must write to measure certain
performance details has a carrying cost. For example, here is how you would measure the performance of
an arbitrary block of code using Open Telemetry (which is a standard for application performance monitoring
supported by several vendors):
#### The App is Only as Fast as Wi-Fi
```
One of the apps we built at Stitch Fix—calledSPECTRE—provided tools for associates in our warehouse to do
their jobs. This app wasn’t part ofstitchfix.comand was only used from specific physical locations with Internet
connections we controlled.
Over time, we’d get an increasing number of complaints that the app was slow. We had set up New Relic, which
allowed us to understand the performance of every controller action in the app. Even the 95th percentile performance
was good, with the average performance being great.
Since we controlled the Internet connection to the warehouse, we were able to access performance monitoring of
the network in the warehouse itself. While the connection to the warehouse was great—fast, tons of bandwidth, tons
of uptime—the computers connecting via wi-fi were experiencing inconsistent performance.
It was these users that were experiencing slowness, and it was because of the wi-fi network, not the app itself. Of
course, to the users, the wi-fi connection was part of the app, and it didn’t matter if the controllers were returning
results quickly.
We didn’t have the capital or expertise to update the network hardware to provide consistent wi-fi performance
throughout the warehouse, so we modified the front-end of the feature that required wi-fi to not require as much
bandwidth, as described in “Single Feature JAM Stack Apps at Stitch Fix” on page 148.
If we hadn’t been measuring the whole system’s performance, we could’ve spent time creating caching or other
performance improvements that would’ve both created a carrying cost for the team and also not solved the actual
performance problem.
```
**class** WidgetCreator
**def** create_widget(widget)
→ OpenTelemetry.tracer_provider**.**
→ tracer('tracer')**.**
→ in_span("WidgetCreator/create_widget/db_operations") **do**
```
widget.widget_status =
WidgetStatus.find_by!( name: "Fresh")
widget.save
if widget.invalid?
return Result.new( created: false, widget: widget)
end
```
→ **end**
HighPricedWidgetCheckJob.perform_async(
widget.id, widget.price_cents)
WidgetFromNewManufacturerCheckJob.perform_async(
widget.id, widget.manufacturer.created_at)
Result.new( **created:** widget.valid?, **widget:** widget)
**end
end**
At a larger scale, this sort of code can be mentally exhausting to write, read, and manage.
Instead, choose a technique or tool that can automatically instrument parts of your app. For example OpenTeleme-
try will automatically track and measure the performance of every controller action, URL, and background job
without you having to write any code at all.
This default set of measurements gives you a baseline to help diagnose a slow app. If the defaults don’t show you
what is performing poorly, _then_ you can add code to measure different parts of your codebase.
If you need to add code to enable custom measurements, do so judiciously and don’t be afraid to remove that
code later if it isn’t needed or didn’t provide the information you wanted. Look for patterns in how you write this
code and try to create conventions around it to allow the team to quickly measure code blocks as needed.
Before we leave this chapter, I want to step back from observability and talk about a more tactical issue which is
how to manage secret values like API keys.
### 24.6 Managing Secrets, Keys, and Passwords
Way back in “Using the Environment for Runtime Configuration” on page 29, I hand-waved over managing
sensitive values that must be stored in the app’s UNIX environment in production. Let’s talk about that now.
The short answer is, of course, that it depends. The other thing to understand is that you cannot absolutely
prevent unauthorized access to your secrets. No system can absolutely prevent the exfiltration of sensitive data.
All security concerns, including managing API keys and secrets, are about reducing risk and managing the
opportunity and carrying cost of doing so. Sure, you could set up your own SIPRNet^6 to keep your marketing
email list safe from hackers, but that expense likely isn’t worth it to mitigate the relatively smaller risk of someone
stealing email addresses.
Thus, you need to weigh the risks of leaking your secrets and keys against the cost you are willing to pay to secure
them. For a small team at a small company, the risks are low, so a low-cost solution will work. For a huge public
company, the calculus is different. Either way, you should constantly re-evaluate your strategy to make sure it’s
appropriate and the trade-offs are correct.
Evaluating the trade-offs is critical. It might seem easy to install something like Hashicorp’s Vault^7 , which is
highly secure and packed with useful features. Operating Vault is another story. It’s extremely complicated and
time-consuming, especially for a team without the experience of operating systems like Vault in production.
A poorly-managed Vault installation will be a far worse solution than storing your secrets in 1Password and
manually rotating them once a quarter.
Don’t be afraid to adopt a simple solution that your team can absolutely manage, even if it’s not perfect (no
solution will be, anyway). If someone brings up an attack vector that’s possible with your proposed solution,
quantify the risk before you seriously consider mitigating that vector. Engineers are great at imagining edge cases,
but it’s the level of risk and likelihood that matters most.
### The End!
And that’s it! We’ve covered a lot of ground in this book. Each technique we’ve discussed should provide value on
its own, but hopefully you’ve come to appreciate how these techniques can reinforce each other and build on each
other when used in combination.
I should also point out that, no matter how hard you try, you won’t be able to hold onto each technique in this
book—or any book—throughout the life of your app. You’ll model something wrong, use the wrong name, miss a
tiny detail, or have an assumption invalidated by the business at just the wrong time. Or, you’ll find that at some
scale, the basic techniques here don’t work and you have to do something fancier. It happens. That’s why we tend
to work iteratively.
The most sustainable way to build software is to embrace change, minimize carrying costs, tame opportunity
costs, and generally focus on problems you have, treating your tools for what they are. Try not to predict the
future, but also don’t be blind to it.
(^6) https://en.wikipedia.org/wiki/SIPRNet
(^7) https://www.vaultproject.io
### PART
### IV
# appendices
## A
# Setting Up Docker for Local Development
All the code written in this book, and all commands executed, are run inside a Docker container. Docker provides
a virtual machine of sorts and allows you to replicate, almost exactly, the environment in which I wrote the code
(see the sidebar “Why Docker?” on the next page). If you don’t know anything about Docker, that’s OK. You
should learn what you need to know here.
```
Docker is traditionally used for deploying applications and services to a production environment like AWS, but
it can also be used for local development. You’ll need to install Docker, after which we’ll create a series of
configuration files that will set up your local Docker container where all the rest of the coding in this book will
take place.
```
### A.1 Installing Docker
```
While the main point of Docker is to create a consistent place for us to work, it does require installing it on
whatever computer you are using, and that is highly dependent on what that computer is!
```
```
Rather than try to capture the specific instructions now, you should head to the Docker Desktop page^1 which
should walk you through how to download, install, and run Docker on your computer.
```
#### Why Docker?
```
I’m the co-author of Agile Web Development With Rails 6 a and have worked on two editions of that book. Each
new revision usually wreaks havoc with the part of the book that walks you through setting up your development
environment. Between Windows, macOS, and Linux, things are different and they change frequently.
While a virtual machine like Virtual Box b can address this issue, Docker is a bit easier to set up, and I find it useful
to understand how Docker works, because more and more applications are deployed using Docker.
Docker also has an ecosystem of configurations for other services you may need to run in development, such
as Postgres or Redis. Using Docker to do this is much simpler than trying to install such software on your personal
computer.
a https://pragprog.com/book/rails6/agile-web-development-with-rails-6
b https://www.virtualbox.org
```
(^1) https://www.docker.com/products/docker-desktop
### A.2 What is Docker?
You can think of Docker as a tool to build and run virtual machines. It’s not _exactly_ that, but the mental model is
close enough. There are some terms with Docker that are confusing, but they are critical to understand, especially
if you experience problems and need help.
**Image** A Docker _image_ can be thought of as the computer you might boot. It’s akin to a disk image, and is the set
of bytes that has everything you need to run a virtual computer. An image can be started or run withdocker
startordocker run.
**Container** A Docker _container_ is an image that’s being executed. It’s a computer that’s running. You can have
multiple containers running from a single image. To use an object-oriented metaphor, if an image is a class,
then a container is an instance of that class. You can run commands in a container withdocker exec.
**Dockerfile** A Dockerfile (often namedDockerfile, but can be named anything) contains instructions on how to
build an image. It is not sophisticated. Most Dockerfiles are a series of shell invocations to install software
packages. If an image is an object-oriented class, the Dockerfile is that class’ source code. An image is built
withdocker build.
**Host** You’ll often see Docker documentation refer to “the host”. This is _your_ computer. Wherever you are running
Docker, _that_ is the host.
To tie all this together (as in the figure “Docker Concepts” below), aDockerfileis used to _build_ an image, which
is then _started_ to become a container _running_ on your host.
### A.3 Overview of the Environment
Rather than reproduce a lengthyDockerfile, helper shell scripts and all that, I’m going to point you to a Github
repository called sustainable-rails/sustainable-rails-dev^2 , which has what you need.
I recommend you clone that and use it, like so (these commands are executed on your computer):
> git clone https://github.com/sustainable-rails/sustainable-rails-dev
> cd sustainable-rails-dev
There is a README there you can use as a reference, but here is how the system works:
```
1.You’ll build an image that contains the basic software you need to do Ruby on Rails development. You’ll use
this image in a later step to create a container in which to work.
2.You’ll start up three container using Docker Compose : the container using the image mentioned above, a
container running Postgres, and a container running Redis (which is used for Sidekiq).
3.You can use a script to execute commands inside your dev container. The simplest command isbash, which
will give the appearance of having logged into your dev container.
```
(^2) https://github.com/sustainable-rails/sustainable-rails-dev
Figure A.1: Docker Concepts
### A.4 Creating the Image
Run the script dx/buildto build the image. This will useDockerfile.dxto produce an image named
davetron5000/sustainable-rails/rails-7.1.
The first time this runs, it may take a long time, since it must download the base image for Ruby, then execute
several shell scripts that may also download other files to save into the image. The output ofdx/buildcontains
the output ofdocker buildand it has a lot of command line animations to it, but when it’s done, it should have
looked something like the following (keeping in mind I have truncated most of the lines as they are very long):
> dx/build
[+] Building 70.8s (19/19) FINISHED
=> [internal] load build definition from Dockerfile.dx...
=> => transferring dockerfile: 4.84kB...
=> [internal] load .dockerignore...
=> => transferring context: 58B...
=> [internal] load metadata for docker.io/library/ruby:3.2...
=> CACHED [ 1/14] FROM docker.io/library/ruby:3.2...
=> [internal] load build context...
=> => transferring context: 121B...
=> [ 2/14] RUN apt-get update -yq && apt-get install -y...
=> [ 3/14] RUN apt-get update -qy && apt-get install -qy lsb...
=> [ 4/14] RUN sh -c'echo "deb [http://apt.postgresql.org/pu](http://apt.postgresql.org/pu)...
=> [ 5/14] RUN apt-get -y install chromium chromium-driver...
=> [ 6/14] RUN echo "gem: --no-document" >> ~/.gemrc &&...
=> [ 7/14] RUN apt-get update -q && apt-get install -qy...
=> [ 8/14] COPY dx/show-help-in-app-container-then-wait.sh /...
=> [ 9/14] RUN apt-get install -y openssh-server...
=> [10/14] RUN mkdir /var/run/sshd && echo'root:passwor...
=> [11/14] RUN echo "# Set here from Dockerfile so that ssh'...
=> [12/14] RUN mkdir -p /root/.ssh && chmod 755 /root/.ssh...
=> [13/14] COPY authorized_keys /root/.ssh/...
=> [14/14] RUN chmod 644 ~/.ssh/authorized_keys...
=> exporting to image
=> => exporting layers
=> => writing image sha256:21c9f171e3eaec00bae0d0f24d8fe73b7...
=> => naming to docker.io/davetron5000/sustainable-rails-dev...
[ dx/build ] Your Docker image has been built tagged'davetro...
[ dx/build ] You can now run dx/start to start it up, though...
Your output may be slightly different, but the final two messages prefixed with[ dx/build ]should indicate that
everything worked. You can also verify this by runningdocker image lslike so:
> docker image ls davetron5000/sustainable-rails-dev:rails-7.1
REPOSITORY TAG IMAGE ID
davetron5000/sustainable-rails-dev rails-7.1 21c9f171e3ea
### A.5 Starting Up the Environment
```
Once your image is built, you can start up the dev environment viadx/start. This may take a few seconds the
first time, depending on how much Internet bandwidth you have. It will download prebuilt images for Postgres
and Redis. Likedx/build,dx/startwraps Docker commands that involve a lot of command-line animations.
```
```
The output will intermix log messages from the image you built in the last section, Postgres, and Redis. The script
will exit if anything goes wrong, so if it does not exit, that should indicate everything is working.
```
```
You can verify this by running commands inside your container.
```
### A.6 Executing Commands and Doing Development
```
In another terminal window on your computer from where you randx/start, rundx/exec ls -l, and you should
see the contents of your dev environment local directory as viewed from inside the container:
```
```
> dx/exec ls -l
[ dx/exec ] Running'ls -l'inside container with service name'sust...
total 264
-rw-r--r-- 1 root root 4798 Oct 31 21:37 Dockerfile.dx
-rw-r--r-- 1 root root 5585 Nov 1 14:31 README.md
-rw-r--r-- 1 root root 230956 Nov 1 14:25 SocialImage.jpg
-rw-r--r-- 1 root root 1032 Oct 31 21:39 docker-compose.dx.yml
drwxr-xr-x 10 root root 320 Nov 1 17:03 dx
```
dx/execusesdx/docker-compose.envto know which container to connect to and run the command you gave it
(ls -lin this case) inside the container where you can do development. The container can access your local files
by virtue of a _bind mount_ set up insidedocker-compose.dx.yml(there is a comment inside there that can provide
more info).
```
This means that you can edit files locally, using whatever editor you like, and anything you run viadx/execwill
see those changes. For example, if you added a new test intest/models/widgets_test.rb, you can run that test
like so:
```
```
> dx/exec bin/rails test test/models/widgets_test.rb
```
```
This will run a test inside the container against the test file you changed. You could also usebashas the command
to run inside the container, which would provide a persistent command-line to run commands without needing
dx/execeach time:
```
```
your-computer> dx/exec bash
inside-the-container> bin/rails test test/models/widgets_test.rb
inside-the-container> bin/rails test test/models/manufacturs_test.rb
```
### A.7 Customizing the Dev Environment
```
If you use the command line frequently, you likely have some dotfiles or other command-line configuration. If you
are like me, you may feel strangely dis-empowered when you have to use the command line without them!
You can modifyDockerfile.dxto make your configuration available, but it may take some doing. There are two
steps: installation additional software, and copying configuration.
Let’s take a simple example of aliasinglsto use exa^3 , a fancier version ofls. Your machine likely hasexainstalled,
but you also have some sort of alias like so:
```
```
alias ls=exa
```
```
Let’s assume you have this in your home directory as the file.bashrc.
```
#### A.7.1 Installing Software
To install software, you’ll use aRUNdirective followed by the exact commands to install software on Debian Linux
(which is what the base image uses). Most software packages provide a way to do this, and you will need to
research the correct way for each tool you have. I strongly recommend you include a link to where you found the
installation instructions as a comment before theRUNdirective.
In the case ofexa, we’ll add this to the end ofDockerfile.dx
```
# Based on https://the.exa.website/install/linux
RUN apt-get install -qy exa
```
```
The-qytellsapt-getto answer “yes” to any question, and to reduce extraneous output.
```
#### A.7.2 Copying Your Dotfiles Into the Image
```
Next, you’ll need to add your.bashrconto the end of/root/.bashrcinside the image./root/.bashrcis where
the user that executes commands (in this case, “root”) will have their bash configuration. You can copy files into a
Docker image via theCOPYdirective, however the files you copy into the image must be local to where you are
runningdx/build. And, the files cannot be symbolic links—they must be real files.
If you keep your dotfiles in GitHub, the simplest solution may be to clone that repo inside your dev environment.
As a demonstration, I’ll just create the files directly:
```
```
> mkdir dotfiles
> echo "alias ls=ex" > dotfiles/.bashrc
```
(^3) https://the.exa.website
However the files get there, they should be inside your dev environment. You can then useCOPYto get them into
the image. Since you want to appenddotfiles/.bashrcto whatever’s there, we’ll copy the files somewhere, then
append them using aRUNdirective. TheWORKDIRdirective creates and changes to a directory inside the container,
so we’ll use that to come up with this addition to theDockerfile.dx:
# Set up a directory where our dotfiles
# can be copied
**WORKDIR** /root/dotfiles
# Now, copy our dotfiles to that directory
**COPY** dotfiles/.bashrc.
# Lastly, append our .bashrc to the canonical one
**RUN** cat .bashrc >> /root/.bashrc
Because of the use ofWORKDIR, the subsequentCOPYandRUNdirectives execute from/root/dotfiles.
Once you do this, rundx/build, hit Ctrl-C wherever you rundx/start, re-rundx/start, and finally rundx/exec
bash, then uselsto see that it’s respecting your alias:
> dx/exec bash
root@a4a92c1ce1ca:~/work# ls -lFh
Permissions Size User Date Modified Name
.rw-r--r-- 4.9k root 1 Nov 17:44 Dockerfile.dx
drwxr-xr-x - root 1 Nov 17:43 dotfiles/
drwxr-xr-x - root 1 Nov 17:03 dx/
.rw-r--r-- 5.6k root 1 Nov 14:31 README.md
.rw-r--r-- 230k root 1 Nov 14:25 SocialImage.jpg
If you have any issues using these scripts, please reach out or open an issue on the repo.
## B Monoliths, Microservices, and Shared Databases
There wasn’t an easy way to put this into the book, but since we discussed APIs in “API Endpoints” on page 337,
there is an implicit assumption you might have more than one Rails app someday, so I want to spend this appendix
talking about that briefly.
When a team is small, and you have only one app, whether you know it or not, you have a monolithic architecture.
A monolithic architecture has a lot of advantages. Starting a new app this way has a very low opportunity cost,
and the carrying cost of a monolithic architecture is quite low for quite a while.
The problems start when the team grows to an inflection point. It’s hard to know what this point is, as it depends
highly on the team members, the scope of work, the change in the business and team, and what everyone is
working on. Most teams notice this inflection point months—sometimes years—after they cross it. Even if you
know the day you hit it, you still have some decisions to make. Namely, do you carry on with a monolithic
architecture? If not, what are the alternatives and how do you implement them?
In this section, I want to try to break down the opportunity and carrying costs of:
- staying with a monolithic architecture.
- deploying a microservices architecture.
- using a shared database amongst multiple user-facing apps.
The third option—sharing the database—is usually discussed as an anti-pattern, but as we’ll see, it’s anything but.
It’s important to understand that your system architecture—even if it’s just one app—is never done. You never
achieve a state of completeness where you can then stop thinking about architecture. Rather, the architecture
changes and evolves as time goes by. It must respond to the realities you are facing, and not drive toward some
idealistic end state.
So, I would strongly encourage you to understand monolithic architectures, microservices, and shared databases
as techniques to apply if the situation calls for it. It’s also worth understanding that any discussion of what a
system’s architecture is has to be discussed in a context. It’s entirely possible to have 100 developers working on
30 apps and, some of which are monolithic... within a given context.
Let’s start with monolithic architectures.
### B.1 Monoliths Get a Bad Rap
If you have a single app, you have a monolithic architecture. In other words, a monolithic architecture is one
where all functions reside in one app that’s built, tested, and deployed together.
When a team is small and when an app is new, a monolith has an extremely low opportunity cost for new features
as well as low carrying cost. The reason is that you can add entire features in one place, and everything you need
access to for most features—the UI, the database, emails, caches—are all directly available.
The larger the team and the more features are needed, the harder a monolith can be to sustain. The carrying cost
of a monolith starts rising due to a few factors.
First, it becomes harder to keep the code properly organized. New domain concepts get uncovered or refined and
this can conflict with how the app is designed. For example, suppose we need to track shipping information and
status per widget. Is that a set of new widget statuses, or is it a new concept? And, if we add this concept, how
will it confuse the existing widget status concept?
This domain refinement will happen no matter what. The way it becomes a problem with a monolith is that the
monolith has everything—all concepts must be present in the same codebase and be universally consistent. This
can be extremely hard to achieve as time goes by. The only way to achieve it is through review, feedback, and
revision. Whether that’s an up front design process or an after-the-fact refactoring, this has an opportunity cost.
A carrying cost is the time to perform quality checks like running the test suite. The more stuff your app does, the
more tests you have and the longer the test suite takes to run. If you run the test before deploys, this means you
are limiting the number and speed of deploys. A single-line copy change could take many minutes (or hours!) to
deploy.
Solving _this_ requires either accepting the slowdown, or creating new tools and techniques to deploy changes
without running the full test suite. This is an obvious opportunity cost, but it also creates a carrying cost
that—hopefully—is less than the carrying cost of running the entire test suite.
Related, a monolith can present particular challenges staying up to date and applying security updates, because
the monolith is going to have a lot of third-party dependencies. You will need to ensure that any updates all work
together and don’t create inter-related problems. This can be hard to predict.
An oft-cited solution to these problems is to create a microservices architecture. This trades some problems for
new ones.
### B.2 Microservices Are Not a Panacea.
Previously known as a _service-oriented architecture_ (SOA), a microservices architecture is one in which functionality
and data is encapsulated behind an API (usually based on HTTP), built, maintained, and deployed as a totally
separate app.
The reason to do this is to solve the issues of the monolith. The internal naming, concepts, and architecture
of a service don’t have to worry about conflicting with other services, because they are completely separate. A
microservice creates a context in which all of its internals can be understood. Taking the status example above,
you might create a widget shipping service that stores a status for each widget. That status is in the context of
shipping, so there’s no conceptual conflict with some other service maintaining some other type of status.
```
Microservices also naturally solve the issue of deployment. Because each service is completely separate, to deploy
a change in, say, the code around widget shipping, only requires running the tests for the widget shipping service.
These tests will certainly be faster than running all the tests in an analogous monolith.
```
```
Microservices are particularly effective when the team gets large and there are clearly-defined boundaries around
which sub-teams can form. This isolation allows teams to work independently and avoid conflicts when inter-team
coordination is not required.
```
```
This sounds great, right? Well, microservices have a pretty large opportunity cost and a not-insignificant carrying
cost. In my experience, the carrying cost is relatively stable despite the size of the team (unlike a monolith, where
the cost increases forever). The opportunity cost—the amount of effort to establish a microservices architecture
on any level—is large.
```
```
The reason is that you change the problem of your operations team from maintaining one app to maintaining
N apps. As I’m sure you are aware, there are only really three numbers in programming: zero, one, and
greater-than-one. Microservices are, by definition, greater-than-one.
```
```
First, you must have clearly-defined boundaries between services. If services are too dependent, or not properly
isolated, you end up with a “distributed monolith”, where you do not reap the benefits of separation. For example,
what if we made a widget data service that stored all data about a widget. When our widget shipping team added
its new status, that would have to be added to the widget data service. These two services are now too tightly
coupled to be managed independently.
```
Second, you must have more sophisticated tooling to make all the services run and operate. As we discussed in
“Use the Simplest Authentication System You Can” on page 340, your microservices need authentication. That
means something, somewhere, has to manage the API keys for each app to talk to each other. That means that
something somewhere has to know how one app locates the other to make API calls.
```
This implies the need for more sophisticated monitoring. Suppose a customer order page is not working. Suppose
the reason is because of a failure in the widget shipping service. Let’s suppose further that the website uses an
order service to render its view and that order service uses the widget shipping service to get some data it needs
to produce an order for the website. This transitive chain of dependencies can be hard to understand when
diagnosing errors.
```
```
If you don’t have the ability to truly observe your microservices architecture, your team will experience incident
fatigue. This will become an exponentially increasing carrying cost as time is wasted, morale lowers, and staff
turnover ensues.
```
```
You should almost never start with microservices on day one. But you should be aware of the carrying costs of
your monolith and consider a transition if you believe they are getting too high. You need to think about an
inflection point at which your monolith is costlier to maintain than an equivalent microservices architecture, as
shown in the figure “Graph Showing the Costs of a Monolith Versus Microservices Over Time” on the next page.
```
```
The transition to microservices can be hard. As the necessary tooling and processes are developed, it can be
disruptive to the team, as shown in “Graph Showing the Costs of a Microservices Transition”, also on the next
page.
```
```
One way to address the problems of the monolith without incurring the costs—at least initially—of microservices
is to use a shared database.
```
```
Figure B.1: Graph Showing the Costs of a Monolith Versus Microservices Over Time
```
### B.3 Sharing a Database Is Viable
When the carrying cost of a monolith starts to become burdensome, there are often obvious domain boundaries
that exist across the team. It is not uncommon for these boundaries to be related to user features. For example,
you may have a team focused on the website and customer experience, but you might also have a team focused
on back-office administrative duties, such as customer support.
Instead of putting both of these features in one app, and _also_ instead of extracting shared services to allow them
to be developed independently, a third strategy is to create a second system for customer support and have it
share the database with the website, as shown in the figure “Sharing a Database” below.
As long as your domain boundaries can work simply be communicating via changes to the database, this can keep
opportunity cost low, since everyone will know how to work on a database-backed Rails app. It keeps carrying
costs low, too, since you don’t have to invest in shared tooling or manage a large complex codebase.
As you discover more isolated needs, either from user groups needing their own user interface or isolated system
requirements, you can add more apps and point them to the shared database as in the figure “Sharing a Database
Figure B.2: Graph Showing the Costs of a Microservices Transition
```
Figure B.3: Sharing a Database
```
with More Apps” on the next page.
The most immediate carrying cost with this approach is maintaining the database migrations and the requisite
Active Record models. Because of how we are writing our code—not putting business logic in the Active
Records—these can be put into a gem that each app uses and that gem should not change often.
Database migrations, however, are not easy to manage when placed in a gem. You also don’t want every app to be
able to change the database that all apps share. You _should_ centrally manage changes to the database since all
apps depend on it. You can do this with a Rails app whose sole job is to manage the database schema. You can
then establish a convention on the team that each proposed change to this app—which implies it is a database
change—must be reviewed by all teams to ensure nothing will break.
See the figure “Managing the Shared Database” on the next page for how this might look.
Sharing the database doesn’t abdicate your responsibility for managing code across boundaries, but it does reduce
what must be managed to the database schema only. And since you are putting constraints and other data integrity
controls directly into the database (as outlined in “The Database” on page 191), you won’t have much risk of one
app polluting the data needed by other apps.
If you are careful with changes, the overall carrying costs of this architecture can be quite low and can surpass a
monolithic architecture, as shown in the figure “Graph Showing the Costs of Sharing the Database” below.
Of course, this architecture will eventually cause problems. When you have a lot of apps sharing a database, you
can certainly cause contention and locking that can be hard to predict or observe. That’s what happened in the
anecdote in the sidebar “A Single Line of Code Almost Took Us Down” on the next page.
The database schema will eventually become difficult to manage, as you end up with either tables that have too
many concepts embedded in them or a bunch of tables that exist only for the private use of a single app. It’s also
possible that you may need one app to trigger logic that lives in another app and have no easy way to do so. You
will likely need to do a microservices transition.
```
Figure B.4: Sharing a Database with More Apps
```
If you use a shared database, however, you can significantly delay your microservices transition—if you ever need
one— _and_ you can reduce the cost of doing so because you will have already done a lot of work on identifying
domain boundaries.
Navigating the evolution of your architecture is difficult. The fact is, your architecture is never done. There is no
end state you should aim for and no point at which you stop evolving. Evolution may slow at times, but it won’t
stop, and if your approach to architecture is to design it and build it, you will fail. Instead, you need principles to
guide you and competent technical leadership.
```
Figure B.5: Managing the Shared Database
```
#### A Single Line of Code Almost Took Us Down
Much of the business logic at Stitch Fix involved updating records in our shared database, and usually several
records at once. We made heavy use of database transactions to ensure those operations didn’t leave our data in
a partially-updated state. One example was updating a shipment record. Any time one was changed, we wrote a
database row to a separate events table that tracked all the changes made to that record.
As we grew and scaled, we eventually started using RabbitMQ for messaging. The library that was responsible for
updating the shipment was eventually augmented to additionally send a message on RabbitMQ about the change to
the shipment. This allowed downstream apps without access to our shared database to know when shipment records
changed.
The line of code to send the message was written inside a transaction. It was fine for years. Until one day it wasn’t.
We started noticing _massive_ slowdowns across all apps and increases in locks inside the database. They would
routinely happen in the early morning, then go away on their own. We could not say with any certainty what was
happening—locks in the database are rarely the problem, but rather an indicator of some other issue.
We started combing our code for transactions that contained potentially slow-running code. We found the above-
mentioned library. We moved the line of code used for sending messages to outside the transaction, distributed the
updated library to all apps, and voilà, the problem stopped.
This was pure luck. If we didn’t find a solution, it would’ve been a stop-the-world emergency that could’ve derailed
412 our team for weeks or even months. Be careful what code you put inside a database transaction.
Figure B.6: Graph Showing the Costs Sharing the Database
## C Technical Leadership is Critical
At times in this book I’ve referenced code reviews, or vague “managing” of changes. Getting a team to work
consistently, follow conventions, and also respond to change is difficult. It requires leadership.
Leadership is a deep topic. A leader isn’t just in charge, and often great leadership comes from people who don’t
have any real authority over others.
The most effective leadership I have experienced is where leaders organize everyone around shared values.
### C.1 Leadership Is About Shared Values
Top-down leadership, where the person in charge tells everyone below them what to do, is not sustainable. Most
programmers don’t enjoy being micromanaged, and the leader in this situation will not make universally good
decisions. It’s simply too hard to manage software from the top, and too unpleasant to be managed this way.
A more effective strategy is to focus everyone on shared values. We discussed some values in the first chapter of
this book, such as sustainability and consistency. Your company certainly has values, your team has more values,
and if there are sub teams within that team, they have their own values too.
A good leader will first make explicit what the team’s values are (not what they should be). Values should be a
form of documentation: what sorts of things does everyone believe to be important? When the team agrees on its
values, the function of leadership is then to apply those values to situations where a decision needs to be made.
For example, suppose the team is using Sidekiq for background jobs. Suppose an engineer has read about the
background job system Que and thinks it would be useful to use. This engineer wants to install it in the app and
start using it, but not everyone on the team agrees. How does this get resolved?
A top-down leadership style would be to tell the team what the decision is. A values-based leadership style would
be to engage the team with its values and help them apply those values to this decision. Does the team value
consistency? If so, this decision does not conform to that value. What if the team also values innovation? Using
something new and exciting might conform to that value.
By re-framing the discussion about the team’s shared values and how the decision relates to them, the team can
arrive at a decision that more or less everyone agrees with... without being told what to do. The great thing
about this is that _anyone_ on the team can show leadership by using this framing. Anyone can say “we all value
consistency, right? So doesn’t using Que make our app _less_ consistent?”.
There is still a reality about leadership and building software to consider, which is that some people on the team
are more accountable for the team’s output than others.
### C.2 Leaders Can be Held Accountable
I’ve continually stressed that you treat Rails as it is, not how you’d like it to be. I would encourage the same
general attitude with your job. You are exchanging your time and labor for money. Your company is paying you
money to get a specific result. Just as you have the right to be paid, the company has a right to those results.
Even in the most egalitarian, values-focused, collaborative environment, someone on the team is more accountable
for the team’s output than the rest of the team members. It’s best to be explicit about this so that everyone
understands that while the decisions they make affect the team, they have a stronger effect on the people who are
held accountable.
The problem arises when the team makes a decision they feel is consistent with their values, but fails to achieve
the desired result. It happens. People make mistakes and there is no formula for building software that avoids all
mistakes. Mistakes can, however, lead to consequences, and the person who is actually accountable will bear
those consequences the most.
As a simple example, suppose the team agrees to use Que in addition to Sidekiq. Suppose that Que is found to
have a serious security vulnerability that leads to the exposure of customer data. The team simply missed this in
their analysis. The team’s manager, however, is the one who could be fired for this mistake.
When you are accountable, you need to be careful. Accountability can lead to a top-down approach that you
might think mitigates risk, however a values-based consensus-driven style can lead to mistakes you are held
accountable for that you didn’t take the opportunity to avoid.
I would highly recommend if you _are_ accountable to make that clear to the team. Make it clear that their decisions
and output will reflect on you and that because of that, you may need to exercise decision-making authority from
time to time. You could use phrases like “veto power” or “51% of the vote” to communicate this concept, but the
team must understand that if they make a decision that is, in your judgement, not the right one, you may decide
to overrule them.
Of course, you should do this as infrequently as you can, as it removes agency from the team. This makes you a
less effective leader in the long run.
To make matters more complicated, accountability isn’t always explicit.
### C.3 Accountability Can be Implicit
It is often the case that a less experienced member of the team will get stuck on something and turn to a more
experienced member for help. Perhaps someone new to the team needs help understanding the domain, or a
developer fresh out of a boot camp can’t get their development environment working.
On any team there are members who are looked to for answers, help, and guidance, even if they aren’t formally
blessed as accountable leaders. These team members are nevertheless implicitly accountable. For example,
suppose you set up the development environment on macOS that everyone is using. You might be the “go-to”
person for the dev environment. If a new engineer decides they want to use Linux, you are now implicitly
accountable for their dev environment by virtue of having set up the system everyone else is using.
As the expert on the dev environment, that engineer will come to you for help if they get stuck, even though you
were not involved in the decision for them to use Linux. Their actions have created a carrying cost for you. It puts
you in a position to either not provide help (“You chose Linux, you live with it”) or to put your more urgent tasks
on hold to provide help. It’s not necessarily fair for this engineer to put you in this position.
I would encourage you to think deeply about each member of the team and what sorts of things would fall to
them to do if no one else were available. Each team member contributes in their own unique way, and thus is
implicitly accountable for those contributions. Perhaps one team member goes the extra mile with documentation.
If you propose a new way of documenting, you are creating additional work regarding something for which they
are implicitly accountable.
Be aware of this when navigating the decisions to be made. Defer to others where appropriate, identify values
where possible, and be explicit about accountability as much as you can.
## Colophon
There’s a lot of technology involved in producing this book. But let’s start where everyone that makes it to the
colophon wants to start: _fonts_.
The cover is set in Helvetica Neue. Titles in the book are set in ITC Avant Garde Gothic with the body text set in
Charter. Diagrams use Rufscript and Inconsolata. Inconsolata is also used to set all the code. The epub versions
largely ignore these fonts and I’m sorry. Beautifully typeset e-books on e-ink screens are technically possible, but
no one cares enough to make it happen.
The book was authored in a modified version of Markdown that executes code samples, runs CLI commands, and
takes screenshots as the files are processed. It’s managed with a custom toolchain I created that you can read
about on my blog^1.
Most diagrams are created using Graphviz or Mermaid, though some were created in Omnigraffle and Numbers.
Screenshots were generated by a custom JavaScript command line app that uses Puppeteer.
The cover was created in Pixelmator, based on a photo I took of the House of Eyrabakki^2 in Iceland, which is
part of the Byggðasafn Árnesinga, a museum in Eyrabakki. The photo was taken with an Olympus OM-1n 35mm
camera, using Ilford Delta 3200 film, developed by me, in my basement, at 2400 ISO using Ilford chemicals. The
back cover of the print versions is another shot of the same house from the same roll of film. Both were lightly
edited in Adobe Lightroom.
All of this is tied together by Pandoc, which also produces the ePub version. The print and PDF versions are
produced via LaTeX. Good ole LaTeX. If you want proper hyphenation and justification, there’s not really any
other option. I’m sure this book has a lot of overfull hboxes.
I would also be remiss in not pointing out that the entire toolchain is held together bymake, which I don’t think
I could live without when trying to do anything moderately complex. And, of course, all this runs in Docker,
because you can’t do anything these days without Docker.
(^1) https://naildrivin5.com/blog/2023/02/03/toolchain-for-building-programming-books.html
(^2) The House of Eyrabakki is one of the oldest structures in Iceland, made of wood at a time when houses were made of turf. It was
transported to Iceland as a kit, and assembled there. Thus, it’s a great analogy for sustainable web development with Ruby on Rails.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment