Xây dựng một ứng dụng Restful API đơn giản với Rails 5 (Phần 2)

phần 1 mình đã xây dựng được một ứng dụng API cơ bản, tiếp theo mình sẽ áp dụng gem active_model_serializers để xây dựng các response chuẩn cho ứng dụng.

Active model serializers

ActiveModelSerializers tạo convention về cấu hình sang dạng Json.

ActiveModelSerializers hoạt động thông qua hai thành phần: serializers và adapter.

Serializers mô tả về các thuộc tính và các mối quan hệ cần được nhắc đến.

Adapters mô tả cách mà các thuộc tính và các mối quan hệ được nhắc đến.

SerializableResource phối hợp các resources , Adapter và Serializer để xuất bản các tài nguyên serialization. Các serialization có #as_json, #to_json và #serializable_hash các phương pháp được sử dụng bởi Rails JSON Renderer.

Cài đặt

Thêm gem "active_model_serializers" vào Gemfile. Sau đó chạy bundle install

Tạo serializers

Chạy lệnh rails g serializer post để khởi tạo serializer chô model post mà ta đã tạo từ trước.

#app/serializers/post_serializer.rb

class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :content
end

Như vậy là chúng ta đã khởi tạo xong serializer để sử dụng.

Response sử dụng serializers

Mình sẽ viết lại các phương thức trong PostsController sử dụng serializer

index

def index
    @posts = Post.order('created_at DESC')
    render json: {
      success: true,
      data: ActiveModel::Serializer::CollectionSerializer.new(@posts, serializer: PostSerializer)
    }
  end

Kết quả test với Postman

show

def show
    render json: {
      success: true,
      data: PostSerializer.new(@post)
    }
  end

create

def create
    @post = Post.new post_params

    @post.save!
    render json: {
      success: true,
      data: PostSerializer.new(@post)
    }
  end

udpate

def update
    @post.update! post_params
    render json: {
      success: true,
      data: PostSerializer.new(@post)
    }
  end

destroy

def destroy
    @post.destroy!
    render json: {
      success: true,
      data: PostSerializer.new(@post)
    }
  end

Bắt các lỗi cơ bản với serializers

Validation Errors

Đầu tiên chúng ta sẽ khai báo một class dùng chung để sử dụng cho các serializer bắt lỗi.

#app/serializers/api/errors/base_errors_serializer.rb

class Api::Errors::BaseErrorsSerializer < ActiveModel::Serializer
  attribute :success
  attribute :errors

  def success
    false
  end
end

Response sẽ gồm 2 attribute là success để thông báo trạng thái và errors để đưa ra danh sách lỗi.

Tạo class bắt lỗi validate kế thừa từ class BaseErrorsSerializer

#app/serializers/api/errors/validation_errors_serializer.rb

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
#app/serializers/api/errors/each_validation_errors_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

Tiếp theo ta cần khai báo để trả về khi xảy ra validate exception.

#app/controllers/concerns/exception_rescue.rb

module ExceptionRescue
  extend ActiveSupport::Concern

  included do
    rescue_from ActiveRecord::RecordInvalid, with: :render_invalidation_response

    def render_invalidation_response exception
      render json: exception.record, serializer: Api::Errors::ValidationErrorsSerializer,
        status: :bad_request
    end
  end
end
#app/controllers/api_controller.rb

class ApiController < ActionController::API
  include Api::ExceptionRescue
end

Cuối cùng là khai báo I18n

#config/locales/en.yml

en:
  api_validation:
    resources:
      post: Post
    fields:
      post:
        title: Title
        content: content
    codes:
      blank: 1000

Record Not Found

Trường hợp lỗi xảy ra khi không tìm thấy record tương ứng, ta cũng xây dựng một class để khai báo lỗi.

#app/serializers/api/errors/active_record_not_found.rb

class Api::Errors::ActiveRecordNotFound
  attr_reader :model, :detail, :message_key

  def initialize error, message: nil
    @model = error.model.underscore
    @detail = error.class.to_s.split("::")[1].underscore
    @message_key = message || :default
    @errors = serialize
  end

  def serialize
    {
      resource: resource,
      code: code,
      message: message
    }
  end

  def to_hash
    {
      success: false,
      errors: serialize
    }
  end

  private
  def message
    I18n.t "params_exception.active_record.record_not_found.message"
  end

  def resource
    I18n.t(
      underscored_resource_name,
      scope: [:api_validation, :resources]
    )
  end

  def code
    I18n.t detail,
      scope: [:api, :errors, :code],
      default: detail,
      locale: :api
  end

  def underscored_resource_name
    @model.to_s.gsub("::", "").underscore
  end
end

Thêm vào file exception_rescue.rb

  rescue_from ActiveRecord::RecordNotFound, with: :render_record_not_found_response
  def render_record_not_found_response exception
    render json: Api::Errors::ActiveRecordNotFound.new(exception).to_hash, status: :not_found
  end

Chỉnh sửa phương thức load_posts trong PostsController để raise ra lỗi với phương thức find_by! khi không tìm thấy record

  def load_post
    @post = Post.find_by! id: params[:id]
  end

I18n

en:
  api_validation:
    resources:
      post: Post
    fields:
      post:
        title: Title
        content: Content
    codes:
      blank: 1000
      invalid: 1002
      record_not_found: 1100
  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.

Tổng kết

Qua 2 bài viết, mình đã tóm tắt lại cách xây dựng một ứng dụng API đơn giản với Ruby on Rails. Từ những kiến thức cơ bản này chúng ta có cơ sở để tìm hiểu sâu hơn và nâng cao hơn về API và RoR. Bài viết này nhằm mục đích ghi nhớ lại kiến thức cơ bản để xây dựng API do mình tự tìm hiểu do đó sẽ còn nhiều thiếu sót, rất mong nhận được đóng góp từ các bạn. Chúc mọi người thành công!