惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Secure Thoughts
Apple Machine Learning Research
Apple Machine Learning Research
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 司徒正美
博客园_首页
博客园 - 叶小钗
Blog — PlanetScale
Blog — PlanetScale
The Cloudflare Blog
量子位
人人都是产品经理
人人都是产品经理
小众软件
小众软件
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
IT之家
IT之家
Attack and Defense Labs
Attack and Defense Labs
Hacker News: Ask HN
Hacker News: Ask HN
TaoSecurity Blog
TaoSecurity Blog
Forbes - Security
Forbes - Security
罗磊的独立博客
Webroot Blog
Webroot Blog
美团技术团队
Help Net Security
Help Net Security
Google DeepMind News
Google DeepMind News
博客园 - 三生石上(FineUI控件)
The GitHub Blog
The GitHub Blog
Microsoft Security Blog
Microsoft Security Blog
H
Heimdal Security Blog
C
Check Point Blog
V2EX - 技术
V2EX - 技术
The Last Watchdog
The Last Watchdog
Microsoft Azure Blog
Microsoft Azure Blog
AI
AI
Cloudbric
Cloudbric
Application and Cybersecurity Blog
Application and Cybersecurity Blog
W
WeLiveSecurity
P
Privacy International News Feed
T
The Exploit Database - CXSecurity.com
P
Privacy & Cybersecurity Law Blog
B
Blog RSS Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
L
LINUX DO - 热门话题
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
A
Arctic Wolf
O
OpenAI News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 【当耐特】
C
Cyber Attacks, Cyber Crime and Cyber Security
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Y
Y Combinator Blog
Know Your Adversary
Know Your Adversary

Pierce Freeman

A browser for agents | Pierce Freeman The grey market of podcast appearances The way I travel | Pierce Freeman Fixing slow AWS uploads | Pierce Freeman Local tools should still use vaults We solved scratch content first Starting a podcast in 2025 Being late but still being early Automating our home video imports Adding my parents to tailscale A deep dive on agent sandboxes Language servers for AI | Pierce Freeman My simple home podcast studio We need centralized infrastructure | Pierce Freeman Coercing agents to follow conventions using AST validation My unified theory of social selling My personal backup strategy | Pierce Freeman July updates to the homelab How the KV Cache works httpx is the right way to do web requests in Python Reputation is becoming everything | Pierce Freeman Building a (kind of) invisible mac app Updated knowledge in language models Making an ascii animation | Pierce Freeman How speculative decoding works | Pierce Freeman Under the hood of Claude Code Doing things because they're easy, not hard Speeding up sideeffects with JIT in mountaineer Firehot for hot reloading in Python Misadventures in Python hot reloading How text diffusion works | Pierce Freeman The tenacity of modern LLMs How language servers work | Pierce Freeman Just add eggs | Pierce Freeman Unfortunately SEO still matters | Pierce Freeman The futility of human-only web requirements Setting up Input Leap | Pierce Freeman Checking in on Waymo | Pierce Freeman The react revolution | Pierce Freeman Speeding up many small transfers to a unifi nas Quick notes on swift libraries AI engineering is a different animal San Francisco | Pierce Freeman Debugging a mountaineer rendering segfault Local network config on macOS Building our home network | Pierce Freeman Introducing Envelope.dev | Pierce Freeman Legacy code and AI copilots Typehinting from day-zero | Pierce Freeman Generating database migrations with acyclic graphs Lofoten | Pierce Freeman Mountaineer v0.1: Webapps in Python and React Constraining LLM Outputs | Pierce Freeman Passthrough above all | Pierce Freeman Accuracy in kudos | Pierce Freeman How quick we are to adapt The curious case of LM repetition Costa Rica | Pierce Freeman Debugging chrome extensions with system-level logging Speeding up runpod | Pierce Freeman Inline footnotes with html templates Parsing Common Crawl in a day for $60 An era of rich CLI All or nothing with remote work The Next 10 Years | Pierce Freeman Adding wheels to flash-attention | Pierce Freeman LLMs as interdisciplinary agents | Pierce Freeman New Zealand | Pierce Freeman Representations in autoregressive models | Pierce Freeman Let's talk about Siri | Pierce Freeman Minimum viable public infrastructure | Pierce Freeman Reasoning vs. Memorization in LLMs Automatically migrate enums in alembic Greater sequence lengths will set us free On learning to ski | Pierce Freeman Dolomites | Pierce Freeman Using grpc with node and typescript Opportunity years | Pierce Freeman Buzzword peaks and valleys | Pierce Freeman Buenos Aires | Pierce Freeman Network routing interaction on MacOS Independent work: November recap | Pierce Freeman Debugging slow pytorch training performance The provenance of copy and paste Debugging tips for neural network training Patagonia | Pierce Freeman Santiago | Pierce Freeman My 2022 digital travel kit AWS vs GCP - GPU Availability V2 Independent work: October recap | Pierce Freeman Planning Patagonia | Pierce Freeman Relationship modeling | Pierce Freeman The power of status updates A new chapter | Pierce Freeman Give my library a coffee shop AWS vs GCP - GPU Availability V1 Switzerland | Pierce Freeman Headfull browsers beat headless | Pierce Freeman Webcrawling tradeoffs | Pierce Freeman Copenhagen | Pierce Freeman
The ergonomics of rails | Pierce Freeman
2025-06-16 · via Pierce Freeman

Few web frameworks have been able to rise to the ranks of Rails - or really even come close. Over 20 years after its founding1 people still swear by it for consistency and productivity. You can get started quickly and keep moving quickly over time.

Stripe processes billions in payments, Shopify handles 80,000+ requests per second, and GitHub hosts the world's largest repos. All on Rails. Even though market penetration is down over time, Rails still has a 55.4% admiration rate. Developers who use it want to keep using it.

Why the success? These are my takes.

2004 In Code

I want to turn the clock back for a second. To understand why Rails felt revolutionary, we need to remind ourselves of what it replaced. I spent this last weekend re-familiarizing myself with some of the old projects I built around 2004. Boy were they a mess.

Here's how you created a user in typical 2004 PHP:

<?php
// user_create.php - The 2004 way
session_start();
include_once('config.php');
include_once('db_connect.php');

if ($_POST['submit']) {
    $username = mysql_real_escape_string($_POST['username']);
    $email = mysql_real_escape_string($_POST['email']);  
    $password = $_POST['password'];
    
    // "Validation"
    $errors = array();
    if (empty($username)) {
        $errors[] = "Username is required";
    }
    if (empty($email)) {
        $errors[] = "Email is required";
    }
    if (strlen($password) < 6) {
        $errors[] = "Password must be at least 6 characters";
    }
    
    if (empty($errors)) {
        // Check if user exists (remember to escape every input manually)
        $query = "SELECT * FROM users WHERE username = '$username' OR email = '$email'";
        $result = mysql_query($query) or die(mysql_error());
        
        if (mysql_num_rows($result) > 0) {
            $errors[] = "User already exists";
        } else {
            // Create user (at least we escaped the input strings above)
            $password_hash = md5($password); // "Security"
            $insert_query = "INSERT INTO users (username, email, password, created_at) VALUES ('$username', '$email', '$password_hash', NOW())";
            
            if (mysql_query($insert_query)) {
                header('Location: login.php?success=1');
                exit();
            } else {
                $errors[] = "Database error: " . mysql_error();
            }
        }
    }
}
?>

<!DOCTYPE html>
<html>
<head><title>Create User</title></head>
<body>
    <?php if (!empty($errors)): ?>
        <div style={{color: 'red'}}>
            <?php foreach($errors as $error): ?>
                <p><?php echo $error; ?></p>
            <?php endforeach; ?>
        </div>
    <?php endif; ?>
    
    <form method="post" action="">
        Username: <input type="text" name="username" value="<?php echo $_POST['username']; ?>"><br>
        Email: <input type="text" name="email" value="<?php echo $_POST['email']; ?>"><br>
        Password: <input type="password" name="password"><br>
        <input type="submit" name="submit" value="Create User">
    </form>
</body>
</html>

Now here's the equivalent in Rails (circa 2005):

# app/models/user.rb
class User < ActiveRecord::Base
  validates :username, presence: true, uniqueness: true
  validates :email, presence: true, uniqueness: true
  validates :password, length: { minimum: 6 }
  
  has_secure_password
end

# app/controllers/users_controller.rb  
class UsersController < ApplicationController
  def new
    @user = User.new
  end
  
  def create
    @user = User.new(user_params)
    
    if @user.save
      redirect_to login_path, notice: 'User created successfully'
    else
      render :new
    end
  end
  
  private
  
  def user_params
    params.require(:user).permit(:username, :email, :password)
  end
end
<!-- app/views/users/new.html.erb -->
<%= form_with model: @user do |form| %>
  <% if @user.errors.any? %>
    <div class="errors">
      <% @user.errors.full_messages.each do |message| %>
        <p><%= message %></p>
      <% end %>
    </div>
  <% end %>

  <%= form.text_field :username, placeholder: "Username" %>
  <%= form.email_field :email, placeholder: "Email" %>  
  <%= form.password_field :password, placeholder: "Password" %>
  <%= form.submit "Create User" %>
<% end %>

Wild difference in lines of code, sure, but also in density. The PHP version is 60+ lines of mixed concerns, manual SQL, and brittle error handling. The Rails version is 31 lines total, with automatic SQL injection protection, proper password hashing, database-backed validations, and clean separation of concerns.

You were also left to roll your own PHP conventions. Most PHP code mixed HTML rendering, business logic, database queries, and session management all in one file. Change the validation rules and you risk breaking the SQL or the form display. Rails enforced the most common architectural boundaries: models handle data and validation, controllers manage HTTP flow, views handle presentation.

Because of the separation of concerns the Rails version scales. Add a new field? One line in the model, one in the form. Need user authentication? has_secure_password handles it. Want to add an API endpoint? Add respond_to :json and you're done.

Security in PHP was largely an afterthought. It required such careful attention to manually escaping every single database input. Miss one mysql_real_escape_string() call and you've created a vulnerability. Rails made security the default by abstracting away manual SQL construction entirely.

The PHP approach felt like hacking multiple layers for a change that otherwise felt trivial. Rails felt way more extendable. It was closer to how a regular person would think about updating a program.2

2004 In Architecture

In 2004 Linode was still the new shiny webhosting service. My big architectural migration around that time was switching from Hostgator to Linode, which incidentally still hosts my site today.

I managed to dig up some of my old deployment notes from these projects3. They read like an archeological dig. I almost forgot how much Heroku changed the game with one-line deployments and Procfiles. Here's what it actually took to get a production environment running. Edited for clarity.

Step 1: MySQL Setup

  • Download mysql-4.0.21.tar.gz from mysql.com
  • ./configure --prefix=/usr/local/mysql
  • make
  • make install
  • Manually create mysql user: groupadd mysql && useradd -g mysql mysql
  • Initialize databases: /usr/local/mysql/bin/mysql_install_db
  • Edit /etc/my.cnf by hand
  • Start mysqld manually and hope it doesn't crash

Step 2: PHP Compilation

PHP usually came bundled on release servers but they were several releases behind.

  • Download php-4.3.8.tar.gz
  • ./configure --with-mysql=/usr/local/mysql --with-apache2=/usr/local/apache2 --enable-mbstring
  • Compilation fails: missing zlib-devel
  • Install zlib-devel, recompile
  • Compilation fails: missing libxml2-devel
  • Install libxml2-devel, recompile

Step 3: Apache Configuration

  • Edit /usr/local/apache2/conf/httpd.conf
  • Add LoadModule php4_module modules/libphp4.so
  • Set up virtual hosts by copying examples and modifying randomly
  • Restart Apache, "Forbidden" errors
  • Fix file permissions
  • Finally works, but only for one specific directory structure

My notes literally say: "linode has different PHP version (4.3.4 vs 4.3.8) and different modules enabled. but working locally. need to head out to school"

Back then "it works on my machine" wasn't just a meme. Every developer had a slightly different stack because everything was compiled from source with different options. Deployment meant uploading files via FTP and praying they'd work in a completely different environment.

Deployment was its own nightmare:

  1. Make changes locally
  2. Open Cyberduck
  3. Upload changed files one by one
  4. Test on production server
  5. Something breaks because production has different PHP settings
  6. Download production files to see what's different
  7. Make changes locally, upload again
  8. Repeat until it works or you give up

There was no version control for most small projects. Github didn't come on the scene until 2008 and even git itself wasn't released until 2005. Backing up in 2004 meant copying your entire htdocs folder to htdocs_backup_jan_15_2004. Database schema changes were applied by running SQL statements manually in phpMyAdmin, with no way to roll back if something went wrong.

Looking back at those notes, it's rather crazy that anyone built websites at all. Perhaps one reason why native apps were still equally en vogue: it was as easy to distribute an Objective-C executable as it was to build an entire remote webapp.

Pre-Ajax

Before Ajax became mainstream (circa 2005-2006), web interaction usually catered to the lowest common denominator. And that denominator was page refreshes. Click a button? Full refresh. Submit a form? Full refresh.

The technology that would become Ajax actually existed back in 2000. Microsoft's Internet Explorer 5 included an XMLHTTP ActiveX control for background HTTP requests. Mozilla implemented XMLHttpRequest as a native Javascript object around 2002, followed by Safari and Opera. But it remained pretty obscure, used mainly in enterprise apps like Outlook Web.

The path to cross-browser Ajax was brutal in part because of these incompatible APIs. Here's the code you needed to make a simple background request work across browsers in 2005:

function createXMLHttpRequest() {
    var xmlhttp = null;
    
    // IE 7+ and other modern browsers
    if (window.XMLHttpRequest) {
        xmlhttp = new XMLHttpRequest();
    }
    // IE 6 and older
    else if (window.ActiveXObject) {
        try {
            // Try the newer ActiveX object first
            xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
        } catch (e) {
            try {
                // Fall back to older ActiveX object
                xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
            } catch (e) {
                xmlhttp = null;
            }
        }
    }
    return xmlhttp;
}

function makeRequest(url, callback) {
    var xhr = createXMLHttpRequest();
    if (!xhr) {
        alert("Your browser doesn't support Ajax!");
        return;
    }
    
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
                callback(xhr.responseText);
            } else {
                alert("Error: " + xhr.status);
            }
        }
    };
    
    xhr.open("GET", url, true);
    xhr.send(null);
}

This was the "simple" version. You needed different code paths for Internet Explorer's ActiveX implementation versus Firefox/Safari's native XMLHttpRequest. Even big web companies like Kayak referred to their Ajax implementation as "the xml http thing" because there wasn't established terminology yet. Jesse James Garrett wouldn't coin the term AJAX until February 2005.

AJAX was a technique not an actual series of APIs. So Rails was free to build on its base and pitch "interaction out of the box". It felt more stable than having to hack around the manual Javascript, because it was a first class feature of the language. Even if the outputted Javascript was just as hacky behind the scenes.

Rails introduced respond_to and RJS (Ruby Javascript) templates around 2005:

# app/controllers/posts_controller.rb
def update
  @post = Post.find(params[:id])
  
  respond_to do |format|
    if @post.update(post_params)
      format.html { redirect_to @post }
      format.js   # Renders update.js.erb
    else
      format.html { render :edit }
      format.js   # Renders error handling
    end
  end
end
<!-- app/views/posts/update.js.erb -->
$('#post_<%= @post.id %>').html('<%= escape_javascript(render @post) %>');
$('#flash').html('<%= escape_javascript("Post updated!") %>').show().delay(3000).fadeOut();

Rails still had to compile down to standard Javascript/HTML to render on the frontend. But the framework hid a ton of tricks behind the scenes4:

  • User clicks "Update Post" button on a form with remote: true attribute
  • Rails' Javascript helpers intercept the form submission, preventing default browser behavior
  • XMLHttpRequest fires to /posts/1 with Accept: text/javascript header
  • If Javascript was disabled, the form would just submit normally to /posts/1 with a default html-accept attached. So the server would know to respond with the page rendered with the full expected html.
  • Controller's respond_to block sees the Javascript request and renders update.js.erb instead of redirecting
  • Server returns raw Javascript code: $('#post_42').html('...updated HTML...');
  • Browser executes the returned Javascript, updating specific DOM elements without page refresh
  • User sees instant updates with no loading screens or lost state

This was pretty crazy. Rails could automatically handle both traditional form submissions and Ajax requests with the same controller action. The RJS templates let you write Javascript operations in Ruby, and Rails handled all the XMLHttpRequest complexity behind the scenes.

You could build truly interactive web applications without writing a single line of Javascript or worrying about browser compatibility. Rails made Ajax feel like a natural extension of server-side rendering instead of a completely different paradigm.

When we do so much of our web development work in Javascript and jsx templates these days, it seems strange that we shied away from using JS back in 2005. But without browser polyfills, a much more fractured browser engine ecosystem, and outdated browsers - every line of JS that you wrote had to be weighed against the risk that a user can't execute it properly and you just lost a customer. Rails made that consideration largely irrelevant.

Why Rails Won

Into that 2004 landscape came Rails, but also a slew of other web frameworks that were hopping on new trends. I think here's what made the difference in Rails:

Bootstrap CLI: Almost all new Rails projects are bootstrapped with their CLI. In the days when we were slinging PHP code manually5, it was one of the first mainstream frameworks that let you get started immediately. You had no blank page problem with starting a new project. You just entered one command and went from there.

Rails optimized for the wow factor of getting something working quickly.

  • rails new blog - full application scaffold
  • rails generate scaffold Post title:string content:text - new CRUD endpoints for the new object
  • rails server - running application

There was a dopamine hit to starting a new project in under a minute. You felt like you had a stable foundation to build from. You didn't even have to wait for your first page of PHP code to start rendering and displaying. And you certainly didn't have to wait to wire up SQL queries to be able to get a sense for object persistence.

Integrated ORM: Most languages just expected that people would write raw SQL queries. Any ORM bolt-on was a third party library where you would have to download a zip archive, extract them locally, and import into your project. Java’s Hibernate launched in 2001 but few web frameworks bundled an ORM out of the box.

Writing manual SQL was prone to errors and query injection. But even more importantly it took you out of the flow. When you're used to working with an object oriented language, switching into explicit SQL felt clunky.

Philosophy: "Optimizing for programmer happiness" was literally in their marketing. This seems obvious now with the amount of effort companies spend on perfecting the best developer experience. But this was radical at the time. Most frameworks optimized for performance, flexibility, or correctness. You were more likely to see benchmark stats with a zip file of php files than you were to see a sexy website and comprehensive docs.

Rails also embraced what other frameworks saw as flaws:

  • "Convention over configuration" seemed like magic. You could start with no code of your own and make a full site
  • ActiveRecord felt magical with its dynamic finders
  • Other frameworks tried to be "explicit" and "transparent" - Rails embraced beneficial complexity

Ruby Metaprogramming: For syntax, Rails can really thank Ruby instead of the framework. Inherently it's more DSL-friendly than most other languages. Especially the compiled ones that were typically the go-to for serious web projects™.

Ruby's flexible syntax allowed Rails to create domain-specific languages just for your project:

has_many :posts, dependent: :destroy
validates :email, presence: true, uniqueness: true
before_action :authenticate_user!

This reads like English, not code. Other languages couldn't replicate this naturalness.

Ruby's metaprogramming allowed Rails to generate methods dynamically (find_by_email, Post.published). Since there was no language server support to worry about, you could rely on more magical runtime behavior. Developer were used to memorizing a deeper API surface area. Having a convention for the metaprogramming actually made things simpler.

Other languages weren't structured to allow this same kind of support. Languages like Java, C#, and C++ were too rigid because they needed to be compiled. Javascript lacked the metaprogramming sophistication because of its Prototype inheritance. Python had it but the community preferred explicit over implicit logic.

The DHH Factor: Without a doubt, DHH and the marketing tactics over at Basecamp undoubtedly helped the rise of Rails. He helped make engineering cool - in vibe, in militant philosophy, in bucking the institutional trends. In an era where the independent hackers of the 80s were quickly getting replaced by briefcase carrying office workers, he represented a lone voice of independence.6

Arguing the counterfactual is always impossible. But I suspect that had DHH advocated for a framework with fewer fundamental advantages, it wouldn't have reached the echelons that Rails did.

2025 Lessons Learned

I've tried to learn from these same lessons when designing Mountaineer - my full featured webapp framework library for Python. The problems are different in 2025 but the ergonomic principles remain the same.

Modern Creation CLI: Just like Rails' rails new blog, Mountaineer provides pipx run create-mountaineer-app. One command gets you a complete project structure with Python backend, React frontend, PostgreSQL database, Docker configuration, and Typescript setup. People still burn more time7 than they should setting up build pipelines, basic API routes, or figuring out how to connect their database.

Integrated Type System: Where Rails had ActiveRecord for database-to-object mapping, Mountaineer goes further with end-to-end type safety. Define your data models in Python, and the framework automatically generates Typescript interfaces for your React components. Change a field type in your database model? Your frontend immediately knows about it. This helps both with IDE typehinting and with passing data from the server to the frontend and back again.

# Python model
class BlogPost(TableBase):
    title: str
    content: str
    published_at: datetime | None = None
// Typescript automatically knows about this structure
const post: BlogPost = {
    title: "My Post",
    content: "Content here",
    published_at: new Date()
};

Convention Over Configuration (2025 Edition): Modern web development is still rather complex. You need to configure bundlers, set up API endpoints, handle serialization, manage state synchronization, and coordinate between multiple build systems. The handoff between frontend data and backend data is still leaky.

Mountaineer provides the same "convention over configuration" philosophy - define your backend controllers in Python, put your React components in the views directory, and the framework handles all the binding automatically. Actions are defined as either simple server calls with @passthrough or calls that will modify state with @sideeffect. Decorate your functions with these and Mountaineer will know to update the right data in the frontend using lightweight http calls.

Agent-First Architecture: There's one more problem that didn't exist in 2004: making codebases navigable by AI agents. Mountaineer's strict conventions help solve this. When an AI agent needs to add user authentication, it knows exactly where to go: controllers in /controllers, React components in /views, database models in /models. No guessing, no architectural debates.

Mountaineer's guaranteed static type hints act as interpreted languages' equivalent to a compilation phase. When an AI agent generates Python code with type annotations, the type checker immediately validates whether function signatures are correct, database field types match frontend expectations, and all required parameters are provided. The agent can iterate until it passes type checking, catching errors that would otherwise require human debugging.

The result is AI agents that can make substantial changes to a Mountaineer codebase with minimal supervision. They know where to put the code, and they know when they've gotten it right.

The core insight remains the same: productivity frameworks succeed by eliminating repetitive manual work. Rails eliminated database configuration and deployment scripts. Mountaineer eliminates API boilerplate and type synchronization. Different decades, same ergonomic principles.

Today's equivalent of Rails' 15-minute blog demo is Mountaineer's ability to add a new data field to your database and immediately have it available in your React components with full type safety - no API endpoints, no manual type definitions, no synchronization bugs. The magic feeling is the same, just applied to the modern web.

If you haven't yet worked in a Mountaineer codebase, give it a try. You may just feel a bit of that spark from the early Rails days.

  1. All the way back in 2004. Think: MTV Cribs, new Spongebob episodes, the works. ↩

  2. And deep down engineers are regular people too. ↩

  3. Made easier thanks to my recent homelab retrofit.

    Was anyone even using README files back then? I seemingly just wrote everything in scattered .txt files on disk. ↩

  4. The most powerful thing a framework can do is figure out what repetitive tasks engineers do over and over and over again. Even if the solution is hacked together with duct tape, when it can cut down extensive boilerplate across a whole project, it still feels like magic. There's something that feels more official about a framework's blessing. ↩

  5. With a phpmyadmin interface to boot. ↩

  6. I suspect some of this was simply true to his personality. I also imagine when it proved successful as a marketing tactic, became more deliberate over time. ↩

  7. Or more tokens. ↩