Crudable Rails
crudable-rails is a Ruby on Rails gem that provides a set of helpers and modules to simplify the implementation of CRUD operations in Rails controllers.
It uses the following gems to enhance the functionality:
Punditfor authorizationKaminarifor paginationFriendlyIdfor friendly findersHasScopefor filteringDiscardfor soft deletion
Installation
Add this line to your application's Gemfile:
gem "crudable-rails"
And then execute:
$ bundle install
Or install it yourself as:
$ gem install crudable-rails
Then run the install generator in your Rails application:
bin/rails generate crudable:install
This sets config.action_controller.raise_on_missing_callback_actions to true in any environment file under config/environments/ that currently sets it to false. Crudable controllers rely on before_action callbacks; enabling this setting helps catch invalid only / except options early in development and test.
You can also run the same step as a Rake task:
bin/rails crudable:install
Usage
Controller Setup
To use crudable-rails in your controllers, call the crudable method.
Generators
The gem ships Rails generators to scaffold resources with a Crudable controller. Generated controllers include crudable, a permitted_params method, and commented examples of every override hook with its default implementation — uncomment and tailor as needed.
Full scaffold — model, migration, views, routes, tests, and a Crudable controller (same output as rails generate scaffold, but with a Crudable controller):
bin/rails generate crudable:scaffold Post title:string body:text
bin/rails generate crudable:scaffold Post title:string body:text --turbo-forms
With --turbo-forms, generated new.turbo_stream.erb and edit.turbo_stream.erb templates re-render the scaffold _form partial on validation failure. HTML views and _form are left to the scaffold generator (for example tailwindcss-rails or your view component extension). The _form partial should expose a dom_id(record, :form) target so turbo_stream responses can replace it inline.
Controller only — when you already have a model and views, or want to wire up the controller yourself:
bin/rails generate crudable:controller Post title:string body:text
You can also pass --crudable to the built-in Rails generators:
bin/rails generate scaffold Post title:string body:text --crudable
bin/rails generate scaffold Post title:string body:text --crudable --turbo-forms
bin/rails generate scaffold_controller Post title:string body:text --crudable
Use rails generate scaffold without --crudable when you want the standard Rails scaffold controller.
Nested routing is supported automatically: when a parent *_id param is present and an association exists between the parent and resource, collections and member actions are scoped through the parent.
Customizing CRUD Actions
You can override the default behavior of CRUD actions by defining the following methods in your controller:
before_authorize_createafter_authorize_createon_successful_createon_successful_create_renderon_failed_create_setupon_failed_create_renderbefore_authorize_updateafter_authorize_updateon_successful_updateon_successful_update_renderon_failed_update_setupon_failed_update_renderon_successful_destroy_renderon_failed_destroy_render
For example:
class ProductsController < ApplicationController
include Crudable::Rails::Controller
crudable
def on_successful_create
# Custom behavior on successful create
end
def on_failed_create_render
respond_to do |format|
format.html { render :new, status: :unprocessable_entity }
end
end
end
By default, Turbo Streams are supported for create, update, and destroy actions. If Turbo Streams are not available, the gem will fallback to rendering HTML responses. If you require support for other formats, you can override the default behavior by defining the appropriate methods in your controller.
Required Private Methods
permitted_params
This method should return the permitted parameters for the resource as an array. It's required to be defined on all controllers.
The example below uses a Product with a has_many :product_sizes association and accepts_nested_attributes_for:
# app/models/product.rb
class Product < ApplicationRecord
has_many :product_sizes
accepts_nested_attributes_for :product_sizes, allow_destroy: true
end
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
crudable
private
def permitted_params
[:name, product_sizes_attributes: [[:id, :name, :_destroy]]]
end
end
A typical request payload for create/update looks like:
{
"product": {
"name": "T-shirt",
"product_sizes_attributes": {
"0": { "name": "Small" },
"1": { "id": "42", "name": "Medium" },
"2": { "id": "43", "_destroy": "1" }
}
}
}
For has_many nested attributes, Rails 8 expect requires double array brackets — an array wrapped in an array — to declare that each nested record is a hash ([[:id, :name, :_destroy]]). This form also works with require.permit on hash-style payloads from fields_for / accepts_nested_attributes_for, so the same permitted_params definition works on both Rails versions.
param_method
Controls how strong parameters are required and permitted in resource_params. On Rails 8+, the default is :expect, which uses params.expect to require and permit in a single step. On earlier Rails versions, it falls back to :require (params.require(...).permit(...)).
Rails 8 (default — :expect)
class ProductsController < ApplicationController
crudable
private
def permitted_params
[:name, product_sizes_attributes: [[:id, :name, :_destroy]]]
end
end
# resource_params is equivalent to:
# params.expect(product: [:name, product_sizes_attributes: [[:id, :name, :_destroy]]])
Rails 7 and earlier (default — :require)
class ProductsController < ApplicationController
crudable
private
def permitted_params
[:name, product_sizes_attributes: [[:id, :name, :_destroy]]]
end
end
# resource_params is equivalent to:
# params.require(:product).permit(:name, product_sizes_attributes: [[:id, :name, :_destroy]])
Override to use require on Rails 8
class ProductsController < ApplicationController
crudable
private
def param_method
:require
end
def permitted_params
[:name, product_sizes_attributes: [[:id, :name, :_destroy]]]
end
end
# resource_params is equivalent to:
# params.require(:product).permit(:name, product_sizes_attributes: [[:id, :name, :_destroy]])
Optional Private Methods
The following private methods are available for use in your controllers:
after_create_redirect_path
This method should return the path to redirect to after a successful create. By default this will redirect to the namespaced index of the resource being created.
after_create_notice
This method should return the notice to display after a successful create. By default this will return a success message via i18n.
after_update_redirect_path
This method should return the path to redirect to after a successful update. By default this will redirect to the namespaced show path of the resource being updated.
after_update_notice
This method should return the notice to display after a successful update. By default this will return a success message via i18n.
after_destroy_redirect_path
This method should return the path to redirect to after a successful destroy. By default this will redirect to the namespaced index of the resource being destroyed.
after_failed_destroy_redirect_path
This method should return the path to redirect to after a failed destroy. Default: after_destroy_redirect_path.
after_destroy_notice
This method should return the notice to display after a successful destroy. By default this will return a success message via i18n.
after_failed_destroy_alert
This method should return the alert to display after a failed destroy. By default this will return an error message via i18n.
discard?
This method should return a boolean value to determine if the resource should be discarded. If it returns false the resource will be destroyed. Default: false.
singleton?
This method should return a boolean value to determine if the resource is a singleton. Default: false.
use_parent_as_scope?
This method should return a boolean value to determine if the resource should be found using the nested parent. Default: true.
parent_id_param
When using nested routes, this determines which *_id param is treated as the parent id. If multiple *_id params are present (multi-level nesting), Crudable will default to the deepest/last *_id from the route path parameters.
finder_param
This method should return the parameter used to find the resource. Default: :id.
paginate_resource?
This method should return a boolean value to determine if the resource should be paginated. Default: true if Kaminari is available, otherwise false.
friendly_finders?
This method should return a boolean value to determine if the resource should be found using friendly finders.
Default: true when FriendlyId is available and the model responds to .friendly, otherwise false.
parent_friendly_finders?
When using nested routes (e.g. /:parent_id/:id), this method controls whether the parent should be found using FriendlyId.
Default: friendly_finders? (and additionally requires FriendlyId to be defined and the parent model to respond to .friendly).
skip_initialize_create?
This method should return a boolean value to determine if the resource should be initialized on create. Default: false.
authorize_with_pundit?
This method controls whether Pundit authorization should be performed. By default, it returns true when Pundit is defined. You can override this method to customize authorization behavior based on feature flags, user roles, or other conditions.
For example:
class ProductsController < ApplicationController
crudable
private
def
super && current_user.present? && feature_enabled?(:authorization)
end
end
(create|update)_params
These methods delegate to resource_params by default. Override them when create and update actions need different permitted parameters — for example, allowing new sizes on create but only updates and destroys on update:
class ProductsController < ApplicationController
crudable
private
def create_params
params.expect(product: [:name, product_sizes_attributes: [[:name]]])
end
def update_params
params.expect(product: [:name, product_sizes_attributes: [[:id, :name, :_destroy]]])
end
end
See param_method and permitted_params for the default strong-parameter handling.
Overriding authorizable_resource with a Namespace
The authorizable_resource method can be overridden to customize the resource authorization logic, especially when dealing with namespaced resources. This method should return the resource that needs to be authorized.
For example, if you have a namespaced controller:
module Admin
class ProductsController < ApplicationController
crudable
private
def
[:admin, @product]
end
end
end
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/your-username/crudable-rails. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.