Tạo Rails API với Grape
Trong việc module hóa cũng như tạo cấu trúc REST API bạn có thể tự tạo cho mình một framework riêng hoặc sử dụng một framework đã có và đang được phát triển và ưa dùng là grape. tham khảo tại đây
Grape api là gì ?
Grape is a REST-like API framework for Ruby. It's designed to run on Rack or complement existing web application frameworks such as Rails and Sinatra by providing a simple DSL to easily develop RESTful APIs. It has built-in support for common conventions, including multiple formats, subdomain/prefix restriction, content negotiation, versioning and much more.
- api frame work
- Restful Apis
- Rack, Sinatra support
- built-in support
Nói gọn lại là Grape là một framework tích hợp và ăn nhập với các server như rack, sinsatra để tạo ra hệ thống Restful APIs. Ưu điểm là nó rất nhanh hơn các rails-api
thông thường. Trong bài này tôi sẽ giới thiệu và demo một ứng dụng sử dụng grape api. Với mong muốn đi sâu hơn vào grape và ứng dụng của grape với rails nên tôi sẽ có thể viết nối tiếp một số bài nữa.
Cài đặt
Trong gemfile
1
gem 'grape'
Run bundle : bundle update
Controller example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
module Twitter
class API < Grape::API
version 'v1', using: :header, vendor: 'twitter'
format :json
prefix :api
helpers do
def current_user
@current_user ||= User.authorize!(env)
end
def authenticate!
error!('401 Unauthorized', 401) unless current_user
end
end
resource :statuses do
desc 'Return a public timeline.'
get :public_timeline do
Status.limit(20)
end
desc 'Return a personal timeline.'
get :home_timeline do
authenticate!
current_user.statuses.limit(20)
end
desc 'Return a status.'
params do
requires :id, type: Integer, desc: 'Status ID.'
end
route_param :id do
get do
Status.find(params[:id])
end
end
desc 'Create a status.'
params do
requires :status, type: String, desc: 'Your status.'
end
post do
authenticate!
Status.create!({
user: current_user,
text: params[:status]
})
end
desc 'Update a status.'
params do
requires :id, type: String, desc: 'Status ID.'
requires :status, type: String, desc: 'Your status.'
end
put ':id' do
authenticate!
current_user.statuses.find(params[:id]).update({
user: current_user,
text: params[:status]
})
end
desc 'Delete a status.'
params do
requires :id, type: String, desc: 'Status ID.'
end
delete ':id' do
authenticate!
current_user.statuses.find(params[:id]).destroy
end
end
end
end
Cấu trúc thư mục
- sơ cấp
1 2 3
app |––controllers |––api
- mount một file
base.rb
1 2 3 4 5
module API class Base < Grape::API mount API::V1::Base end end
cấu trúc thư mục
1 2 3 4
app |––controllers |––api |––base.rb
Khái niệm cơ bản
mount
mount: là cơ chế nói cho rails biết là grape đang tạo ra số lượng api tương ứng với file
1
Twitter::API.compile!
Đối với rails để mount một file và tạo ra routes ta cần làm như sau:
- rails < 5.2 sửa lại file
application.rb
1 2
config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
- rails thêm vào file
config/routes.rb
1
mount Twitter::API => '/'
- rails 6.0 sửa file
config/initializers/inflections.rb
1 2 3
ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'API' end
Trong việc mount là một cơ chế kỳ ảo của grape đảm bảo việc nested và mở rộng module hóa cũng quản lý version.
version
Từ version 1 bạn có thể phát triển lên version 2, 3. Grape dễ dàng quản lý điều đó dựa trên các câu lệnh cùng thư mục
1
2
3
4
5
6
app
|––controllers
|––api
|––base.rb
|––v1
|––base.rb
Các khái niệm cơ bản: There are four strategies in which clients can reach your API’s endpoints: :path, :header, :accept_version_header and :param
. The default strategy is :path.
- path
version 'v1', using: :path
use with curl:curl http://localhost:9292/v1/statuses/public_timeline
- header
1
version 'v1', using: :header, vendor: 'twitter'
use with curl:
1
curl -H Accept:application/vnd.twitter-v1+json http://localhost:9292/statuses/public_timeline
- accept_version_header
1
version 'v1', using: :accept_version_header
example:
curl -H "Accept-Version:v1" http://localhost:9292/statuses/public_timeline
- param
1
version 'v1', using: :param, parameter: 'v'
ex:
curl http://localhost:9292/statuses/public_timeline?v=v1
version, mount, nested
Giả sử bạn cần mở rộng module v1 bằng việc khải báo thêm các api. bước 1: Tạo folder v1
1
2
3
4
5
app
|––controllers
|––api
|––base.rb
|––v1
bước 2: với mỗi version tạo một base.rb
riêng:
1
2
3
4
5
6
app
|––controllers
|––api
|––base.rb
|––v1
|––base.rb
bước 3: Trong file base khai báo mount thư mục đồng cấp
1
2
3
4
5
6
7
8
module API
module V1
class Base < Grape::API
mount V1::Users
# mount API::V1::AnotherResource
end
end
end
bước 4: khai báo api tương ứng user.rb
1
2
3
4
5
6
7
app
|––controllers
|––api
|––base.rb
|––v1
|––base.rb
|––users.rb
api end point
Như ở trên khi bạn khai báo 1 api đồng cấp, đó cũng là api end point. Đối với một api end point:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module API
module V1
class Users < Grape::API
include API::V1::Defaults
resource :users do
desc "Return all users"
get "", root: :users do
User.all
end
desc "Return a user"
params do
requires :id, type: String, desc: "ID of the user"
end
get ":id", root: "user" do
User.where(id: params[:user_id]).first
end
end
end
end
end
resource
: khai báo router desc
: mô tả get "", root: :users do
khai báo url api lấy toàn bộ user ex: http://localhost:3000/api/v1/users
tool: POSTMAN
Action, Helper
Mixin, format dữ liệu, các hàm dùng chung
được khai báo tại file defaults.rb
, ex: app/controllers/api/v1/defaults.rb
- khai báo định dạng output như json
- các hàm như authencation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
module API module V1 module Defaults extend ActiveSupport::Concern included do prefix "api" version "v1", using: :path default_format :json format :json formatter :json, Grape::Formatter::ActiveModelSerializers helpers do def permitted_params @permitted_params ||= declared(params, include_missing: false) end def logger Rails.logger end end # check authentice_user def authenticate_user! uid = request.headers["Uid"] token = request.headers["Access-Token"] @current_user = User.find_by(uid: uid) unless @current_user && @current_user.valid_token?(token) api_error!("You need to log in to use the app.", "failure", 401, {}) end end # Hàm hiển thị errors message khi lỗi def api_error!(message, error_code, status, header) error!({message: message, code: error_code}, status, header) end # # Hàm raise errors message khi lỗi def api_error_log(message) @logger ||= Logger.new(ProjectLogger.log_path("project_api")) @logger.info("=============#{Time.zone.now.to_s}==================\n") @logger.info("#{message}\n") end rescue_from ActiveRecord::RecordNotFound do |e| error_response(message: e.message, status: 404) end rescue_from ActiveRecord::RecordInvalid do |e| error_response(message: e.message, status: 422) end rescue_from Grape::Exceptions::ValidationErrors do |e| error_response(message: e.message, status: 400) end end end end end
Action callback
Grape vấn support đầy đủ các hàm call back như:
1. before
2. before_validation
3. validations
4. after_validation
5. the API call
6. after 7. finally
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyAPI < Grape::API
get '/' do
"root - #{@blah}"
end
namespace :foo do
before do
@blah = 'blah'
end
get '/' do
"root - foo - #{@blah}"
end
namespace :bar do
get '/' do
"root - foo - bar - #{@blah}"
end
end
end
end
kết quả
1
2
3
GET / # 'root - '
GET /foo # 'root - foo - blah'
GET /foo/bar # 'root - foo - bar - blah'
example version
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Test < Grape::API
resource :foo do
version 'v1', :using => :path do
before do
@output ||= 'v1-'
end
get '/' do
@output += 'hello'
end
end
version 'v2', :using => :path do
before do
@output ||= 'v2-'
end
get '/' do
@output += 'hello'
end
end
end
end
kết quả
1
2
GET /foo/v1 # 'v1-hello'
GET /foo/v2 # 'v2-hello'
Rspec
Viết rồi kiểm thử làm sao ? -> viết rspec cho api ta có thể dùng gem airborne. install:
1
gem install airborne
cấu trúc thư mục
1
2
3
4
spec
|––api
|––v1
|––users_spec
nội dung file test:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require "rails_helper"
require "airborne"
describe "API::V1::Users" do
after(:all){I18n.locale = :ja}
describe "POST api/v1/users" do
let!(:user) do
FactoryGirl.create :user, id: 1, email: "test@gmail.com", first_name: "James",
last_name: "Bond", provider: "email"
end
context "when user update successfully" do
let(:api_response){FactoryGirl.build(:api_update_user_success_response).deep_symbolize_keys}
before do
post("/api/v1/users", {first_name: "hitorri"},
{"Accept-Language": "en", "App-Version": "knt/1.0", "Uid": user.uid, "Access-Token": user.access_token})
end
it{expect_json(api_response)}
end
end
end
CORS
using gem rack-cors
, cấu hình tại file config.ru
1
2
3
4
5
6
7
8
9
10
require 'rack/cors'
use Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: :get
end
end
run Twitter::API
Serializing
Công việc của chúng ta là convert mảng sang JSON. Để làm được việc này chúng ta cài thêm gem grape-active_model_serializers
1
2
3
4
5
6
7
8
9
10
11
12
13
module API
module V1
module Defaults
extend ActiveSupport::Concern
included do
prefix "api"
version "v1", using: :path
default_format :json
format :json
formatter :json,
Grape::Formatter::ActiveModelSerializers
...
Viết file serializers
Create a directory, serializers
, in the top level of your app. Create a graduate_serializer.rb
file in that directory. Here is where our serializer will live. app/serializers/graduate_serializer.rb
1
2
3
4
5
6
class GraduateSerializer < ActiveModel::Serializer
attributes :id, :first_name, :last_name, :cohort,
:current_job, :bio, :news, :website, :picture,
:created_at, :updated_at
end
Grape Gem addition
Grape Swagger
- Add gem ‘grape-swagger’ to your Gemfile and
bundle install
. - Add grape-swagger documentation to the root or base class of your API. tại file
app/controllers/api/v1/base.rb
thêm đoạn sau:1 2 3 4 5 6 7 8 9
require "grape-swagger" ... add_swagger_documentation( api_version: "v1", hide_documentation_path: true, mount_path: "/api/v1/swagger_doc", hide_format: true ) end
- Add the documentation endpoint to your routes. tại file
config/routes.rb
:1
mount GrapeSwaggerRails::Engine, at: "/documentation"
vào url:
http://localhost/documentation
bạn sẽ redirect tới:http://localhost:3000/api/v1/swagger_doc
Grape Entity
Có thể tham khảo thêm tại đây
Grape Logger
Có thể tham khảo tại đây
Grape Swagger representable
Có thể tham khảo thêm tại đây
Tài liệu tham khảo
- https://viblo.asia/p/xay-dung-api-voi-grape-bWrZne1vKxw
- https://www.thegreatcodeadventure.com/making-a-rails-api-with-grap/
- https://github.com/ruby-grape/grape#what-is-grape