← All posts

Building a ResourceController in Rails

November 12, 2025 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 ResourceController and 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_variable creates @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_params strict. The base class trusts whatever resource_params returns. This is the one method every subclass must get right — don’t permit! everything.
  • Test subclasses, not the base. Write integration tests for UsersController, ProjectsController, etc. They exercise the base class logic in context. Testing ResourceController in isolation covers abstract code that never runs on its own.
  • Use resource_scope for 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.
Let’s work together! Tell Me About Your Project