Sử dụng gem Grape và Serializer trong API project
Bài đăng này đã không được cập nhật trong 8 năm
1. Giới thiệu
Trong nhiều dự án viết API, nếu chỉ đơn thuần đáp ứng theo mục đích của API là nhận input request, xử lý và response data cần thiết, thì ta hoàn toàn có thể sử dụng theo cấu trúc của Rails đó dùng Controller.
Tuy nhiên, thực tế, API đòi hỏi nhiều hơn là chỉ xử lý, và trả về dữ liệu, có thể phải custom url API, custom error response, custom output response theo yêu cầu của client, ..., lúc đấy thì nếu chỉ xử lý trong controller thì mọi thứ sẽ rất khó khăn và dẫn đến lặp code nhiều. Trong dự án vừa rồi, mình đã được tìm hiều và vận dụng gem Grape và Serializer trong dự án về API
2. Cách sử dụng
Thêm gem grape
và grape-active_model_serializers
vào trong gemfile rồi chạy bundle instal, khi đó các xử lý API sẽ nằm trong thư mục api
được tạo ra mặc định.
Để có thể tăng tốc viết API, thì trước tiên ta phải xây dựng base để đảm bảo tính mở rộng, phân biệt version cho API, custom Error response cho từng API
module BaseAPI
extend ActiveSupport::Concern
extend APIValidation
included do
format :json # sử dụng json cho response data
formatter :json, Grape::Formatter::ActiveModelSerializers
error_formatter :json, ErrorFormatter
before do
### Gán header response result_code nếu xử lý thành công, để trong setting Settings.result_codes.success
header[Settings.response_headers.result_code] = Settings.result_codes.success
end
rescue_from Grape::Exceptions::ValidationErrors do
### Khai báo format error khi có lỗi và đặt result_code trong setting Settings.result_codes.failure
error!({error_code: Settings.http_code.code_400,
reason: "Validations Error!"},
Settings.http_code.code_200,result_code: Settings.result_codes.failure)
end
rescue_from APIError::Base do |e|
key_error = e.class.name.split("::").drop(1).map(&:underscore)
if e.message.is_a?(Hash)
error_code = e.message[:error_code]
message = e.message[:reason]
else
error_code = Settings.error_codes.common.server_error
message = e.message
end
error!({error_code: error_code, reason: message},
Settings.http_code.code_200,
Settings.response_headers.result_code => Settings.result_codes.failure)
end
helpers do
# khai báo các hàm xử lý đầu vào
def authenticate!
raise APIError::Common::Unauthorized unless current_user
end
def render_record_not_found!
raise APIError::Common::NotFound
end
def current_user
# Dựa vào từng yêu cầu của project để xử lý current_user, có thể dựa vào devise hoặc lấy từ database theo xử lý nào đó
end
end
end
end
Tạo file chung lưu trữ các API cùng nhóm cho các version
class ProjectAPI < Grape::API
include BaseAPI
helpers do
def current_user
### Dựa vào từng yêu cầu của project để xử lý current_user
end
end
mount V1
end
Khai báo link url cho nhóm api này trong routes.rb
Rails.application.routes.draw do
mount ProjectAPI => "/project/"
end
Tương ứng với class ProjectAPI, ta tạo thư mực trong folder api là project_api
, chứa các version liên quan tới nhóm project api này
Trong folder project_api
, tạo file v1.rb
và folder v1 để lưu các api cho version 1
với file v1.rb được khai báo như sau:
class ProjectAPI::V1 < Grape::API
version "v1", using: :path
mount UserAPI
desc "Return the current ProjectAPI version - V1."
get do
{version: "ProjectAPI v1"}
end
end
Ví dụ, ta muốn xây dựng api liên quan đến user như create, show, list, edit, update, destroy, thì trong thư mục v1, ta tạo file user_api.rb
và folder user_api
Giả sử user có 2 thuộc tính là name, address
File user_api.rb
là nơi để mount các API liên quan đến user
class ProjectAPI::V1::UserAPI < Grape::API
mount DetailAPI
mount UpdateAPI
mount RegistrationAPI
mount ListAPI
end
Mỗi khi thêm 1 API cho nhóm user, thì ta tạo file api (list_api.rb, detail_api.rb, update_api.rb, ...) trong thư mục user_api, và thêm khai báo mount + tên class vào trong file user_api.rb
Ví dụ với list_api.rb
class ProjectAPI::V1::UserAPI::ListAPI < Grape::API
before do
authenticate!
end
resources :user do
namespace :list do
params do
## nếu muốn thực hiện list user theo name, ta nhận input là name cấn search
optional :search_name, type: String
end
post :get do
users = User.all.by_search_name(params.search_name)
raise APIError::User::List:ListEmpty unless users
## Thông thường các API luôn có 1 key bao bên ngoài data response, để diễn đạt mục đích của API, vd ở đây là user_list
## Trong đây ta sẽ sử dụng serializer để định nghĩa data user response
serializer_list = ActiveModel::Serializer::CollectionSerializer.new(users, serializer: User::ListSerializer, current_id: current_user.id)
{user_list: serializer_list}
end
end
end
end
API sẽ sử dụng url là local_host/project/v1/user/list/get
Ta sử dụng serializer cụ thể cho api list này là User::ListSerializer
vì user sẽ có nhiều api, mà mỗi api sẽ trả về các thông tin của user khác nhau, nên để dễ xử lý và mở rộng, ta nhóm các serializer của cùng model vào folder riêng
Tạo file serializer chung cho user , khai báo các thuộc tính chung mà User sẽ trả về
class UserSerializer < ActiveModel::Serializer
attributes :name, :address
end
Tùy từng API mà sẽ custom serializer tương ứng
Tạo file list_serializer.rb trong folder users trong folder serializer( được sinh ra khi chạy gem grape-active_model_serializers
)
class Users::ListSerializer < UserSerializer
attributes :current_id, :money_count
def current_id
## ở đây ta cần gửi thông tin từ api vào để xử lý, ta dùng instance_options để get
instance_options[:current_id]
end
def money_count
## Giả sử user quan hệ has_many với money
object.moneys.count
end
end
Tương tự, ta tạo các file serializer cho các api của user, kế thừa UserSerializer
giúp loai bỏ code lặp.
Tạo module khai báo error để sinh lỗi cho API
module APIError
class Base < Grape::Exceptions::Base
def initialize *args
if args.length == 0
t_key = self.class.name.underscore.gsub(%r{\/}, ".")
super message: I18n.t(t_key)
else
super(*args)
end
end
end
module Common
class ConnectionRefused < APIError::Base
end
class Unauthorized < APIError::Base
end
class NotFound < APIError::Base
end
module User
modulde List
class ListEmpty < APIError::Base
end
end
module Update
class Failed < APIError::Base
end
end
end
end
Để response mã code cho từng error, trong file setting, ta viets như sau:
error_codes:
common:
connection_refused: 400
server_error: 500
error: 600
unauthorized: 601
user:
list:
list_empty: 123
update:
failed: 456
Để response reason cho error, ta viết trong file yml
en:
api_error:
common:
connection_refused: "Connection is Refused!"
validation_errors: "Validations Error!"
unauthorized: "Unauthorized!"
user:
list:
list_empty: "List empty!"
update:
failed: "Update failed!"
Từ đây, nếu muốn thêm error thì ta định nghĩa trong module APIError, thêm error code trong setting, thêm reason error trong yml, nếu không thêm error code thì ta xử lý lấy mặc định error code.
3. Kết luận
Trên đây là cách xây dựng API dựa theo gem grape và serializer, dựa theo base đã viết sẵn, ta sẽ chỉ phải tập trung logic cho từng api, còn các quá trình viết lỗi, và mô tả data response thì sẽ lặp lại theo quy trình đã xây dựng trên.
Để biết thêm nhiều tùy biến của grape và serializer, các bạn có thể truy cập vào link gem tương ứng grape, serializer
All rights reserved