# Responders: removing respond_to blocks from Rails controllers with I18n-driven flash

> Responders is a Ruby gem from heartcombo that adds four responder modules to Rails controllers, replacing respond_to blocks and controller-level flash strings with a single respond_with call and I18n lookup keys. One setup detail the generator leaves silent: the alert entries for update and destroy actions are commented out in the generated locale file by default, leaving failed updates and destroys without any flash output until those lines are manually uncommented.

**heartcombo/responders** — A set of Rails responders to dry up your application

- Repository: https://github.com/heartcombo/responders
- Stars: 2,059 · Forks: 163
- Language: Ruby
- License: MIT
- Published: 2026-10-11 · Updated: 2026-10-11 · Language: en
- Canonical page: https://hysenlabs.com/projects/heartcombo-responders

## What respond_with removes from a standard Rails controller

In a standard Rails resource controller, each action that handles multiple formats requires a respond_to block. A create action needs one, an update action needs another, and so does destroy. Every block duplicates the same redirect-or-render pattern, and every controller that follows resource conventions adds the same blocks. Applied to a dozen resource controllers, that pattern means hundreds of lines of near-identical code.

The responders gem, maintained under the heartcombo organization, adds respond_with back to Rails controllers as a set of composable modules. When a POST request succeeds, respond_with redirects to the resource's show path; when it fails, the form re-renders automatically. Format negotiation and redirect logic happen inside the responder, not the action.

Four modules ship with the gem: FlashResponder, HttpCacheResponder, CollectionResponder and LocationResponder. Each addresses a different part of the request-response cycle, and none is active until an application wires it in through a responder class.

## FlashResponder reads messages from I18n keys, not from the controller

FlashResponder sets the flash on every non-GET request based on the controller action and the state of the model. A successful POST looks up the I18n key at flash.actions.create.notice. A record with errors on a POST uses flash.actions.create.failure. Flash strings live in the locale file, not in the controller, so changing a message means editing only the locale entry.

By default, it uses :notice and :alert as the flash keys. Both can be replaced across the entire application:

```ruby
config.responders.flash_keys = [ :success, :failure ]
```

A detail in the setup that a quick read-through misses: the generated I18n file comments out the alert keys for update and destroy actions by default. That means a failed update or a failed destroy produces no flash message at all until a developer manually uncomments those lines. The create section is active from the start, but the failure paths for the other two actions are silent until someone edits the locale file. For any application that surfaces validation errors through flash messages rather than inline errors, this is the first thing to check after install.

Flash values can also carry HTML. A key ending in _html tells Rails to render the value without escaping:

```yaml
flash:
  actions:
    create:
      alert_html: "<strong>OH NOES!</strong> You did it wrong!"
  posts:
    create:
      notice_html: "<strong>Yay!</strong> You did it!"
```

The lookup hierarchy descends from controller-specific keys to action-level defaults. For a PostsController create action, the gem checks flash.posts.create.notice before falling back to flash.actions.create.notice. A namespace_lookup option extends this further, searching the full key hierarchy.

## rails g responders:install creates ApplicationResponder and updates ApplicationController

Adding the gem takes three steps, starting with the Gemfile:

```ruby
gem "responders"
```

Then run bundle install and the install generator:

```bash
$ bundle install
$ rails g responders:install
```

The generator creates two files in the repository. The first lands at lib/application_responder.rb:

```ruby
# lib/application_responder.rb
class ApplicationResponder < ActionController::Responder
  include Responders::FlashResponder
  include Responders::HttpCacheResponder
end
```

The second modifies app/controllers/application_controller.rb to require the new file and assign the responder class:

```ruby
# app/controllers/application_controller.rb
require "application_responder"

class ApplicationController < ActionController::Base
  self.responder = ApplicationResponder
  respond_to :html
end
```

With those two files in place, any controller action that calls respond_with will use FlashResponder and HttpCacheResponder. CollectionResponder is not included in the generated file; it must be added to ApplicationResponder by hand. LocationResponder behaves differently from the other three: it is included by default at the gem level, so adding it explicitly to ApplicationResponder issues a deprecation warning. The generated file does not list it for that reason.

For an application adding the gem only for backward compatibility with older Rails response behavior, running bundle install alone is sufficient. The generator step is not required in that case.

## Failure detection reads the errors object, not valid?

Responders does not call valid? to determine whether an action succeeded. It inspects the errors object on the model instead, which is a different contract from the persistence-based checks Rails uses elsewhere. An empty errors object means success; anything in it means failure. This contract differs from what much of Rails assumes, and it affects both controller logic and test assertions.

The direct consequence appears on the create action. Building a record with Widget.new without saving it leaves errors empty:

```ruby
def create
  @widget = Widget.new(widget_params)
  # @widget will be a valid record for responders, as we haven't called `save`
  # on it, and will always redirect to the `widgets_path`.
  respond_with @widget, location: -> { widgets_path }
end
```

The responder sees no errors and redirects, even though the record never reached the database. Calling save or create on the model is what triggers validations and puts errors on it, and only then does the responder re-render the form.

A contrasting example shows errors added manually before the responder runs:

```ruby
def create
  @widget = Widget.new(widget_params)
  @widget.errors.add(:base, :invalid)
  # `respond_with` will render the `new` template again,
  # and set the status based on the configured `error_status`.
  respond_with @widget
end
```

This contract suits applications where service objects or callbacks populate the errors object directly rather than relying on a save call. For test assertions on controller behavior, the errors object, not valid?, is what to assert against.

## LocationResponder handles callable objects and namespaced routes

LocationResponder solves a specific problem with route helpers that require a persisted object. A route like thing_path(@thing) raises an error if @thing has no id, which happens when a create action fails validation and the record was never saved. Passing a lambda for the location defers route evaluation until the responder needs it:

```ruby
class ThingsController < ApplicationController
  respond_to :html

  def create
    @thing = Thing.create(params[:thing])
    respond_with @thing, location: -> { thing_path(@thing) }
  end
end
```

Without the lambda, Rails evaluates thing_path(@thing) immediately at the respond_with call site. The lambda defers evaluation to the moment the responder decides to redirect, so the route helper is only called when the record exists.

For namespaced routes, the namespace symbols must be passed explicitly as the first arguments to respond_with:

```ruby
class Api::V1::ThingsController < ApplicationController
  respond_to :json

  # POST /api/v1/things
  def create
    @thing = Thing.create(thing_params)
    respond_with :api, :v1, @thing
  end
end
```

Without those namespace symbols, the responder cannot find the correct route helper. Among the four modules, LocationResponder alone is included at the gem level by default, without any explicit include. The generated ApplicationResponder does not list it, and adding it explicitly there will issue a deprecation warning.

## HttpCacheResponder and CollectionResponder cover API and collection routing

HttpCacheResponder adds Last-Modified headers to API responses automatically. When a client requests a resource that has not been modified since the client last saw it, the server returns a not_modified status rather than the full body. There are no configuration options documented for this module.

CollectionResponder changes the redirect target for create and update actions. Where Rails defaults to redirecting to the resource's show path on success, CollectionResponder sends the user to the collection path instead. Unlike the other two modules in ApplicationResponder, it is not added by the generator and requires a manual include line.

Both modules can be activated or deactivated per controller using the responders class method:

```ruby
class InvitationsController < ApplicationController
  responders :flash, :http_cache
end
```

This overrides whatever ApplicationResponder includes for that one controller. The cherry-picking method works alongside flash_interpolation_options:

```ruby
def flash_interpolation_options
  { resource_name: @invitation.email }
end
```

With that method defined, the flash message reads "name@example.com was successfully created" instead of "Invitation was successfully created." The method returns a hash, and any key in it is available as an interpolation variable in the I18n flash string.

## verify_requested_format! stops side effects before the action runs

There is a gap in how respond_with handles unsupported formats. When a client sends a request with a MIME type not declared through the class-level respond_to call, the action still executes before respond_with raises ActionController::UnknownFormat. A POST to a JSON-only endpoint with an HTML Content-Type header will create the record in the database before the exception is raised.

The gem provides verify_requested_format! to close this gap. Adding it as a before_action raises the exception before the action body runs:

```ruby
class WidgetsController < ApplicationController
  respond_to :json
  before_action :verify_requested_format!
```

With that filter in place, a request with an unsupported format stops before the controller action executes. No records are created and no side effects occur, because the action never runs.

This matters most for write actions in API controllers. A misconfigured client or a proxy that rewrites the Content-Type header will silently trigger database writes without getting any useful response back if this filter is absent. The code example in the README is truncated before the complete method body, so the full before_action wiring is not shown there.

Three releases appear in the repository listing: v3.1.1 from October 2023, v3.2.0 from October 2025 and v3.2.1 from 2026-08-27; the last push was on 2026-10-06, and the gem carries an MIT license.

## Conclusion

Responders suits Rails applications that want to remove repeated respond_to blocks and flash string assignments from resource controllers. Run the install generator, then check the generated locale file: the alert keys for update and destroy are commented out by default, so failed updates produce no flash message until those lines are uncommented. For API controllers, add verify_requested_format! as a before_action on write actions. Skip it when controllers bypass respond_with with custom response handling. Before writing test assertions, confirm that actions call save before respond_with, because the responder checks errors, not valid?.

## FAQ

### What does the responders gem add to Rails?

Responders adds four responder modules: FlashResponder, HttpCacheResponder, CollectionResponder and LocationResponder. They let controllers use respond_with instead of repeated respond_to blocks, and flash messages are resolved through I18n keys rather than hardcoded strings in the controller.

### How do I install the responders gem in a Rails application?

Add gem "responders" to your Gemfile and run bundle install. Then run rails g responders:install to generate ApplicationResponder in lib/ and update ApplicationController to assign the new responder class.

### Why does my create action redirect even when validation fails with respond_with?

Responders checks whether the model's errors object is empty, not whether valid? returns true. A record built with Widget.new but never saved has no errors, so the responder treats it as success. Call save before respond_with to trigger validations and populate the errors object.

### Does the responders gem work with namespaced routes?

Yes. Pass the namespace symbols as the first arguments to respond_with, such as respond_with :api, :v1, @thing. LocationResponder uses those symbols to construct the correct route helper.

### What flash keys does FlashResponder use by default?

FlashResponder uses :notice and :alert by default. Both can be changed application-wide with config.responders.flash_keys = [ :success, :failure ] in the application configuration.

## Sources

- [heartcombo/responders on GitHub](https://github.com/heartcombo/responders)
- [Issues](https://github.com/heartcombo/responders/issues)
- [License: MIT](https://github.com/heartcombo/responders/blob/main/LICENSE)
- [README](https://github.com/heartcombo/responders/blob/main/README.md)
- [Releases](https://github.com/heartcombo/responders/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/heartcombo-responders
