Skip to content

Instantly share code, notes, and snippets.

@peterc
Created April 14, 2025 13:02
Show Gist options
  • Save peterc/cbd5011d2373ae59b3de8a32ccaa3b73 to your computer and use it in GitHub Desktop.
Save peterc/cbd5011d2373ae59b3de8a32ccaa3b73 to your computer and use it in GitHub Desktop.
Hanami plain text docs for LLMs
Hanami is a Ruby framework designed to create software that is well-architected, maintainable and a pleasure to work on.
These guides aim to introduce you to the Hanami framework and demonstrate how its components fit together to produce a coherent app.
To create a Hanami app, you will need Ruby 3.1 or greater. Check your ruby version with `ruby --version`
To install: `gem install hanami`
Hanami provides a hanami new command for generating a new app: `hanami new bookshelf` (where bookshelf is an example app name)
Running `hanami new ...` create a variety of files and folders to lay out a Hanami app.
To run a generated app: `bundle exec hanami dev`
Then to view: `open http://localhost:2300`
In the file spec/requests/root_spec.rb, Hanami provides a request spec confirming the absence of a defined root page (which is why the welcome screen shows instead). We can run that spec now to prove that it works: `bundle exec rspec spec/requests/root_spec.rb`
`config/routes.rb` is used to define routes available in an app. For example:
```
module Bookshelf
class Routes < Hanami::Routes
root to: "home.index"
end
end
```
Hanami includes a generator to create actions:
`bundle exec hanami generate action home.index --skip-route --skip-tests` (note that you may not want to skip route or test generation but these are options)
This would create a file `app/actions/home/index.rb` like so:
```
module Bookshelf
module Actions
module Home
class Index < Bookshelf::Action
def handle(request, response)
end
end
end
end
end
```
And also a view at `app/templates/home/index.html.erb` which is an ERB view.
To compile assets: `bundle exec hanami assets compile`
If we were to create another view like so: `bundle exec hanami generate action books.index --skip-tests
` then the routes file would now also contain: `get "/books", to: "books.index"`
We could then have:
```
# app/views/books/index.rb
module Bookshelf
module Views
module Books
class Index < Bookshelf::View
expose :books do
[
{title: "Test Driven Development"},
{title: "Practical Object-Oriented Design in Ruby"}
]
end
end
end
end
end
```
`expose` exposes an object or result of a block to the associated view and could be used like so:
```
<!-- app/templates/books/index.html.erb -->
<h1>Books</h1>
<ul>
<% books.each do |book| %>
<li><%= book[:title] %></li>
<% end %>
</ul>
```
A books table could be added using a migration with: `bundle exec hanami generate migration create_books`
We could then edit the generated migration file to add some database columns:
```
# config/db/migrate/20221113050928_create_books.rb
ROM::SQL.migration do
change do
create_table :books do
primary_key :id
column :title, :text, null: false
column :author, :text, null: false
end
end
end
```
To run migrations: `bundle exec hanami db migrate`
To generate a relation so we can use the table: `bundle exec hanami generate relation books`
This creates the following file at app/relations/books.rb:
```
module Bookshelf
module Relations
class Books < Bookshelf::DB::Relation
schema :books, infer: true
end
end
end
```
A test could then be updated to actually generate some books which would be rendered by the view:
```
# spec/features/books/index_spec.rb
RSpec.feature "Books index" do
let(:books) { Hanami.app["relations.books"] }
before do
books.insert(title: "Practical Object-Oriented Design in Ruby", author: "Sandi Metz")
books.insert(title: "Test Driven Development", author: "Kent Beck")
end
it "shows a list of books" do
visit "/books"
expect(page).to have_selector "li", text: "Test Driven Development, by Kent Beck"
expect(page).to have_selector "li", text: "Practical Object-Oriented Design in Ruby, by Sandi Metz"
end
end
```
To get this spec to pass, we’ll need to update our books index view to retrieve books from our database. For this we can generate a book repo: `bundle exec hanami generate repo book`
Repos serve as the interface to our persisted data from our domain layer. Let’s edit the repo to add a method that returns all books ordered by title:
```
# app/repos/book_repo.rb
module Bookshelf
module Repos
class BookRepo < Bookshelf::DB::Repo
def all_by_title
books.order(books[:title].asc).to_a
end
end
end
end
```
To access this book repo from the view, we can use Hanami’s Deps mixin. We can use include Deps["repos.book_repo"] to make the repo available via a book_repo method within our view.
We can now call this repo from our exposure:
```
# app/views/books/index.rb
module Bookshelf
module Views
module Books
class Index < Bookshelf::View
include Deps["repos.book_repo"]
expose :books do
book_repo.all_by_title
end
end
end
end
end
```
Then we can update our template to include the author:
```
<!-- app/templates/books/index.html.erb -->
<h1>Books</h1>
<ul>
<% books.each do |book| %>
<li><%= book[:title] %>, by <%= book[:author] %></li>
<% end %>
</ul>
```
Pagination is possible:
```
# app/relations/books.rb
module Bookshelf
module Relations
class Books < Bookshelf::DB::Relation
schema :books, infer: true
use :pagination
per_page 5
end
end
end
```
Request params can be used to control the pagination in the actions:
```
# app/actions/books/index.rb
module Bookshelf
module Actions
module Books
class Index < Bookshelf::Action
def handle(request, response)
response.render(
view,
page: request.params[:page] || 1,
per_page: request.params[:per_page] || 5
)
end
end
end
end
end
```
In the view we can do this:
```
# app/views/books/index.rb
module Bookshelf
module Views
module Books
class Index < Bookshelf::View
include Deps["repos.book_repo"]
expose :books do |page:, per_page:|
book_repo.all_by_title(page:, per_page:)
end
end
end
end
end
```
And in the repo:
```
# app/repos/book_repo.rb
module Bookshelf
module Repos
class BookRepo < Bookshelf::DB::Repo
def all_by_title(page:, per_page:)
books
.order(books[:title].asc)
.page(page)
.per_page(per_page)
.to_a
end
end
end
end
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment