Recently, I started delving into the Hanami framework which version 2 had been released a couple of months ago. Since I got to know Docker and Docker Compose, I use them over and over again because they help with running varied services on my machine without installing required dependencies, etc. If you don’t know them yet, I really encourage you to become acquainted with them. I find them highly convenient and I would like to use them also for setting API service connected with a relational database, in my case, it will be PostgreSQL. Let’s get the show on the road.

First of all, we have to make sure that the ruby version that we use is at least in version 3.0.0. That requirement is constituted by Hanami. To begin, we have to install the hanami gem.

gem install hanami

Once it’s done, let’s create a new Hanami application from the scratch.

hanami new simple-api

This process created a bunch of files inside of the directory that follows the name of applications that we provided in the previous command. In my case it’s simple_api. Now, we need to create Dockerfile to specify the runtime environment for our application.

# simple_api/Dockerfile.dev

FROM ruby:3.2.1-alpine

WORKDIR /usr/src/app

COPY Gemfile Gemfile.lock ./

RUN apk update && apk add --no-cache build-base ruby-dev
RUN bundle install

EXPOSE 2300

COPY . .

We are going to use ruby:3.2.1-alpine image to avoid using an overloaded ruby image but there is required to install two libraries to make it work: build-base and ruby-dev. If you are familiar with Rails, you might know the port number 3000 which is widely used, in the case of Hanami applications the default port is 2300. Once the Dockerfile is ready, we can build an image

docker build -t simple-api -f Dockerfile.dev .

Everything looks good, so we can jump into a docker-compose file.

# simple_api/docker-compose.yaml
version: '3.8'
volumes:
  postgres-data:
services:
  db:
    image: postgres
    volumes:
      - postgres-data:/var/lib/postgresql/data
    env_file: .env
  app:
    image: simple-api
    command: sh -c "hanami server"
    ports:
      - "2300:2300"
    depends_on:
      - db
    env_file: .env

What we specified here is the db service that is PostgreSQL database and our web application server - app. Image naming is pretty straightforward, as you could notice ports as well. We are going to use the default port number. In addition, we have to add POSTGRES_PASSWORD environment variable to the .env file, in my case: POSTGRES_PASSWORD=XYZ123QWE.

# simple_api/.env
POSTGRES_PASSWORD=XYZ123QWE

Let’s run our containers by running

docker-compose up

Awesome, everything looks good but there is still one important thing to do, we have to connect simple-api web server with postgres. In the beginning, let’s add a few gems to the Gemfile.

# simple_api/Gemfile
gem "rom", "~> 5.3"
gem "rom-sql", "~> 3.6"
gem "pg"

Next we have to add postgresql-dev library to the Dockerfile.dev file so that it can successfully install the pg gem. The line from the file should look like below:

RUN apk update && apk add --no-cache build-base ruby-dev postgresql-dev

Let’s stop containers, rebuild the image, and run containers again. Before we start connecting the API application with the database server, we can create a database. We have to connect to psql on the database container and call SQL statement, so:

docker-compose exec db psql -U postgres
CREATE DATABASE simple_api_development;

Now it’s time to get a closer look at Hanami framework itself because we have to add a persistence provider. We are supposed to create the following file:

# simple_api/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/simple_api/persistence"),
      namespace: "SimpleAPI::Persistence"
    )

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

This target["settings"].database_url refers to an environment variable with DATABASE_URL name, let’s add DATABASE_URL=postgres://postgres:XYZ123QWE@db:5432/simple_api_development to the .env file.

# simple_api/.env
DATABASE_URL=postgres://postgres:XYZ123QWE@db:5432/simple_api_development
POSTGRES_PASSWORD=XYZ123QWE

Unfortunately, that’s not everything, we have to also add the database_url setting to the Settings class:

# simple_api/config/settings.rb
module SimpleAPI
  class Settings < Hanami::Settings
    setting :database_url, constructor: Types::String
  end
end

Now we can rebuild our simple-api image and restart containers.

Testing

Let’s prove that the connection is established and we can use the DB. We are going to create a table, insert sample data, and try to read it. As a first step, we are going to enable rake migrations by appending the following code to the Rakefile.

# simple_api/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

We will add all the necessary code and then we will rebuild the simple-api image.

A migration for table creation

# simple_api/db/migrate/20230228200134_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

and a relation

# simple_api/lib/simple_api/persistence/relations/books.rb
module SimpleAPI
  module Persistence
    module Relations
      class Books < ROM::Relation[:sql]
        schema(:books, infer: true)
      end
    end
  end
end

Once it’s finished, we can rebuild the image and restart the containers. Then we can connect with a shell on the app container by calling docker-compose exec app sh . We have to run migrations by executing bundle exec rake db:migrate . Then hanami console and app["persistence.rom"].relations[:books].insert(title: 'The Alloy of Law', author: 'Brandon Sanderson') . Inserting data itself shows us that the connection is working, but let’s try to read data: app["persistence.rom"].relations[:books].to_a .

Voila! We have the Hanami application connected with PostgreSQL database. For more, you can visit the Hanami 2.0 getting started guide. I appreciate you reached the end of the article, thank you. Enjoy playing with Hanami.


Thanks

Goumbik for the background photo