Introduction: Getting Started

Hello. If you're reading this page, it's very likely that you want to learn more about Hanami.
That's great, congrats! If you're looking for new ways to build maintainable, secure, faster and testable web applications, you're in good hands.

Hanami is built for people like you.

I warn you that whether you're a total beginner or an experienced developer this learning process can be hard.
Over time, we build expectations about how things should be, and it can be painful to change. But without change, there is no challenge and without challenge, there is no growth.

Sometimes a feature doesn't look right, that doesn't mean it's you.
It can be a matter of formed habits, a design fallacy or even a bug.

Myself and the rest of the Community are putting best efforts to make Hanami better every day.

In this guide we will set up our first Hanami project and build a simple bookshelf web application.
We'll touch on all the major components of Hanami framework, all guided by tests.

If you feel alone, or frustrated, don't give up, jump in our chat and ask for help.
There will be someone more than happy to talk with you.

Enjoy,
Luca GuidiHanami creator

Prerequisites

Before we get started, let’s get some prerequisites out of the way.
First, we’re going to assume a basic knowledge of developing web applications.

apps contains one or more web applications compatible with Rack, where we can find the first generated Hanami application called web.
It’s the place where we find our controllers, views, routes and, templates.

config contains configuration files.

config.ru is for Rack servers.

db contains our database schema and migrations.

lib contains our business logic and domain model, including entities and repositories.

public will contain compiled static assets.

spec contains our tests.

Go ahead and install our gem dependencies with Bundler; then we can launch a development server:

$ bundle install
$ bundle exec hanami server

And… bask in the glory of your first Hanami project at
http://localhost:2300! We should see a screen similar to this:

Hanami’s Architecture

Hanami’s architecture revolves around your project containing many apps.
These all live together in the same codebase, and exist in the same Ruby process.

They live under apps/.

By default, we have a web app, which can be thought of as the standard, user-facing web interface.
This is the most popular, so you’ll probably want to keep it in your future Hanami projects.
However, there’s nothing unique about this app, it’s just so common that Hanami generates it for us.

Later (in a real project), we would add other apps, such as an admin panel, a JSON api, or an analytics dashboard.
We could also break our web app into smaller parts, extracting isolated pieces of functionality.
Hanami fully supports that, too!

Different apps represent delivery mechanisms.
That means they’re different ways of interacting with the core of your project, or the “business logic”.

Hanami doesn’t want us to repeat ourselves therefore “business logic” is shared.
Web applications almost always store and interact with data stored in a database.
Both our “business logic” and our persistence live in lib/.

Hanami is ready for a Behavior Driven Development (BDD) workflow out of the box, but it is in no way bound to any particular testing framework.
It does not come with any special testing integrations or libraries, so if you know RSpec (or Minitest), there’s nothing new to learn there.

We have to migrate our schema in the test database by running:

$ HANAMI_ENV=test bundle exec hanami db prepare

The HANAMI_ENV at the beginning is an environment variable, which tells Hanami to use the test environment.
This is necessary here because the default is HANAMI_ENV=development.

This is an empty action that doesn’t do anything special.
Each action in Hanami is defined by a single class, which makes it simple to test.
Moreover, each action has a corresponding view, which is also defined by its class.
This one needs to be added in order to complete the request.

Generating New Actions

Let’s use our new knowledge about Hanami routes, actions, views, and templates.

The purpose of our sample Bookshelf project is to manage books.

We’ll store books in our database and let the user manage them with our project.

Our first step is to list out all the books we know about.

Let’s write a new feature test describing what we want to achieve:

# spec/web/features/list_books_spec.rbrequire'features_helper'RSpec.describe'List books'doit'displays each book on the page'dovisit'/books'within'#books'doexpect(page).tohave_css('.book',count:2)endendend

This test means that when we go to /books,
we’ll see two HTML elements that have class book,
and both will be inside of an HTML element that has an id of books.

Our test suite is Unable to find visible css "#books".

Not only are we missing that element,
we don’t even have a page to put that element on!

Let’s create a new action to fix that.

Hanami Generators

Hanami ships with a number of generators, which are tools that write some code for you.

In our terminal, let’s run:

$ bundle exec hanami generate action web books#index

If you're using ZSH and that doesn't work
(with an error like zsh: no matches found: books#index),
Hanami lets us write this instead: hanami generate action web books/index

This does a lot for us:

Creating an action at apps/web/controllers/books/index.rb (and spec for it),

Creating a view at apps/web/views/books/index.rb (and a spec for it),

Creating a template at apps/web/templates/books/index.html.erb.

(If you’re confused by ‘action’ vs. ‘controller’: Hanami only has action classes, so a controller is just a module to group several related actions together.)

These files are all pretty much empty.
They have some basic code in there, so Hanami knows how to use the class.
Thankfully we don’t have to manually create those five files,
with that specific code in them.

The generator also adds a new route for us in the web app’s routes file (apps/web/config/routes.rb).

get'/books',to:'books#index'

To make our tests pass,
we need to edit our newly generated template file in apps/web/templates/books/index.html.erb:

And remove the duplicate lines from the other templates,
since they’re duplicated now.

A layout template is like any other template, but it is used to wrap your regular templates.
The yield line is replaced with the contents of our regular template.
It’s the perfect place to put our repeating headers and footers.

Hanami repositories have methods to load one or more entities from our database, and to create and update existing records.
The repository is also the place where you would define new methods to implement custom queries.

To recap, we’ve seen how Hanami uses entities and repositories to model our data.
Entities represent our behavior, while repositories use mappings to translate our entities to our data store.
We can use migrations to apply changes to our database schema.

Displaying Dynamic Data

With our new experience modeling data, we can get to work displaying dynamic data on our book listing page.
Let’s adjust the feature test we created earlier:

# spec/web/features/list_books_spec.rbrequire'features_helper'RSpec.describe'List books'dolet(:repository){BookRepository.new}beforedorepository.clearrepository.create(title:'PoEAA',author:'Martin Fowler')repository.create(title:'TDD',author:'Kent Beck')endit'displays each book on the page'dovisit'/books'within'#books'doexpect(page).tohave_selector('.book',count:2),'Expected to find 2 books'endendend

We create the required records in our test and then assert the correct number of book classes on the page.
When we run this test, it should pass. If it does not pass, a likely reason is that the test database was not migrated.

Now we can change our template and remove the static HTML.
Our view needs to loop over all available records and render them.
Let’s write a test to force this change in our view:

# spec/web/views/books/index_spec.rbRSpec.describeWeb::Views::Books::Indexdolet(:exposures){Hash[books:[]]}let(:template){Hanami::View::Template.new('apps/web/templates/books/index.html.erb')}let(:view){described_class.new(template,exposures)}let(:rendered){view.render}it'exposes #books'doexpect(view.books).toeq(exposures.fetch(:books))endcontext'when there are no books'doit'shows a placeholder message'doexpect(rendered).toinclude('<p class="placeholder">There are no books yet.</p>')endendcontext'when there are books'dolet(:book1){Book.new(title:'Refactoring',author:'Martin Fowler')}let(:book2){Book.new(title:'Domain Driven Design',author:'Eric Evans')}let(:exposures){Hash[books:[book1,book2]]}it'lists them all'doexpect(rendered.scan(/class="book"/).length).toeq(2)expect(rendered).toinclude('Refactoring')expect(rendered).toinclude('Domain Driven Design')endit'hides the placeholder message'doexpect(rendered).to_notinclude('<p class="placeholder">There are no books yet.</p>')endendend

We specify that our index page will show a simple placeholder message when there are no books to display; when there are, it lists every one of them.
Note how rendering a view with some data is relatively straight-forward.
Hanami is designed around simple objects with minimal interfaces that are easy to test in isolation, yet still work great together.

Writing tests for controller actions is basically two-fold: you either assert on the response object, which is a Rack-compatible array of status, headers, and content; or on the action itself, which will contain exposures after we’ve called it.
Now we’ve specified that the action exposes :books, we can implement our action:

By using the expose method in our action class, we can expose the contents of our @books instance variable to the outside world, so that Hanami can pass it to the view.
That’s enough to make all our tests pass again!

Implementing Create Action

Our books#create action needs to do two things.
Let’s express them as unit tests:

# spec/web/controllers/books/create_spec.rbRSpec.describeWeb::Controllers::Books::Createdolet(:action){described_class.new}let(:params){Hash[book:{title:'Confident Ruby',author:'Avdi Grimm'}]}let(:repository){BookRepository.new}beforedorepository.clearendit'creates a new book'doaction.call(params)book=repository.lastexpect(book.id).to_notbe_nilendit'redirects the user to the books listing'doresponse=action.call(params)expect(response[0]).toeq(302)expect(response[1]['Location']).toeq('/books')endend

Making these tests pass is easy enough.
We’ve already seen how we can write entities to our database, and we can use redirect_to to implement our redirection:

Securing Our Form With Validations

Hold your horses! We need some extra measures to build a truly robust form.
Imagine what would happen if the user were to submit the form without entering any values?

We could fill our database with bad data or see an exception for data integrity violations.
We clearly need a way of keeping invalid data out of our system!

To express our validations in a test, we need to wonder: what would happen if our validations failed?
One option would be to re-render the books#new form, so we can give our users another shot at completing it correctly.
Let’s specify this behaviour as unit tests:

Now our tests specify two alternative scenarios: our original happy path, and a new scenario in which validations fail.
To make our tests pass, we need to implement validations.

Although you can add validation rules to the entity, Hanami also allows you to define validation rules as close to the source of the input as possible, i.e., the action.
Hanami controller actions can use the params class method to define acceptable incoming parameters.

This approach both whitelists what params are used (others are discarded to prevent mass-assignment vulnerabilities from untrusted user input) and adds rules to define what values are acceptable — in this case, we’ve specified that the nested attributes for a book’s title and author should be present.

With our validations in place, we can limit our entity creation and redirection to cases where the incoming params are valid:

When the params are valid, the Book is created, and the action redirects to a different URL.
However, when the params are not valid, what happens?

First, the HTTP status code is set to
422 (Unprocessable Entity).
Then the control will pass to the corresponding view, which needs to know which template to render.
In this case, apps/web/templates/books/new.html.erb will be used to render the form again.

This approach will work nicely because Hanami’s form builder is smart enough to inspect the params in this action and populate the form fields with values found in the params.
If the user fills in only one field before submitting, they are presented with their original input, saving them the frustration of typing it again.

Run your tests again and see they are all passing again!

Displaying Validation Errors

Rather than just shoving the user a form under their nose when something has gone wrong, we should give them a hint of what’s expected of them. Let’s adapt our form to show a notice about invalid fields.

First, we expect a list of errors to be included in the page when params contains errors:

# spec/web/views/books/new_spec.rbRSpec.describeWeb::Views::Books::Newdolet(:params){OpenStruct.new(valid?:false,error_messages:['Title must be filled','Author must be filled'])}let(:exposures){Hash[params:params]}let(:template){Hanami::View::Template.new('apps/web/templates/books/new.html.erb')}let(:view){described_class.new(template,exposures)}let(:rendered){view.render}it'displays list of errors when params contains errors'doexpect(rendered).toinclude('There was a problem with your submission')expect(rendered).toinclude('Title must be filled')expect(rendered).toinclude('Author must be filled')endend

We should also update our feature spec to reflect this new behavior:

# spec/web/features/add_book_spec.rbrequire'features_helper'RSpec.describe'Add a book'do# Spec written earlier omitted for brevityit'displays list of errors when params contains errors'dovisit'/books/new'within'form#book-form'doclick_button'Create'endexpect(current_path).toeq('/books')expect(page).tohave_content('There was a problem with your submission')expect(page).tohave_content('Title must be filled')expect(page).tohave_content('Author must be filled')endend

In our template, we can loop over params.error_messages (if there are any) and display a friendly message.
Open up apps/web/templates/books/new.html.erb:

The output for hanami routes shows you the name of the defined helper method (you can suffix this name with _path or _url and call it on the routes helper), the allowed HTTP method, the path and finally the controller action that will be used to handle the request.

Now we’ve applied the resources helper method; we can take advantage of the named route methods.
Remember how we built our form using form_for?

It’s silly to include a hard-coded path in our template when our router is already perfectly aware of which route to point the form to.
We can use the routes helper method that is available in our views and actions to access route-specific helper methods:

Wrapping Up

Congratulations on completing your first Hanami project!

Let’s review what we’ve done: we’ve traced requests through Hanami’s major frameworks to understand how they relate to each other; we’ve seen how we can model our domain using entities and repositories; we’ve seen solutions for building forms, maintaining our database schema, and validating user input.