Back-end Engineering Articles

I write and talk about backend stuff like Ruby, Ruby On Rails, Databases, Testing, Architecture / Infrastructure / System Design, Cloud, DevOps, Backgroud Jobs, and more...

Twitter:
@daniel_moralesp

2020-02-28

Building a Rails API

Resources:

There are 4 types of Rails APIs behaviors

  1. Rails API Only, where the project was created from scratch with the command rails new name_app —api
  2. Tradicional Rails app previously created, and then we need to support API endpoints, so we next to mix both
  3. New Rails traditional App, when we know that we'll need to support API endpoints from the very beginning
  4. Consume an API from a Rails APP
The difference between the 3 can be a bit tricky, because some endpoints will need to change something from the main app, or give two types of resposes, html and json. So here we are going to see them

1- Rails API Only

Where the project was created from scratch with the command rails new name_app —api

Idea-API as an example

https://github.com/danielmoralesp/idea-api

2- Tradicional Rails app 

previously created, and then we need to support API endpoints, so we next to mix both

Windows server api as an example of current normal rails app

https://github.com/danielmoralesp/rails_windows_server

3- Consume an API from a Rails APP


Here we're going to use the tradicional rails new command (without the —api flag) and then change the endpoints and config to consume API endpoints.

The example app is here: https://github.com/danielmoralesp/rails_windows_front

This means that we're going to act as the frontend of an API and we'll use HTTP gem https://github.com/httprb/http

Another thing is that we don't have a model with records inside our app, because we're consuming an API that host all of that information, what we're doing here is the CRUD but all via endpoitns, so some things will change here, form controllers and from views

# creating rails APP
$ rails new rails_windows_front
$ rails g scaffold Projects title

# this also can be a previolsy created app

We created a Scaffold, that means that some things are as default, here we're going to change or review the important code

Routes

Rails.application.routes.draw do
  resources :projects
end

Controllers

Assuming that the API that we're consuming is built also in Rails and deal with Devise Auth Token, we need to login and send auth tocken on each request. Please check the document about devise auth token

class ProjectsController < ApplicationController
  before_action :auth_api
  before_action :set_project, only: %i[ show edit update destroy ]

  AXIOM_RAILS_LOCALHOST = "<http://www.www.localhost:3001>"
  AXIOM_RAILS_STAGING_MINE = "<https://www.staging.click>"
  AXIOM_RAILS_STAGING_PARSO = "<https://www.staging.cl>"
  AXIOM_RAILS_PRODUCTION_MIME = "<https://www.railsapi.click>"
  AXIOM_RAILS_PRODUCTION_PARSO = "<https://www.rails-production.com>"

  # GET /projects or /projects.json
  def index
    # @projects = Project.all

    response = HTTP.headers("access-token": @access_token, "uid": @uid, "client": @client)
                   .get("#{AXIOM_RAILS_STAGING_MINE}/api/v1/projects")

    if response.status.code != 200
      raise Exception.new("receive an status #{response.status.code}")
    else
      @projects = JSON.parse(response.body.to_s)
    end
  end

  # GET /projects/1 or /projects/1.json
  def show
    response = HTTP.headers("access-token": @access_token, "uid": @uid, "client": @client)
                   .get("#{AXIOM_RAILS_STAGING_MINE}/api/v1/projects/#{@project_id}")

    if response.status.code != 200
      raise Exception.new("receive an status #{response.status.code}")
    else
      @project = JSON.parse(response.body.to_s)
    end
  end

  # GET /projects/new
  def new
    @project = Project.new
  end

  # GET /projects/1/edit
  def edit
    response = HTTP.headers("access-token": @access_token, "uid": @uid, "client": @client)
                   .get("#{AXIOM_RAILS_STAGING_MINE}/api/v1/projects/#{@project_id}")

    if response.status.code != 200
      raise Exception.new("receive an status #{response.status.code}")
    else
      @project = JSON.parse(response.body.to_s)
    end
  end

  # POST /projects or /projects.json
  def create
    # @project = Project.new(project_params)

    #byebug

    response = HTTP.headers('content-type': 'application/json', 
                            "access-token": @access_token, 
                            "uid": @uid, 
                            "client": @client)
                   .post("#{AXIOM_RAILS_STAGING_MINE}/api/v1/projects", 
                            json: { title: project_params['title']})

    if !response.status.success?
      raise Exception.new("receive an status #{response.status.code}")
    else
      @project = JSON.parse(response.body.to_s)

      respond_to do |format|
        format.html { redirect_to @project, notice: "Project was successfully created." }
        format.json { render :show, status: :created, location: @project }
      end
    end

    # respond_to do |format|
    #   if @project.save
    #     format.html { redirect_to @project, notice: "Project was successfully created." }
    #     format.json { render :show, status: :created, location: @project }
    #   else
    #     format.html { render :new, status: :unprocessable_entity }
    #     format.json { render json: @project.errors, status: :unprocessable_entity }
    #   end
    # end
  end

  # PATCH/PUT /projects/1 or /projects/1.json
  def update
    response = HTTP.headers('content-type': 'application/json', 
                            "access-token": @access_token, 
                            "uid": @uid, 
                            "client": @client)
                   .put("#{AXIOM_RAILS_STAGING_MINE}/api/v1/projects/#{@project_id}", 
                        json: { title: project_params['title']})

    if !response.status.success?
      raise Exception.new("receive an status #{response.status.code}")
    else
      @project = JSON.parse(response.body.to_s)

      respond_to do |format|
        format.html { redirect_to @project, notice: "Project was successfully updated." }
        format.json { render :show, status: :ok, location: @project }
      end
    end

    # respond_to do |format|
    #   if @project.update(project_params)
    #     format.html { redirect_to @project, notice: "Project was successfully updated." }
    #     format.json { render :show, status: :ok, location: @project }
    #   else
    #     format.html { render :edit, status: :unprocessable_entity }
    #     format.json { render json: @project.errors, status: :unprocessable_entity }
    #   end
    # end
  end

  # DELETE /projects/1 or /projects/1.json
  def destroy
    response = HTTP.headers('content-type': 'application/json', 
                            "access-token": @access_token, 
                            "uid": @uid, 
                            "client": @client)
                    .delete("#{AXIOM_RAILS_STAGING_MINE}/api/v1/projects/#{@project_id}")

    if !response.status.success?
      raise Exception.new("receive an status #{response.status.code}")
    else
      # @project = JSON.parse(response.body.to_s)

      respond_to do |format|
        format.html { redirect_to projects_url, notice: "Project was successfully destroyed." }
        format.json { head :no_content }
      end
    end

    # @project.destroy
    # respond_to do |format|
    #   format.html { redirect_to projects_url, notice: "Project was successfully destroyed." }
    #   format.json { head :no_content }
    # end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_project
      @project_id = params[:id]
    end

    # Only allow a list of trusted parameters through.
    def project_params
      params.require(:project).permit(:title)
    end

    def auth_api
      # authentication
      auth_response = HTTP.post("#{AXIOM_RAILS_STAGING_MINE}/auth/sign_in", form: {
        "email": "email,
        "password": "password"
      })
      @access_token = auth_response["access-token"]
      @uid = auth_response["uid"]
      @client = auth_response["client"]
      # authentication
    end
end

As we can see in the auth_api we have the authentication and then the endpoiints slighly change given the fact we're consuming an API and not having our ouw records in our own models, so we don't need to create any migration file or DB to connect to the given API

Views

# views/projects/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Projects</h1>

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <%= @projects %>

    <% @projects.each do |project| %>
      <tr>
        <td><%= project['id'].to_i %></td>
        <td><%= project['title'] %></td>
        <td><%= project_path(project['id']) %></td>
        <td><%= link_to 'Show', project_path(project['id']) %></td>
        <td><%= link_to 'Edit', edit_project_path(project['id']) %></td>
        <td><%= link_to 'Destroy', project_path(project['id']), method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Project', new_project_path %>

# views/projects/new.html.erb

<h1>New Project</h1>

<%= render 'form', project: @project, url: projects_path, method: "post" %>

<%= link_to 'Back', projects_path %>

# views/projects/edit.html.erb

<h1>Editing Project</h1>

<%= render 'form', project: @project, url: project_path(@project['id']), method: "put" %>

<%= link_to 'Show', @project %> |
<%= link_to 'Back', projects_path %>

# views/projects/show.html.erb

<p id="notice"><%= notice %></p>

<p>
  <strong>Title:</strong>
  <%= @project['title'] %>
</p>

<%= link_to 'Edit', edit_project_path(@project['id']) %> |
<%= link_to 'Back', projects_path %>

With this we have the conection