The project is in a healthy, maintained state
BetterController provides tools and utilities to improve Rails controllers, making them more maintainable and feature-rich.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 6.0
~> 1.50
~> 0.22

Runtime

 Project Readme

๐ŸŽฎ BetterController

Gem Version License: MIT Ruby

A Ruby gem for building modern Rails controllers with a declarative DSL, Hotwire/Turbo support, and ViewComponent integration.

๐Ÿ’ก Why BetterController?

After seeing too many Rails projects with business logic scattered across controllers, I created BetterController. It's part of the Better gems family with a simple mission: enforce separation of concerns and provide a clear standard for every project. Lean controllers, services where they belong, and a DSL that makes everything readable.

โœจ Features

  • Declarative Action DSL - Define controller actions with a clean, expressive syntax
  • Hotwire/Turbo Support - Built-in support for Turbo Frames and Turbo Streams
  • ViewComponent Integration - Seamless rendering of ViewComponents with page configurations
  • Flexible Response Handling - Unified handling of HTML, Turbo Stream, and JSON responses
  • Error Handling - Comprehensive error classification and response formatting
  • BYOS (Bring Your Own Services/Serializers) - Use any service pattern or serializer you prefer

๐Ÿ“ฆ Installation

Add to your Gemfile:

gem 'better_controller'

Then run:

bundle install

Optional Dependencies

For full Turbo and ViewComponent support:

gem 'turbo-rails', '>= 1.0'
gem 'view_component', '>= 3.0'

๐Ÿš€ Quick Start

Basic Setup

Include BetterController in your controller:

class UsersController < ApplicationController
  include BetterController

  action :index do
    service Users::IndexService
  end

  action :show do
    service Users::ShowService
  end

  action :create do
    service Users::CreateService

    on_success do
      html { redirect_to users_path }
      turbo_stream do
        prepend :users_list
        update :users_count
      end
    end

    on_error :validation do
      render_page status: :unprocessable_entity
    end
  end
end

Service Integration

BetterController works seamlessly with any service pattern you prefer. Services should return a result hash:

# Use any service pattern: Interactor, Trailblazer, simple PORO, etc.
class Users::IndexService
  def call
    users = User.all.order(created_at: :desc)

    {
      success: true,
      collection: users,
      page_config: {
        type: :index,
        title: 'Users',
        items: users
      }
    }
  end
end

๐ŸŽฏ Action DSL

Defining Actions

Use the action method to define controller actions:

action :index do
  service Users::IndexService
end

Service Configuration

Specify which service handles the action:

action :show do
  service Users::ShowService

  # Optional: transform params before passing to service
  params_mapping do |params|
    { id: params[:id], include_posts: true }
  end
end

๐Ÿ“ค Response Handlers

Define how to respond based on success or failure:

action :create do
  service Users::CreateService

  on_success do
    html { redirect_to :index, notice: 'User created' }
    turbo_stream do
      prepend :users_list, partial: 'users/user'
      update :flash
    end
    json { render json: @result }
  end

  on_error :validation do
    html { render_page status: :unprocessable_entity }
    turbo_stream do
      replace :user_form
      update :form_errors
    end
    json { render json: @result, status: :unprocessable_entity }
  end

  on_error :not_found do
    html { redirect_to users_path, alert: 'User not found' }
  end
end

โš ๏ธ Error Types

Built-in error classifications:

  • :validation - ActiveRecord::RecordInvalid
  • :not_found - ActiveRecord::RecordNotFound
  • :authorization - Authorization failures
  • :any - Catch-all for other errors

๐Ÿ–ผ๏ธ Page and Component Rendering

Render ViewComponents directly:

action :dashboard do
  component DashboardComponent
end

action :help do
  page HelpPage
end

๐Ÿ“‹ Action DSL Reference

Complete list of all available parameters in the action block:

โš™๏ธ Service Configuration

Parameter Type Description
service(klass, method:) Class, Symbol Service class to call. Default method: :call
params_key(key) Symbol Key for strong parameters (e.g., :user)
permit(*attrs) Array Permitted attributes for strong parameters
action :create do
  service Users::CreateService, method: :execute
  params_key :user
  permit :name, :email, :role, address: [:street, :city]
end

๐Ÿ–ผ๏ธ Page and Component

Parameter Type Description
page(klass) Class Page class that generates page_config via #to_config
component(klass, locals:) Class, Hash ViewComponent to render directly
page_config(&block) Block Modifier block for page_config from service
action :dashboard do
  service Users::DashboardService
  page Users::DashboardPage  # Fallback if service has no viewer
end

action :profile do
  component Users::ProfileComponent, locals: { show_avatar: true }
end

action :admin_index do
  service Users::IndexService
  page_config do |config|
    config[:title] = "Admin Users"
  end
end

๐Ÿ“ค Response Handlers

Parameter Type Description
on_success(&block) Block Handler for successful service result
on_error(type, &block) Symbol, Block Handler for specific error type

Error types: :validation, :not_found, :authorization, :any

action :create do
  service Users::CreateService

  on_success do
    html { redirect_to users_path, notice: 'Created!' }
    turbo_stream do
      prepend :users_list
      update :flash
    end
    json { render json: @result, status: :created }
  end

  on_error :validation do
    html { render_page status: :unprocessable_entity }
    turbo_stream { replace :user_form }
  end

  on_error :not_found do
    html { redirect_to users_path, alert: 'Not found' }
  end

  on_error :any do
    html { render_page status: :internal_server_error }
  end
end

๐Ÿ”„ Callbacks

Parameter Type Description
before(&block) Block Callback executed before the action
after(&block) Block Callback executed after the action (receives result)
action :update do
  before { @original_user = User.find(params[:id]).dup }
  service Users::UpdateService
  after { notify_changes(@original_user, @result[:resource]) }
end

๐Ÿ” Authentication and Authorization

Parameter Type Description
skip_authentication(value) Boolean Skip authentication for this action (default: true)
skip_authorization(value) Boolean Skip authorization for this action (default: true)
action :public_profile do
  skip_authentication
  skip_authorization
  service Users::PublicProfileService
end

โšก Turbo Support

Turbo Frame Handler

Handle Turbo Frame requests with explicit control:

on_success do
  html { render_page }

  turbo_frame do
    component Users::ListComponent, locals: { title: 'Users' }
  end
end

The turbo_frame {} handler supports:

  • component(klass, locals: {}) - Render a ViewComponent
  • partial(path, locals: {}) - Render a partial
  • render_page(status: :ok) - Render using page config
  • layout(true/false) - Control layout rendering (default: false)

When no turbo_frame {} handler is defined, it falls back to html {}.

Turbo Stream Actions

Build Turbo Stream responses declaratively:

on_success do
  turbo_stream do
    append :notifications, partial: 'shared/notification'
    prepend :items_list
    replace :item_counter
    update :flash
    remove :loading_spinner
  end
end

Turbo Frame Detection

Check request context in your actions:

def show
  if turbo_frame_request?
    render partial: 'user_card', locals: { user: @user }
  else
    render :show
  end
end

Available Helpers

turbo_frame_request?    # Is this a Turbo Frame request?
turbo_stream_request?   # Is this a Turbo Stream request?
current_turbo_frame     # Get the Turbo Frame ID
turbo_native_app?       # Is this from a Turbo Native app?

Stream Helpers

Build individual streams:

stream_append(:list, partial: 'item')
stream_prepend(:list, partial: 'item')
stream_replace(:item, partial: 'item')
stream_update(:counter, partial: 'counter')
stream_remove(:notification)
stream_before(:item, partial: 'new_item')
stream_after(:item, partial: 'new_item')

๐Ÿงฉ ViewComponent Integration

Page Config Rendering

When your service returns a page_config, BetterController automatically resolves and renders the appropriate component:

# Service returns:
{
  success: true,
  page_config: {
    type: :index,
    title: 'Users',
    items: users
  }
}

# BetterController looks for:
# Templates::Index::PageComponent

Component Rendering Helpers

Render components directly:

render_component UserCardComponent, locals: { user: @user }
render_component_to_string AvatarComponent, locals: { user: @user }
render_component_collection users, UserRowComponent, item_key: :user

Default Locals

Components automatically receive:

  • current_user - If available
  • page_config - The page configuration
  • result - The service result
  • resource - Single resource from result
  • collection - Collection from result

โš™๏ธ Configuration

Create an initializer:

# config/initializers/better_controller.rb
BetterController.configure do |config|
  # API version included in all responses (default: 'v1')
  config.api_version = 'v1'

  # ViewComponent namespace for page types
  config.html_page_component_namespace = 'Templates'

  # Pagination settings
  config.pagination_enabled = true
  config.pagination_per_page = 25

  # Error handling
  config.error_handling_log_errors = true
  config.error_handling_detailed_errors = Rails.env.development?

  # Turbo settings
  config.turbo_enabled = true
  config.turbo_auto_flash = true
  config.turbo_auto_form_errors = true

  # HTML partials
  config.html_flash_partial = 'shared/flash'
  config.html_form_errors_partial = 'shared/form_errors'
end

๐Ÿ” Search/Filter Pattern

Handle initial page load and subsequent filter updates:

action :index do
  service Users::IndexService

  on_success do
    html { render_page }

    turbo_stream do
      replace :users_table, partial: 'users/table'
      update :users_count
      update :active_filters
      update :pagination
    end
  end
end

The HTML request renders the full page, while Turbo Stream requests update only the changed elements.

๐Ÿ”Œ API Controllers

For JSON APIs, use BetterControllerApi:

class Api::UsersController < ApplicationController
  include BetterControllerApi

  def index
    users = User.all
    respond_with_success(users)
  end

  def show
    user = User.find(params[:id])
    respond_with_success(user)
  rescue ActiveRecord::RecordNotFound => e
    respond_with_error(e, status: :not_found)
  end

  def create
    user = User.create!(user_params)
    respond_with_success(user, status: :created)
  rescue ActiveRecord::RecordInvalid => e
    respond_with_error(e, status: :unprocessable_entity)
  end
end

Response Format

All API responses follow a consistent structure with data and meta:

Success response:

{
  "data": { "id": 1, "name": "John" },
  "meta": { "version": "v1" }
}

Error response:

{
  "data": {
    "error": {
      "type": "ActiveRecord::RecordNotFound",
      "message": "Couldn't find User with id=999"
    }
  },
  "meta": { "version": "v1" }
}

The meta.version is configurable (see Configuration section).

๐Ÿ› ๏ธ Utilities

Parameter Validation

class UsersController < ApplicationController
  include BetterController::Utils::ParameterValidation

  requires_params :create, :name, :email

  param_schema :update, {
    name: { required: true, type: String },
    age: { type: Integer },
    role: { in: %w[admin user guest] }
  }
end

Pagination

class UsersController < ApplicationController
  include BetterController::Utils::Pagination

  def index
    users = paginate(User.all)
    respond_with_pagination(users)
  end
end

Logging

class UsersController < ApplicationController
  include BetterController::Utils::Logging

  def create
    log_info('Creating user', user_email: params[:email])
    # ...
  rescue => e
    log_exception(e)
    raise
  end
end

๐Ÿงช Testing

BetterController is fully tested with RSpec. Run the test suite:

# Unit tests
bundle exec rspec spec/better_controller

# Integration tests (require Rails)
INTEGRATION_TESTS=true bundle exec rspec spec/integration spec/generators

Current coverage: 98%+ with 587 examples.

๐Ÿ“Œ Requirements

  • Ruby >= 3.0
  • Rails >= 6.0
  • Optional: turbo-rails >= 1.0
  • Optional: view_component >= 3.0

๐Ÿค Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/my-feature)
  3. Write tests for your changes
  4. Ensure all tests pass (bundle exec rspec)
  5. Ensure code style compliance (bundle exec rubocop)
  6. Commit your changes (git commit -am 'Add new feature')
  7. Push to the branch (git push origin feature/my-feature)
  8. Create a Pull Request

๐Ÿ“„ License

This gem is available as open source under the terms of the MIT License.

๐Ÿ‘ค Author

Alessio Bussolari - alessio.bussolari@pandev.it