Skip to content

Instantly share code, notes, and snippets.

@rexmortus
Created May 29, 2019 00:47
Show Gist options
  • Save rexmortus/8f950781517b88764fb080abc1819f40 to your computer and use it in GitHub Desktop.
Save rexmortus/8f950781517b88764fb080abc1819f40 to your computer and use it in GitHub Desktop.
Notes on 'Building an API'

Building an API

0.0 Prerequisites

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

1. Create application

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

2. Generate models

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.

3. API Setup

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

4. Index endpoint

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.

5. Show endpoint

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!

6. Seeding data

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.

7. Updating Restaurants

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 Users 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:

  1. We’re adding:

    acts_as_token_authentication_handler_for User, except: [ :index, :show ]

    Which applies our new tokenauthentication strategy to #update, but not #index and #show.

    1. We’re setting @restaurant in #update:

      before_action :set_restaurant, only: [ :show, :update ]
    2. 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 the form-data yielded from #restaurant_params (which we’ll discuss in a moment.

      @restaurant.update returns a boolean value, so if the #update is successful, we’ll render the template for #show, otherwise we’ll render a simple error for the user.

    3. 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:

  1. Re-seed the database

    rake db:seed
  2. Using the rails console, re-save the user (which creates a token):

    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!

8. Creating Restaurants

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:

  1. If #save returns true then we’ll execute:

    render :show, status: :created

    That renders the #show template, additionally passing in a variable called status.

    1. If #save returns false then we’ll invoke render_error, which was described earlier in this example.

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!

9. Deleting restaurants

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment