Rails as an HTML container
  • I started working with Rails 13 years ago (v 3.0), and have used it almost exclusively since then. In that time, I've easily worked on 50+ codebases in some form or another, and seen every library and pattern under the sun on small and large teams. What's remarkable is that, while Rails itself has changed a lot in that time (we're now at v 7), the underlying fundamentals of the web have changed remarkably little, and the approach I now follow for building apps with Rails would have been completely possible with the first version of Rails I learned, way back in 2010.

  • The crux of my approach is this: Web apps are built around HTML. Browsers are extremely good at fetching and displaying HTML - so that part is taken care of for us. Additionally, relational databases have been around for a very long time and do their job - storing and retrieving data - very well.

  • So without Rails, as programmers we already have out-of-the-box solutions for storing data, and for delivering HTML. What we don't have is a way to combine them both - to hydrate the HTML with specific data based on what user is requesting it.

  • HTML: the eighth wonder of the world

  • As software eats the world and demand for programmers seems to continuously exceed the supply, it's generally believed that in order to be a coder, one must be very smart. And yet, HTML - the bedrock around which all of this software is built - is actually very approachable and easy to learn: So much so that children and adults can understand and learn the basics in a day. This is a remarkable fact.

  • As someone who's spent a lot of time dealing with the pain of finding and hiring software engineers, I've thought a lot about why it's so difficult. The result of that thinking can be boiled down to the following bullet points;

    • Holding that the only necessary complexity to building web software - beyond knowing How HTML Works and How Databases Work - is knowing how to hydrate html with the contents of a database, we can infer that the rest of the complexity is incidental complexity.

    • Most of the incidental complexity comes from 1. Tooling (git), and 2. Framework-specific concepts.

    • As a result, when hiring developers, the primary thing we're evaluating is their existing technical knowledge.

  • What if we think of Rails not as "Rails is a large framework for building complex web apps, whose libraries and patterns (hotwire, stimulus, turbo...) need to be studied", to "Rails is a way to access a collection of utilities that make it easier to work with HTML"

  • At this point I've worked with dozens of people using this approach, who had little to no prior rails experience, who were able to build and extend reasonably mature codebases after just a few hours. One exercise that helped was drafting up a cheat sheet with some of the most common code snippets. For people who have some web experience but no Rails experience, simply reading through this is a psychological-barrier-lowering exercise.


  • Rails (as an HTML Container) Cheat Sheet

  • A simple way to specify routes

  • # config/routes.rb
    Rails.application.routes.draw do
      get "/info" => "site#info", :as => "info"
    end
  • <a href="<%= info_path %>">Build</a>
  • A simple way to handle identity

  • gem 'devise'
  • before_action :authorize_user!
  • if user_signed_in?
      do_something
    end
  • A set of simple helpers for building html templates

  • <div id="menu">
      <%= render :partial => "common/menu" %>
    </div>
  • A simple, secure way to work with a database

  • user = User.new
    user.first_name = "Tony"
    user.save
    user.valid?
    user.errors.full_messages
  • people = User.where("location = ?","ireland").page(3)
  • <%- @people.each do |person| %>
      <h1><%= person.first_name %></h1>
      <img src="<%= person.image.url %>"></img>
    <% end %>
  • A simple way to access and use data submitted by forms

  • params[:first_name]
  • As you can see, there's really not a lot of fundamental knowledge needed in order to use Rails in this way.


  • Other Implications

  • We don't use the asset pipeline by default

  • ...or any kind of asset compilation. The HTML spec has had a way to organize and load CSS and Javascript files since 1997, and yet you will not find a modern framework that doesn't add a transpilation layer on top. This practice is so ingrained and dogmatic that I almost didn't run this experiment for fear I was missing something obvious, but I'm glad I did. It's remarkable how many developers get stuck (new and experienced) debugging when all they need is to load a CSS file into a web page.

  • Remember, the approach here is to center html and minimise the learning curve of everything else. I'm not claiming there are no benefits to pre/post compilation, just that you substantially lower the barriers when you remove them, and having operated without it in 3 relatively large codebases for over 6 months, I think the likelihood that the average codebase actually needs asset compilation is much lower than one would think.

  • How do we manage stylesheets and javascript? We put them in the /public folder and load them into our page as normal.

  • <link rel="stylesheet" href="/stylesheets/styles.css" />
    <script src="/js/custom.js"></script>
  • We are default-skeptical of adding new obfuscation layers

  • There are libraries that introduce a new set of capabilities that weren't previously possible, and then there are libraries that wrap existing capabilities in a new syntax. These syntactic libraries are generally designed to simplify, but they also obfuscate - in "protecting" the end-developer from seeing the language they're sitting on top of, they limit their ability to debug them, understand their capabilitiies, and use them in a performant manner.

  • For example, the following two code snippets do functionally the same thing. The latter is easier to read, easier to construct, and more secure, but it does obscure the original SQL, which - particularly in more complex use cases - can lead to slower queries and increased time spent debugging

  • INSERT INTO users (name, email) VALUES ('Joe Bloggs', 'joe@example.com');
  • User.create!(name:"Joe Bloggs",email:"joe@example.com")
  • I personally happen to believe that the above example (ActiveRecord) is a warranted obfuscation layer. But that doesn't mean all obfuscation layers are warranted.

  • Here are some that don't pass the "Center HTML and minimize the learning curve of everything else" test:

  • Rswag

  • Lets start with a less-controversial example. If you've attempted to write openAPI compliant docs for a rails app, you've likely come across something like rswag. On the surface, rswag seems like a great idea. My thought process went something like "I'm not familiar with openapi, but I'm familiar with ruby, so this will be easier to wrap my head around". In reality, if your API has even moderate complexity, you'll end up spending much more time scanning the documentation to find the rswag method you're looking for, debugging why it's not behaving as expected and hacking around things that you can't find or that aren't possible. And you'll still end up needing to understand the underlying openAPI spec anyway. You are much better off simply creating a static openapi.yml file and serving it directly - the only thing the additional DSL brings (in my experience) is "my code feels prettier". OpenAPI already has a solution to keep the spec DRY.

  • Rails Form Helpers

  • Now for something more controversial: Personally, I happen to quite like the form_with helper, but once I saw this through a HTML-centered lens, I couldn't un-see it. If you already understand a little bit of html, including how basic forms work, there's very little value in using something like this...

  • Instead of this

  • <form action="<%= new_note_path %" method="post">
      <input name="" type=""
    </form>
  • If you're a developer that knows HTML but not rails, you'll have to do very little work to make changes to t

  • I could do a longer blog post on this

  • We don't use gems whose only purpose is to load a CSS or javascript library

  • Why? Difficult to debug - can't see the source of the code that's modifying the behaviour

  • We use htmx and tailwind

  • Ok, I lied a little when I implied the platform has everything covered, and that we should use as few libraries as possible. But htmx and tailwind are not like

  • One very obvious limitation of HTML is that it still considers everything a "page"

  • The fact that these libraries sit inside HTML mean that the learning curve is remarkably low.

  • Common Objections

  • Why go to all this trouble? Can't you just hire more experienced devs?

  • See my opinions on the very real and very painful developer hiring problem, and Novice-Friendliness as a core design principle

  • Ok so you made the codebase easier to work with. There's still a huge learning curve for the tooling and infrastructure

  • See our adventures in Remote Development, which totally remove a whole class of environment and infrastructure setup issues.

  • You said you don't use pre or post compilation, but you do use Tailwind. How is that so?

  • We created dumb-tailwind, which includes most of the utility with none of the compilation. There is a performance implication, so it doesn't work in all cases, but in most the hit is unnoticeable.

  • But the markup hurts my eyes

  • I feel you. If that's more important for you, then don't follow the advice in this article.

    • Ok, but your apps are going to feel slow and crappy

    • This isn't just Rails

    • Remote Form Behaviour

    • Prefer HTMX over Turbolinks and Turbo forms


    • Website Page

    • Covered By✅ Browser✅ Browser + CSS + Js✅ Relational Database❌
      Task
      Fetch and deliver HTML
      Decorate HTML (styling and interactions)
      Store Data
      Hydrate HTML with user data