Ruby on Rails vs Hanami
Ruby developers building web applications typically reach for Rails by default. But Hanami offers a different take on full-stack Ruby development, prioritizing explicit code and modular architecture over Rails' convention-heavy approach. Both frameworks aim to help you build complete web applications, yet they diverge sharply in how they structure code and manage dependencies.
This comparison explores how Rails and Hanami handle common development tasks, revealing where each framework shines and helping you decide which matches your project needs.
What is Ruby on Rails?
Ruby on Rails has defined full-stack Ruby web development since 2004. Rails bundles everything needed to build database-backed web applications: an ORM (ActiveRecord), routing, view rendering, background jobs, caching, and testing tools.
The framework follows "convention over configuration," reducing boilerplate through naming patterns and implicit connections. When you create an ArticlesController, Rails automatically looks for Article models and renders views from app/views/articles/. This magic accelerates development but requires understanding Rails' many conventions.
Rails remains the dominant Ruby web framework, powering GitHub, Shopify, Basecamp, and thousands of other applications. The ecosystem includes gems for virtually any web development need.
What is Hanami?
Hanami emerged in 2014 as a rethinking of full-stack Ruby frameworks. Hanami provides the same components as Rails (routing, models, views, and more) but organizes them differently, favoring explicit dependencies and isolated components over global state and conventions.
The framework applies object-oriented design principles strictly. Instead of ActiveRecord's approach where models inherit database behavior, Hanami separates persistence from business logic through repository objects. Rather than controller classes with many actions, Hanami uses single-action classes. These architectural choices reduce coupling and improve testability.
Hanami 2.0, released in 2022, rebuilt the framework around dry-rb libraries, bringing modern Ruby patterns like dependency injection and functional programming concepts to full-stack development.
Quick Comparison
| Feature | Ruby on Rails | Hanami |
|---|---|---|
| Approach | Convention over configuration | Explicit over implicit |
| Architecture | Monolithic with modules | Modular with slices |
| ORM | ActiveRecord (Active Record pattern) | ROM (Repository pattern) |
| Controllers | Multi-action controller classes | Single-action classes |
| Views | Template-centric with helpers | View objects with templates |
| Routing | Resourceful with conventions | Explicit route definitions |
| Dependencies | Global state and autoloading | Dependency injection container |
| Testing | Integration-focused by default | Unit testing encouraged |
| Memory Usage | ~60-100MB base | ~40-60MB base |
| Maturity | 20+ years, stable | 10 years, evolving |
Project initialization
I wanted to build a library management system, so I created new projects in both frameworks. Rails installation and setup:
This generated 87 files before I wrote any code. A database configuration, web server setup, and complete directory structure appeared instantly. The sheer amount of generated scaffolding felt overwhelming. I hadn't written anything yet but already had views, controllers, and configuration spread across dozens of directories.
Hanami follows a similar installation pattern:
This created 43 files. Less scaffolding meant less to understand upfront, but also less hand-holding. Where Rails assumed I'd use PostgreSQL and gave me database.yml configured for it, Hanami made me think about these choices.
Starting both servers revealed another difference:
Rails felt noticeably slower to boot and used more memory sitting idle. Hanami started faster with a lighter footprint. For development, those extra seconds waiting for Rails to restart after code changes accumulated into real time lost.
Routing decisions
Adding book management routes showed how differently these frameworks think. In Rails, I wrote resources :books in routes.rb and got seven routes instantly. Rails assumed I wanted index, show, new, create, edit, update, and destroy actions because that's what CRUD applications need.
Running rails routes showed Rails generated URLs, HTTP verbs, and controller mappings for me. This felt productive until I needed custom routes. Then I had to remember Rails' routing DSL: the difference between member and collection routes, how concerns work, when to use scope versus namespace.
Hanami made me write each route explicitly:
This verbosity annoyed me initially. Why write five lines when Rails needs one? But debugging routing issues later, I appreciated seeing exactly which URLs mapped where. No guessing about what resources :books actually generated or why a nested route wasn't working as expected.
Controller structure
Creating a books controller revealed fundamentally different architectures. Rails gave me a controller class with seven empty methods waiting for me to fill them:
One class handling seven different HTTP requests felt natural from years of Rails experience. But when I wanted to test just the create action, I had to instantiate the entire controller class with all its callbacks, filters, and inherited behavior.
Hanami split each action into its own class:
More files, more classes, more boilerplate. But each action became completely isolated. Testing the create action meant instantiating just that one class. No worrying about before_action callbacks from the parent controller or shared state between actions. The explicitness felt verbose until I needed to refactor. Then moving one action to a different domain slice took seconds because nothing else depended on it.
Data persistence patterns
Saving books to the database exposed the biggest divide between these frameworks. Rails' ActiveRecord made the Book class responsible for both business logic and database operations:
Everything lived in one place. The Book object knew how to validate itself, save itself, and query for other books. This convenience meant I could write Book.where(available: true).order(:title) and get exactly what I needed without ceremony.
The downside hit when testing. Every Book test required a database connection because the object couldn't exist without ActiveRecord. Testing business logic like available? required setting up database transactions and fixtures even though no data actually needed persisting.
Hanami separated these concerns completely. Entities held business logic, repositories handled persistence:
More code, more classes, more indirection. But testing Book#available? now required zero database setup. Just instantiate a Book with test data. The repository tests hit the database, the entity tests didn't. This separation seemed academic until I had 200 tests running and Hanami's entity tests completed in 0.3 seconds while Rails' model tests took 8 seconds loading ActiveRecord.
View rendering strategies
Displaying book lists showed another architectural split. Rails mixed Ruby logic and HTML templates, using instance variables to pass data from controllers:
This worked fine for simple views. But as presentation logic grew (formatting dates, handling nil values, determining CSS classes), my templates filled with conditionals and helper method calls. Where should the logic for "format author name as Last, First" live? In the template? A helper module? The model?
Hanami introduced view objects between actions and templates:
Extra presenter classes felt like overkill initially. But when product requirements changed and authors needed different display formats across five different views, updating one presenter method beat hunting through five templates for scattered formatting logic.
Form handling complexity
Building the book creation form pushed me into each framework's validation strategy. Rails integrated validations into models, making forms feel like they "just worked":
Everything connected automatically. Invalid data triggered model validations, errors appeared in the form, field values persisted across failed submissions. Rails' form helpers knew to check @book.errors and highlight invalid fields.
The catch appeared when I needed different validation rules for different contexts. Creating a book required an ISBN, but importing legacy books from old records didn't have ISBNs. Rails' solution involved conditional validations with if: and unless:, which quickly became tangled when multiple contexts shared some but not all rules.
Hanami validated at the action level, making each endpoint's requirements explicit:
Now my legacy book import action defined different rules without affecting the create action:
Each action's validations lived right there in the action file. No hunting through model callbacks or conditional validation logic. The price was duplication. If both actions needed the same title validation, I wrote it twice. Hanami's answer was extracting shared validation schemas, but that meant more objects to manage.
Testing strategies
Writing tests revealed how architectural decisions affect testing approaches. Rails' integrated testing felt straightforward for the first few tests:
These tests required loading Rails' entire testing infrastructure: database transactions, fixture loading, request/response cycle simulation. Running 50 model tests took 12 seconds because each one went through ActiveRecord even when testing pure business logic.
Hanami's separated architecture let me test entities without touching the database:
These entity tests ran in 0.4 seconds total. No database, no framework loading. Repository tests hit the database when actually needed:
I ran entity tests constantly while developing because they completed instantly. Repository and action tests ran before commits since they needed database setup. Rails collapsed these distinctions. Everything touched the database whether it needed to or not.
Dependency management
Adding background jobs for overdue book notifications showed completely different approaches to managing dependencies. Rails used global state. I could call Book.find(id) from anywhere because ActiveRecord was always available:
This convenience meant I could access any model, mailer, or service from any job. But it also meant tracking dependencies required reading the entire method body. What does this job actually need? Book? User? BookMailer? All three? The code didn't say explicitly.
Hanami injected dependencies through the container:
The include Deps line declared exactly what this operation needed. Testing became simpler because I could inject mocks:
No database, no real mailer, just unit testing the logic. Rails could achieve similar isolation with careful mocking, but Hanami's architecture pushed me toward it naturally.
Final thoughts
This article looks at the different values behind Rails and Hanami in Ruby web development.
Rails is a great choice when speed matters most. Its maturity, large ecosystem, and strong conventions let teams deliver products quickly. The trade-off is that as your application grows, you’ll need discipline to keep things well-organized.
Hanami is better suited when long-term clarity and maintainability matter more than getting started fast. Its structure encourages clean design patterns and better testing, though it often means writing more setup code in the beginning.