Search code examples
ruby-on-railscrud

Rails CRUD route and controller method best practices


CRUD stands for Create, Read, Update, Delete. That's four methods (or five if you distinguish between viewing one record and viewing all records). In Rails, it seems like the canonical way to handle CRUD involves seven methods. For instance, making routes for an Order object using the resources :orders shorthand generates the following seven routes:

  • index
  • new
  • create
  • show
  • edit
  • update
  • destroy

Here's the source of my confusion. What's the point of having separate actions/routes for new/create and edit/update? What is the benefit of having separate actions for viewing the page vs. creating the record in the database? I understand how it is done in Rails, e.g.:

class OrdersController < ApplicationController
  def new
    @order = Order.new
    render
  end

  def create
    @order = Order.new(order_params)
    if @order.save
      redirect_to @order, notice: 'Successfully created an order.'
    else
      render :new
    end
  end
end

Before working with Rails, I used Yii (PHP), which had a built-in CRUD generator that generated code like this (translated to Rails):

class OrdersController < ApplicationController

  def create

    if request.method == "POST"
      @order = Order.new(order_params)
      if @order.save
        redirct_to @order, notice: 'Successfully created an order.'
      end
    else
      @order = Order.new
    end

    render 

  end
end

The reason I prefer this pattern is because it avoids having to render a template for a different controller action. In the first code example, the user goes to orders/new and posts to orders/create. If validation fails, the user is still stuck at orders/create but viewing the template for orders/new. This might confuse the user, and it also seems to defeat the whole idea of having separate actions for viewing a page vs. creating a record in the database. And if you redirect to orders/new rather than using render :new then you lose all your validation error messages.

I find myself subconsciously slipping back to the Yii way in my Rails code. Can anyone explain why the standard way is advantageous? Are there any problems I am going to run into if I deviate from the canonical Rails pattern?

StackOverflow is warning me that the question is subjective and likely to be closed, so let me clarify. I am not trying to debate Rails vs. Yii or determine the best theoretical way to organize CRUD code. I want to know if there is anything about Rails that will break if I deviate from the canonical Rails way of handling CRUD.


Solution

  • The simple explanation is:

    because that's how God and DHH intended it.

    The Rails CRUD conventions are quite pragmatic and allow you to avoid many pitfalls related to browser caching and security.

    Lets take one example:

    # config/routes.rb
    resources :users, only: [:new, :create]
    
    # app/controllers/users_controller.rb
    class UsersController < ApplicationController
      def new
        @user = User.new
      end
    
      def create
        @user = User.new(user_params)
    
        if @user.save
          sign_in(@user)
          redirect_to root_path
        else
          render :new
        end
      end
    
      private 
        def user_params
          params.require(:user).permit(:email, :password, :password_confirmation)
        end
    end
    

    Here we have two separate routes GET /users/new and POST /users.

    The first route is idempotent - it should look the same for any visitor and can be cached. The second is not - it should show the result of creating or attempting to create a resource.

    When a user visits /users/new we POST the form to a different URI. That avoids history issues in the client.

    We also render the form in the same request cycle if the input is invalid. This avoids the security issues that would arise if we tried to pass the form data back in redirect to /users/new and lets us return the semantically correct response code instead of a redirect.

    It also ensures that our application is stateless in the restful sense since the previous action does not influence what we see if we were to visit /users/new.

    On an architecture level, it allows you to leverage convention over configuration. You can just do:

    def new
      @user = User.new
    end
    

    And it will render views/users/new.html.erb because Rails can derive that the new action should render the new template. Having each controller action perform one single task is far better from a design and testing standpoint as it eliminates the need to test two separate code paths in the same method.