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
:idfrom 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.paramsas 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.
Forward link
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:
- Actions inherit from
Bookshelf::Actionand implement ahandlemethod - Use
request.paramsto access route parameters, query strings, and POST data - Views use a DSL for building HTML safely
- Set
response.format = :jsonand useresponse.bodyfor JSON responses - 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