实作 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,例如:
scope :path => '/api/v1/', :module => "api_v1", :as => 'v1', :defaults => { :format => :json } do
resources :topics
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,认证方式也不同:
# app/controllers/api_controller.rb
class ApiController < ActionController::Base
end
- 新增 API 专用的 Controller,并继承上述的 ApiController。例如
rails g controller api_v1::events
# app/controllers/api_v1/events_controller.rb
class ApiV1::EventsController < ApiController
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
- to_json 方法
- 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
,不然会差一点。
上述范例
- 实作 https://github.com/ihower/rails-exercise-ac8/blob/master/app/views/api_v1/topics/index.json.jbuilder
- 测试 https://github.com/ihower/rails-exercise-ac8/blob/master/spec/requests/api_v1/topics_spec.rb
实作 Web APIs 使用者认证
不像浏览器有 cookie,每个 request 都必须带有我们自行设计的 token 参数,我们才可以识别使用者。
首先是 Model 部分,主要新增一个字段 authentication_token
字段,并用 Devise.friendly_token 产生乱数 token:
产生 Migration,指令是 rails g migration add_token_to_users
class AddTokenToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :authentication_token, :string
add_index :users, :authentication_token, :unique => true
User.find_each do |u|
puts "generate user #{u.id} token"
u.generate_authentication_token
u.save!
end
end
end
修改 User Model 加上generate_authentication_token
方法:
class User < ApplicationRecord
#.....
before_create :generate_authentication_token
def generate_authentication_token
self.authentication_token = Devise.friendly_token
end
end
接着我们在 ApiController 上实作 before_action :authenticate_user_from_token!
,如果有带auth_token
就进行登入(但这里没有强制一定要登入):
class ApiController < ActionController::Base
before_action :authenticate_user_from_token!
def authenticate_user_from_token!
if params[:auth_token].present?
user = User.find_by_authentication_token( params[:auth_token] )
# Devise: 设定 current_user
sign_in(user, store: false) if user
end
end
end
如果是强制一定要登入的action
,用法跟之前 Device 一样,例如以下整个 EventsController 就一定要登入了,不然会回传错误:
class ApiV1::EventsController < ApiController
before_action :authenticate_user!
end
上述的部分也可以安装现成的套件 https://github.com/gonzalo-bulnes/simple_token_authentication
接下来实作 API 的登入和登出,让使用者可以用帐号密码,或是 facebook access_token 来登入,来换得上述的 authentication_token
。也就是 POST /api/v1/login
先用 email 帐号密码登入,拿到 auth_token:
- 实作 Controller Code: https://github.com/ihower/rails-exercise-ac7/blob/master/app/controllers/api_v1/auth_controller.rb
- 实作 Model Code: https://github.com/ihower/rails-exercise-ac7/blob/master/app/models/user.rb#L24 (透过 get_fb_data 这个方法去验证用户传过来的 facebook token 是否正确)
- 测试 Request Spec: https://github.com/ihower/rails-exercise-ac7/blob/master/spec/requests/auth_spec.rb
用户端拿到 auth_token 后,之后的每个 request 都必须带入 auth_token。
上述的作法是整合 Devise 和 omniauth-facebook,如果不想整合 Devise 的话,也可以自己把 current_user
做出来,例如这份 Example code