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ữ 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 grapegrape-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