实作 Web APIs

Good code is its own best documentation. As you’re about to add a comment, ask yourself, ‘How can I improve the code so that this comment isn’t needed?’ Improve the code and then document it to make it even clearer. – Steve McConnell, Code Complete 作者

  • 设计 Web APIs 的用途包括:
    • 提供给手机 iOS, Android 应用程式的 Web API
    • 提供 JavaScript MVC application 的 Web API
    • 建立 API 平台,开放 APIs 给第三方开发者使用

Rails 5.0 提供了 API mode 可以产生一个 Rails 专案「只 Only」作为 API server,但这是不必要的,除非你想挤一点效能出来,但是你会关掉整个 ActionView。

Router 路由实作

拆开到 /api/v1,例如:

  1. scope :path => '/api/v1/', :module => "api_v1", :as => 'v1', :defaults => { :format => :json } do
  2. resources :topics
  3. end
  • Why? 保持 API Versioning 相容性,因应不随意变更 API 格式
  • 也有人偏好不用 resources 语法,乖乖一条条将路由规格列出来。因为每个 API 都需要列出来与 client 端合作,写文件时仍会一条条列出来。

Controller 实作

  • 和路由一样也是拆开,因为 API 需求跟 server-rending HTML 会差很多,例如 params 参数设计的格式就长的很不一样,后者会搭配 ActionView 的 Form helper 变成 params[:event][:name],前者则偏好简单设计成 params[:name]
  • 也因此 business logic 尽量重构到 Model,这样才可以提高 code reusability
  • 新加 ApiController 与 ApplicationController 拆开:因为不需要防御 CSRF,认证方式也不同:
  1. # app/controllers/api_controller.rb
  2. class ApiController < ActionController::Base
  3. end
  • 新增 API 专用的 Controller,并继承上述的 ApiController。例如 rails g controller api_v1::events
  1. # app/controllers/api_v1/events_controller.rb
  2. class ApiV1::EventsController < ApiController
  3. end
  • 使用正确的 HTTP Status Code
    • 例如 render :json => { :message => "your error message" }, :status => 400
  • Response Format 采用 JSON。但是要如何产生 JSON 格式呢? 有几种方法:
    • to_json 方法
      • 超级简单,直接在 controller 里面就可以 render :json => obj.to_json
      • 弹性低,不好扩充。比较适合简单的情况,例如 400 时。
    • jbuilder 方式: https://github.com/rails/jbuilder
      • 也是 Rails 内建
      • template 里面可以根据不同条件组合,例如有登入没登入
      • 可拆 partial,弹性高
    • serializer 方式: https://github.com/rails-api/active_model_serializers
      • 需额外安装 gem
  • Request Format 支援哪些? (Client 端用什么格式送出资料?)
    • Rails 支援 application/x-www-form-urlencoded、multipart/form-data 和 application/json
    • 四种常见的POST 提交数据方式
    • Rails 可以吃 form data 也可以吃 JSON
    • 浏览器表单是用 application/x-www-form-urlencoded,如果有档案上传(在 form attribute 加上 enctype=”multipart/form-data” 则改用 multipart/form-data。
    • JSON 不能做档案上传,档案上传要用 multipart/form-data

jBuilder 锦囊妙计

重点包括:

  • 如何输出 array 资料
  • 可以使用 partial template 作 re-use
  • 如何输出使用者上传档案(例如用 paperclip) 的网址
  • 如何输出分页 paging 的资料,加上总共有几页等资讯
  • 如何处理 inline relationship 资料

API 的自动化测试

手动测试的方式,参考 https://ihower.tw/cs/web-apis.html#sec3

  • 写 RSpec Request 测试,不然很难测试非 GET 的操作
  • 测试中 Ruby Hash 的 symbol key 转 JSON 再转回来,key 会变成字串
  • 测试中若要比对 Ruby Time 时间物件和 JSON.parse 出来的时间物件,前者要多转一次 as_json,不然会差一点。

上述范例

实作 Web APIs 使用者认证

不像浏览器有 cookie,每个 request 都必须带有我们自行设计的 token 参数,我们才可以识别使用者。

首先是 Model 部分,主要新增一个字段 authentication_token 字段,并用 Devise.friendly_token 产生乱数 token:

产生 Migration,指令是 rails g migration add_token_to_users

  1. class AddTokenToUsers < ActiveRecord::Migration[5.1]
  2. def change
  3. add_column :users, :authentication_token, :string
  4. add_index :users, :authentication_token, :unique => true
  5. User.find_each do |u|
  6. puts "generate user #{u.id} token"
  7. u.generate_authentication_token
  8. u.save!
  9. end
  10. end
  11. end

修改 User Model 加上generate_authentication_token方法:

  1. class User < ApplicationRecord
  2. #.....
  3. before_create :generate_authentication_token
  4. def generate_authentication_token
  5. self.authentication_token = Devise.friendly_token
  6. end
  7. end

接着我们在 ApiController 上实作 before_action :authenticate_user_from_token! ,如果有带auth_token就进行登入(但这里没有强制一定要登入):

  1. class ApiController < ActionController::Base
  2. before_action :authenticate_user_from_token!
  3. def authenticate_user_from_token!
  4. if params[:auth_token].present?
  5. user = User.find_by_authentication_token( params[:auth_token] )
  6. # Devise: 设定 current_user
  7. sign_in(user, store: false) if user
  8. end
  9. end
  10. end

如果是强制一定要登入的action,用法跟之前 Device 一样,例如以下整个 EventsController 就一定要登入了,不然会回传错误:

  1. class ApiV1::EventsController < ApiController
  2. before_action :authenticate_user!
  3. end

上述的部分也可以安装现成的套件 https://github.com/gonzalo-bulnes/simple_token_authentication

接下来实作 API 的登入和登出,让使用者可以用帐号密码,或是 facebook access_token 来登入,来换得上述的 authentication_token。也就是 POST /api/v1/login 先用 email 帐号密码登入,拿到 auth_token:

用户端拿到 auth_token 后,之后的每个 request 都必须带入 auth_token。

上述的作法是整合 Devise 和 omniauth-facebook,如果不想整合 Devise 的话,也可以自己把 current_user 做出来,例如这份 Example code

其他议题