0.1 Check which version of Ruby is installed:
ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580)
Better yet, use which
because it gives us the full path of our Ruby interpreter (and indicates whether we’re using rvm
):
which ruby
~/.rvm/rubies/ruby-2.6.3/bin/ruby
0.2 What version of Rails is installed?
rails -v
Rails 5.2.3
Or:
which rails
~/.rvm/gems/ruby-2.6.3/bin/rails
0.3 Is Postgres running?
If you have the Postgres OS X app installed, simply check the menu-bar application to check.
There are multiple ways to check the status of postgres
from the command line, including pg-ctl
but we won’t cover that now.
0.4 Is yarn
installed?
Because we’re using the webpacker
gem, we’ll need yarn
(the Javascript dependency manager).
brew install yarn
Then install the Javascript dependencies:
yarn install
Due to reasons, you may also need to manually install some of the Javascript dependencies:
yarn add bootstrap jquery popper.js
Use rails new
to create a new Ruby on Rails application.
rails new \
--webpack \
--database postgresql \
-m https://raw.githubusercontent.com/lewagon/rails-templates/master/devise.rb \
restaurants-api
cd
into our new application and install the pundit
gem. Open Gemfile
and add:
gem 'pundit'
Then run:
bundle install
Finally, use rails generate
to setup Pundit
:
rails generate pundit:install
For this json
API application, let’s keep the model simple: Restaurant
and Comment
. Let’s use rails generate
to do create them:
rails g model restaurant name address user:references
rails g model comment content:text restaurant:references user:references
Restaurant
references User
, and Comment
references Restaurant
and User
.
Now, let’s migrate the database:
rake db:migrate
Don’t forget to add has_many :restaurants
to User
and has_many :comments
to Restaurant
.
Pundit
is a gem that handles User
authentication policy, which makes controlling access to our various API endpoints a bit easier.
Let’s use the Pundit
rails generate
plugin:
rails generate pundit:policy restaurant
Running via Spring preloader in process 3739
create app/policies/restaurant_policy.rb
invoke test_unit
create test/policies/restaurant_policy_test.rb
We’ll come back to the Pundit
policy a bit later.
Our API will have the following endpoints:
GET /api/v1/restaurants (unauthenticated)
GET /api/v1/restaurants/:id (unauthenticated)
PATCH /api/v1/restaurants/:id (authenticated)
POST /api/v1/restaurants (authenticated)
DELETE /api/v1/restaurants/:id (authenticated)
Though these endpoints differ in the particulars, for example some can be accessed without authentication (while others not), in fact they have a lot in common. All our endpoints must work with authentication
and errors
in the same way, therefore we will create a BaseController
that implements all this common functionality.
First, let’s create the BaseController
. Instead of using rails generate
we’ll create the files manually:
mkdir -p app/controllers/api/v1
touch app/controllers/api/v1/base_controller.rb
Open app/controllers/api/v1/base_controller.rb
and add:
class Api::V1::BaseController < ActionController::API
include Pundit
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
rescue_from StandardError, with: :internal_server_error
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def user_not_authorized(exception)
render json: {
error: "Unauthorized #{exception.policy.class.to_s.underscore.camelize}.#{exception.query}"
}, status: :unauthorized
end
def not_found(exception)
render json: { error: exception.message }, status: :not_found
end
def internal_server_error(exception)
if Rails.env.development?
response = { type: exception.class.to_s, message: exception.message, backtrace: exception.backtrace }
else
response = { error: "Internal Server Error" }
end
render json: response, status: :internal_server_error
end
end
There’s a fair bit going on in here. Let’s start at the top:
include Pundit
Here we’re including Pundit
so we can use its authentication features.
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
When you are developing an application with Pundit
it can be easy to forget to authorise some action. People are forgetful after all. Since Pundit encourages you to add the authorise call manually to each controller action, it's really easy to miss one.
Thankfully, Pundit
has a handy feature which reminds you in case you forget. Pundit
tracks whether you have called authorise anywhere in your controller action. Pundit also adds a method to your controllers called verify_authorized.
This method will raise an exception if authorise has not yet been called.
rescue_from StandardError, with: :internal_server_error
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
rescue_from ActiveRecord::RecordNotFound, with: :not_found
To make our API as opaque as possible, we must be careful not to tip our hand with verbose error messages that might give clues away about how it works under the hood.
We can use rescue_from
to catch various error types (e.g. StandardError
, Pundit::NotAuthorizedError
and ActiveRecord::RecordNotFound
) and respond with some simple JSON
responses.
Api::V1::BaseController#user_not_authorized
is invoked when our controller catches a Pundit::NotAuthorizedError
. It returns a little JSON
blob with a few fields detailing the error:
private
def user_not_authorized(exception)
render json: {
error: "Unauthorized #{exception.policy.class.to_s.underscore.camelize}.#{exception.query}"
},
status: :unauthorized
end
Api::V1::BaseController#not_found
is invoked when our controller catches an ActiveRecord::RecordNotFound
. It, too, returns a little JSON
blob:
def not_found(exception)
render json: { error: exception.message }, status: :not_found
end
Api::V1::BaseController#internal_server_error
is invoked when our controller catches a StandardError
which is a generic catchall error. It, too, returns a little JSON
blob, but the content depends on whether our application is running in the development
environment or not. If it’s running in development
, it’s safe to give the user verbose error messages. Otherwise, we’ll just say “Internal Server Error”.
def internal_server_error(exception)
if Rails.env.development?
response = { type: exception.class.to_s, message: exception.message, backtrace: exception.backtrace }
else
response = { error: "Internal Server Error" }
end
render json: response, status: :internal_server_error
end
The first API endpoint we’ll implement is GET /api/v1/restaurants
which will return an array of Restaurant
models in an application/json
response.
To begin, let’s create a controller called Api::V1::RestaurantsController
to handle the request. Again, we’ll manually create these instead of using rails generate
:
touch app/controllers/api/v1/restaurants_controller.rb
mkdir -p app/views/api/v1/restaurants
Open the new controller and add:
class Api::V1::RestaurantsController < Api::V1::BaseController
def index
@restaurants = policy_scope(Restaurant)
end
end
The first thing to note is that RestaurantController
should inherit from Api::V1::BaseController
so that it gets all of the functionality we implemented around authentication and error handling!
Next, we make Api::V1::RestaurantsController#index
load a list of restaurants, limited to just those the current_user
is permitted to view:
def index
@restaurants = policy_scope(Restaurant)
end
Note that our controller isn’t explicitly rendering anything. It is still expecting to load the @restaurants
variable into a view at the default location (i.e. app/views/api/v1/restaurants/index.<whatever>
). We’ll create this later.
Now, let’s plug GET /api/v1/restaurants
into our controller. Open config/routes.rb
and add:
namespace :api, defaults: { format: :json } do
namespace :v1 do
resources :restaurants, only: [ :index ]
end
end
This creates the namespace /api/vi/
and then plugs /restaurants
into RestaurantsController#index
.
Finally, let’s create a view to render our data. Create app/views/api/v1/restaurants/index.json.jbuilder
:
touch app/views/api/v1/restaurants/index.json.jbuilder
And add:
json.array! @restaurants do |restaurant|
json.extract! restaurant, :id, :name, :address
end
This will return an array of Restaurant
with just the id
, name
and address
fields. We are purposefully excluding a lot of data from Restaurant
including for example :created_at
etc. (data which the User
shouldn’t really be too concerned with.
With our server running, send a live request to the endpoint:
curl -v -s http://localhost:3000/api/v1/restaurants
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /api/v1/restaurants HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< ETag: W/"4f53cda18c2baa0c0354bb5f9a3ecbe5"
< Cache-Control: max-age=0, private, must-revalidate
< X-Request-Id: 82407482-5f3f-402b-83bf-da9a5d521e62
< X-Runtime: 0.009189
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
[]
If we’d seeded the application with some data (db/seeds.rb
) then we’d get something other than []
in response. But look! The content-type is application/json
so that’s awesome.
The second API endpoint we’ll implement is GET /api/v1/restaurants/:id
which will return an object representing a single Restaurant
model, along with any associated Comments
, wrapped up in a nice application/json
response.
To begin, let’s implement #show
in Api::V1::RestaurantsController
to handle the request.
Open application/controllers/restaurants_controller.rb
and add:
class Api::V1::RestaurantsController < Api::V1::BaseController
before_action :set_restaurant, only: [ :show ]
def index
@restaurants = policy_scope(Restaurant)
end
def show
end
private
def set_restaurant
@restaurant = Restaurant.find(params[:id])
authorize @restaurant # For Pundit
end
end
Instead of loading the Restaurant
with policy_scope
(as we do in #index
), we create a private method called set_restaurant
which executes before #show
is invoked. I literally don’t understand the advantage of doing it that way, but apparently it’s part of how Pundit
works. So there.
Two more steps! Remember how #show
isn’t explicitly rendering anything? It’s still expecting to load the @restaurant
variable into a view at the default location (i.e. app/views/api/v1/restaurants/show.<whatever>
). We’ll create this later.
Now, let’s plug GET /api/v1/restaurants/:id
into our controller. Open config/routes.rb
and alter the resources :restaurants
line to show:
resources :restaurants, only: [ :index, :show ]
This plugs /restaurants/:id
into RestaurantsController#show
.
Finally, let’s create a view to render our data. Create app/views/api/v1/restaurants/show.json.jbuilder
:
touch app/views/api/v1/restaurants/show.json.jbuilder
And add:
json.extract! @restaurant, :id, :name, :address
json.comments @restaurant.comments do |comment|
json.extract! comment, :id, :content
end
This will return a Restaurant
with just the id
, name
and address
fields, along with any Comments
associated it.
With our server running, send a live request to the endpoint:
curl -v -s http://localhost:3000/api/v1/restaurants/1
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /api/v1/restaurants/1 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Type: application/json; charset=utf-8
< Cache-Control: no-cache
< X-Request-Id: b4f7fdc0-54a1-474e-8de9-a3ae48ac916a
< X-Runtime: 0.004788
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"error":"Couldn't find Restaurant with 'id'=1"}
So…if we’d seeded the application with some data (db/seeds.rb
) then wouldn’t get {"error":"Couldn't find Restaurant with 'id'=1"}
.
But hey, It does tell is that the error handler we defined in Api::V1::BaseController
is doing its job!
Let’s take a quick detour to seed some data. Open db/seeds.rb
and add:
puts 'Cleaning database...'
Comment.destroy_all
Restaurant.destroy_all
User.destroy_all
puts 'Creating user...'
user = User.create! :email => 'api_user@gmail.com', :password => 'api_user', :password_confirmation => 'api_user'
puts 'Creating restaurant...'
restaurant = Restaurant.create! :name => 'rexmortus', :address => '100 Bourke Street, Melbourne', :user => user
puts 'Creating comments...'
Comment.create! :restaurant => restaurant, :user => user, :content => 'Example Comment 1'
Comment.create! :restaurant => restaurant, :user => user, :content => 'Example Comment 2'
Comment.create! :restaurant => restaurant, :user => user, :content => 'Example Comment 3'
puts 'Finished!'
Now run:
rake db:seed
Cleaning database...
Creating user...
Creating restaurant...
Creating comments...
Finished!
Boom. Now, when we hit GET /api/v1/restaurants/1
we should get some sweet, juicy data. BUT WHAT’S THIS?
curl -s -v http://localhost:3000/api/v1/restaurants/1
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /api/v1/restaurants/1 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Content-Type: application/json; charset=utf-8
< Cache-Control: no-cache
< X-Request-Id: 139578f8-8a7e-4bb2-8926-50aa30f586bf
< X-Runtime: 0.107412
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"error":"Unauthorized RestaurantPolicy.show?"}
Blast! An error. Open /app/policies/application_policy.rb
, and see howshow?
returns false
? We have to override this in RestaurantPolicy
. Open /app/policies/restaurant_policy.rb
and add:
class RestaurantPolicy < ApplicationPolicy
class Scope < Scope
def resolve
scope.all
end
end
def show?
true
end
end
I’m honestly not sure why Scope
isn’t catching this particular invocation but hey, there you go.
Now let’s send another request to our endpoint, and fingers crossed, we’ll get some delicious data:
curl -s -v http://localhost:3000/api/v1/restaurants/1
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /api/v1/restaurants/1 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< ETag: W/"1f473e861e9ab440410be4f6a45ed050"
< Cache-Control: max-age=0, private, must-revalidate
< X-Request-Id: ff4e2a98-00bb-49d1-908f-fc2f771b82db
< X-Runtime: 0.009095
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"id":1,"name":"rexmortus","address":"100 Bourke Street, Melbourne","comments":[{"id":1,"content":"Example Comment 1"},{"id":2,"content":"Example Comment 2"},{"id":3,"content":"Example Comment 3"}]}
Woo! It works.
The third API endpoint we will implement is PATCH /api/v1/restaurants/:id
which updates a single existing Restaurant
model, and then returns then it in a application/json
response.
Now, PATCH
functionality has a special requirement: only authenticated users should be able to access it.
Because this is an API, we need to implement a way for User
s of our API to authenticate themselves without using the sign-in form that ships as part of Devise
. We need to issue API
tokens!
Fortunately, this is a common requirement and there are many ready-made solutions. For this example we’ll use simple_token_authentication
, a particularly straightforward solution. Open your Gemfile
and add:
gem 'simple_token_authentication'
Then, install it:
bundle install
...
Fetching simple_token_authentication 1.15.1
Installing simple_token_authentication 1.15.1
...
Bundle complete! 21 Gemfile dependencies, 78 gems now installed.
Next, let’s use rails generate
to generate a migration that adds token
to the User
model, which we’ll use later as the main mechanism for authenticating Users
and restricting access to parts of our API.
rails generate migration AddTokenToUsers "authentication_token:string{30}:uniq"
Running via Spring preloader in process 28098
invoke active_record
create db/migrate/20190528105531_add_token_to_users.rb
To begin, lets implement #update
in Api::V1::RestaurantsController
to handle the request.
Finally, let’s run the new migration:
rake db:migrate
== 20190528105531 AddTokenToUsers: migrating ==================================
-- add_column(:users, :authentication_token, :string, {:limit=>30})
-> 0.0023s
-- add_index(:users, :authentication_token, {:unique=>true})
-> 0.0097s
== 20190528105531 AddTokenToUsers: migrated (0.0123s) =========================
Now that our database has been migrated, let’s enable the token
feature on the User
model. Open app/models/user.rb
and add:
class User < ApplicationRecord
acts_as_token_authenticatable
# [...]
end
Now simple_token_authentication
knows where to look for the actual token
, (i.e. the model User
) !
Next up, we have to configure our controller to use simple_token_authentication
as an authentication strategy. Open api/v1/restaurants_controller.rb
and add:
class Api::V1::RestaurantsController < Api::V1::BaseController
acts_as_token_authentication_handler_for User, except: [ :index, :show ]
before_action :set_restaurant, only: [ :show, :update ]
def index
@restaurants = policy_scope(Restaurant)
end
def show
end
def update
if @restaurant.update(restaurant_params)
render :show
else
render_error
end
end
private
def restaurant_params
params.require(:restaurant).permit(:name, :address)
end
def render_error
render json: { errors: @restaurant.errors.full_messages },
status: :unprocessable_entity
end
def set_restaurant
@restaurant = Restaurant.find(params[:id])
authorize @restaurant # For Pundit
end
end
There are a few things going on here:
-
We’re adding:
acts_as_token_authentication_handler_for User, except: [ :index, :show ]
Which applies our new
token
authentication strategy to#update
, but not#index
and#show
.-
We’re setting
@restaurant
in#update
:before_action :set_restaurant, only: [ :show, :update ]
-
Also we’re defining
#update
:def update if @restaurant.update(restaurant_params) render :show else render_error end end
This updates a
Restaurant
(with the id specified as:id
in/api/v1/restaurants/:id
) with theform-data
yielded from#restaurant_params
(which we’ll discuss in a moment.@restaurant.update
returns aboolean
value, so if the#update
is successful, we’ll render the template for#show
, otherwise we’ll render a simple error for the user. -
We’re also defining
#restaurant_params
which follows the Rails convention of whitelisting just the parameters we want for the action:def restaurant_params params.require(:restaurant).permit(:name, :address) end
-
Now that RestaurantsController
implements update
, let’s plug it into our router. Open /config/routes.rb
and add :update
:
resources :restaurants, only: [ :index, :show, :update ]
Ok, one last thing… We’ve configured the User
model to use our token
for authentication, but the User
we seeded earlier won’t have one! We have two options:
-
Re-seed the database
rake db:seed
-
Using the
rails console
, re-save the user (which creates atoken
):user = User.find_by(email: "api_user@gmail.com") user.save # The user did not have any token yet. This call generated one. user.reload.authentication_token # => "a6hYpzsfNJdYC6zEMxs3"
Now, we are finally ready to send a request:
curl -i -v -X PATCH \
-H 'Content-Type: application/json' \
-H 'X-User-Email: api_user@gmail.com' \
-H 'X-User-Token: RG7MWnncjsaUvdEhRemL' \
-d '{ "restaurant": { "name": "New name" } }' \
http://localhost:3000/api/v1/restaurants/2
...
{"error":"Unauthorized RestaurantPolicy.update?"}
Gotcha! Still have to update our RestaurantPolicy
to allow this action. Open config/policies/restaurant_policy.rb
and add:
def update?
true
end
Ok, NOW we can actually do the request:
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> PATCH /api/v1/restaurants/2 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/json
> X-User-Email: api_user@gmail.com
> X-User-Token: RG7MWnncjsaUvdEhRemL
> Content-Length: 40
>
* upload completely sent off: 40 out of 40 bytes
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
Content-Type: application/json; charset=utf-8
< ETag: W/"b3c82c9d643e3aca9f6ce84cfcc928c5"
ETag: W/"b3c82c9d643e3aca9f6ce84cfcc928c5"
< Cache-Control: max-age=0, private, must-revalidate
Cache-Control: max-age=0, private, must-revalidate
< X-Request-Id: e131bab9-c18d-4bc9-a6fe-d941f777d911
X-Request-Id: e131bab9-c18d-4bc9-a6fe-d941f777d911
< X-Runtime: 0.015231
X-Runtime: 0.015231
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
{"id":2,"name":"New name","address":"100 Bourke Street, Melbourne","comments":[{"id":4,"content":"Example Comment 1"},{"id":5,"content":"Example Comment 2"},{"id":6,"content":"Example Comment 3"}]}Charitys-MacBook-Pro:restaurants-api
Take a moment to study the headers. Evidence of our work is everywhere!
Now let’s implement the 4th endpoint: POST /api/v1/restaurants
, which creates a new Restaurant
model from the POST
parameters.
We should restrict unauthenticated users from using this action, so that only requests with a valid token
can create new Restaurant
instances.
Lucky for us, we’ve already configured Pudnit
and simple_token_authentication
, so this implementation is very straightforward.
First, let’s start by implementing #create
on API::V1::RestaurantsController
to handle this request. Open app/controllers/restaurants_controller.rb
and add:
def create
@restaurant = Restaurant.new(restaurant_params)
@restaurant.user = current_user
authorize @restaurant
if @restaurant.save
render :show, status: :created
else
render_error
end
end
First, we call Restaurant#new
and pass it the parameters yielded from restaurant_params
(which we previously implemented for #update
).
Then, we set the Restaurant
User
to current_user
, which should yield whatever User
is associated with the token
we submit with the request.
Next, we use Pundit
to apply our access policy (i.e.authorize @restaurant
).
A note: this action will be disallowed until we update RestaurantPolicy
. Open config/policies/restaurant_policy.rb
and add:
def create?
true
end
Finally, we invoke restaurant#save
which will attempt to write the new data. Remember, #save
returns a BOOL
so there are two possible outcomes:
-
If
#save
returnstrue
then we’ll execute:render :show, status: :created
That renders the
#show
template, additionally passing in a variable calledstatus
.- If
#save
returnsfalse
then we’ll invokerender_error
, which was described earlier in this example.
- If
The final step is to plug POST /api/v1/restaurants
into RestaurantsController#create
. Open config/routes.rb
and add :create
:
resources :restaurants, only: [ :index, :show, :update, :create ]
Ok! We are ready to create a new Restaurant
via. our new route handler:
curl -i -v POST \
-H 'Content-Type: application/json' \
-H 'X-User-Email: api_user@gmail.com' \
-H 'X-User-Token: RG7MWnncjsaUvdEhRemL' \
-d '{ "restaurant": { "name": "New restaurant", "address": "Paris" } }' \
http://localhost:3000/api/v1/restaurants
* Connected to localhost (::1) port 3000 (#1)
> POST /api/v1/restaurants HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/json
> X-User-Email: api_user@gmail.com
> X-User-Token: RG7MWnncjsaUvdEhRemL
> Content-Length: 66
>
* upload completely sent off: 66 out of 66 bytes
< HTTP/1.1 201 Created
HTTP/1.1 201 Created
< Content-Type: application/json; charset=utf-8
Content-Type: application/json; charset=utf-8
< ETag: W/"792cd735c8f59e4921f916e00f7bd363"
ETag: W/"792cd735c8f59e4921f916e00f7bd363"
< Cache-Control: max-age=0, private, must-revalidate
Cache-Control: max-age=0, private, must-revalidate
< X-Request-Id: 1f28cdd7-7c00-4911-a707-74f9c2dbd6ce
X-Request-Id: 1f28cdd7-7c00-4911-a707-74f9c2dbd6ce
< X-Runtime: 0.067653
X-Runtime: 0.067653
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
<
* Connection #1 to host localhost left intact
{"id":5,"name":"New restaurant","address":"Paris","comments":[]}
Shaboom shaboom!
Now let’s implement the 5th and final endpoint: DELETE /api/v1/restaurants/:id
, which deletes a Restaurant
model with the id
yielded by /api/v1/restaurants/:id
.
We should restrict unauthenticated users from using this action, so that only requests with a valid token
can delete Restaurant
instances.
Let’s start by implementing #delete
on API::V1::RestaurantsController
to handle this request. Open app/controllers/restaurants_controller.rb
and add:
before_action :set_restaurant, only: [ :show, :destroy ]
def destroy
@restaurant.destroy
head :no_content
end
Two things are happening here. The first is that we’re configuring destroy
to execute #set_restaurant
before it’s invoked:
before_action :set_restaurant, only: [ :show, :update, :destroy ]
That way @restaurant
is defined just prior invoking #destroy
on it:
def destroy
@restaurant.destroy
head :no_content
end
Again, let’s update our RestaurantPolicy
to allow this action. Open config/policies/restaurant_policy.rb
and add:
def destroy?
true
end
Now that our API::V1::RestaurantsController
is configured to handle #destroy
, let’s plug DELETE /api/v1/restaurants/:id
into it. Open config/routes.rb
and add:
resources :restaurants, only: [ :index, :show, :update, :create, :destroy ]
At long last, let’s DELETE
one of these suckers:
curl -i -X DELETE
-H 'X-User-Email: api_user@gmail.com'
-H 'X-User-Token: RG7MWnncjsaUvdEhRemL'
http://localhost:3000/api/v1/restaurants/3
HTTP/1.1 204 No Content
Cache-Control: no-cache
X-Request-Id: 8ecd6ffe-bd38-4403-8980-e09544fced77
X-Runtime: 0.012054
Note: if a Restaurant
has associated Comments
, attempting to delete it will cause a “foreign key” error. So, like, just don’t have any comments for now!