Sử dụng gem Grape và Serializer trong API project
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ữ ...
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