Các cách định nghĩa JSON format khi tạo một rails API

Bài viết hôm nay, tôi xin giới thiệu một số cách phổ biến hay được dùng để định nghĩa json format khi phát triển API dùng Rails: dùng respond_to block và as_json, sử dụng Presenter, dùng Jbuilder, dùng active_model_serializers.

Để phục vụ cho các phần sau, trước tiên chúng ta sẽ tạo 2 models là Post và Author:

rails g model Author name
rails g model Post title body:text author:references

author.rb

class Author < ActiveRecord::Base
  has_many :posts, dependent: :destroy
end

post.rb

class Poss < ActiveRecord::Base
  belongs_to :author
end

1, Dùng respond_to block và as_json

Đây là cách đơn giản nhất để xấy dựng JSON APIs, những gì chúng ta cần làm ở đây là dùng respond_to block trong controller action và render model tương ứng. Ví dụ, nếu bạn muốn có một JSON là thể hiện cho một post cụ thể, bạn sẽ viết code trong show action của posts controller như sau:

class PostsController < ApplicationController
  def show
    @post = Post.find params[:id]
    respond_to do |format|
      format.html
      format.json {render json: @post.as_json(only: [:id, :title, :body], include: [{author: {only: [:id, :name]}}])}
    end
  end
end

Chúng tạo một GET request với endpoint sau:

http://localhost:3000/posts/1.json
Kết quả output như sau:

{
  "id": 1,
  "title": "Slicker Than Rain",
  "body": "Farm-to-table selfies flannel gluten-free cardigan kale chips vice. Authentic meditation swag cred. Cardigan austin vinegar letterpress waistcoat.",
  "author": {
    "id": 2,
    "name": "Celine Von"
  }
}

Từ ví dụ ta thấy nếu độ phức tạp của JSON format của post tăng lên chẳng hạn nếu chúng ta muốn có comments bên trong post khi đó code trong as_json của chúng ta rất công kềnh và khó hiểu. Chú ý ngay cả khi chúng ta customize hàm as_json bên trong Post model thay thế việc sử dụng trong controller thì nó vẫn không tốt hơn bởi vì chúng ta đang chuyển một đoạn code phức tạp từ controller tới model

2, Sử dụng Presenter

Presenter là một design pattern trong Rails, nó đưa presentation logics trong model tới một nơi khác, tránh việc model chứa quá nhiều logic phức tạp.

Để sử dụng Presenter vào ví dụ chúng ta đang xét, chúng ta sẽ tạo một class là PostPresenter chứa các logic của việc định nghĩa cho json format. Tạo file app/presenters/post_presenter.rb có nội dung như sau:

class PostPresenter
  def initialize post
    @post = post
  end

  def as_json
    post.as_json.except("created_at", "updated_at", "author_id")
      .merge author: post.author.as_json.except("created_at", "updated_at")
  end

  private
  attr_reader :post
end

Để dùng PostPresenter chúng ta sửa lại posts_controllers.rb như sau:

class PostsController < ApplicationController
  def show
    @post = Post.find params[:id]
    respond_to do |format|
      format.html
      format.json{render json: PostPresenter.new(@post).as_json}
    end
  end
end

Tạo một GET request với endpoint là

http://localhost:3000/posts/1

để xem kết quả của các bước chúng ta vừa thực hiện

{
  "id": 1,
  "title": "Slicker Than Rain",
  "body": "Farm-to-table selfies flannel gluten-free cardigan kale chips vice. Authentic meditation swag cred. Cardigan austin vinegar letterpress waistcoat.",
  "author": {
    "id": 2,
    "name": "Celine Von"
  }
}

Kết quả giống hệt như những gì chúng ta có trước đó. Như vậy việc sử dụng Presenter giúp cho logic trong controller và model không cồng kềnh và phức tạp, phân tách chức năng các phần trong application một cách rõ ràng hơn, từ đó giúp cho việc viết test, maintain trở lên dễ dàng hơn rất nhiều.

3, Dùng Jbuilder

Jbuilder là một template engin cho việc render JSON responses. Nó cho chúng ta những cách đơn giản để khai báo các cấu trúc JSON kể cả đó là một cấu trúc phức tạp chăng nữa.
Trong Rails 4 jbuilder gem đã được thêm sẵn trong Gemfile, nếu bạn đang dùng các Rails vesions thấp hơn cần thêm jbuilder vào trong Gemfile:

gem 'jbuilder'

sau đó chạy

bundle install

Khi sử dụng jbuilder để format JSON responses, chúng ta không cần sử dụng respond_to block trong controller action nữa, thay vào đó server sẽ tự động tìm kiếm format tương ứng với request từ phía client. Trong trường hợp này nó sẽ tìm kiếm một template tên là show.json.jbuilder và show action của posts controller trở thành đơn giản như sau:

class PostsController < ApplicationController
  def show
    @post = Post.find params[:id]
  end
end

Tiếp theo, chúng ta cần định nghĩa cấu trúc JSON response cho post trong template show.json.jbuilder. Trong view template này, chúng ta sẽ dùng Ruby code để định nghĩa JSON output. File show.json.jbuilder sẽ có nội dung như sau:

json.id @post.id
json.name @post.title
json.name @post.body

Ở đây chúng ta chỉ đơn thuần liệt kê các attributes cần thiết cho một post. Có một cách ngắn gọn hơn việc liệt kê từng attribute như trên là chúng ta sẽ sử dụng extract! Function như sau:

json.extract! @post, :id, :title, :body

Từ Ruby 1.9 trở đi cú pháp trên còn có thể ngắn gọn hơn nữa như:

json.(@post, :id, :title, :body)

Bây giờ muốn cho JSON output của post phức tạp hơn nữa chúng ta sẽ đưa author và mỗi post đơn giản bằng cách sau:

json.(@post, :id, :title, :body)
json.author @post.author, :id, :name

Để thấy được kết quả của các bước trên như thế nào chúng ta thực hiện một GET request với endpoint:

http://localhost:3000/posts/1.json

Output:

{
  "id": 1,
  "title": "Slicker Than Rain",
  "body": "Farm-to-table selfies flannel gluten-free cardigan kale chips vice. Authentic meditation swag cred. Cardigan austin vinegar letterpress waistcoat.",
  "author": {
    "id": 2,
    "name": "Celine Von"
  }
}

Jbuilder cũng cho phép chúng ta thực hiện những thứ phức tạp hơn. Ví dụ nếu muốn có một url attribute tới author, chúng ta chỉ cần đưa vào author một block và gọi helper method bên trong block đó. Khi đó file show.json.jbuilder sẽ có nội dung như sau:

json.(@post, :id, :title, :body)
json.author do |json|
  json.(@post.author, :id, :name)
  json.url author_url(@post.author)
end

Nhớ là bạn cần định nghĩa controller và routes cho authors nếu không code trên sẽ raise error vì author_url không tồn tại

Khi thực hiện một GET request tới

http://localhost:3000/posts/1.json ,
output như sau:

{
  "id": 1,
  "title": "Slicker Than Rain",
  "body": "Farm-to-table selfies flannel gluten-free cardigan kale chips vice. Authentic meditation swag cred. Cardigan austin vinegar letterpress waistcoat.",
  "author": {
    "id": 2,
    "name": "Celine Von",
    "url": "http://localhost:3000/authors/2"
  }
}

Ngoài ra Jbuilder còn có các tính năng khác như: rendering partials, rendering collection of partials, Fragment caching và Key formatting. Bạn có thể tham khảo bài viết sau:
https://github.com/rails/jbuilder
Từ những ví dụ trên chúng ta thấy Jbuilder là một sự thay thế tốt cho một số cách truyền thống mà chúng ta hay sử dụng để cấu trúc JSON output đặc biệt khi APIs của chúng ta có độ phức tạp cao.


4, Dùng active_model_serializers

Giống như jbuilder, active_model_serializers cũng là một gem để customise json output.
Trước hết, chúng ta cần install active_model_serializers gem
Thêm dòng sau tới Gemfile

gem 'active_model_serializers', git: '[email protected]:rails-api/active_model_serializers.git', branch: '0-8-stable'

Sau đó chạy:

bundle install

Tiếp theo chúng ta sẽ đi tìm hiểu các tính năng của active_model_serializers

Để xác định cấu trúc json format cho post, chúng ta cần tạo một file post_serializer.rb sau đó định nghĩa cấu trúc json trong đó. Active_model_serializers gem có cung cấp một generate command giúp chúng ta tạo file đó một cách dễ dàng:

rails g serializer post

Một file post_serializer.rb sẽ được tạo trong thư mục app/serializers

Để post json format chỉ đơn giản chứa các attributes của post object, chúng ta sửa nội dung file post_serializer.rb như sau:

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

Mặc định sau khi được generated, attributes method chỉ chứa :id, nếu muốn json output của post object chứa các attributes nào chúng ta chỉ cần liệt kê chúng ra như trên là xong. Thật quá đơn giản !

Để đưa author vào trong post json object, chúng ta cần tạo thêm AuthorSerializer để định nghĩa cấu trúc json cho author object:

rails g serializer author

Sau đó sửa nội dung file serializers/author_serializer.rb như sau:

class AuthorSerializer < ActiveModel::Serializer
  attributes :id, :name, :url

  def url
    author_url(id: self.id)
  end
end

Chúng ta cần thêm association vào PostSerializer như sau:

class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :body
  has_one :author
end

Khi tạo một GET request với endpoint:

http://localhost:3000/posts/1.json

output sẽ như sau:

{
  "post": {
    "id": 1,
    "title": "Slicker Than Rain",
    "body": "Farm-to-table selfies flannel gluten-free cardigan kale chips vice. Authentic meditation swag cred. Cardigan austin vinegar letterpress waistcoat.",
    "author": {
      "id": 2,
      "name": "Celine Von",
      "url": "http://localhost:3000/authors/2"
    }
  }
}

active_model_serializers còn rất nhiều tính năng khác, bạn có thể tham khảo bài viết sau:
https://github.com/rails-api/active_model_serializers/blob/master/docs/general/getting_started.md
Giống như jbuilder, active_model_serializers là một “vũ khí” giúp chúng ta định nghĩa các cấu trúc json phức tạp cho APIs phức tạp bằng những cách đơn giản.

5. Kết luận

Trên đây là những cách phổ biến được dùng để định nghĩa cấu trúc cho json output cho API.
Mỗi cách đều có ưu nhược điểm của nó, việc lựa chọn một phụ thuộc vào yêu cầu của API bạn đang xây dựng, nếu json output đơn giản bạn có thể dùng cách 1 hoặc cách 2 ngược lại bạn có thể dùng cách 3 hoặc cách 4. Đừng chỉ dừng lại ở một phương pháp nào đó hãy linh động theo ngữ cảnh để chọn cho mình một cách tối ưu nhất.
Cảm ơn các bạn đã theo dõi bài viết, hẹn gặp lại ở các bài viết tiếp theo.
</br> Tài liệu tham khảo:
https://github.com/rails-api/active_model_serializers
https://github.com/rails/jbuilder
https://richonrails.com/articles/getting-started-with-jbuilder
https://blog.codelation.com/rails-restful-api-just-add-water/
https://makzan.net/ruby-on-rails-101/using-jbuilder/
https://samurails.com/gems/jbuilder/


All Rights Reserved