2

Let's build a Hanami app

 3 months ago
source link: https://www.honeybadger.io/blog/build-hanami-app/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Let's build a Hanami app

Hanami is a fresh take on building full-stack Ruby apps. With its focus on abstractions, minimal defaults, and speed, it could be the framework to take on Rails' dominance of the Ruby landscape. In this tutorial, we build a simple app using Hanami and, along the way, learn the framework and how it works.

Hanami is a full-stack Ruby web framework. 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:

.
├── app
│   ├── action.rb
│   └── actions
├── config
│   ├── app.rb
│   ├── puma.rb
│   ├── routes.rb
│   └── settings.rb
├── config.ru
├── Gemfile
├── Gemfile.lock
├── Guardfile
├── lib
│   ├── hanami_blog_app
│   │   └── types.rb
│   └── tasks
├── Rakefile
├── README.md
└── spec
    ├── requests
    │   └── root_spec.rb
    ├── spec_helper.rb
    └── support
        ├── requests.rb
        └── rspec.rb

10 directories, 16 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.

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

Before proceeding, it's important to note that Hanami version 2.0 (current version as of this writing) does not yet have the Hanami view integrated, so in order to generate views later in the tutorial, you'll need to add the Hanami view gem in your app's Gemfile:

# Gemfile
...
gem 'hanami-view', github: 'hanami/view', branch: 'main'

Then run bundle install. Now, we're ready to outline the app we'll be working on next.

“Everyone is in love with Honeybadger ... the UI is spot on.”
molly.jpg?1710432211Molly Struve, Sr. Site Reliability Engineer, Netflix
Start free trial

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 persistence to a Hanami app

Since a natively integrated persistence library is planned for the upcoming Hanami 2.1, for now, we'll be using the awesome Ruby Object Mapper (ROM) library to build the persistence layer in the simple blog app.

Install the ROM library

Open your 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"

Get Honeybadger's best Ruby articles in your inbox

We publish 1-2 times per month. Subscribe to get our Ruby articles as soon as we publish them.

We're Honeybadger. We'll never send you spam; we will send you cool stuff like exclusive content, memes, and swag.

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 now 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.

Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
Start free trial

Creating and running the first migration

As mentioned before, once ROM is integrated into the upcoming 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 "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]
        # create, update, delete post
        commands :create
      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]
        # create, update, delete post
        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.

Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
Start free trial

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. 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

Let's see how this happens.

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.

For example, we can create an action to handle a request for showing a post using the command bundle exec hanami generate action posts.show. This will generate the show action shown below:

# app/actions/posts/create.rb

module HanamiBlogApp
  module Actions
    module Posts
      class Show < HanamiBlogApp::Action
        def handle(*, response)
          response.body = self.class.name
        end
      end
    end
  end
end

As well as an accompanying show view and template (which we shall get into a bit later), and append the show route to the routes file:

# config/routes.rb

module HanamiBlogApp
  class Routes < Hanami::Routes
    ...

    get "/posts/:id", to: "posts.show", as: :show_post
  end
end

We can modify our show action to simply respond with the accompanying view while passing it a post_id as the params so that the view will know which post to actually request for, as shown below:

# app/actons/posts/show.rb

module HanamiBlogApp
  module Actions
    module Posts
      class Show < HanamiBlogApp::Action
        def handle(request, response)
          response.render(view, id: request.params[:id])
        end
      end
    end
  end
end

And that's it for the actions; let's move on to the view.

Hanami views

A Hanami view has two main jobs: decide what data to expose to the template and which template to render. When you generated the show action, an accompanying view and template were also generated (assuming you've installed the hanami-view gem):

bundle exec hanami generate action posts.show

Created app/actions/posts/show.rb
Created app/views/posts/show.rb              # this is the view
Created app/templates/posts/show.html.erb    # this is the template

The show view looks like this:

# app/views/posts/show.rb

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

Which we can edit as follows:

# app/views/posts/show.rb

module HanamiBlogApp
  module Views
    module Posts
      class Show < Hanami::View
        include Deps[
            repo: "persistence/repositories/post"
            ]

        expose :post do
          repo.find(req.params[:id])
        end
      end
    end
  end
end

Here's what we just did with the above code:

  • First, we inject a dependency to include the post repo which we defined earlier, then,
  • We define an expose block, which returns a post fetched using the repo dependency included in the Deps mixin.

Now, we're ready to render the post via a HTML template.

“Wow — Customers are blown away that I email them so quickly after an error.”
chris.jpg?1710432211Chris Patton, Founder of Punchpass.com
Start free trial

View templates

In Hanami, view templates are responsible for rendering the responses from actions (and views) within HTML, JSON, or other rendering formats.

That said, for our show template to work as expected, we'll first need to define an app template similar to the application.html.erb layout in Rails apps, which will define an app-wide HTML structure.

Create a file app/templates/layouts/app.html.erb and edit it as below:

<!-- app/templates/layouts/app.html.erb -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hanami Blog App</title>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

And now, 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. 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 taken steps to build a Hanami 2.0 app, starting from gem installation to learning how to handle an incoming HTTP request and responding with views and templates.

Even so, there are several concepts we did not go through in this build, such as taking user data via forms, saving data to the database, catching errors, handling parameters, and so forth. 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 learn more about this exciting new Ruby framework.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK