- Update a resource using Rails
- Define custom routes in addition to
resources
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 |
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.
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!
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
}
One drawback to the approach for our likes feature is that our frontend is required to keep track of the current number of likes, and do the work of incrementing that number before sending the request with the updated number of likes.
We could take some of that burden off of the frontend by providing a custom route that will do the work of calculating the number of likes and incrementing it, so that 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 a
create
action in this controller. The creator of Rails, DHH, advocates for this approach for managing sub-resources.
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.