How To Keep Your Controllers Thin with Form Objects

Typically in Rails, forms which post data to a #create or #update action are concerned with one particular resource. For example, a form to update a user’s profile. The sequence looks something like this:

  1. The form submits some data to the #create action in UsersController.
  2. The #create action, with something like User.create(params) creates the record in the User table.

This is easy to reason about. We have one form to create one user. It submits user data to a users controller, which then creates a record in the User table with a call to the User model.

Though some percentage of the forms in our app can be described this simply, most forms we find ourselves needing to build are not that simple. They might need to save multiple records, or update more than one table, and/or perform some additional processing (like sending an email). There might even be more than one context under which a record can be updated (for example a multi-step application form).

Trying to stuff a bunch of logic in your controller can turn out to be quite painful in the long run, as they become longer and harder to understand. Moreover, you might find yourself having to perform gymnastics with your views to get them to pass in parameters correctly.

For example, imagine you’re building a form for a project management app. This form creates a user like in our first example, but in addition, has to do a few other things. First, it has to create a record in the Message table. Then it has to create a record in the Task table. Finally it has to send an email to the project manager.

Some definitions and assumptions before we go forward:

  1. The Message table is used for internal messaging amongst various members of the project.
  2. The Task table is used to store and manage tasks assigned to members of the project.
  3. When a new user is created, we want to send an internal message and assign a new task to the project manager.
  4. We also want to send an email notification to the project manager when a new user is created.

Compared to the first example, it’s obvious that this form requires a flow that doesn’t quite as neatly fit in to the conventional form flow that we saw earlier. There are a few ways we can handle this:

  1. Add in the code to UsersController to accomplish the extra stuff.
  2. If your models are associated with each other (via has_many or belongs_to), build a nested form using either Rails’ built-in fields_for helper, or Ryan Bates’ nested_form gem. You’ll still have to send the email in the controller.
  3. Create a new controller and corresponding “form object” that encapsulates what you want to do.

What is a Form Object

A form object is a Plain Old Ruby Object (PORO). It takes over from the controller wherever it needs to talk to the database and other parts of your app like mailers, service objects and so on. A form object typically functions together with a dedicated controller and a view.

Why use a Form Object

Now, while there are good reasons to go with either option 1 or 2, I’m going to elaborate on option 3. There are a few benefits to using a form object in a situation like this:

  1. Your app will be easy to change.

    If your app is a decent sized web-app, you will likely have a multitude of paths and views through which data gets saved and/or retrieved from your Users table. It’s worth thinking about if UsersController or the User model should be where these paths meet, because if you’re not careful your controller can devolve into a mess of hard-to-change conditional code.

  2. Your app will be easy to reason about

    Conventionally in Rails, the simplest way to reason about a controller is to have it concerned with the seven RESTful actions and views; these actions would only interact with one model and ideally the interaction would be a one-liner like @user.update(params). The closer your controller is to this pattern, the easier your controller will be to reason about.

  3. Your view will likely be simpler to write (read: no deeply nested forms) and contain less logic.

What a Form Object looks like in practice

First things first, decide on the name of your new controller. This will give you quick feedback on if your abstractions will make things easier to reason about.

I always ask myself this, what happens when the form is submitted? In our example above, a user is created and the rest of the project team is notified. So a reasonable name might be ‘UserRegistrationsController`. You can double check by trying to apply the seven RESTful actions to this controller:

  • Can I “create” a user_registration?
  • Can I “update” a user_registration?
  • Can I “destroy” a user_registration?
  • … and so on

All seven actions might not always make sense, which is why I find it handy to define my routes like this, for example:

resource :user_registrations, only: [:create, :update, :new, :edit] 

In my Rails apps, the convention I follow is to always name the form object class FormObject, and namespace them with something context dependent. So in this case, my form object would be UserRegistration::FormObject.

So your form, which resides at user_registrations/new.html.erb, posts some data to the #create action of the UserRegistrationsController, which calls #save on UserRegistration::FormObject with the params you pass in.

Form Object parameters

To be able to specify an input in your form, you need to expose the related attribute in your form object. In our example, one of the inputs we want is the user’s name. So in our form object, we’d do:

module UserRegistration
  class FormObject
    include ActiveModel::Model
    attr_accessor :name
    ...
    ...
    def self.model_name
      ActiveModel::Name.new(self, nil, 'UserRegistration')
    end
  end
end

Because we’ve said attr_accessor :name, we can now in our view say something like <%= f.text_field :name %>.

You’ll also notice a couple of other things:

  1. We said include ActiveModel::Model. This is cool, because we can now use methods like validates and perform any validations we want. An advantage of using validations in the form object is that they are specific to the form object and won’t clutter up your model(s).

  2. We also defined the model_name class method. The Rails form builder methods (form_for and the rest) need this to be defined.

Form Object Initialize and Save

The behavior of our form object will be governed by two methods. #initialize and #save. This is because in our controller, we want to be able define our create action like so:

def create
  @form_object = UserRegistration::FormObject.new(params)
  if @form_object.save
    ... #typically we flash and redirect here
  else
    render :new
  end
end

You can see above how the form object directly replaces the model, allowing us to keep our controller clean and short.

Your initialize method would look something like:

def initialize(params)
  self.name = params.fetch(:name, '')
  ... # and so on and so forth
end

And save:

...
def save
  return false unless valid? #valid? comes from ActiveModel::Model
  User.create(name: name)
  notify_project_manager
  assign_task_to_project_manager
  true
end

private

def notify_project_manager
  ... # here we talk to the Message model
end

def assign_task_to_project_manager
  ... # here we talk to the Task model
end
...
# and so on and so forth

The important thing with the #save method is to return false and true correctly depending on if the form submission meets your criteria, so that your controller can make the right decision.

Recap

I’m a big fan of form objects, and a general rule of thumb for me is to use them whenever I feel things are getting complicated in the controller and view. They can even be used in situations where you’re not dealing directly with the database (like for example interacting with a third party API).

I encourage you, if you haven’t already, to consider how form objects might fit into your app. They will go a long way in ensuring your controllers and models are thin and keeping your code maintainable.

Have you used form objects in your Rails apps? Have they helped or hindered you? How else do you keep your controllers thin? Let me know in the comments section, I’d love to hear what you think.

29 thoughts on “How To Keep Your Controllers Thin with Form Objects

  1. Why you name your controller as UserRegistration instead of User. Because it restful resource and it create/update/delete users not user registrations.

    1. If your UsersController gets too big, meaning it is doing more than just creating/updating/deleting etc a User record, that’s when you might benefit from the Form Object pattern.

  2. Hi there, nice article. Where would you usually put your new FormObject within the App? Under a services directory? Or perhaps in the models directory? Thanks

    1. Thanks for reading Tom. In general, I’d say the important thing is to agree on a convention with your team and stick to it. Both the services or models directory could work, depending on your app.

      I’ve also placed them in the lib/ directory with a directory structure reflecting how they are scoped.

      So for example if I have Users in my app and one of the forms they need to fill out is an ‘ID verification’ form (which talks to a 3rd party API on submit) – I’d have the form object under lib/users/id_verification/form_object.rb and instantiate with User::IdVerification::FormObject.new(...).

      Hope that helps!

  3. IMHO this is not a form object. And it does not differ much from having all this in your AR model (or a subclass) directly.
    The object has way to many responsibilities. Good luck with testing this in a easy way.

    1. I think you get to decide where to draw the line in your app. If your form object gets too big for your liking, then extract out the relevant functionality into another object.

      Would love to see how you approach it.

      1. I’d test validations, accessors and the save method. For the parts which talk to AR models or other objects I’d just test that the calls were being made, with something like expect(User).to receive(:create, with:...).

    2. Agree. This stuff with private methods looks like SRP violation.

      If we accept using form objects, then we should definitely move this logic into separate services.

      But, in this case, why do we need form object at all?
      Why not using something like chain of interactors in the controller, for example?

  4. Great post! If you have many form objects, wouldn’t it make sense to flip your namespacing, creating FormObject::UserRegistration instead of UserRegistration::FormObject? That way your directory structure would only need the one form_object folder, which has all of the files defining specific form objects (eg ../form_object/user_registration.rb).

    1. Thanks for reading Thomas. I think it depends, but probably either way works.

      For example, I’d prefer to use UserRegistration::FormObject if I had other objects which were namespaced under UserRegistration – maybe something like UserRegistration::ProcessPayment (and ../user_registration/process_payment.rb)

      1. Can you do something like this FormObject::UserRegistration::ProcessPayment
        so if you have more stuff that is not FormObject in that directory they will be organized ?
        Great Post. my app getting a little bigger on the code and will love to implement this approach .

      2. Thanks for reading Francisco!

        I suppose you could, but then the implication is that ProcessPayment can/should only be used in the context of a form object. You might have to experiment and see what works in your app.

        Let me know how implementing form objects in your app goes!

  5. Thank you for your article.

    We use similar from objects for a long time. The difference is that we do not implement save method because this is not form responsibility I think.

    Also you can use virtus or dry-rb in addition to ActiveModel::Model for form objects.

    1. Thanks for reading. Interesting that you don’t implement save in the form object. Where do you talk to Rails objects (models, mailers etc) – In another service object?

      Will check out virtus and dry-rb – thanks for the recommendation!

  6. Nice article, using form objects in complex apps was a game changer for me.

    Just wondering if putting logic like persistence, sending notifications etc. in form object is the best approach. Used to do it like that, but it turned out that the form object had too many responsibilities and it was more difficult to test it. What worked for me was to separate logic to service object, which was called from controller, like UserRegistration.run(params) and this service would initialize form object, form object would be responsibile for validations and mapping attributes to related models and then the service object would handle the notifications logic, persistence and so forth. What do you think?

    1. Thanks for reading Karol!

      What you say makes a lot of sense. I like the idea of a form object which only does validations and processes incoming params.

      That being said, I think if you’re starting from a controller that is doing too much, then extraction to a form object which talks directly to models and other Rails objects is a good first step. It would clean things up to the point where you can see if further separation of concerns is needed.

      As long as you keep your methods small, you should be able to see dependencies clearly when the time comes to extract. Thoughts?

      1. As an intermediate step I think it’s fine, sometimes it might not be worth extracting everything from the beginning when the use case / abstraction is not clear enough. But the end goal ideally would be a separation between service object and form, having small methods that are easy to extract would definitely be helpful.

    1. Interesting discussion – thanks for the link.

      Re: your question about moving code from one place to another and how it fixes things – there are many benefits to not stuffing everything in one place. Things like the code being easier to understand, easier to test (which also helps in understanding) and most importantly easier to change. So instead of having one UsersController that does registration, allows admin to creates/update users and users themselves to modify their account, you might find it easier to create 3 controllers that are responsible for these functions respectively.

      That being said, it’s also important not to “over design” and in a way let your intuition/experience guide you when refactoring (easier said than done, but useful to try anyway).

      I searched for “DHH form objects” & found this.

  7. Would it not be better to namespace all form objects under FormObject?

    FormObject::SuperDuperController
    FormObject::OtherSuperController

    1. Thanks for reading Chris.

      I probably wouldn’t namespace my controllers with FormObject. If I namespace them, I normally do so with something domain related – for example something like Admin::UsersController.

      Check out my response to Thomas and Francisco above as well.

Leave a Reply

Your email address will not be published.