Tạo API trong Rails 5

Như chúng ta đã biết thì Rails là Framework dùng để build 1 ứng dụng web, bên cạnh đó Rails còn hỗ trợ để xây dụng ứng dụng API. Nên trong loạt bài này mình sẽ giới thiệu đến các bạn việc xây dựng 1 ứng dụng API bằng rails như thế nào, về cơ bản sẽ giúp các bạn hình dung ra các tạo ứng dung API đơn giản với Rails.

Ứng dụng chúng ta sẽ xây dựng là Todo-list API.

API Endpoints

Chúng ta sẽ xây dựng API dựa trên chuẩn RESTful API. và RESTful endpoints của ứng dụng sẽ là như sau :

Endpoint Chức năng
POST /signup Signup
POST /auth/login Login
GET /auth/logout Logout
GET /todos List all todos
POST /todos Create a new todo
GET /todos/:id Get a todo
PUT /todos/:id Update a todo
DELETE /todos/:id Delete a todo and its items
GET /todos/:id/items Get a todo item
PUT /todos/:id/items Update a todo item
DELETE /todos/:id/items Delete a todo item

Thiết lập Project

Chúng ta sẽ tạo 1 Project Todos api như sau:

$ rails new todos-api --api
  • Chú ý 1 tý thì việc sử dụng "--api" ở đây chúng ta muốn một ứng dụng Rails API, tức là tạo ApplicationController kế thừa ActionController::API thay vì ActionController::Base trong các ứng dụng Rails chúng ta vẫn thường hay làm.

Tạo Model

Chúng ta sẽ tạo Model Todo

$ rails g model Todo title:string created_by:string

Bằng việc sử dụng generate command chúng ta đã tạo 1 model Todo như sau:

class CreateTodos < ActiveRecord::Migration[5.0]
  def change
    create_table :todos do |t|
      t.string :title
      t.string :created_by

      t.timestamps
    end
  end
end

Tiếp theo mà model Item

$ rails g model Item name:string done:boolean todo:references

Bằng việc sử dụng todo:references chúng ta đã tạo ra 1 liên kết giữa Item với Todo mode, và nó sẽ như thế này:

  • Nó sẽ add 1 khóa ngoài todo_id trong bảng items
  • Và từ đó chúng ta phải thiết lập liên kết belongs_to trong model Item
class CreateItems < ActiveRecord::Migration[5.0]
  def change
    create_table :items do |t|
      t.string :name
      t.boolean :done
      t.references :todo, foreign_key: true

      t.timestamps
    end
  end
end

Giờ thì migrations để nó tạo bảng trong DB thôi:

$ rails db:migrate

Ở 2 model của chúng ta sẽ như thế này:

# app/models/todo.rb
class Todo < ApplicationRecord
  # model association
  has_many :items, dependent: :destroy

  # validations
  validates_presence_of :title, :created_by
end
# app/models/item.rb
class Item < ApplicationRecord
  # model association
  belongs_to :todo

  # validation
  validates_presence_of :name
end

Tiếp theo cái không thể thiếu đó mà Controller, chúng ta sẽ generate 2 controller TodosItems

$ rails g controller Todos
$ rails g controller Items

Giờ thì định nghĩa routes

# config/routes.rb
Rails.application.routes.draw do
  resources :todos do
    resources :items
  end
end

Trong routes chúng ta đang định nghĩa todo resources có một nested là items resources. điều này thể hiện cho quan hệ 1-many để có thể thấy được cụ thể các routes, ban có thể thực hiện :

$ rails routes

Tạo Controller

Giờ thì hãy định nghĩa Controller:

# app/controllers/todos_controller.rb
class TodosController < ApplicationController
  before_action :set_todo, only: [:show, :update, :destroy]

  # GET /todos
  def index
    @todos = Todo.all
    json_response(@todos)
  end

  # POST /todos
  def create
    @todo = Todo.create!(todo_params)
    json_response(@todo, :created)
  end

  # GET /todos/:id
  def show
    json_response(@todo)
  end

  # PUT /todos/:id
  def update
    @todo.update(todo_params)
    head :no_content
  end

  # DELETE /todos/:id
  def destroy
    @todo.destroy
    head :no_content
  end

  private

  def todo_params
    # whitelist params
    params.permit(:title, :created_by)
  end

  def set_todo
    @todo = Todo.find(params[:id])
  end
end

json_response đây là hepler trả về JSON và HTTP status, cái này sẽ được định nghĩa trong concerns folder

# app/controllers/concerns/response.rb
module Response
  def json_response(object, status = :ok)
    render json: object, status: status
  end
end
  • set_todo callback sẽ thực hiện tìm kiếm 1 todo bằng id, nhưng nếu trong trường hợp không có record nào tồn tại thì ActiveRecord sẽ ném ra 1 exception ActiveRecord::RecordNotFound, chúng ta sẽ xử lý exception này và trả về thông báo 404.
# app/controllers/concerns/exception_handler.rb
module ExceptionHandler
  extend ActiveSupport::Concern

  included do
    rescue_from ActiveRecord::RecordNotFound do |e|
      json_response({ message: e.message }, :not_found)
    end

    rescue_from ActiveRecord::RecordInvalid do |e|
      json_response({ message: e.message }, :unprocessable_entity)
    end
  end
end

create method trong TodosController, ở đây chúng ta sử dụng create! thay vì sử dụng create, cách này sẽ ném ra exception ActiveRecord::RecordInvalid vì thế chúng ta sẽ xử lý exception này trong ExceptionHandler module. Tuy nhiên nếu chỉ như vậy thì controller của chúng ta sẽ ko thể biết được về sự tồn tại của helpers này, vậy nên chúng ta sẽ including nó trong application controller

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include Response
  include ExceptionHandler
end

Vâng , đến đây thì có thể check xem nó chạy như thế nào được rồi .

$ rails s

và thực hiện tạo request với Postman hoặc có thể là httpie

# GET /todos
$ http :3000/todos
# POST /todos
$ http POST :3000/todos title=123123 created_by=1
# PUT /todos/:id
$ http PUT :3000/todos/1 title=qweqweqwe
# DELETE /todos/:id
$ http DELETE :3000/todos/1

Giờ thì chúng ta sẽ viết Controller co Item của Todo.

# app/controllers/items_controller.rb
class ItemsController < ApplicationController
  before_action :set_todo
  before_action :set_todo_item, only: [:show, :update, :destroy]

  # GET /todos/:todo_id/items
  def index
    json_response(@todo.items)
  end

  # GET /todos/:todo_id/items/:id
  def show
    json_response(@item)
  end

  # POST /todos/:todo_id/items
  def create
    @todo.items.create!(item_params)
    json_response(@todo, :created)
  end

  # PUT /todos/:todo_id/items/:id
  def update
    @item.update(item_params)
    head :no_content
  end

  # DELETE /todos/:todo_id/items/:id
  def destroy
    @item.destroy
    head :no_content
  end

  private

  def item_params
    params.permit(:name, :done)
  end

  def set_todo
    @todo = Todo.find(params[:todo_id])
  end

  def set_todo_item
    @item = @todo.items.find_by!(id: params[:id]) if @todo
  end
end

và thực hiện kiếm tra API từ các request:

# GET /todos/:todo_id/items
$ http :3000/todos/2/items
# POST /todos/:todo_id/items
$ http POST :3000/todos/2/items name='zxczxc' done=false
# PUT /todos/:todo_id/items/:id
$ http PUT :3000/todos/2/items/1 done=true
# DELETE /todos/:todo_id/items/1
$ http DELETE :3000/todos/2/items/1

Trong phần sau chúng ta sẽ thực hiện Authentication với JWT (JSON WEB TOKEN).