Skip to content

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 ValueBehavior
Instance of a page or componentThat 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::HttpStatusThis 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
URIRedirect 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 instanceGenerates 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::DownloadDownload a file
Brut::FrontEnd::GenericResponsewrap 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:

ruby
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:

ruby
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:

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.