Created
April 14, 2025 13:02
-
-
Save peterc/cbd5011d2373ae59b3de8a32ccaa3b73 to your computer and use it in GitHub Desktop.
Hanami plain text docs for LLMs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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