Rails API với Versioning, Serializers và Pagination
Bài đăng này đã không được cập nhật trong 7 năm
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=test@email.com 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 ý. )
All rights reserved