Exploring Rails codebases: Writebook
To become a better developer, they say, read a lot of code. Although we, as a profession, often find more enjoyable to write a hundred lines of code rather than read ten, this advice stands more than ever. So let’s explore some codebases, starting with Rails applications.
Last june, 37signals released a new product, Writebook – free of charge, and full source code included1. An excellent occasion to see how the company that initiated Ruby on Rails writes code.
I’ve downloaded and run the app, explored the source code, and took some notes. Here are my takes on this code base.
What is Writebook
Writebook is a web app for publishing book-like content on the web. Books supports pages composed in Markdown, sections separators, and full-page pictures.
Writebook’s presentation on 37signals website will show you how the app looks like, and what it does. I recommend you to have a short look at this presentation, to know what we’re talking about – then come back to this article.
How to obtain the code
Writebook is free, but its code is not truly open-source. You are allowed to read the code, and to make modifications, but not to publish them, or to re-use part of the code in another product.
This means the code is not hosted in a public repository (for instance on GitHub). Instead, once you “purchase” the application (for free), you get access to a zip file containing the full source code2.
Exploring the code
The stack
- Rails, directly on the trunk
- Database: SQLite
- Webserver: Puma
- Templating: ERB
- Javascript: propshaft, import-maps, Stimulus
- Tests: Minitest
Models
- Models in themselves are very small (max 10 lines of code) – but they each include a handful of concerns. The code is extracted into concerns even if used only in a single model.
- There’s no model validations. At all. All validations are done at the browser level (and, in a lesser extent, by the database).
- Book’s leaves are ordered using a
Positionnable
module, which acts on aposition_score
database attribute. The position score is a floating-point number, which means a leaf can be moved by changing only a single score : the one of the moved leaf itself. If ever the interval between two positions becomes too small, positions are rebalanced using a post-commit hook. - Leaves use
delegated_types
, rather than Single-Table Inheritance. -
There is a pretty syntax to declare enums:
class Book < ApplicationRecord enum :action, %w[ revision trash ].index_by(&:itself)
- Business processes (what would in other codebases be implemented as “Services”) are here plain-Ruby-objects, stored in
app/models
:FirstRun
,DemoContent
, etc.
Controllers
-
Controller methods are reduced to their shortest expression. Most action methods are only a few-lines long:
class BooksController < ApplicationController def index @books = Book.accessable_or_published.ordered end def new @book = Book.new end def create book = Book.create! book_params redirect_to book_slug_url(book) end
-
To support these short controller methods, many pre-checks are extracted to callbacks:
class BooksController < ApplicationController before_action :set_book, only: %i[ show edit update destroy ] before_action :set_users, only: %i[ new edit ] before_action :ensure_editable, only: %i[ edit update destroy ]
Some of them are further extracted into concerns – like
UserScoped
,BookScoped
, which contain methods likebefore_action :set_user
, etc. -
No model validations means that most controllers simply use exception-throwing methods (like
update!
), and propagate the exception in case of invalid data. There’s no need to handle the failure case explicitely, which helps to make the methods shorter.My take: I wonder if this approach of simplifying all controller code by removing error handling is workable in larger-scale apps. Maybe letting the browser handle required fields and formats is sufficient, but I’m not fully conviced yet.
Views
- Templating language used: ERB.
- Accessibility gets quite some attention: there’s
aria
attributes, afor-screen-reader
CSS class, and so on. - Views are cached using the
cache
directive. - Many views make a liberal use of
content_for
, for configuring:title
,:head
,:header
,:footer
, and so on. - Icons are displayed using small SVG files (that may be colored using CSS and the
filter
property). - The Markdown editor is based on
ActionText
, but doesn’t useTrix
(ActionText’s defaut editor). Instead, it usesHouse
, a new internal editor from 37signals. The editor is vendored as a singlevendor/house.min.js
file.
CSS
- Language used: plain CSS (no SCSS or similar things).
- Stylesheets closely matching a single component use a loose BEM syntax (like
assets/product.css
). - But stylesheets for more global layout are more a mixed bag of classes,
:has
rules and:is
scopes (seeassets/pages.css
). :has
is used everywhere.allow_browser versions: :modern
is used to restrict access to browsers that support it.- Dark-mode is implemented.
Javascript
- Framework used: Hotwire (Turbo, Stimulus)
- No Javascript bundler; all import maps.
- There is quite a lot of Javascript: around 20 Stimulus controllers, some of them quite meaty.
- A lot of custom code too. For instance, re-organizing pages using drag-dropping is a fully custom implementation (
arrangement_controller.js
, a ~250 lines Stimulus controller). No external library is used for the drag-n-drop code. - Use of “modern” Javascript facilities: lot of
async
,#privateMethods
, and so on.
Turbo
- Quite a lot of turbo-frames, used to refresh small form elements (like the “Publish” toggle).
- A handful of manual turbo-streams; mostly to add, move or delete pages.
- Page updates are broadcasted to all editors using
Turbo::StreamsChannel.broadcast_render_later_to
, to add a small “This page is being edited by another editor” indicator on the page. The code to support this inleafables_controller.rb
is surprisingly small.
Design
- The design language feels quite nice. It recalls Basecamp: round buttons, horizontally-centered controls, few borders and mostly plain-page aligned elements.
Tests
- Test framework: Minitest
- Test data are generated using static YML files in
test/fixtures
(rather than factories) - The app is well tested, but there isn’t a ton of test either. Most of them are model tests, and controller test (as
ActionDispatch::Integration
) tests. - Only two system tests, for testing editing a page and publishing a book.
- Many tests use the
assert_changes
andassert_difference
helpers. - Test cases are surprisingly short and concise. I guess preloaded fixtures really help there.
test/test_helper.rb
is also very short, almost no configuration.
Some global remarks
- A handful of files look extracted from other 37signals applications. I’m thinking about
concerns/positionable.rb
(implements a list with customized ordering), the authentification and session code, things like this. -
There is not a single comment in the code. None. The only comments are in Rails-generated boilerplate.
My take: it looks like that the general philosophy is to have methods and variables named sufficiently clearly to make comments unnecessary. I disagree: sometimes why the code does something can’t be captured by naming alone.
-
Authentification and sessions don’t use any external gem (like
devise
). Instead the app declares its ownSessionsController
andAuthenticationConcern
. The actual Sessions are stored in a dedicated database table.My take: the code of this authentication infrastructure is surprisingly light and clear – but non-trivial nonetheless. I guess the benefits of writing your own code is that you don’t pay the price for the full configurability and extra levels of indirection of an external library.
- The source files tend to push complexity outward. Something becomes hairy? Move it to an external concern, or a Rails framework extension. This is probably also how Rails is built: by pushing into Rails the complexity of 37signal apps.
Conclusion
I’m amazed by the terseness and concision of the code base. Methods are short, and don’t leak complexity everywhere. That said, there’s a real business complexity in some parts: digging in the complexities of the Leafable
model or the drag-dropping Javascript code can take a while. Short code isn’t always easy to read, but it feels simple, and not overwhelming.
The terseness of the code also comes from the use of Rails by Rails creators. They know the framework by heart, use it to the maximum, and push code to the framework when needed. After reading this code base, I think more of Rails as 37signal’s public web framework.
This codebase also embodies Rails as a one-person framework: a single developper, knowing the inside of the framework perfectly well, can write an ambitious web app using all the available resources Rails has to offer.
Next, I consider exploring other large Rails codebases: GitLab, Mastodon, maybe others. Let’s see where it goes.
-
More precisely, Writebook’s source code is available – but not open source. As the FAQ states, “While you are free to review the code and make modifications to Writebook for your own use, you can not use or repurpose the code for your own purposes outside Writebook.” ↩
-
The “source available” nature of Writebook means that this article cannot link to the actual code: it would have to be hosted publicly, which is not allowed under Writebook’s license. ↩