Rails API với Versioning, Serializers và Pagination

Tiếp nối series loại bài về Rails API, mình sẽ giới thiệu đến mọi người về việc sử dụng Versioning, Serializers và Pagination.

Versioning trong Rails API

Khi chúng ta xây dựng 1 appp API thì việc quản lý version là điều quan trọng. Điều đó cảng quang trọng hơn khi chúng ta public API này với khách hàng theo 1 hợp đồng. Mỗi sự thay đổi thì sẽ hình thành 1 version mới.

Về việc sử dụng version trong Rails API chúng ta cần làm 2 việc:

  • Thêm 1 route constraint : việc này sẽ lựa chọn 1 phiên bản trong request headers
  • Namespace controllers: có các namespace khác nhau để xử những version khác nhau.

Rails support advanced constraints. sẽ định nghĩa 1 method là *matches? *. Việc này có thể giúp bạn handle controller ở route dành cho từng version khác nhau.

Tiếp theo, thì chúng ta sẽ thực hiện điều đó : Chúng ta sẽ thực hiện định nghĩa 1 class là ApiVersion, cái này sẽ check version từ request headers, và sau đó route đến module controller thích hợp. chúng ta sẽ đặt nó nằm trong app/lib.

class ApiVersion
  attr_reader :version, :default

  def initialize(version, default = false)
    @version = version
    @default = default
  end

  # check whether version is specified or is default
  def matches?(request)
    check_headers(request.headers) || default
  end

  private

  def check_headers(headers)
    # check version from Accept headers; expect custom media type `todos`
    accept = headers[:accept]
    accept && accept.include?("application/vnd.todos.#{version}+json")
  end
end

Class Apiversion nhận 2 parameter là version và default trong initialize. Đúng theo Rails constraints, sẽ thực hiện method matches? , method này sẽ được gọi với 1 request object, từ request object đó chúng ta có thể access được Accept headers và thực hiện kiểm tra version hoặc là version default của request đó. Quá trình này được gọi là Content Negotiation (đọc tài liệu thì gọi là thế chứ translate ra thì chuối lắm =)) ).

Content Negotiation

REST gắn bó khá chặt chẽ với HTTP, HTTP định nghĩa những cơ chế để có thể thể phục vụ những version khác nhau khi cùng thực hiện trên 1 URI. Mọi người có thể tìm hiểu thêm về Content Negotiation

ApiVersion của chúng ta thực hiện ở server, nơi khách hàng thực hiện 1 thông báo cho máy chủ một media type nào đó được hiểu bằng cách cũng cấp một Accept HTTP header. Theo như Media Type Specification, chúng ta có thể định nghĩa media types sử dụng vendor tree, ví dụ : application/vnd.example.resource+json. Như thế, chúng ta có thể custom một vendor media types application/vnd.todos.{version_number}+json, như thế. Cho phép người dùng có thể lựa chọn API version mà họ yêu cầu.

Bây giờ, chúng ta sẽ thay đổi route để phù hợp hơn, Chúng ta sẽ không muốn có 1 version API hiển thị ở URI như thế này, api/v1/todos, chúng ta sẽ sử dụng namespace của controller như thế này:

Rails.application.routes.draw do
  namespace :api do
    scope module: :v1, constraints: ApiVersion.new('v1', true) do
      resources :todos do
        resources :items
      end
    end
  end
  post 'auth/login', to: 'authentication#authenticate'
  post 'signup', to: 'users#create'
  # match '*path', via: [:options], to:  lambda {|_| [204, {'Access-Control-Allow-Headers' => "Origin, Content-Type, Accept,
  #   Authorization, Token", 'Access-Control-Allow-Origin' => "*", 'Content-Type' => 'text/plain'}, []]}
end

Như thế, cái này sẽ áp dụng cho toàn bộ resource bên trong nó. Chúng ta sẽ cúng cấp v1 là version mặc định, trong trường hợp version không được cung cấp thì v1 là version mặc định.

Tiếp theo, chúng ta sẽ di chuyển toàn bộ các todos và item đang có vào đúng namespace v1, tạo 1 module directory v1 trong controller. và đưa các file controller hiện có và đó. sau đó chúng ta sẽ modifle một chút cho phù hợp với namespace như sau:

class Api::V1::TodosController < ApplicationController
  ...
end

Tương tự như vậy đối với Items:

class Api::V1::ItemsController < ApplicationController
    ...
end

Giờ thì chúng ta có thể test serve như thế này rồi. :

# get auth token
$ http :3000/auth/login [email protected] password=test123
# get todos from API v1
$ http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'ey...AWH3FNTd3T0jMB7HnLw2bYQbK0g'
# get from API v2
$ http :3000/todos Accept:'application/vnd.todos.v2+json' Authorization:'ey...AWH3FNTd3T0jMB7HnLw2bYQbK0g'

Như thế, nếu chúng ta có thêm 1 version nữa, thì chúng ta sẽ tạo 1 v2 ngang với với v1 và thêm route cho nó :

Rails.application.routes.draw do
  namespace :api do
    scope module: :v2, constraints: ApiVersion.new('v2') do
      resources :todos, only: :index
    end
    scope module: :v1, constraints: ApiVersion.new('v1', true) do
      resources :todos do
        resources :items
      end
    end
  end
  post 'auth/login', to: 'authentication#authenticate'
  post 'signup', to: 'users#create'
  # match '*path', via: [:options], to:  lambda {|_| [204, {'Access-Control-Allow-Headers' => "Origin, Content-Type, Accept,
  #   Authorization, Token", 'Access-Control-Allow-Origin' => "*", 'Content-Type' => 'text/plain'}, []]}
end

và chúng ý răng các version không phải default thì phải luôn nằm trên version default, ví trong Rails nó sẽ matches từ trên xuống dưới và tìm kiếm đến route được matches? là true.

Lúc đó controller V2 sẽ định nghĩa như thế này:

class Api::V2::TodosController < ApplicationController
  def index
    json_response({ message: 'Hello there'})
  end
end

Như vậy là xong ở phần Versioning, tiếp theo chúng ta sẽ bàn về Serializers.

Serializers

Để có thể lấy todos và items thì chúng ta sẽ phải thự hiện 2 request API, nhưng như thế không phải là 1 ý tưởng hay. Chúng ta có thể thực hiện điều đó với chỉ 1 request nhờ Serializers. Serializers cho phép chúng ta tùy chỉnh JSON trả về. Để có thế get todos và các items tương ứng với chúng, ta cần định nghĩa serializers ở model Todo để nó có thể lấy được các attributes và các model quan hệ với nó.

Trước tiên chúng ta cần add GEM :

 gem 'active_model_serializers', '~> 0.10.0'

sau khi đã bundle install thì chúng ta sẽ thực hiên tạo serializer từ model Todo:

$ rails g todo_serializer.rb todo

Việc này tạo ra 1 thư mục với app/serializers và 1 file là todo_serializer.rb . Bây giờ chúng ta hay thực hiện định nghĩa serializer Todo và những dữ liệu chúng ta muốn có.

class TodoSerializer < ActiveModel::Serializer
  # attributes to be serialized
  attributes :id, :title, :created_by, :created_at, :updated_at
  # model association
  has_many :items

  # def cache_key
  #   [object, scope]
  # end
end

Pagination

Khi dữ liệu lớn lên, để đảm báo việc truy xuất dữ liệu được hiệu quả hơn thì chúng ta sẽ lấy với 1 số lượng nào đó cho mỗi lần, đảm bảo việc truy xuất vẫn nhưng và hiệu xuất cao. Để làm điều đó chúng ta sử dụng GEM will_paginate

sau khi add gem và install, chúng ta sẽ thực hiện việc get dữ liệu, việc đó sẽ được thực hiện ở Controller.

class Api::V1::TodosController < ApplicationController
  before_action :set_todo, only: [:show, :update, :destroy]

  #GET /todos
  def index
    @todos = current_user.todos.search(params).paginate(page: params[:page], per_page: params[:per_page] || 20)
    # json_response(hash_data)
    render json: @todos, meta: { pagination:
                                  { per_page: params[:per_page] || 20,
                                    total_pages: @todos.total_pages,
                                    total_objects: @todos.total_entries } }
  end
[...]
end

như vậy, chúng ta đã có thể get todos với số lượng 20 hoặc tùy theo params mà bạn truyền vào.

Tóm lại

Vậy là đã kết thúc series giới thiệu về Rails API của mình, qua đó mình đã giới thiệu đến mọi người những kiến thức cơ bản về rails API, những tính năng cần có. để tạo 1 ứng dụng API có thể hoạt động tốt, như Authentication với JWT, Versioning, Serializers, Pagination. Phần 1: Tạo API trong Rails 5 Phần 2 : Authentincation với JWT trong Rails 5 Chỉ tạo API không thôi thì chẳng biết để là gì =)), nên loạt bài sau, mình sẽ viết cái gì đó để dùng nó như AngularJS hày ReactJS chẳng hạn, để mọi người có thể biết nó dùng như thế nào. =)) Rất mong nhận được sự góp ý của tất cả mọi nguời nếu có thiếu sót. Cảm ơn đã đọc và góp ý. 😃)