A Love Letter to CoffeeScript and HAML: When Rails Frontend Development Was Pure Joy

The author reflects on the nostalgia of older coding practices, specifically with Ruby on Rails, CoffeeScript, and HAML. They appreciate the simplicity, conciseness, and readability of these technologies compared to modern alternatives like TypeScript. While acknowledging TypeScript’s superiority in type safety, they express a longing for the elegant developer experience of the past.

There’s something bittersweet about looking back at old codebases. Recently, I found myself diving into a Ruby on Rails project from 2012, and I was immediately transported back to an era when frontend development felt different. Better, even. The stack was CoffeeScript, HAML, and Rails’ asset pipeline, and you know what? It was glorious.

I know what you’re thinking. “CoffeeScript? That thing died years ago. TypeScript won. Get over it.” And you’re right. TypeScript did win. It’s everywhere now, and for good reasons. But let me tell you why, after all these years, I still get a little nostalgic pang when I think about writing CoffeeScript, and why part of me still thinks it was the better language.

The Rails Way: Opinionated and Proud

First, let’s set the scene. This was the golden age of Rails, when “convention over configuration” wasn’t just a tagline. It was a philosophy that permeated everything. The asset pipeline handled all your JavaScript and CSS compilation. You’d drop a .coffee file in app/assets/javascripts, write your code, and Rails would handle the rest. No webpack configs, no Babel presets, no decision fatigue about which bundler to use.

Your views lived in HAML files that looked like this:

.user-profile
  .header
    %h1= @user.name
    %p.bio= @user.bio
  
  .actions
    = link_to "Edit Profile", edit_user_path(@user), class: "btn btn-primary"
    = link_to "Delete Account", user_path(@user), method: :delete, 
      data: { confirm: "Are you sure?" }, class: "btn btn-danger"
Code language: JavaScript (javascript)

And your JavaScript looked like this:

class UserProfile
  constructor: (@element) ->
    @setupEventListeners()
  
  setupEventListeners: ->
    @element.find('.btn-danger').on 'click', (e) =>
      @handleDelete(e)
  
  handleDelete: (e) ->
    return unless confirm('Really delete?')
    
    $.ajax
      url: $(e.target).attr('href')
      method: 'DELETE'
      success: => @onDeleteSuccess()
      error: => @onDeleteError()
  
  onDeleteSuccess: ->
    @element.fadeOut()
    Notifications.show 'Account deleted successfully'

$ ->
  $('.user-profile').each ->
    new UserProfile($(this))
Code language: CSS (css)

Look at that. It’s beautiful. It’s concise. It’s expressive. And it just works.

Why HAML Was a Breath of Fresh Air

Let’s talk about HAML first. If you’ve never used it, HAML (HTML Abstraction Markup Language) was a templating language that let you write HTML without all the angle brackets. Instead of this:

<div class="container">
  <div class="row">
    <div class="col-md-6">
      <h1>Welcome</h1>
      <p class="lead">This is my website</p>
    </div>
  </div>
</div>
Code language: HTML, XML (xml)

You wrote this:

.container
  .row
    .col-md-6
      %h1 Welcome
      %p.lead This is my website
Code language: CSS (css)

The difference is striking. HAML forced you to write clean, properly indented markup. You couldn’t forget to close a tag because there were no closing tags. The structure was defined by indentation, Python-style. This meant your templates were always consistently formatted, always readable, and always correctly nested.

HAML also integrated beautifully with Ruby. Want to interpolate a variable? Just use =. Want to add a conditional? Use standard Ruby syntax. The mental model was simple: it’s just Ruby that outputs HTML.

- if current_user.admin?
  .admin-panel
    %h2 Admin Controls
    = render partial: 'admin/controls'
- else
  .user-message
    %p You don't have access to this section.
Code language: PHP (php)

No context switching between template syntax and programming language syntax. It was all Ruby, all the way down.

CoffeeScript: JavaScript for People Who Don’t Like JavaScript

Now, let’s get to the controversial part: CoffeeScript. For those who missed it, CoffeeScript was a language that compiled to JavaScript, created by Jeremy Ashkenas in 2009. It took heavy inspiration from Ruby and Python, offering a cleaner syntax that eliminated much of JavaScript’s syntactic noise.

Here’s the thing people forget: JavaScript in 2011 was terrible. No modules, no classes, no arrow functions, no destructuring, no template strings, no const or let. You had var, function expressions, and pain. So much pain.

CoffeeScript gave us:

Arrow functions (before ES6):

numbers = [1, 2, 3, 4, 5]
doubled = numbers.map (n) -> n * 2

Class syntax (before ES6):

class Animal
  constructor: (@name) ->
  
  speak: ->
    console.log "#{@name} makes a sound"

class Dog extends Animal
  speak: ->
    console.log "#{@name} barks"
Code language: CSS (css)

String interpolation (before ES6):

name = "Ivan"
greeting = "Hello, #{name}!"
Code language: JavaScript (javascript)

Destructuring (before ES6):

{name, age} = user
[first, second, rest...] = numbers

Comprehensions (still not in JavaScript):

adults = (person for person in people when person.age >= 18)

CoffeeScript didn’t just add syntax sugar. It changed how you thought about JavaScript. The code was more expressive, more concise, and more Ruby-like. For Rails developers, it felt like home.

The Magic of the Asset Pipeline

What made this stack truly shine was how it all fit together. The Rails asset pipeline was like magic. Possibly black magic, but magic nonetheless.

You’d organize your code like this:

app/assets/
  javascripts/
    application.coffee
    models/
      user.coffee
      post.coffee
    views/
      users/
        profile.coffee
      posts/
        index.coffee

In your application.coffee, you’d require your dependencies:

#= require jquery
#= require jquery_ujs
#= require_tree ./models
#= require_tree ./views
Code language: PHP (php)

Rails would automatically compile everything, concatenate it in the right order, minify it for production, and serve it with cache-busting fingerprints. You didn’t think about build tools. You just wrote code.

The same applied to stylesheets. Drop a .scss file in app/assets/stylesheets, and it would be compiled and served. Want to use a gem that includes assets? Add it to your Gemfile, and its assets would automatically be available. No CDN links, no manual script tags.

Was it perfect? No. Was it sometimes confusing when assets weren’t loading in the order you expected? Yes. But the developer experience was smooth. You could go from idea to implementation incredibly quickly.

Why CoffeeScript Still Feels Better Than TypeScript

Okay, here’s where I’m going to lose some of you. TypeScript is objectively the right choice for modern JavaScript development. It has type safety, incredible tooling, massive community support, and it’s actively developed by Microsoft. CoffeeScript is essentially dead, with minimal updates and a dwindling community.

And yet… CoffeeScript still feels better to write.

Let me explain. TypeScript added types to JavaScript, which is fantastic. But it kept JavaScript’s verbose syntax. You still have curly braces everywhere, you still need semicolons (or don’t, which becomes its own debate), you still have visual noise.

Compare these:

TypeScript:

interface User {
  name: string;
  email: string;
  age: number;
}

class UserService {
  constructor(private apiClient: ApiClient) {}
  
  async getUser(id: string): Promise<User> {
    const response = await this.apiClient.get(`/users/${id}`);
    return response.data;
  }
  
  filterAdults(users: User[]): User[] {
    return users.filter((user) => user.age >= 18);
  }
}
Code language: JavaScript (javascript)

CoffeeScript:

class UserService
  constructor: (@apiClient) ->
  
  getUser: (id) ->
    response = await @apiClient.get "/users/#{id}"
    response.data
  
  filterAdults: (users) ->
    users.filter (user) -> user.age >= 18
Code language: CSS (css)

The CoffeeScript version is cleaner. There’s less visual noise, less ceremony. The @ symbol for instance properties is brilliant. It’s immediately obvious what’s a property and what’s a local variable. The implicit returns mean you’re not constantly writing return statements. The significant whitespace enforces good formatting.

Yes, TypeScript gives you type safety. That’s huge. But CoffeeScript gives you readability. And in my experience, readable code is maintainable code. I can glance at CoffeeScript and immediately understand what it’s doing. TypeScript requires more parsing, more mental overhead.

The list comprehensions in CoffeeScript are particularly beautiful:

# CoffeeScript
evenSquares = (n * n for n in numbers when n % 2 == 0)

# TypeScript
const evenSquares = numbers
  .filter((n) => n % 2 === 0)
  .map((n) => n * n);
Code language: PHP (php)

Both work, but the CoffeeScript version reads like English: “n squared for each n in numbers when n is even.” It’s declarative and expressive.

The Existential Operator: A Love Story

One of CoffeeScript’s best features was the existential operator (?). It was like optional chaining before optional chaining existed, but more powerful:

# Safe property access
name = user?.profile?.name

# Default values
speed = options?.speed ? 75

# Function existence check
callback?()

# Existence assignment
value ?= "default"
Code language: PHP (php)

That last one, ?=, was particularly great. It means “assign if the variable is null or undefined.” It’s cleaner than value = value || "default" and more correct (because it doesn’t overwrite falsy-but-valid values like 0 or "").

TypeScript eventually got optional chaining (?.) and nullish coalescing (??), which is great. But it took years, and CoffeeScript had it from the start.

What We Lost

When the JavaScript community moved from CoffeeScript to ES6 and then TypeScript, we gained a lot. Type safety, better tooling, standardization. But we also lost something.

We lost the joy of writing concise code. We lost the elegance of Ruby-inspired syntax. We lost the community that valued readability and expressiveness over completeness and type safety.

Modern JavaScript development often feels like you’re fighting the tools. Configuring TypeScript, setting up ESLint, configuring Prettier, choosing between competing libraries, debugging sourcemaps, dealing with module resolution issues. It’s powerful, but it’s exhausting.

With CoffeeScript and Rails, you just wrote code. The decisions were made for you. The tools were integrated. The conventions were clear. It was opinionated, and that was a feature, not a bug.

The Verdict

Would I start a new project with CoffeeScript and HAML today? Probably not. The ecosystem has moved on. TypeScript has won, and for most use cases, it’s the right choice. React and Vue have replaced server-rendered templates. The world has changed.

But do I miss it? Absolutely.

I miss the simplicity. I miss the elegance. I miss being able to write beautiful, concise code without worrying about types and interfaces and generics. I miss HAML’s clean markup and the way it forced you to write good HTML. I miss the Rails asset pipeline just working without configuration.

Most of all, I miss the developer experience of that era. We were moving fast, building things quickly, and having fun doing it. The code was readable, the stack was coherent, and everything felt like it fit together.

Maybe that’s just nostalgia talking. Maybe I’m romanticizing the past and forgetting the pain points. But when I look at that old CoffeeScript code, I don’t see technical debt. I see craft. I see code that was written with care, that values clarity over cleverness, that respects the reader’s time.

And honestly? I still think CoffeeScript’s syntax is better. TypeScript is more powerful, more practical, and more maintainable at scale. But CoffeeScript is more beautiful.

Sometimes, that matters too.


What are your thoughts? Did you work with CoffeeScript and HAML back in the day? Do you miss them, or are you glad we’ve moved on? Let me know in the comments or reach out on Twitter.