Handlers & Actions
Handlers process form submissions, and actions work similarly to process any arbitrary HTTP request.
Overview
Where a page renders a web page in HTML, a handler responds to all other HTTP requests. To respond to such HTTP requests, you'd first create a route, using form
, action
, or path
.
Handler Structure
A handler's initializer is subject to keyword injection, with the addition of the form:
keyword, which, if present, will be an instance of the associated form class, populated with the data in the form submission (not available for path
or action
routes).
You must implement handle
to process the form. A handler's public API, as called by Brut and your tests, is handle!
, however you implement handle
, which handle!
calls.
handle
's return value dictates what will happen next:
Return Value | Behavior |
---|---|
Instance of a page or component | That page or component's HTML is generated and returned. This is not a redirect, but more like render :new in a Rails controller |
Brut::FrontEnd::HttpStatus | This HTTP status is returned with no body. Use http_status from Brut::FrontEnd::HandlingResults (included in all handlers) to create an instance from a number |
URI | Redirect to the URI. Use redirect_to from Brut::FrontEnd::HandlingResults (included in all handlers) to generate a URI from a page class and parameters |
Two element array with element 0 being a Brut::FrontEnd::HttpStatus , and element 1 being a page or component instance | Generates the page or component's HTML, but sets the given status instead of 200. Useful for Ajax responses where the HTTP status affects client-side behavior |
Brut::FrontEnd::Download | Download a file |
Brut::FrontEnd::GenericResponse | wrap any Rack response |
IMPORTANT
The only way to render something other than HTML is to do so as a GenericResponse
, which is basically the low-level Rack API. Brut encourages Ajax responses to be HTML and for you to use the browser's APIs to interact with that HTML. Brut may make it easier to work with other types of content in the future.
Handling a Form Submission
A common pattern when handling a form submisssion is to check for any constraint violations. If there are some, re-generate the HTML for the page containing the form, highlighting the violations. Otherwise, save the data and redirect to another page.
Here's how that looks:
class NewWidgetHandler < AppHandler
def initialize(form:)
@form = form
end
def handle
# if no client-side violations were submitted
if !@form.constraint_violations?
widget = DB::Widget.find(name: form.name)
if widget
@form.server_side_constraint_violation(
input_name: :name,
key: :name_is_taken
)
end
end
if @form.constraint_violations?
NewWidgetPage.new(form: @form)
else
DB::Widget.create(name: form.name,
quantity: form.quantity,
description: form.description)
redirect_to(WidgetsPage)
end
end
end
Unlike a Rails controller, the return value of handle
controls the behavior. As we saw in Form Constraints, the HTML generated by NewWidgetPage
will show constraint violations, so by returning NewWidgetPage.new(form: @form)
, the page has the same form instance, including all the constraint violations, and the visitor will see the problems.
Handling Other Requests
Non-form submissions work similarly, however there is no form available. If the request could potentially involve a visitor-initiated error, you can use the flash to communicate back. The flash is available for injection into the initializer and, assuming your page uses it to show messages, can allow for communication when something is wrong:
class App
# ...
routes do
action "/delete_widget/:widget_id"
# ...
end
end
class DeleteWidgetByIdHandler < AppHandler
def initialize(widget_id:, flash:)
@widget_id = widget_id
@flash = flash
end
def handle
widget = DB::Widget.find!(id: @widget_id)
if widget.can_delete?
widget.delete
@flash.notice = :widget_deleted
redirect_to(WidgetsPage)
else
@flash.alert = :widget_cannot_be_deleted
WidgetsPage.new
end
end
end
Hooks
A handler's public API is handle!
, because it first calls before_handle
. This operates as a before hook, and has the same return values as handle
, with the exception of also recognizing nil
, which indicates processing should proceed to handle
.
Generally, you don't need to implement hooksd on a per-handler basis, but may find it useful in a shared super class to implement cross-cutting behavior.
Testing
See Unit Testing for some basic assumptions and configuration available for all Brut unit tests.
Handler tests should be straightforward: you create your handler, call handle!
(remember, handle!
is the public API, and will call your hooks, which you want in a test), then examine the result returned and any ancillary behavior, such as updated database records.
Some matchers are available to make assertions about handle!
's return value:
have_redirected_to
will check that the handler redirected to a give URI. SeeBrut::SpecSupport::Matchers::HaveRedirectedTo
.have_generated
will check that the handler generated a specific page or component's HTML. See<D-f>Brut::SpecSupport::Matchers::HaveGenerated
.have_returned_http_status
will check that the handler returned an HTTP status. SeeBrut::SpecSupport::Matchers::HaveReturnedHttpStatus
.have_constraint_violation
will check if a form had a particular constraint violation set on it. SeeBrut::SpecSupport::Matchers::HaveConstraintViolation
.
Recommended Practices
You Don't Always Need Resourceful or RESTful Routes
For any code where the browser is performing a submission, use either form
or action
to declare your route. These will both use an HTTP POST
to your server and handler. In the example above, we had a POST
to /delete_widget/:widget_id
, and not, say, a DELETE
to it.
The main reason is that a browser can only submit to a server using GET
or POST
, so there's little value in "tunneling" another verb of POST. It doesn't really matter. And, even though you may use Ajax to submit such data, having it degrade to normal browser-based HTTP is a good practice.
For API-like calls where a browser will never directly interact with the route, and it would only be via a server-to-server call, RESTful routes makes sense. But they don't need to be the default.
Avoid Business Logic in Handlers
Since handlers bridge the gap between HTTP and your app, their API is naturally simplistic and String-based. The handler should defer to business logic (which can be done by either passing the form object directly, or extracting its data and passing that). Based on the response, the handler will then decide what HTTP response is approriate.
This means that your handlers will be relatively simple and their tests will as well. It does mean that their tests may require the use of mocks or stubs, but that's fine. Mocks and stubs exist for a reason.
Technical Notes
IMPORTANT
Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's internals, the source code is always more correct.
Last Updated May 5, 2025
None at this time.