Building a ResourceController in Rails
Every Rails app accumulates controllers that look almost identical. UsersController has the same seven actions as ProjectsController, with the same flash messages, the same redirect patterns, the same respond_to blocks. The only differences are the model class, the permitted params, and maybe a scope or two.
A ResourceController extracts that shared logic into a base class. Subclasses declare what model they manage and override only what’s different.
The Base Controller
app/controllers/resource_controller.rb:
class ResourceController < ApplicationController
before_action :set_resource, only: [:show, :edit, :update, :destroy]
def index
@resources = resource_scope.order(default_sort)
instance_variable_set("@#{resource_name.pluralize}", @resources)
end
def show
end
def new
@resource = resource_class.new
set_instance_variable
end
def create
@resource = resource_class.new(resource_params)
set_instance_variable
if @resource.save
redirect_to @resource, notice: "#{resource_class.model_name.human} was created."
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @resource.update(resource_params)
redirect_to @resource, notice: "#{resource_class.model_name.human} was updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@resource.destroy
redirect_to action: :index, notice: "#{resource_class.model_name.human} was deleted."
end
private
def resource_class
raise NotImplementedError, "#{self.class} must implement #resource_class"
end
def resource_params
raise NotImplementedError, "#{self.class} must implement #resource_params"
end
def resource_scope
resource_class.all
end
def resource_name
resource_class.model_name.singular
end
def default_sort
{ created_at: :desc }
end
def set_resource
@resource = resource_scope.find(params[:id])
set_instance_variable
end
def set_instance_variable
instance_variable_set("@#{resource_name}", @resource)
end
end
The key methods subclasses must implement are resource_class and resource_params. Everything else has sensible defaults that can be overridden.
set_instance_variable is a small convenience — it creates @user or @project so views work with the variable name they expect instead of a generic @resource.
A Subclass
app/controllers/users_controller.rb:
class UsersController < ResourceController
private
def resource_class
User
end
def resource_params
params.require(:user).permit(:name, :email, :role, :active)
end
def default_sort
{ name: :asc }
end
end
That’s the entire controller. Seven actions, all inherited. The only custom code is what model to use, what params to permit, and a sort override.
Scoping Resources
Override resource_scope to filter records. This is useful for multi-tenant apps or role-based access:
class ProjectsController < ResourceController
private
def resource_class
Project
end
def resource_params
params.require(:project).permit(:name, :description, :client_id, :status)
end
def resource_scope
current_user.admin? ? Project.all : current_user.projects
end
end
The scope flows through to index, show, edit, update, and destroy automatically — if a non-admin user tries to access a project they don’t own, find raises ActiveRecord::RecordNotFound and Rails returns a 404.
Custom Actions
Subclasses can add actions beyond the standard seven without touching the base class:
class UsersController < ResourceController
def deactivate
set_resource
@resource.update!(active: false)
redirect_to @resource, notice: "User deactivated."
end
private
def resource_class
User
end
def resource_params
params.require(:user).permit(:name, :email, :role, :active)
end
end
Overriding Actions
Sometimes one controller needs different behavior for a single action. Override just that action:
class InvoicesController < ResourceController
def create
@resource = resource_class.new(resource_params)
@resource.number = Invoice.next_number
set_instance_variable
if @resource.save
InvoiceMailer.created(@resource).deliver_later
redirect_to @resource, notice: "Invoice created and sent."
else
render :new, status: :unprocessable_entity
end
end
private
def resource_class
Invoice
end
def resource_params
params.require(:invoice).permit(:client_id, :amount, :due_on)
end
end
The other six actions still come from the base class. You only write what’s different.
Namespaced Resources
For admin namespaces, create a namespaced base:
app/controllers/admin/resource_controller.rb:
class Admin::ResourceController < ResourceController
layout "admin"
before_action :require_admin
private
def require_admin
redirect_to root_path unless current_user&.admin?
end
end
Admin controllers inherit from this instead:
class Admin::UsersController < Admin::ResourceController
private
def resource_class
User
end
def resource_params
params.require(:user).permit(:name, :email, :role, :active, :admin)
end
end
The admin layout, access check, and CRUD logic all come from the inheritance chain. The subclass only declares its model and params.
Tips
- Don’t fight the pattern. If a controller needs significantly different behavior for most actions, skip
ResourceControllerand write it from scratch. The base class saves time when controllers are 80% identical — not when they’re 80% different. - Views still work normally. Since
set_instance_variablecreates@user,@project, etc., your existing view files don’t need any changes. Form partials, show pages, and index tables all reference the same instance variables. - Keep
resource_paramsstrict. The base class trusts whateverresource_paramsreturns. This is the one method every subclass must get right — don’tpermit!everything. - Test subclasses, not the base. Write integration tests for
UsersController,ProjectsController, etc. They exercise the base class logic in context. TestingResourceControllerin isolation covers abstract code that never runs on its own. - Use
resource_scopefor authorization. It’s simpler than a separate authorization layer for basic access control. For complex permission systems, pair it with Pundit or Action Policy instead.