Codementor Events

Rails to Hanami

Published May 10, 2019

A couple of month ago I discovered a small community of ruby developers that where building a new web framework, Hanami. Since then, I’ve been following pretty closely the progress of the project. In the last version, Hanami integrated with ROM in their data layer, so, I decided it was a good idea to play a little bit with it and create a simple REST API to see how it worked out.

We are going to build a REST JSON API for a classic TODO application

Endpoints

Lists

  • GET - Fetch list of lists (/lists)
  • GET - Fetch list (/lists/:id)
  • POST - Create list (/lists)

Tasks

  • GET - Fetch tasks (/lists/:list_id/tasks)
  • GET - Fetch task (/lists/:list_id/tasks/:id)
  • POST - Create task (/lists/:list_id/tasks)

Entities

List

  • id (Integer)
  • name (String)

Task

  • id (Integer)
  • description (String)
  • list_id (Integer)

Lets start by creating the project. Hanami comes with an initialization command just like rails.

hanami new hanami_todo_app --database=postgres
cd hanami_todo_app
bundle install

After running the new command, Hanami should have created a new directory with the name hanami_todo_app. We can cd into the directory and take a look at the structure. The architecture of Hanami applications it’s a little bit different than rails. The application is split in two main directories, lib and apps. In the lib directory most of your business logic will sit. This includes your entities (almost like a Rails model but without the persistence apis, more on this later), services, interactors or whatever code organization design you use for your application. Under the apps folder you will likely have only one folder in a simple app. We are going to have only one folder called api that is were all our http actions will sit. If you have a big application you will likely have multiple folders under apps, for example, admin (administrative UI), web, etc. Rails has engines for splitting big application into different modular smaller apps.

By default Hanami will create a default app called web, we are going to delete that application and create a new one called api just to help my OCD.

hanami destroy app web
hanami generate app api

After those command are executed we should have removed the web folder under apps and a new api folder should have been created.

Entities and Repositories (Models)

We are going to start modeling our system now. Two simple entities should be enough, List and Task.

➜ hanami_todo_app git:(master) ✗ hanami generate model list
      create lib/hanami_todo_app/entities/list.rb
      create lib/hanami_todo_app/repositories/list_repository.rb
      create db/migrations/20170531214217_create_lists.rb
      create spec/hanami_todo_app/entities/list_spec.rb
      create spec/hanami_todo_app/repositories/list_repository_spec.rb

➜ hanami_todo_app git:(master) ✗ hanami generate model task
      create lib/hanami_todo_app/entities/task.rb
      create lib/hanami_todo_app/repositories/task_repository.rb
      create db/migrations/20170531214247_create_tasks.rb
      create spec/hanami_todo_app/entities/task_spec.rb
      create spec/hanami_todo_app/repositories/task_repository_spec.rb

As you can see from the command’s output, Hanami created the entity but it also created a repository. We will discuss more on them later. Now we can go to the generated migration files and adapt them as we need.

Hanami::Model.migration do
  change do
    create_table :lists do
      primary_key :id
      column :name, String, null: false

      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false
    end
  end
end
Hanami::Model.migration do
  change do
    create_table :tasks do
      primary_key :id
      column :description, String, null: false
      foreign_key :list_id, :lists, on_delete: :cascade, null: false

      column :created_at, DateTime, null: false
      column :updated_at, DateTime, null: false
    end
  end
end

Hanami’s migration DSL is very similar to the one ActiveRecord provides. We need to provide column name, data type and any constraint that we want for the column. In this cases we added nullability constraint to enforce non null columns. Hanami automatically generates accessors and setters for our entities based on the database schema, same as rails.

For running the migrations we can use hanami db migrate, since we didn’t create the database yet we can run hanami db prepare that will create and run migrations for us. For more database related tasks you can check them with hanami db.

One important difference between Hanami and Rails regarding data modeling is that Hanami doesn’t provide an enormous api on the entities classes. Entities are very simple ruby classes that inherit from Hanami::Entity. They don’t have api’s for querying, creating or updating database records, the responsibility for doing all those tasks is encapsulated in the entitiy associated Repository (remember we mentioned them ?).

This change will be of high impact on the design of your system. By splitting the persistence from the model you are not coupling your business logic with the underlying data store. Every Repository has it’s own independent data store, in our case the ListRepository and TaskRepository will be persisted in the postgres db.

Lets imagine that our application it’s getting a lot of traffic and we detected that our SQL db is the bottleneck. One possible solution to the problem could be to move the highly accessed data to a faster data store, for example, redis. Instead of refactoring all our code we can simple change our Repository to start using a different data store and we won’t need to actually touch our entity class. This is a super simple and naive example of how this can be useful by I hope you get the idea.

We are going to play with the hanami console and create some objects using the repository.

# Let create a new repository instance
2.3.1 :011 > list_repository = ListRepository.new
 => #<ListRepository relations=[:lists]>

# Create and store a new list object
2.3.1 :012 > list_repository.create(name: 'Grocery')
[hanami_todo_app] [INFO] [2017-05-31 19:08:39 -0300] (0.000608s) SELECT "id", "name", "created_at", "updated_at" FROM "lists" LIMIT 1
[hanami_todo_app] [INFO] [2017-05-31 19:08:39 -0300] (0.003811s) INSERT INTO "lists" ("name", "created_at", "updated_at") VALUES ('Grocery', '2017-05-31 22:08:39.068319+0000', '2017-05-31 22:08:39.068319+0000') RETURNING *
 => #<List:0x007fa244403380 @attributes={:id=>1, :name=>"Grocery", :created_at=>2017-05-31 22:08:39 UTC, :updated_at=>2017-05-31 22:08:39 UTC}>

Repositories provide the apis for interacting with the persistence layer. From the Hanami docs:

An object that mediates between entities and the persistence layer. It offers a standardized API to query and execute commands on a database.

The methods you are used to call in your Rails models are now in the repository, so, instead of doing User.create({ ... }) you will do UserRepository.new.create({ ... }). Same applies for find and update, I suggest looking at the docs for a better understanding since apis are not exactly the same.

Repositories also encapsulate your custom queries. In Rails your probably created some scopes in the model to encapsulate your query logic. In Hanami you write methods in your repository. One big difference between Hanami and Rails in this case is that Hanami doesn’t expose a method to perform custom queries outside the Repository class. This is a really good idea because you will be forced to write all your query logic in one place instead of having your queries spread all along your codebase 😃.

This is how our ListRepository looks like.

class ListRepository < Hanami::Repository
  associations do
    has_many :tasks
  end

  def with_name(name)
    lists.where(name: name).as(List).to_a
  end

  def find_with_tasks(id)
    aggregate(:tasks).where(id: id).as(List).one
  end
end

As you can see, we have with_name that hides the logic of querying our datastore for a List with a given name.

The other important element that we define in the repository are the associations. This is the same concept that we have in ActiveRecord, it defines how our different entities/models relate to each other. This association definitions will be helpful when we want to query data based on multiple tables. At the moment Hanami only supports has_many associations but you can use the develop branch to get belongs_to associations too. Associations are very powerful, they are powered by ROM. At the moment the feature is experimental but I guess this is going to become stable as soon as more people start using Hanami.

Actions (Rails controller actions)

We can move to our final step to get our api working, the actions (same as controller’s methods in Rails).

Rails uses controllers to encapsulate the http requests handlers that your application has. Hanami decided to have one class per action instead of having a controller that groups the actions of a given resource.

Lets use the generators to create the action classes. We can do hanami generate action api 'lists#index' to create the index action.

Generator will add the correct routes in your config/routes file so you don’t have to worry about that.

module Api::Controllers::Lists
  class Index
    include Api::Action
    include JSONAPI::Hanami::Action

    def call(params)
      self.data = list_repository.all
      self.status = 200
    end

    def list_repository
      @list_repository ||= ListRepository.new
    end
  end
end

The important method here is call. This method will be called when the route that is associated with this action is requested. By using the gem gem 'jsonapi-hanami' (don’t forget to add it in your Gemfile) we can serialize our Lists into a valid jsonapi response. For more information on this nice gem take a look at here.

Let’s look at a different action now, one that receives data from the client.

module Api::Controllers::Lists
  class Create
    include Api::Action
    include JSONAPI::Hanami::Action

    deserializable_resource :list

    params do
      required(:list).schema do
        required(:name)
      end
    end

    def call(params)
      self.data = list_repository.create(params[:list])
      self.status = 200
    end

    def list_repository
      @list_repository ||= ListRepository.new
    end
  end
end

In this actions we have some other elements to discuss. First, deserializable_resource is telling the parser to parse the incoming payload looking for a list element, this is needed to parse a jsonapi valid payload. The second important aspect is the validation we are performing on the params. As you can see we are calling a params class method using a special DSL to add constraints to our input data. This is powered by dry-validation. Take a look at the docs for more info on how to validate data, it’s a really powerful tool.

Conclusion

We have seen a very basic overview of the different building blocks that Hanami offers in the post. There is A LOT more in Hanami that I encourage everyone to investigate, community is really awesome and people are very helpful, you can find them on Gitter.

If you want to take a look at the source code you can find it here bilby91/hanami-todo-app

If you have any questions or want to explore different parts of Hanami please write a comment. Im just starting with the framework so looking into the different components of the project is something I will like to do.

Enjoy 🎉

Discover and read more posts from Martín Fernández
get started
post commentsBe the first to share your opinion
Show more replies