Strong Opinions About Strong Parameters
Tags: rails
Published: 2020-04-10 17:11:47

---
title: Strong Opinions About Strong Parameters
published: true
description: A set of conventions to follow when implementing (or fixing) strong parameters in your application.
tags: rails
---

Rails announced `strong_parameters` as a replacement for `protected_attributes` nearly eight years ago. In 2020 many Rails apps have not completed the migration. Others have made the migration, but are worse off than before. Was `strong_parameters` a bad idea? I don't think so. But like many things, it depends on how you use it.


## Instances of `ActionController:Parameters` leak outside of the controller

This is undoubtedly the most common mistake I've seen. The reason that `strong_parameters` is superior to `protected_attributes` is that it narrows the concern of mass-assignment protection to the controller. With `protected_attributes`, the entire app had to consider which attributes could mass-assigned, and which could not. This led to anti-patterns like the use of `without_protection` which allowed developers to skip mass-assignment protection under certain circumstances.

The benefit of `strong_parameters` is lost when `ActionController::Parameters` are passed outside of a controller and suddenly you have to be concerned about mass-assignment protection everywhere, again.

```ruby
  # FILE: app/lib/payment_processor.rb
  class PaymentProcessor
    def initialize(params)
      @params = params
    end

    def process
      # ... payment processing logic, etc
      Payment.create(payment_params)
    end

    private

    def payment_params
      @params.require(:payment).permit(:some, :payment, :attrs)
    end
  end

  # FILE: app/controllers/payments_controller.rb
  class PaymentsController < ApplicationController
    def create
      payment = PaymentProcessor.new(params)

      if payment.process
        ...
      else
        ...
      end
    end
  end
```

<br />

**Strong Opinion**: Parameters should be permitted at the controller level. If you pass some params on to another object (like a service class), first permit the params that are allowed for mass-assignment, then call `to_h` on it to convert the object to a good ol' `ActiveSupport::HashWithIndifferentAccess`.


```ruby
  # FILE: app/lib/payment_processor.rb
  class PaymentProcessor
    def initialize(payment_params)
      @payment_params = payment_params
    end

    def process
      # ... payment processing logic, etc
      Payment.create(@payment_params)
    end
  end

  # FILE: app/controllers/payments_controller.rb
  class PaymentsController < ApplicationController
    def create
      payment = PaymentProcessor.new(payment_params.to_h)

      if payment.process
        ...
      else
        ...
      end
    end

    private

    def payment_params
      params.require(:payment).permit(:some, :payment, :attrs)
    end
  end
```

_________________________________________________________


## Use of strong parameters where mass assignment is not being performed

The Rails community is pretty split on this one. Should you *always* permit parameters? Or only permit parameters when mass-assignment is being performed? Here's an example:

```ruby
  # FILE: app/controllers/users_controller.rb
  class UsersController < ApplicationController
    def special_update_action
      user = User.find(user_params[:id])

      user.special_attribute = user_params[:special_attribute]

      if user.save
        ...
      else
        ...
      end
    end

    private

    def user_params
      params.require(:user).permit(
        :id,
        :special_attribute,
        :some,
        :user,
        :attrs,
        :including
      )
    end
  end
```

<br />

**Strong Opinion**: Don't complicate your controllers by permitting params when mass assignment is *not* being performed.

```ruby
  # FILE: app/controllers/users_controller.rb
  class UsersController < ApplicationController
    def special_update_action
      user = User.find(params[:id])

      # Notice that special_attribute is pulled off of `params` directly
      user.special_attribute = params[:special_attribute]

      if user.save
        ...
      else
        ...
      end
    end
  end
```

_________________________________________________________

## Order of operations when mutating a params object

Remember, `strong_parameters` is meant to sanitize *externally-provided* parameters. If you add a key to the params object, then call `permit`, you'll end up having to permit the parameter you just added.

```ruby
  # FILE: app/controllers/credit_cards_controller.rb
  class CreditCardsController < ApplicationController
    def update
      credit_card = CreditCard.find(params[:id])

      params[:top_secret_token] = TopSecretToken.new(args)

      if credit_card.update(credit_card_params)
        ...
      else
        ...
      end
    end

    private

    def credit_card_params
      # Notice the addition of top_secret_token is included in this
      # scenario so that it can be mass-assigned above
      params.require(:credit_card).permit(
        :top_secret_token,
        :some,
        :credit_card,
        :attrs
      )
    end
  end
```

<br />

**Strong Opinion**: Permit your params *before* mutating the object. This allows you to permit only the externally-provided parameters, then modify the object as you see fit.

```ruby
  # FILE: app/controllers/credit_cards_controller.rb
  class CreditCardsController < ApplicationController
    def update
      credit_card = CreditCard.find(params[:id])

      assignable_params = credit_card_params
      assignable_params[:top_secret_token] = TopSecretToken.new(args)

      if credit_card.update(assignable_params)
        ...
      else
        ...
      end
    end

    private

    def credit_card_params
      # Notice that top_secret_token does *not* need to be included
      # here because it is assigned to the (permitted) return value
      # of credit_card_params
      params.require(:credit_card).permit(:some, :credit_card, :attrs)
    end
  end
```

_________________________________________________________

## Defining permitted attributes multiple (sometimes many) times

With `protected_attributes`, the model provided a central place to specify which attributes were permitted for mass-assignment. With `strong_parameters`, permitted parameters are often defined multiple times for the same resource in different controllers. This is not DRY and it is not maintainable. Add in consideration for `accepts_nested_attributes_for` and you're maintaining countless lists of the same parameters.

*Note: In the previous examples parameters were permitted directly in the controller as seen below. This was done for simplicity's sake, and also on the basis that no other controllers duplicated the same set of permitted parameters.*

```ruby
  # FILE: app/controllers/departments_controller.rb
  class DepartmentsController < ApplicationController
    def create
      department = Department.new(department_params)

      if department.save
        ...
      else
        ...
      end
    end

    private

    def department_params
      params.require(:department).permit(
        :some, :department, :attrs,
        employees_attributes: [
          :some, :employee, :attrs
        ]
      )
    end
  end

  # FILE: app/controllers/special/departments_controller.rb
  module Special
    class DepartmentsController < ApplicationController
      def create
        department = Department.new(department_params)

        if department.save
          ...
        else
          ...
        end
      end

      private

      # Copy pasta
      def department_params
        params.require(:department).permit(
          :some, :department, :attrs,
          employees_attributes: [
            :some, :employee, :attrs
          ]
        )
      end
    end
  end
```

<br />

**Strong Opinion**: Duplicated parameters should be permitted in a module that can be included wherever it is needed. Furthermore, nested attributes should not be redefined multiple times. Here's an example:

```ruby

  # FILE: app/controllers/concerns/strong_parameters/employee.rb
  module Concerns
    module StrongParameters
      module Employee
        def employee_params
          params.require(:employee).permit(*self.permitted_attrs)
        end

        def self.permitted_attrs
          %i(some employee attrs)
        end
      end
    end
  end

  # FILE: app/controllers/concerns/strong_parameters/department.rb
  module Concerns
    module StrongParameters
      module Department
        def department_params
          params.require(:department).permit(*self.permitted_attrs)
        end

        def self.permitted_attrs
          [
            :some, :department, :attrs,
            { employees_attributes: [
              :id, :_destroy, *Concerns::StrongParameters::Employee.permitted_attrs] }
          ]
        end
      end
    end
  end

  # FILE: app/controllers/departments_controller.rb
  class DepartmentsController < ApplicationController
    include Concerns::StrongParameters::Department

    def create
      department = Department.new(department_params)

      if department.save
        ...
      else
        ...
      end
    end
  end

  # FILE: app/controllers/special/departments_controller.rb
  module Special
    class DepartmentsController < ApplicationController
      include Concerns::StrongParameters::Department

      def create
        department = Department.new(department_params)

        if department.save
          ...
        else
          ...
        end
      end
    end
  end

  # FILE: app/controllers/employees_controller.rb
  class EmployeesController < ApplicationController
    include Concerns::StrongParameters::Employee

    def create
      employee = employee.new(employee_params)

      if employee.save
        ...
      else
        ...
      end
    end
  end
```

## Summary

We follow these practices at Hint, and it's helped us (and our clients) a lot. Does your organization need help wrangling strong parameters? We can help. Ping me on twitter: @benjaminwood.

Originally published on the hint.io blog here: https://hint.io/blog/strong-parameters-strong-opinions