rubyguides

Hanami Actions and Views: Handle Requests and Render Responses

Hanami actions and views form the request-response backbone of every Hanami application. In our Getting Started with Hanami 2 guide, we set up a new Hanami application and explored the basic project structure. Now it is time to dive into one of the most important concepts in any web framework: how to handle requests and return responses.

Hanami uses a pattern called actions for handling HTTP requests and views for rendering responses. This separation of concerns keeps your code clean and maintainable. In this guide, you’ll learn how to create actions, work with parameters, render templates, and return JSON responses.

Intro context

This is the point in the Hanami basics series where the framework starts to feel like a working web app instead of just a project skeleton. Actions receive requests, views shape the output, and the route table connects the two. If you understand that flow, the rest of the framework becomes much easier to place in your head.

The page assumes you already have a Hanami app running from the previous guide. If you do, this lesson will show you how requests move through the app and how the code stays divided between request handling and presentation.

Key takeaways

  • Actions are the entry point for HTTP requests.
  • Views keep rendering logic separate from request handling.
  • Route parameters and query parameters both land in request.params.
  • JSON responses are a natural fit when the client does not need HTML.

Understanding actions in Hanami

Actions are the entry point for HTTP requests in Hanami. Each action corresponds to a specific URL and HTTP method combination. When a request comes in, Hanami dispatches it to the appropriate action, which then processes the request and returns a response.

Here’s a simple action in Hanami:

# app/actions/books/index.rb
module Bookshelf
  module Actions
    module Books
      class Index < Bookshelf::Action
        def handle(request, response)
          response[:books] = ['Ruby Programming', 'Rails 7 Guide']
        end
      end
    end
  end
end

The action inherits from Bookshelf::Action, which provides the handle method with two parameters: the request object and the response object. The request contains all the information about the incoming HTTP request, while the response is what you’ll use to build the response.

That split matters because it keeps the job of the action clear. The action decides what should happen, and the response object stores the result. In practice, you can keep the request logic in one place without mixing it with markup or data formatting that belongs later in the pipeline.

Routing and Parameters

Hanami uses a DSL for defining routes. Each route maps a URL pattern to an action. Let’s look at how to set up routing:

# config/routes.rb
module Bookshelf
  class Routes
    define do
      root to: 'home#index'
      
      get '/books', to: 'books#index'
      get '/books/:id', to: 'books#show'
      post '/books', to: 'books#create'
      put '/books/:id', to: 'books#update'
      delete '/books/:id', to: 'books#destroy'
    end
  end
end

The :id part of the route is a route parameter. You can access it in your action through the request object:

# app/actions/books/show.rb
module Bookshelf
  module Actions
    module Books
      class Show < Bookshelf::Action
        def handle(request, response)
          book_id = request.params[:id]
          
          book = BookRepository.new.find(book_id)
          
          if book
            response[:book] = book
          else
            response.status = 404
            response[:error] = 'Book not found'
          end
        end
      end
    end
  end
end

The request.params method gives you access to all parameters, including:

  • Route parameters (like :id from the URL)
  • Query string parameters (like ?page=2)
  • POST body parameters (from form submissions)

Query parameters vs route parameters

Hanami combines all types of parameters into a single params object. Here’s how to work with query parameters:

# app/actions/books/index.rb
module Bookshelf
  module Actions
    module Books
      class Index < Bookshelf::Action
        def handle(request, response)
          page = request.params[:page] || 1
          per_page = request.params[:per_page] || 10
          
          books = BookRepository.new.all(paginate: (page.to_i - 1) * per_page.to_i, limit: per_page.to_i)
          
          response[:books] = books
          response[:pagination] = {
            page: page.to_i,
            per_page: per_page.to_i
          }
        end
      end
    end
  end
end

Now you can filter books using URLs like /books?page=2&per_page=25.

That example shows how Hanami keeps route definitions simple while still providing the parameters you need. The URL stays readable, the action stays small, and request.params gives you one place to inspect every input — route segments, query strings, and POST bodies all arrive through the same interface.

Working with Views

In Hanami, actions and views are separate components. The action handles the request logic, and the view handles rendering the response. This separation of concerns keeps your code clean and easier to test.

To render a view from your action, use the render method:

# app/actions/books/index.rb
module Bookshelf
  module Actions
    module Books
      class Index < Bookshelf::Action
        def handle(request, response)
          books = BookRepository.new.all
          
          render response, :index, books: books
        end
      end
    end
  end
end

The corresponding view would look like this:

# app/views/books/index.rb
module Bookshelf
  module Views
    module Books
      class Index < Bookshelf::View
        def template
          html do
            ul do
              books.each do |book|
                li book.title
              end
            end
          end
        end
      end
    end
  end
end

Hanami views use a DSL that lets you build HTML programmatically. This is safer than using inline ERB templates because it’s less prone to XSS attacks.

The separation between actions and views also makes testing easier. You can verify the request handling without rendering HTML, or you can verify the view output without worrying about where the data came from. That makes each test narrower and easier to understand.

Returning JSON Responses

Modern web applications often need to return JSON instead of HTML. Hanami makes this straightforward:

# app/actions/books/show.rb
module Bookshelf
  module Actions
    module Books
      class Show < Bookshelf::Action
        def handle(request, response)
          book = BookRepository.new.find(request.params[:id])
          
          if book
            response.format = :json
            response.body = JSON.generate({
              id: book.id,
              title: book.title,
              author: book.author,
              published_year: book.published_year
            })
          else
            response.status = 404
            response.format = :json
            response.body = JSON.generate({ error: 'Book not found' })
          end
        end
      end
    end
  end
end

For better organization, you can also create a dedicated JSON view:

# app/views/books/json_show.rb
module Bookshelf
  module Views
    module Books
      class JsonShow < Bookshelf::View
        format :json
        
        def template
          render json: {
            id: book.id,
            title: book.title,
            author: book.author
          }
        end
      end
    end
  end
end

When you return JSON, the goal is usually to give the client exactly the data it needs and no more. A dedicated JSON view keeps that shape obvious, which is handy when the same action might also have an HTML version for browsers. The response format stays explicit, and the caller does not need to guess how the payload is built.

Handling form submissions

Let’s see how to handle a POST request with form data:

# app/actions/books/create.rb
module Bookshelf
  module Actions
    module Books
      class Create < Bookshelf::Action
        def handle(request, response)
          book_params = request.params[:book]
          
          if book_params[:title].nil? || book_params[:title].empty?
            response.status = 422
            response[:errors] = { title: ['is required'] }
            return
          end
          
          book = BookRepository.new.create(book_params)
          
          response.status = 201
          response[:book] = book
        end
      end
    end
  end
end

When handling form submissions, the action receives nested parameters under a key like book. Rails developers will recognize this pattern from strong parameters. Hanami keeps validation inside the action itself, which means you can check required fields and return meaningful error statuses before any data hits the persistence layer.

The form would submit to /books with the parameters nested under the book key:

<form action="/books" method="POST">
  <input type="text" name="book[title]" />
  <input type="text" name="book[author]" />
  <button type="submit">Create Book</button>
</form>

Common mistakes

  • Putting rendering logic directly into the action when a view would be clearer.
  • Treating request.params as a single source of truth without checking which fields are actually required.
  • Returning too much JSON when a smaller payload would be easier for the client to use.
  • Letting the action grow until it handles routing, validation, persistence, and formatting all at once.

Frequently asked questions

Should every action have a view?

No. If an endpoint only needs to return JSON or a simple status response, a separate view may not be necessary. Use the extra layer when it makes the rendering logic easier to reason about.

When should I switch from HTML to JSON?

Choose JSON when the consumer is another application, a JavaScript frontend, or a client that does not need markup. HTML is still the better fit when the browser should render the page directly.

How do I keep actions thin?

Move presentation into views, keep validation small, and push multi-step business work into a separate object when the action starts to feel crowded.

When to use actions and views

Use actions when you need to:

  • Process HTTP requests
  • Validate input parameters
  • Interact with your database or external services
  • Return different response types (HTML, JSON, etc.)

Use views when you need to:

  • Render HTML templates
  • Format data for display
  • Reuse presentation logic across multiple pages

The key principle is that actions should be thin — they handle request routing and coordination, while views handle the presentation logic. This makes your application easier to test and maintain.

If you want to keep learning the rest of the Hanami basics series, the next topic is persistence with ROM. After you understand actions and views, the database layer will make more sense because you already know where request data enters the app and where rendered output leaves it.

Summary

You’ve learned how to create actions and views in Hanami 2. Actions handle incoming HTTP requests and route them to the appropriate logic. Views handle rendering the response, whether it’s HTML or JSON. The key takeaways are:

  1. Actions inherit from Bookshelf::Action and implement a handle method
  2. Use request.params to access route parameters, query strings, and POST data
  3. Views use a DSL for building HTML safely
  4. Set response.format = :json and use response.body for JSON responses
  5. Keep actions thin — delegate presentation logic to views

In the next tutorial, we’ll explore persistence with ROM in Hanami, showing you how to interact with databases using Hanami’s data mapping layer.


Next Tutorial: Persistence with ROM in Hanami