Vanilla Rails
After reading this guide, you will know:
- What vanilla Rails means as an architecture.
- What
preset :vanilla_railschecks. - Which conventions ArchSpec cannot check.
The Architecture
37signals builds Basecamp and HEY with rich models and thin controllers, and
without the layers many Rails shops add on top: no service objects, no form
objects, no policy objects, no presenters. Business logic lives on models.
Plain Ruby objects live in app/models next to them.
Jorge Manrubia describes the approach in Vanilla Rails is plenty. The 37signals-skills project collects these conventions as instructions for AI coding agents. Instructions only cover code generation. ArchSpec checks the code afterwards.
The Preset
preset :vanilla_rails
This runs the Rails MVC checks, then requires these directories to stay empty, using the components rule:
| Directory | Reason |
|---|---|
app/services |
behavior belongs on models, not service objects |
app/forms |
use strong parameters and model validations |
app/policies |
authorization is predicate methods on models |
app/decorators |
use helpers and partials |
app/presenters |
presentation objects are POROs in app/models |
app/components |
use helpers and ERB partials |
If a change adds app/services/create_user.rb, the check fails:
[components.empty] app/services/create_user.rb:1:1
services must stay empty: behavior belongs on models, not service objects
evidence: app/services/create_user.rb belongs to services
More Rules
These conventions from the same playbook are also checkable. Add the ones your app follows.
Jobs receive context as arguments instead of reading Current inside
perform:
component :jobs, in: "app/jobs/**/*.rb"
jobs.cannot_reference_constants "Current"
The same controllers serve HTML and JSON, so there is no API namespace:
component(:api_controllers, in: "app/controllers/api/**/*.rb")
.must_be_empty(because: "the same controllers serve HTML and JSON via respond_to")
Conventional file names define conventional constants:
verify_zeitwerk_names!
What ArchSpec Cannot Check
Some vanilla Rails conventions are about design judgment, not file structure. ArchSpec does not check:
- CRUD-only routing (
resource :closureinstead ofpost :close) - Concern size and cohesion
- State as records instead of booleans (a
Closuremodel vsclosed: true) - Helpers taking explicit arguments instead of reading instance variables
- Jobs being one-line delegations to model methods
- Minitest and fixtures over RSpec and factories
Use code review and agent instructions for these.