Hanami is a full-stack Ruby web framework that's been around for over a decade. The Hanami team recently released version 2.1 which now includes everything you need to build a full stack application including a view layer and frontend asset support. Unlike Rails, which has many default assumptions about how an app should be built, Hanami promises developer freedom by not imposing too many such defaults.

The framework is also blazingly fast due to its low memory footprint and focus on minimalism. Combine that with a focus on strict abstractions, and you get a fully-featured Ruby framework that could rival Rails for building some applications, such as APIs and micro-services.

In this tutorial, we'll learn about the framework's structure and features as we go through the steps of building a simple blog application.

Let's get started.

Prerequisites

In order to follow along with this tutorial, ensure you have the following ready to go:

  • Ruby 3.0+ installed in your development environment.
  • A local installation of Postgresql

That's it.

Installing Hanami and generating a new application

Hanami can be installed by running the command:

gem install hanami

Next, generate a new Hanami app with the command below:

hanami new hanami_blog_app

This should give you an app directory structure like the one shown below (the node_modules directory is omitted to keep things simple):

tree -I 'node_modules'
.
├── app
│   ├── action.rb
│   ├── actions
│   ├── assets
│   │   ├── css
│   │   │   └── app.css
│   │   ├── images
│   │   │   └── favicon.ico
│   │   └── js
│   │       └── app.js
│   ├── templates
│   │   └── layouts
│   │       └── app.html.erb
│   ├── view.rb
│   └── views
│       └── helpers.rb
├── bin
│   └── dev
├── config
│   ├── app.rb
│   ├── assets.js
│   ├── puma.rb
│   ├── routes.rb
│   └── settings.rb
├── config.ru
├── Gemfile
├── Gemfile.lock
├── Guardfile
├── lib
│   ├── hanami_blog_app
│   │   └── types.rb
│   └── tasks
├── package.json
├── package-lock.json
├── Procfile.dev
├── public
│   ├── 404.html
│   └── 500.html
├── Rakefile
├── README.md
└── spec
    ├── requests
    │   └── root_spec.rb
    ├── spec_helper.rb
    └── support
        ├── features.rb
        ├── requests.rb
        └── rspec.rb

18 directories, 30 files

As you can see, Hanami's directory structure is very similar to what Rails gives you, but there are notable differences:

  • app - Most of your application's code will go here.
  • lib - Here's where you'll put any code to support your app's main codebase.
  • config - This is configuration files will go e.g. Puma configurations, routes and other app settings.
  • assets - Coming with the release of version 2.1 is support for frontend assets which you'll find resident in this directory.
  • views - Hanami 2.1 also introduces a fully functioning view layer which is found here.

To get a complete picture of the Hanami directory structure, see the project's documentation; for now, we'll leave it at that.

You can quickly run the newly generated application using the command below:

bundle exec hanami run dev

And if everything works as expected, you should see a screen similar to the one below when you visit localhost running off the default port 2300:

Default Hanami app running

Moving on, let's get an overview on the app we'll be building next.

The app we'll be building

In this tutorial, we'll build a simple blog app that will showcase some of the internals of Hanami and give you a good base from which you can build more robust apps that leverage the framework's strengths.

You can find the source code for the completed app here.

Adding database persistence to a Hanami app

The blog app we're building will need to store blog records in a database. For this, we'll be using the awesome Ruby Object Mapper (ROM) as the object relation library to connect to a Postgresql database.

Install the ROM library

Open the app's Gemfile and add the gems listed below, then run bundle install:

# Gemfile

gem "rom", "~> 5.3"
gem "rom-sql", "~> 3.6"
gem "pg"

Create the app database

Next, create a Postgresql database via the terminal in your development machine and give it an appropriate name (I called mine "hanami_blog_app"). If you don't know how to do this, just follow the Postgresql guides here.

Note: It's possible to use the Hanami command createdb <db_name> from within your project root, but depending on how you've configured your Postgresql installation, this might work or not.

Add a persistence provider

The next step is to add a persistence provider. In Hanami, providers are a way of registering app components that exist outside the app's automatic component registration lifecycle. You can read more about them here.

Create a new file config/providers/persistence.rb and edit it as shown below:

# config/providers/persistence.rb

Hanami.app.register_provider :persistence, namespace: true do
  prepare do
    require "rom"

    config = ROM::Configuration.new(:sql, target["settings"].database_url)

    register "config", config
    register "db", config.gateways[:default].connection
  end

  start do
    config = target["persistence.config"]

    config.auto_registration(
      target.root.join("lib/hanami_blog_app/persistence"),
      namespace: "HanamiBlogApp::Persistence"
    )

    register "rom", ROM.container(config)
  end
end

Providers go through a lifecycle with the steps of prepare, start and stop. In the persistence example above, a prepare step takes care of getting a database connection (which we'll define next), and a start step defines what happens after a connection is established.

Adding the database settings

The app's config/settings.rb file is where you define your own app-specific settings and parameters using constructors within the Settings class.

The important thing to note here is that these settings are very different from the App config which are configurations that affect the Hanami framework within the app.

To define the app's database connection string, go ahead and modify the settings file like so:

# config/settings.rb

module HanamiBlogApp
  class Settings < Hanami::Settings
    setting :database_url, constructor: Types::String
  end
end

And since Hanami uses the dotenv gem to read environment variables in development, you can go ahead and define the database URL in an .env file in your app's root like so:

# .env

DATABASE_URL=postgres://<user>:<password>@localhost:5432/<database_name>

You can confirm that the database connection setting is working by opening a Hanami console session with bundle exec hanami console, and running app["settings"].database_url, which should return the database connection URL you've just defined in the .env file.

Creating and running the first migration

As mentioned before, once ROM is integrated into an upcoming version of Hanami, migration commands will be included as well. In the meantime, let's create one manually using some rake tasks.

Open up the Rakefile and edit as shown below:

# Rakefile
...

require "hanami/rake_tasks"
require "rom/sql/rake_task"

task :environment do
  require_relative "config/app"
  require "hanami/prepare"
end

namespace :db do
  task setup: :environment do
    Hanami.app.prepare(:persistence)
    ROM::SQL::RakeSupport.env = Hanami.app["persistence.config"]
  end
end

Then, generate the first migration with the following command:

bundle exec rake db:create_migration[create_posts]

In case you have a zsh shell, the above command will not work since you'll need to escape the square brackets:

bundle exec rake db:create_migration\[create_posts\]

Running that should create a new db/migrate folder with a new migration file in it:

db
└── migrate
    └── 20240115072209_create_posts.rb

2 directories, 1 file

Open up this migration and edit as follows:

# db/migrate/<timestamp>_create_posts.rb

ROM::SQL.migration do
  change do
    create_table :posts do
      primary_key :id
      column :title, String, null: false
      column :body, String, null: false
    end
  end
end

Then run it with bundle exec rake db:migrate.

Up to this point, we have most of what we need in terms of persistence, and the only thing remaining is an interface to read and write data to the newly created posts table. In Hanami, this persistence interface is called a relation. Let's build it next.

Adding a relation

Relations provide adapter-specific API interfaces for reading and writing data to the database. You define relations using explicit classes with methods for reading and writing data as you wish it to be done.

The ROM relations documentation provides a wealth of information on the subject, and I encourage the reader to check it out.

Go ahead and create a relation for interacting with the posts table:

# lib/hanami_blog_app/persistence/posts.rb

module HanamiBlogApp
  module Persistence
    module Relations
      class Posts < ROM::Relation[:sql]
        schema(:posts, infer: true)
      end
    end
  end
end

A couple of things to note here:

  • The class name Posts will be used to interact with the posts table which is set to :posts.
  • ROM::Relation[:sql] specifies that the rom-sql adapter will be used for this relation.
  • schema(:posts, infer: true), here the schema will be inferred from the database schema and will include all the posts table columns.

As you can see, the relation defines how data will be read and written to the database, but we still need a way to actually fetch and write posts data. Since Hanami leverages the ROM library, we can easily do this using a repository, which we will cover next.

The post repository

Repositories, also referred to as "repos," provide convenient methods for accessing data from relations. Depending on the set up, a repo can access one or many relations.

Let's set up a repo with some CRUD methods to work with the posts relation.

# lib/hanami_blog_app/persistence/repositories/post.rb

module HanamiBlogApp
  module Persistence
    module Repositories
      class Post < ROM::Repository[:posts]
        commands :create # define CRUD actions here e.g. create, update, delete etc.
      end
    end
  end
end

Here, we define a repo for the posts relation and include a commands macro which defines a Post#create method.

Next, let's add methods for updating and deleting a post.

# lib/hanami_blog_app/persistence/repositories/post.rb

module HanamiBlogApp
  module Persistence
    module Repositories
      class Post < ROM::Repository[:posts]
        commands :create, update: :by_pk, delete: :by_pk
      end
    end
  end
end

Tip: The by_pk ("by primary key") tells ROM the method is to be applied to a particular post specified by its primary key.

Finally, let's add finder methods for fetching all posts and a single post:

# lib/hanami_blog_app/persistence/repositories/post.rb

module HanamiBlogApp
  module Persistence
    module Repositories
      class Post < ROM::Repository[:posts]
        ...

        # find all posts
        def all
          posts.to_a
        end

        # find single post
        def find(id)
          posts.by_pk(id).one!
        end
      end
    end
  end
end

And that's it; we've successfully set up the infrastructure for creating and reading posts. However, we're still missing a way for a user to interact with this infrastructure. Let's work on that next.

Hanami's view layer

The view layer in a Hanami app is used to render the app's data in HTML, JSON, and other formats. With the recent launch of version 2.1, Hanami now includes a complete view system for rendering HTML, JSON and other formats. But before we can build any views, it's important to get an overview of a request cycle in a Hanami app.

The diagram below shows how Hanami handles an incoming HTTP request and the parts involved in processing it and sending a response back:

Simplified Hanami view structure

We'll go through what each part of the view layer does, but before we do that, it's important to note that the latest Hanami release now makes it a breeze to generate views. As an example, the command shown below will generate the entire show layer:

bundle exec hanami generate action posts.show

Updated config/routes.rb
Created app/actions/posts/
Created app/actions/posts/show.rb
Created app/views/posts/
Created app/views/posts/show.rb
Created app/templates/posts/
Created app/templates/posts/show.html.erb
Created spec/actions/posts/show_spec.rb

Let's now learn what each view layer is about and how they work starting with actions.

Hanami actions

When a HTTP request comes in, the request will first encounter an action which is an individual class that determines how a HTTP request will be handled e.g. what HTTP response to send back, whether to redirect the request and so forth.

An action will have a handle method which takes two arguments:

  • request - basically an object representing the incoming HTTP request.
  • response - an object defining the response back.

The code snippet below shows the default generated post show action:

# app/actions/posts/show.rb

module HanamiBlogApp
  module Actions
    module Posts
      class Show < HanamiBlogApp::Action
        def handle(request, response)
        end
      end
    end
  end
end

Now we need to modify this code so that it's able to reference the repository we just created and then make the proper response to the view which we'll talk about shortly:

# app/actions/posts/show

module HanamiBlogApp
  module Actions
    module Posts
      class Show < HanamiBlogApp::Action
        include Deps[
                  repo: 'repositories.posts'
                ]

        def handle(request, response)
          requested_post = request.params[:id] # first get the id from params...
          post = repo.by_id(requested_post) # ..then fetch the respective post referencing posts repo
          response.render(view, post: post)
        end
      end
    end
  end
end

So here's what's going on. We first get the post_id from the params in the incoming request, then reference the repository we created to fetch the respective post via the dependency injection, and then pass this on to the view.

Obviously, this is a very basic outline of what a Hanami action is about and I highly recommend you check out the documentation to dig deeper. For now, let's move on to views.

Hanami views

In Hanami, a view class has two main jobs: decide the data to expose to the template and, to decide the corresponding template to render. When you generated the view layer for show, part of the generated files included an accompanying view and template.

The code snippet below shows the default show view:

# app/views/posts/show.rb

module HanamiBlogApp
  module Views
    module Posts
      class Show < HanamiBlogApp::View
      end
    end
  end
end

Now let's modify this view code to take in the post object passed on by the posts show action that we just dealt with:

# app/views/posts/show.rb

module HanamiBlogApp
  module Views
    module Posts
      class Show < HanamiBlogApp::View
        expose :post
      end
    end
  end
end

In order for the relevant post show template can render the post we want it to, we need for the accompanying view to pass the post object to it, and the way we do this is using an exposure. Read more on the subject here.

Next, let's see how the HTML view is rendered.

View templates

In Hanami, view templates are responsible for rendering the responses from views using HTML, JSON, or other formats.

First, we edit the show template as shown below:

<!-- app/templates/posts/show.html.erb -->

<h1><%= post.title %></h1>

<p><%= post.body %></p>

Now run the app with bundle exec hanami server, and assuming everything works as expected, visiting http://localhost:2300/posts/1 gives us a post view template similar to the one shown in the screenshot below:

Hanami server running

Wrapping up and next steps

In this tutorial, we've gone through the steps to build a Hanami 2.1 app, from gem installation to learning how to handle an incoming HTTP request and responding with views and templates. Even so, there are many concepts we did not go through in this build, such as taking user data via forms, saving data to the database, catching errors, and so much more. Doing so would make for a very long article; instead, from the foundation set by this tutorial, the reader is encouraged to try and build these features to delve deeper into the Hanami framework.

Get the Honeybadger newsletter

Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo
    Aestimo Kirina

    Aestimo is a family guy, Ruby developer, and SaaS enterpreneur. In his free time, he enjoys playing with his kids and spending time outdoors enjoying the sun, running, hiking, or camping.

    More articles by Aestimo Kirina
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required