APIS ON RAILS - Chapter 2: The API

2.1. Lên kế hoạch

Để cho đơn giản thì app của chúng ta sẽ bao gồm 5 models. Đừng lo lắng về việc không hiểu rõ tất cả vào lúc này, chúng ta sẽ cùng review lại và build từng phần khi chúng ta chuyển sang bài viết khác. Ngắn gọn là chúng ta sẽ có User với khả năng tạo ra nhiều orders, upload lên nhiều products, mỗi products có thể có nhiều images hoặc comments từ các user khác trong app. Chúng ta sẽ không xây dựng view để hiển thị hay tương tác với API, nên cũng đừng làm cho app bự hơn làm gì.

2.2. Setting API

Một API được Wiki định nghĩa là an Application Programming Interface, nhằm định nghĩa cách các components tương tác với nhau. Nói cách khác system tương tác với nhau qua những interface, trường hợp của chúng ta thì web service được xây dựng bằng JSON. Có một số phương thức giao tiếp khác như SOAP nhưng chúng ta sẽ không đề cập tới nó ở đây. JSON là một kiểu dữ liệu số của Internet được chấp nhận bởi tính dễ đọc, rộng và dễ dàng áp dụng với các framework hiện tại như AJS, EmberJS, ... RESTful API cần follow tối thiểu 3 quy tắc sau:

  • Base URI, chẳng hạn như: http://example.com/resources/
  • Internet media type để đại diện cho dữ liệu, thông thường nó là JSON và sẽ được thiết lập thông qua việc trao đổi headers.
  • Follow những quy tắc cơ bản HTTP methods như là GET, POST, PUT and DELETE.
Resource GET POST PUT DELETE
http://example.com/resources Đọc một đối tượng resource hoặc tất cả resource dựa trên URI truyền vào Tạo mới một entry vào trong tập các resources Update tập các resources hoặc một phần tử resource Xoá đi một phần tử resource hoặc tất cả resources

Có lẽ như vậy đã đủ làm rõ và cung cấp đầy đủ thông tin rồi, nhưng hình như có khá nhiều thông tin cần phải nắm ở đây. Đừng lo, chúng ta sẽ đi chi tiết từng phần một qua mỗi bài viết.

2.2.1. Routes, Constraints và Namespaces

Trước khi chúng ta viết bất kỳ dòng code nào, chúng ta phải chuẩn bị code với git, chúng ta sẽ sử dụng một branch cho mỗi bài viết. Sau khi code xong thì upload nó lên github rồi merge chúng với master. Vậy thì mở terminal lên và gõ những dòng sau:

cd market_place
git checkout -b setting-api

Chúng ta sẽ chỉ làm việc trên config/routes.rb thôi, chúng ta sẽ set cái constraints, base_uri và default response format cho mỗi request nhé. Đầu tiên chúng ta hãy xoá đi các comment có trong file routes đi và tạo một commit cho khởi động tay chân nhé.

git add config/routes.rb
git commit -m "Removes comments from the routes file"

Chúng ta sẽ tách api controllers ra một namespace khác, trong Rails việc này đơn giản thôi, chỉ cần tạo một thư mục bên trong app/controllers với tên là api, lưu ý là tên của namespace này quan trọng vì chúng ta sẽ cần phải quản lý controller cho api nữa mà.

mkdir app/controllers/api

Sau đó chúng ta thêm namespaces vừa tạo vào trong routes.rb:

Rails.application.routes.draw do
  namespace :api do
  end
end

Việc định nghĩa namespace vào bên trong file routes này thì Rails sẽ auto map cái namespace đó với thư mục mà cùng tên ở bên trong thư mục controllers, trong trường hợp này là thư mục api/ Rails có thể xử lý đến 21 loại media type khác nhau, bạn có thể xem danh sách các SET class dưới module de Mime:

rails c
Mime::SET.collect(&:to_s)

Điều này quan trọng vì chúng ta sẽ làm việc với JSON, một kiểu buil-in mà MIME types chấp nhận bởi Rails, vì vậy chúng ta chỉ cần gọi ra cái format này làm mặc định:

Rails.application.routes.draw do
  namespace :api, defaults: {format: :json} do
    
  end
end

Tới thời điểm này thì chúng ta cũng chưa làm gì quá to lớn cả, những gì chúng ta cần đạt được tiếp theo sẽ là làm cách nào để generate ra base_uri dưới subdomain, trong trường hợp này là api.market_pace_api.dev. Cấu hình API dưới subdomain là một điều kiện luyện tập tốt bởi vì nó cho phép đưa ứng dụng đến level của DNS. Vậy làm cách nào để đạt được điều này?

Rails.application.routes.draw do
  namespace :api, defaults: { format: :json }, contraints: { subdomain: "api" }, path: "/" do

  end
end

Chú ý những gì vừa thay đổi nhé, chúng ta không chỉ thêm hash constraints vào để chỉ định cái subdomain mà còn thêm option path vào, lại còn đặt nó có giá trị là "/" nữa. Điều này sẽ báo cho Rails rằng để tạo path mặc định cho mỗi request là path root. Đến giờ commit lại rồi nào:

git add config/routes.rb
git commit -m "Set the routes constraints for the api"

2.2.2. API versioning

Tại thời điểm này, bạn có thể đã có một routes mapping ổn sử dụng subdomain rồi, file routes.rb của bạn lúc này như sau:

Rails.application.routes.draw do
  namespace :api, defaults: { format: :json },
    constraints: { subdomain: "api" }, path: "/" do

  end
end

Bây giờ là lúc thiết lập một số constraints cho mục đích của versioning. Bạn có thể care về versioning của ứng dụng từ khi bắt đầu, do đó nó sẽ được cấu trúc tốt hơn và khi có sự thay đổi cần thiết nào thì bạn có thể đưa cho dev mà có nhiệm vụ thêm mới chức năng trong khi cái cũ thì lại đang không tốt. API versioning của bạn bị sai là bởi vì tôi quyết định làm nó với 3 cách sai khác nhau. Để thiết lập version của api thì, đầu tiên bạn cần thêm thư mục khác dưới thư mục api đã tạo:

mkdir app/controllers/api/v1

Với cách này chúng ta có thể giới hạn api của chúng ta vào trong những version khác nhau một cách dễ dàng, bây giờ thì thêm đoạn code cần thiết vào routes.rb:

Rails.application.routes.draw do
  namespace :api, defaults: { format: :json },
    constraints: { subdomain: "api" }, path: "/" do
       scope module: :v1 do
       end
  end
end

Với config hiện tại thì URL cho việc lấy ra một product là: http://api.marketplace.dev/v1/products/1

2.2.3. Improving the versioning

Hiện tại chúng ta đã có API version scope thông qua URL rồi, nhưng mà có gì đó vẫn không đúng phải không? Điều tôi muốn đề cập là từ góc nhìn của dev thì vẫn không nhận biết được version nào đang được sử dụng, mặc định thì nó sẽ sử dụng version cuối cùng rồi. Nhưng làm cách nào để nó sử dụng version cuối? Trước hết, chúng ta cần cải thiện API version thông qua việc access vào HTTP Headers. Điều này sẽ đem lại 2 lợi thế:

  • Xoá đi cái version của URL.
  • Mô tả của API sẽ được xử lý thông qua request headers.
Tên header Mô tả Ví dụ
Accept Content-Types hợp lệ cho response Accept: text/plain
Authorization Chứng chỉ authentication cho HTTP authentication Authorization : Basic ...
Content-Type Kiểu MIME của phần thân request (được sử dụng với POST hoặc PUT request) Content-Type: application/x-www-form-urlencoded
Origin Khởi tạo request để gửi resource Origin: http://www...

Trong Rails rất dễ để thêm kiểu versioning thông qua Accept header. Chúng ta sẽ tạo một class bên trong thư mục lib và nhớ rằng chúng ta viết test trước code sau nên điều đầu tiên là thêm một số gem cần thiết vào gemfile nhé:

group :test do 
 gem "rspec-rails"
 gem "factory_girl_rails"
 gem "faker"
end

Rồi sau đó chạy các lệnh sau để bundle và khởi tạo các config để test:

bundle install
rails g rspec:install

Thêm config sau vào config/application.rb để không tạo ra RSpec test cho views và helpers

config.generators do |g|
    g.test_framework :rspec, fixture: true
    g.fixture_replacement :factory_girl, dir: 'spec/factories'
    g.view_specs false
    g.helper_specs false
    g.stylesheets = false
    g.javascripts = false
    g.helper = false
  end

  config.autoload_paths += %W(\#{config.root}/lib)

Sau đó chúng ta thêm thư mục spec bên trong lib và thêm api_constraints_spec.rb:

mkdir lib/spec
touch lib/spec/api_constraints_spec.rb

Sau đó ta viết test để mô tả class của chúng ta:

require "spec_helper"

describe ApiConstraints do
  let(:api_constraints_v1) { ApiConstraints.new version: 1 }
  let(:api_constraints_v2) { ApiConstraints.new version: 2, default: true }

  describe "matches?" do
    it "returns true when the version matches the 'Accept' headers" do
      request = double(host: "api.marketplace.dev", headers: {"Accept": "application/vnd.marketplace.v1"})
      api_constraints_v1.matches?(request).should be_true
    end

    it "returns the default version when 'default' option is specified" do
      request = double(host: "api.marketplace.dev")
      api_constraints_v2.matches?(request).shoul be_true
    end
  end
end

Đoạn code trên chúng ta đã tạo ra class với option hash có chứa version của api và giá trị mặc định để xử lý default version. Chúng ta sử dụng hàm matches? với router sẽ làm cho constraint biết được default version được require hay không, hay là Accept header matches được chuỗi đã đưa ra. Để test được pass thì chúng ta tạo file lib/api_constrants.rb như sau:

class ApiConstraints
  def initialize(options)
    @version = options[:version]
    @default = options[:default]
  end

  def matches?(req)
    @default || req.headers['Accept'].include?("application/vnd.marketplace.v#{@version}")
  end
end

Và bạn cần phải thêm class vào trong routes.rb và set nó như là constraint scope.

Rails.application.routes.draw do
  namespace :api, defaults: { format: :json }, constraints: { subdomain: "api" }, path: "/" do
    scope module: :v1, constraints: ApiConstraints.new(version: 1, default: true) do

    end
  end
end

Lúc này khi chạy test bạn đã pass 100% rồi. Nhiệm vụ hoàn thành, push code lên nào.

git add -A
git commit -m "Finish chapter 2"

Bài viết số 2 đến đây là kết thúc, cảm ơn các bạn đã theo dõi! Nguồn: http://apionrails.icalialabs.com/book/chapter_two