TODO với React + Rails Part 3: Xây dựng Base API
Bài đăng này đã không được cập nhật trong 6 năm
Introduction
Trong bài part 1 và part 2, chúng ta đã xây dựng xong TODO App với các chức năng đơn giản với Reactjs. Hôm này chúng ta sẽ tiếp tục với phần API với Rails. Trong bài này, mình sẽ xây dựng API Base: data response, error response, bắt các exception như thế nào ...?
Response format
Việc đầu tiên, chúng ta sẽ cần có một response format làm chuẩn. Về việc thống nhất là response trả về như thế nào là tuỳ bạn. Nhưng ở đây mình gợi ý một format mình thường dùng trong dự án như sau:
- Trường hợp response success:
 
{
  "success": true,
  "data": {
      "id": 1, 
      "title": "task1"
  }
}
success: có thể true/false.
data: có thể là object / hash / array.
- Trường hợp lỗi validation:
 
{
  "success": false,
  "errors": [
      {
        "resource": "task",
        "fields": "title",
        "code": 1000,
        "message": "Title can't be blank"
      }
  ]
}
resource: là model bị lỗi fields: là trường bị lỗi
- Trường hợp lỗi như: missing params, record not found, ...
 
  {
     "success": false
     "errors": [
         {
           "message": "Record not found",
           "code": 1000    
         }
     ]
  }
Xây dựng API Base
Ở trên mình có response format làm chuẩn dùng cho App của mình rồi. Vậy làm thế nào để nhận được response tương ứng.
Mình sẽ sử dụng gem active_model_serializers để hỗ trợ xử lý response trả về. Cách sử dụng chi tiết , bạn xem ở đây https://github.com/rails-api/active_model_serializers
Base controller cho api
- tạo 
app/controllers/api_controller.rb: 
tất cả controller sẽ kế thừa từ controller này. Trong này chứa ExceptionRescue là module chung để bắt các exception và trả về format response như trên.
class ApiController < ActionController::API
  include Api::ExceptionRescue
end
- tạo 
app/controllers/api/exception_rescue.rb 
module Api::ExceptionRescue
  extend ActiveSupport::Concern
  included do
    rescue_from ActiveRecord::RecordInvalid, with: :render_invalidation_response
    rescue_from ActionController::ParameterMissing, ActiveRecord::RecordNotFound,
      ArgumentError, ActiveRecord::RecordNotDestroyed, with: :render_params_error_response
    def render_invalidation_response exception
      render json: exception.record, serializer: Api::Errors::ValidationErrorsSerializer,
        status: :bad_request
    end
    def render_params_error_response exception
      render json: exception, serializer: Api::Errors::ParamsErrorsSerializer,
        status: :bad_request
    end
  end
end
Module trên xử lý bắt các exceptions và sử dụng serliaizer để parse nó thành format mình cần. Serlializer đó bao gồm
Api::Errors::ValidationErrorsSerializer: trả về response format cho các lỗi validationApi::Errors::ParamsErrorsSerializer: trả về response format cho các lỗi như: missing params, record not found, ...
Base serializer để xử lý lỗi
- tạo 
BaseErrorsSerializer:app/serializers/api/errors/base_errors_serializer.rb: là class cha để xử lý lỗi, các class khác sẽ kế thừa từ class này. 
class Api::Errors::BaseErrorsSerializer < ActiveModel::Serializer
  attribute :success
  attribute :errors
  def success
    false
  end
end
- tạo 
app/serializers/api/errors/validation_errors_serializer.rb: trả về response format cho các lỗi validation 
class Api::Errors::ValidationErrorsSerializer < Api::Errors::BaseErrorsSerializer
  def errors
    object.errors.details.map do |field, details|
      details.map.with_index do |error_details, index|
        Api::Errors::EachValidationErrorSerializer.new(
          object, field, error_details, object.errors[field][index]).generate
      end
    end.flatten
  end
end
trong class trên mình cần thêm EachValidationErrorSerializer
- tạo 
app/serializers/api/errors/each_validation_error_serializer.rb 
class Api::Errors::EachValidationErrorSerializer
  def initialize record, error_field, details, message
    @record = record
    @error_field = error_field
    @details = details
    @message = "#{field} #{message}"
  end
  def generate
    {
      resource: resource,
      field: @error_field,
      code: code,
      message: @message
    }
  end
  private
  def resource
    I18n.t(
      underscored_resource_name,
      scope: [:api_validation, :resources]
    )
  end
  def field
     I18n.t(
      @error_field,
      scope: [:api_validation, :fields, underscored_resource_name]
    )
  end
  def code
    I18n.t(
      @details[:error],
      scope: [:api_validation, :codes]
    )
  end
  def underscored_resource_name
    @record.class.to_s.gsub("::", "").underscore
  end
end
Class trên mình sẽ bắt được các exception và nhận được response format theo mong muốn. Nhưng mình cần thêm I18n có dạng như sau:
en:
  api_validation:
    resources:
      task: Task
    fields:
      task:
        title: Title
    codes:
      blank: 1000
      taken: 1001
      invalid: 1002
      confirmation: 1003
      too_long: 1007
      too_short: 1008
      not_a_number: 1009
      greater_than: 1010
      not_an_integer: 1011
- tạo 
app/serializers/api/errors/params_errors_serializer.rb: trả về response format cho các lỗi như: missing params, record not found, ... 
class Api::Errors::ParamsErrorsSerializer < Api::Errors::BaseErrorsSerializer
  I18N_SCOPE = [:params_exception]
  def errors
    [{code: code, message: message}]
  end
  private
  def code
    error_type[:code]
  end
  def message
    error_type[:message]
  end
  def error_type
    @error_type ||= I18n.t class_name_underscore, scope: I18N_SCOPE
  end
  def class_name_underscore
    object.class.name.underscore.gsub(%r{\/}, ".")
  end
end
I18n scope cần có dạng:
  params_exception:
    active_record:
      record_not_found:
        code: 3001
        message: No data found.
    argument_error:
      code: 3002
      message: Incorrect parameter.
    record_not_destroyed:
      code: 3003
      message: Cannot be deleted.
Đến đây là chúng ta đã làm xong phần Base cho API rồi, baì tiếp theo chúng ta sẽ làm tiếp từng các api cần thiết cho App của mình.
All rights reserved