Rails Resource Routing: Update

Learning Goals

  • Update a resource using Rails
  • Define custom routes in addition to resources

Introduction

In this lesson, we'll continue working on our Bird API by adding an update action, so that clients can use our API to update birds. To get set up, run:

$ bundle install
$ rails db:migrate db:seed

This will download all the dependencies for our app and set up the database.

HTTP Verb Path Controller#Action Description
GET /birds birds#index Show all birds
POST /birds birds#create Create a new bird
GET /birds/:id birds#show Show a specific bird
PATCH or PUT /birds/:id birds#update Update a specific bird
DELETE /birds/:id birds#destroy Delete a specific bird

Adding Features

Our birding app has grown wildly in popularity, which means it's time to add a new feature to keep our users happy! Market research suggests we can increase user engagement by adding a "like" feature to our application. To do this, we'll need to update our Bird model to keep track of the number of likes.

We'll also need to create a new API endpoint so that users can update the number of likes for a specific bird.

Changing Our Model With Migrations

Let's start by creating a new migration to update our Bird model and the associated birds table:

$ rails g migration AddLikesToBird likes:integer --no-test-framework

Note: the --no-test-framework argument isn't actually needed in this case because the Rails migration generator does not create tests. However, it doesn't hurt to include it so we do so to encourage the habit.

This will create a new migration file for updating our birds table with a new column for likes. Let's also add a default value of 0 likes, and ensure we're not permitting null values to be saved to the likes column:

class AddLikesToBird < ActiveRecord::Migration[6.1]
  def change
    add_column :birds, :likes, :integer, null: false, default: 0
  end
end

For a refresher on migrations, check out the Active Record docs!

Next, run the migration:

$ rails db:migrate

We'll also want to re-seed our database. You can do so with this command:

$ rails db:reset

This will drop our old development database, and re-create it from scratch based on our schema and seed file.

With our data set up, let's turn to the next action: updating likes!

Updating Existing Birds

To start, we'll need to create a new route and controller action to give our clients the ability to update birds. Recall that following RESTful conventions, we'll want to set up a PATCH /birds/:id route. Just like for our show route, we need the ID in the URL to identify which bird is being updated.

We can use resources to add this route by adding the :update action in our routes.rb file:

resources :birds, only: [:index, :show, :create, :update]

Next, add an update action in our controller. Our goal in this action is to:

  • find the bird that matches the ID from the route params
  • update the bird using the params from the body of the request
class BirdsController < ApplicationController

  # rest of actions here...

  # PATCH /birds/:id
  def update
    bird = Bird.find_by(id: params[:id])
    if bird
      bird.update(bird_params)
      render json: bird
    else
      render json: { error: "Bird not found" }, status: :not_found
    end
  end

end

Just like in the create action, we are using strong params when updating the bird. We can modify the strong params in the bird_params method to allow the likes as well:

def bird_params
  params.permit(:name, :species, :likes)
end

Run rails s and test out this route in Postman. Try updating the likes for one specific bird:

PATCH /birds/1


Headers
-------
Content-Type: application/json


Request Body
------
{
  "likes": 1
}

If we had the client application built out, to implement this feature, we would add a "Like" button to each bird's information. When the button is clicked, the frontend code would access the current value of likes, add 1 to it, then send that information in the request body of a PUT OR PATCH request. But responsibility for keeping track of and updating the likes doesn't really belong in the frontend. To fix this, we can use a custom route.

Creating Custom Routes

To take the responsibility for handling likes off of the frontend, we can provide a custom route that will do the work of calculating the number of likes and incrementing it. With this approach, all the frontend has to do is send a request to our new custom route, without worrying about sending any data in the body of the request.

Update the routes.rb file like so:

Rails.application.routes.draw do
  resources :birds, only: [:index, :show, :create, :update]
  patch "/birds/:id/like", to: "birds#increment_likes"
end

Then create the increment_likes controller action:

def increment_likes
  bird = Bird.find_by(id: params[:id])
  if bird
    bird.update(likes: bird.likes + 1)
    render json: bird
  else
    render json: { error: "Bird not found" }, status: :not_found
  end
end

Notice that in this action, the only information we need from params is the id; we're able to use the bird's current number of likes to calculate the next number of likes! Our client app no longer needs to concern itself with sending that data or performing that calculation.

A note on breaking convention: by creating this custom route, we are breaking the REST conventions we had been following up to this point. One alternate way to structure this kind of feature and keep our routes and controllers RESTful would be to create a new controller, such as Birds::LikesController, and add an update action in this controller. The creator of Rails, DHH, advocates for this approach for managing sub-resources.

Conclusion

Continuing on our journey with REST and CRUD, we've seen how to update a record, using PATCH /birds/:id. We also saw how to break RESTful conventions and create a custom route.

Check For Understanding

Before you move on, make sure you can answer the following questions:

  1. Under what circumstances does it make sense to create a custom route?
  2. What are the advantages and disadvantages of using custom routes?

Resources