• Skip to primary navigation
  • Skip to main content
  • Skip to footer
Code The Dream School
Code the Dream Labs Logo

Code The Dream School

Main hub for class materials for Code the Dream’s classes

  • Code the Dream Home

Search Code The Dream School

More REST APIs

In the previous part of the lesson, we implemented REST authentication. Of course, when building an API, you do more than authenticate. In this part of the lesson we will add some additional APIs for CRUD operations. Create a new branch called more-rest for this part of the assignment.

Models

First, create your models. WIthin the repository directory, you do the following commands:

bin/rails generate model Member first_name:string last_name:string user:references
bin/rails generate model Fact member:references fact_text:string likes:integer

We want to implement authorization as well as authentication. Each user will have their own set of member and fact records. Accordingly, there is a one-to-many relationship between users and members. We will implement authorization checks so that a user can only see their own records. There is also a one-to-many relationship between members and facts.

Next you edit the model files. The file app/models/member.rb should look like this:

class Member < ApplicationRecord
  belongs_to :user
  validates :first_name, presence: true
  validates :last_name, presence: true
  validates :user_id, presence: true
  has_many :facts
end

And app/models/fact.rb should look like this:

class Fact < ApplicationRecord
  validates :fact_text, presence: true
  validates :likes, presence: true
  validates :member_id, presence: true
  validates_associated :member
  belongs_to :member
end

Note that you have validations, just as in Rails UI applications with views. Next, you set up the development and test databases as follows:

bin/rails db:migrate
bin/rails db:migrate RAILS_ENV=test

Controllers

Now, you set up your controllers. We are going to set them up with a route namespace, that includes a version number for the API. This is best practice, as your API may change over time.

bin/rails g controller api/v1/Members
bin/rails g controller api/v1/Facts

Next you set up your routes. You should add the following section to your config/routes.rb file:

  namespace :api do
    namespace :v1 do
      resources :members do
        resources :facts
      end
    end
  end

These routes are similar to what you have used before, with the exception that you are using route namespaces to separate them out. The routes for facts are nested within the member routes, corresponding to the one-to-many association between members and facts.

Adding Application Logic

Your application logic goes in your controllers. Because this is an API, there are no files corresponding to views. When a request comes in, the response will always render json, to send the responses in json format back to the caller. In other respects, the processing is much as in Rails UI applications. The HTTP status code returned will be, by default, 200, but there are other status codes that are appropriate sometimes. For example, 201 means resource created, and the 400 series codes imply a client side error. We will require authentication for access to these controller operations, so we need to include AuthenticationCheck and call is_user_logged_in. This is an unfinished version of your app/controllers/api/v1/members_controller.rb file:

class Api::V1::MembersController < ApplicationController
  include AuthenticationCheck

  before_action :is_user_logged_in
  before_action :set_member, only: [:show, :update, :destroy]

  # GET /members
  def index
    @members = Member.where(user_id: current_user.id)
    render json: {members: @members}
  end

  # GET /members/:id
  def show
    if check_access
      # your code goes here
    end
  end

  # POST /members
  def create
    @member = Member.new(member_params)
    @member.user_id = current_user.id
    if @member.save
      render json: @member, status: 201
    else
      render json: { error:
        "Unable to create member: #{@member.errors.full_messages.to_sentence}"},
        status: 400
    end
  end

  # PUT /members/:id
  def update
    if check_access
      # your code goes here
    end
  end

  # DELETE /members/:id
  def destroy
    if check_access
      @member.destroy
      render json: { message: 'Member record successfully deleted.'}, status: 200
    end
  end

  private

  def member_params
    params.require(:member).permit(:first_name, :last_name)
  end

  def set_member
    @member = Member.find(params[:id])
  end

  def check_access
    if (@member.user_id != current_user.id) 
      render json: { message: "The current user is not authorized for that data."}, status: :unauthorized
      return false
    end
    true
  end
end

You will have to complete the update and show methods yourself.. Include error handling! For the app/controllers/api/v1/facts_controller.rb file, you can use the following outline, but most of the methods you will have to complete yourself.

class Api::V1::FactsController < ApplicationController
  include AuthenticationCheck

  before_action :is_user_logged_in
  before_action :set_fact, only: [:show, :update, :destroy]
  before_action :check_access

  # GET /members/:member_id/facts
  def index
    @member = Member.find(params[:member_id])
    render json: @member.facts # note that because the facts route is nested inside members
                             # we return only the facts belonging to that member
  end

  # GET /members/:member_id/facts/:id
  def show
    # your code goes here
  end

  # POST /members/:member_id/facts
  def create
     @member = Member.find(params[:member_id])
    @fact = @member.facts.new(fact_params)
    if @fact.save
      render json: @fact, status: 201
    else
      render json: { error: 
"The fact entry could not be created. #{@fact.errors.full_messages.to_sentence}"},
      status: 400
    end
  end

  # PUT /members/:member_id/facts/:id
  def update
    # your code goes here
  end

  # DELETE /members/:member_id/facts/:id
  def destroy
    # your code goes here
  end

  private

  def fact_params
    params.require(:fact).permit(:fact_text, :likes)
  end

  def set_fact
    @fact = Fact.find_by(id: params[:id], member_id: params[:member_id])
  end
  
  def check_access 
    @member = Member.find(params[:member_id])
    if @member.user_id != current_user.id
      render json: { message: "The current user is not authorized for that data."}, status: :unauthorized
    end
  end
end

Exception Handling

The client application may send some bad json, or specify the id of a user or fact that does not exist. You need to catch those errors and return an appropriate error message and HTTP result code to the calling client application. This is done by creating an exception handler module, which is app/controllers/concerns/exception_handler.rb:

# app/controllers/concerns/exception_handler.rb
module ExceptionHandler
  # provides the more graceful `included` method
  extend ActiveSupport::Concern

  included do
    rescue_from ActiveRecord::RecordNotFound do |e|
      render json: { error: e.message }, status: :not_found
    end

    rescue_from ActiveRecord::RecordInvalid do |e|
      render json: { error: e.message }, status: :unprocessable_entity
    end

    rescue_from ActionController::ParameterMissing do |e|
      render json: { error: e.message }, status: :unprocessable_entity
    end
  end
end

Then add this line to app/controllers/application_controller.rb, just before the end statement:

  include ExceptionHandler

Testing Your Code Using Postman

Postman will send JSON to the URL you specify, and will also report back the JOHN it receives. You can send the following commands, to see what happens. First, start the server using

bin/rails s 

Then create Postman requests for each of the operations on members and facts. These would include a GET for all members, a GET for a particular member, a POST to create a member, a PUT to update a member, and a DELETE for a member. You will have to use the ID of the member in the URL for operations on a particular member. Then do the same for all CRUD operations on facts. For example, suppose you have a member with ID 2. Then you could retrieve all the facts for that member from the following URL:

http://localhost:3000/api/v1/members/2/facts

Once you have created all these requests, try them out. Of course, you must be logged in to perform these operations.

More on Security

We are using HttpOnly cookies as the store for the authentication token. This approach reduces the risks associated with cross site scripting attacks. However, it introduces a new security vulnerability, called cross site request forgery (CSRF). CSRF attacks work as follows. The user logs in to our application, and then while still logged in, they access another application that includes a hostile script. That script posts a request to our application, and because the cookies reside in the browser, the user’s logon credentials accompany the post from the hostile script. In this way, the hostile script can change user data in our application.

To prevent this attack, we need another security token. This is stored in a cookie too, but one that is accessible from JavaScript. It is inaccessible to hostile scripts that reside outside the application, however. We then set the X-CSRF-Token security header, and enable forgery protection in Rails.

Add the following line to app/controllers/users/registrations_controller.rb, right after the responds_to line:

  skip_forgery_protection only: [:create]

The same line must be added to app/controllers/users/sessions_controller.rb, at the same place. Then, add these lines to each of these files:

      cookies["CSRF-TOKEN"] = form_authenticity_token
      response.set_header('X-CSRF-Token', form_authenticity_token)

They go right above the “signed up successfully” in the registrations controller, and the “you are logged in” in the sessions controller. It is not really necessary to set the header to make this work, but it is convenient when we add swagger enablement in the next session.

At the top of the app/controllers/application_controller.rb file, you will see this line:

class ApplicationController < ActionController::API

One side effect is that forgery protection is disabled in Rails. So the line should be changed to read:

class ApplicationController < ActionController::Base

Once these changes are in place, you can restart the server and retest using Postman. You will now find that some requests don’t work, in particular any POST, PUT, or DELETE requests, except for the two that handle registration and logon.

To get these other requests working again, you need a way to capture the CSRF token. First, in Postman, click on the New at the top of the window and select Environment. Give your new environment some name. This is where the token will be stored. When you finish, the name of the environment you created should be in the upper right of the window. Then edit the registration request, and click on the tests tab. Add the following line:

pm.environment.set("CSRF",pm.cookies.get('CSRF-TOKEN'))

Make the same change to the logon request. The “test”, which is really request post processing, captures the token from the cookie when logon or registration occurs and stores it in the Postman environment. Then, for each of your Postman POST, PUT, and DELETE requests (except registration and logon) you need to change the headers to add:

X-CSRF-Token {{CSRF}}

This sets the required header from the environment variable. Do the logon request, and then try the other POST, PUT, and DELETE requests. They should now work.

Submitting Your Work

Once you get this far, stop the server, commit your changes, push them to github, and open a pull request. It’s time to submit your homework for this week. We will do the next steps in a new branch called swagger. After you have pushed your changes, while the more-rest branch is active, create the swagger branch.

Footer

Copyright © 2025 Code the Dream School | All Rights Reserved | Privacy Policy