Api for ruby on rails with grape
Bài đăng này đã không được cập nhật trong 8 năm
Chắc hẳn các bạn đã sử viết api cho rails rất nhiều lần rồi, và không ít người trong đó sử dụng gem grape
để viết. Khi đó nhiều người hay sử dụng Serializer để cấu trúc response trả ra, mình thấy sử dụng Serializer thì khó có thể xác định được mình trả ra những gì khi đọc code. Hơn nữa nó lại không thể linh hoạt được xử lý đồng thời với những kiểu dữ liệu đặc thù được.
Ở đây, mình giới thiệu thêm cho các bạn một gem khác đó chính là grape-entity
để định nghĩa cấu trúc response của một api. Sử dụng gem này rất là đơn giản, và nó hầu như có thể xử lý được tất cả các tình huống mà bạn cần.
Hơn nữa, khi sử dụng gem grape
để viết api. Thì các bạn nên sử dụng gem doorkeeper
để authorize bới vì nó cũng được tích hợp sẵn với grape chứ không cần phải viết lại.
Ngoài ra, khi viết api chính là một service cung cấp cho client mà việc đặc tả nó thì là một việc rất cần thiết. Việc này mà viết bằng tay thì rất là mất thời gian hơn nữa có thể không chính xác. Mình cũng đề xuất thêm gem autodoc-grape
. Đây là một gem có thể tự động sinh ra file document cho các bạn khi các bạn viết test cho api đó. Nó sẽ mô phỏng lại request và response của bạn như thế nào.
Bộ 4 gem này là đầy đủ để cho các bạn có thể viết api cho server của bạn một cách nhanh chóng, dễ hiệu và linh hoạt nhất.
gem 'grape'
gem 'grape-entity'
gem 'autodoc-grape'
gem 'doorkeeper'
Việc sử dụng hay hoạt động của gem grape mình sẽ không đề cập cụ thể chi tiết nữa. Mà mình đi vào chi tiết 3 gem còn lại.
Lưu ý: Khi sử dụng gem grape để viết api với các thứ tiếng như Nhật, Việt Nam, Trung Quốc ... những ngôn ngữ có các thể loại encode khác nhau thì các bạn nên ép ngay kiểu trả về là utf-8
để tránh trường hợp bị lỗi font khi trả về.
Hãy cài đặt đó là default với tất cả các API.
VD:
class API::BaseAPI < Grape::API
format :json
content_type :json, "application/json;charset=utf-8"
mount API::V1::BaseAPI
mount API::V1::BaseAuthenticateAPI
end
I) Gem 'doorkeeper'
Link github: doorkeeper
Đây chính là một gem viết ra để thực hiện authenticate cho cả web lần api. Chính vì vậy mà nó rất hay được dùng đối với một server yêu cầu cả 2 dịch vụ trên.
Ở đây mình chỉ đề cập tới việc login đối với user. Và cách mà chúng ta hay sử dụng nhất chính là việc gửi email và password lên để login hay lấy access_token.
Để chuẩn bị cho việc login thì đương nhiên là chúng ta phải có bảng user, và một phương thức để authenticate
đối tượng với email và password. Việc này ta có thể sử dụng gem devise
hoặc là gem sorcery
. Ở đây mình dùng gem sorcery
để demo.
Khi bundle xong thì chúng ta chạy các lệnh sau để chuẩn bị database:
rails generate sorcery:install
rails generate doorkeeper:install
rake db:migrate
Sau đó, ta config lại resource_owner_from_credentials
trong file config/initializes/doorkeeper.rb
để sử dụng authenticate
của gem sorcery
(các bạn hoàn toàn có thể thay thế bằng phương thức của gem devise)
Doorkeeper.configure do
# Change the ORM that doorkeeper will use (needs plugins)
orm :active_record
resource_owner_from_credentials do
User.authenticate(params[:email], params[:password])
end
end
Doorkeeper.configuration.token_grant_types << "password"
Ở trên ta phải thêm password
vào config token_grant_types
của Doorkeeper để có thể login bằng password mà ko cần sử dụng authorization_code
(chi tiết tại api-endpoint
Vậy là đã xong bước chuẩn bị. Khi login thì chúng ta sử dụng api sau để lấy access_token:
curl -F grant_type=password \
-F email=test@gmail.com \
-F password=11111111 \
-X POST http://localhost:3000/oauth/token
Như trên ta thấy được request ở đây là method POST
với url /oauth/token
và các params truyền lên là
grant_type=password
giá trị bằngpassword
là bắt buộc- password
{"access_token":"f9fbdec2b5e15d7feddd0f57ef53c3faa80385385e11275ec4977f247aa83521","token_type":"bearer","expires_in":7200,"created_at":1472399786}
Đã có token để authorize rồi. Vậy viết api như thế nào để sử dụng authorize của doorkeper đây.
Vâng, nó cũng rất là đơn giản. Vì doorkeeper
cũng đã cung cấp sẵn cho grape
rồi.
require 'doorkeeper/grape/helpers'
class API::V1::BaseAuthenticateAPI < Grape::API
version 'v1'
helpers Doorkeeper::Grape::Helpers
before do
doorkeeper_authorize!
end
mount API::V1::UsersAPI
end
Nhìn vào nội dung này hẳn các bạn thấy cú pháp quen chứ ạ?
- Thêm
require 'doorkeeper/grape/helpers'
- Thêm
helpers Doorkeeper::Grape::Helpers
- Thêm
doorkeeper_authorize!
vàobefore do
=> Tất cả các api trong API::V1::UsersAPI
sẽ đều phải đi qua authorize trước khi thực hiện các bước tiếp theo. Nó khá giống với before_action :authenticate_user!
của devise đúng không.
Sự khác nhau ở đây chính là. Khi sử dụng web để truy cập vào các controller thì trình duyệt sử dụng session để nhận biết được user đó. Còn api thì sử dụng access_token mà client gửi lên.
Để truy cập vào các api trong user thì khi request ta phải gửi thêm Authorization: Bearer token
và trong header.
Ví dụ
curl -X GET http://localhost:3000/api/v1/users -H "Authorization: Bearer f9fbdec2b5e15d7feddd0f57ef53c3faa80385385e11275ec4977f247aa83521"
II) Gem 'grape-entity'
Link grap-entity
Khi sử dụng grape-entity thì nó cũng khác giống với grape-active_model_serializers
, cũng có kế thừa, cũng có định nghĩa các trường phải trả ra trong response.
Nhưng điểm khác biệt ở đây chính là grape-entity
có option format_with đối với mỗi trường.
Ví dụ:
module API
module Entities
class User < Grape::Entity
format_with(:iso_timestamp) { |dt| dt.iso8601 }
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
expose :deleted_at, format_with: :iso_timestamp
end
end
end
Như các bạn thấy, ta có thể định nghĩa một format bất kỳ trong entity và sử dụng nó với tất cả các trường khác. Việc này khá là linh hoạt. Nhất là đối với các dữ liệu nil
trong databse. Việc response trả ra string null
trong response sẽ khiến cho client hay gặp các lỗi khi build object. Kiểu integer, float, hay string cũng đều trả ra null
sẽ khiến cho việc ép kiểu của client bị lỗi. Chính vì vậy, sử dụng format với các trường dữ liệu mà không require là thực sự hữu ích và cần thiết.
Chúng ta có thể định nghĩa các format này ở trong class base của entity (tự tạo ra) sau đó các class con kế thứa class base này sẽ dùng được.
VD:
module API::Entities
class BaseEntity < Grape::Entity
format_with(:integer) { |int| int.to_i }
format_with(:string) { |int| int.to_s }
end
end
module API::Entities::Users
class Index < API::Entities::BaseEntity
expose :id
expose :age, format_with: :integer
end
end
module API::Entities::Faqs
class Index < API::Entities::BaseEntity
expose :id
expose :question, format_with: :string
expose :answer, format_with: :string
end
end
=> các dữ liệu nil sẽ bị convert thành 0 hoặc '' với các kiểu dữ liệu integer hoặc string khi sử dụng với format.
Hơn nữa, khi nhìn file entity này ta có thể biết được trường đó trả về dữ liệu là kiểu nào. Nó khá là rõ ràng, minh bạch.
Với gem grape-entity có khá nhiều cách sử dụng. Các bạn tham khảo thêm tại đó nhé.
III) Gem 'autodoc-grape'
Khi chúng ta viết spec thì có khá là nhiều gem hỗ trợ để tạo ra doc cho chúng. Nhưng với grape thì ta có gem autodoc-grape
.
Sử đụng gem này khá là đơn giản. Ta chỉ việc thêm autodoc: true
vào chính test case mà bạn muốn sinh document từ đó là song.
Nhưng nó cũng có một nhược điểm là: Ngay cả khi bạn đã định nghĩa params trên user thì bạn vẫn phải định nghĩa lại nó ở trong params do end
của mỗi api. Nếu không khi genrenate ra document no sẽ bị lỗi.
VD:
route_param :id do
params do
requires :id, type: Integer
end
get do
present User.find(params[:id]), with: API::Entities::Users::Index
end
end
Dưới đây là ví dụ mình viết rspec cho api và genrate ra document của nó:
File spec:
require "rails_helper"
describe API::V1::UsersAPI, type: :request do
let(:json) {JSON.parse(response.body)}
let(:user) { User.create email: "email@gmail.com"}
let(:access_token) { Doorkeeper::AccessToken.create(resource_owner_id: user.id).token }
describe "GET /api/v1/users" do
let(:path) { "/api/v1/users" }
context "authorize", autodoc: true do
let(:description) { "Get list user with authorize" }
before do
2.times do |n|
User.create email: "email-#{n}@gmail.com", age: n
end
get(path, {}, "CONTENT_TYPE" => "application/json",
"Authorization" => "Bearer #{access_token}")
end
it { expect(json["users"].count).to eq 3 }
end
context "unauthorize" do
let(:description) { "Get list user unauthorize" }
before do
2.times do |n|
User.create email: "email-#{n}@gmail.com", age: n
end
get(path, {}, "CONTENT_TYPE" => "application/json",
"Authorization" => "Bearer xxx")
end
it { expect(json["error"]).to eq "The access token is invalid" }
end
end
end
Khi chạy spec thì thêm AUTODOC=1 vào để genrate ra document nếu bạn muốn
AUTODOC=1 rspec spec/lib/api/v1/users_api_spec.rb
=> Kết quả như dưới
GET /api/v1/users
Get list user with authorize
Example
Request
GET /api/v1/users HTTP/1.1
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Authorization: Bearer 22bd5ce86f5043a3a6f2655ed6956b6aee4cb670d1a67f8d50dec46f98710321
Content-Length: 0
Content-Type: application/json
Host: www.example.com
Response
HTTP/1.1 200
Cache-Control: max-age=0, private, must-revalidate
Content-Length: 62
Content-Type: application/json;charset=utf-8
ETag: W/"d4f6afed0d8b5919760c40ab465dbeeb"
X-Request-Id: 4c75ee97-e0e1-4796-bccb-9a886ce53b6e
X-Runtime: 0.052645
{
"users": [
{
"id": 1,
"age": 0
},
{
"id": 2,
"age": 1
},
{
"id": 3,
"age": 0
}
]
}
Chi tiết source code tại: https://github.com/banlv54/api-for-rails/
All rights reserved