Xây dựng một API hoàn chỉnh với Rails 5
Ở phiên bản rails 5, thì gem rails-api đã được tích hợp vào phần core của Rails, vì vậy chúng ta có thể khởi tạo API trong Rails 1 cách dễ dàng và nhanh chóng. Cho đến bây giờ, Grape vẫn được xem là sự lựa chọn tốt nhất để viết API trong Ruby, vậy nếu mình đã quen với cách việc với Rails thuần thì ...
Ở phiên bản rails 5, thì gem rails-api đã được tích hợp vào phần core của Rails, vì vậy chúng ta có thể khởi tạo API trong Rails 1 cách dễ dàng và nhanh chóng. Cho đến bây giờ, Grape vẫn được xem là sự lựa chọn tốt nhất để viết API trong Ruby, vậy nếu mình đã quen với cách việc với Rails thuần thì sao ? Trong bài viết này mình sẽ hướng dẫn cơ bản cho các bạn cách để tạo một API đơn giản, step by step trong Rails 5. Bài viết của mình có những mục sau:
- Cài đặt project với chế độ API trong rails 5
- Sử dụng Rspec để testing
- Xây dựng API
- Serializing API Output
- Enabling CORS
- Phiên bản API
- Bảo vệ API cuả bạn
- Authentication API
- Viết document cho API sử dụng Swagger UI
1. Cài đặt project với chế độ API trong rails 5
Đầu tiên, hãy chắc chắn rằng bạn đã cài phiên bản Ruby 2.2.2 trở lên và Rails version 5. Theo hướng dẫn của Rails guide, thì tất cả những gì chúng ta cần làm để tạo một API trong rails là thêm --api phía sau câu lệnh khởi tạo project mới trong Rails. rails new api_app_name --api Tiếp theo chúng ta chạy lệnh bundle để thực hiện cài đặt các gem mặc định và tiến hành cài đặt database trong thư mục chứa project.
> cd <parent-folder-path>/api_app_name > bundle install > rails db:setup
2. Sử dụng Rspec để testing
Sau khi cài đặt xong project, chúng ta tiến hành cài đạt RSpec để tiến hành việc testing cho API. Lý do tại sao chúng ta tiến hành cài đặt RSpec đầu tiên vì nó sẽ giúp chúng ta tiết kiệm thời gian bằng cách sử dụng RSpec, thì nó sẽ tiến hành tạo tự động các file test controller và model khi chúng ta sử dụng câu lệnh rails generate scaffold để tự tạo các resources nhanh chóng. Để cài đặt RSpec, chúng ta thêm gem rspec-rails vào Gemfile trong nhóm :development, :test:
group :development, :test do gem '"rspec-rails" gem "factory_girl_rails" end
Tiến hành cập nhật lại bundle, sau đó cài đặt RSpec vào project của chúng ta.
> bundle > bin/rails g rspec:install
3. Xây dựng API
Sau khi cài đặt xong thì chúng ta sẽ tiến hành xây dựng API, sử dụng câu lệnh scaffold để khởi tạo model và controller tương ứng.
> bin/rails g scaffold user name email
Nó sẽ sinh ra các file có cấu trúc như sau:
invoke active_record identical db/migrate/20151222022044_create_users.rb identical app/models/user.rb invoke rspec create spec/models/user_spec.rb invoke factory_girl create spec/factories/users.rb invoke resource_route route resources :users invoke scaffold_controller identical app/controllers/users_controller.rb invoke rspec create spec/controllers/users_controller_spec.rb create spec/routing/users_routing_spec.rb invoke rspec create spec/requests/users_spec.rb
Chú ý rằng do đây là API nên sẽ có thư mục view được tạo ra. Bây giờ chúng ta sẽ tiến hành migrate database và chạy app nhé.
> bin/rails db:migrate > bin/rails s
API của chúng ta bây giờ đã chạy ở địa chỉ https://localhost:3000. Tuy nhiên đây chưa phải là điều quan trọng nhất, vì tất cả chỉ dừng ở mức cơ bản, và chúng ta còn nhiều việc phải làm để tạo thành một API hoàn chỉnh.
4. Serializing API Output
Trong API, thì dữ liệu của chúng ta trả về sẽ dưới định dạng JSON, tuy nhiên làm cách nào để chuyển dữ liệu từ database sang json một cách dễ dàng và đơn giản nhât. May mắn thay, Active Model Serializers đã giúp ta giải quyết vấn đề đó. Tiến hành cài đặt gem active_model_serializers bằng cách thêm vào Gemfile:
gem 'active_model_serializers'
Cập nhật bundle và tiến hành khởi tạo serializer tương ứng với model của chúng ta, ở đây là model User.
> bundle > rails g serializer user
Trong file app/serializers/user_serializer.rb chúng ta sẽ có đoạn code như sau:
class UserSerializer < ActiveModel::Serializer attributes :id end
Mặc định sẽ khởi tạo một thuộc tính là :id, tuy nhiên chúng ta cần nhiều thuộc tính hơn nữa để sử dụng, vì vậy chúng ta tiến hành thêm hai thuộc tính là :name và :email vào serializer để có thể lấy toàn bộ thông tin cần thiết trong database để chuyển về kiểu JSON. Như vậy, có thể thấy, mỗi serializer sẽ ánh xạ với một model trong database, để có thể chuyển dữ liệu từ database sang kiểu JSON để trả về cho phía front end sử dụng.
class UserSerializer < ActiveModel::Serializer attributes :id, :name, :email end
Các bạn có thể vào trang chủ của nó để tìm hiểu thêm nhé gem active_model_serializers
5. Enabling CORS
Có lẽ nhiều bạn sẽ thắc mắc CORS là gì vậy. CORS là viết tắt của Cross-origin resource sharing, là một cơ chế đặc biệt cho phép resource đặt tại một domain này có thể được request từ một domain khác với domain đó, nghĩa là làm sao để có thể gửi và nhận dữ liệu giữa 2 server với nhau. Trong trường hợp bạn muốn public API của mình, thì enable CORS là việc cần làm nếu như bạn không muốn sử dụng JSONP, và hầu hết các trình duyệt hiện nay đều đã hỗ trợ CORS. Tìm hiểu thêm về CORS cũng như cơ chế hoạt động của nó, bạn có thể vào link này. Để làm điều đó trong API của chúng ta, thì chỉ việc cài đặt gem rack-cors vào Gemfile:
gem 'rack-cors'
Chạy cập nhật bundle và thêm vào vài dòng code như ở dưới vào file config/application.rb. Ở ví dụ này, nó sẽ cho phép thực hiện các request như GET, POST hoặc bất cứ OPTIONS request nào khác từ bất kì đâu.
module YourApp class Application < Rails::Application # ... config.middleware.insert_before 0, "Rack::Cors" do allow do origins '*' resource '*', headers: :any, :methods => [:get, :post, :options] end end end end
6. Phiên bản API
Trước khi release public API của bạn, bạn nên chia API của mình thành các phiên bản, để có thê nâng cấp và sửa chữa khi có phản hồi từ khách hàng, giúp bạn dễ dàng bảo trì và nâng cấp API của mình mà không làm ảnh hưởng đến quá trình sử dụng của người dùng. Hướng dẫn này sẽ giúp bạn cài đặt version API của mình theo URL format sau:
> GET http://api.mysite.com/v1/users/
Có môt cách khác nữa là sử dụng subdomain, cả 2 cách đều được, bạn thích cái nào thì dùng cái đó thôi. Ở trong thư mục app/controller, chúng ta sẽ có cấu trúc thư mục như sau:
app/controllers/ . |-- api | |-- v1 | |-- api_controller.rb | |-- users_controller.rb |-- application_controller.rb
Và đây là những gì bên trong controller:
# app/controllers/api/v1/api_controller.rb module Api::V1 class ApiController < ApplicationController # Generic API stuff here end end # app/controllers/api/v1/users_controller.rb module Api::V1 class UsersController < ApiController # GET /v1/users def index render json: User.all end end end
Tiếp theo cài đặt ở routes, trong file config/routes, ta viết như sau:
constraints subdomain: 'api' do scope module: 'api' do namespace :v1 do resources :users end end end
7. Bảo vệ API cuả bạn
Để bảo vệ API của chúng ta khỏi các cuộc tấn công DDoS, brute force attacks, smurf attack hoặc là muốn giới hạn lượt truy cập của người dùng, chúng ta có thể sử dụng một rack middleware là Rack::Attack. Gem rack-attack cung cấp cho chúng ta những phương thức bảo vệ như sau:
- whitelist: cho phép truy cập bình thường nếu thỏa mãn điều kiện mà chúng ta đặt ra.
- blacklist: gửi thông báo từ chối truy cập cho một request nào đó.
- throttle: phân quyền truy cập của người dùng.
- track: theo dõi các request, cung cấp thông tin về các request.
Tiến hành thêm gem 'rack-attack vào Gemfile:
gem "rack-attack"
Sau khi chạy bundle, ta tiến hành include middleware vào file config/application.rb
module YourApp class Application < Rails::Application # ... config.middleware.use Rack::Attack end end
Trong thư mục** config/initializers**, ta khởi tạo file có tên là rack_attack.rb, giúp chúng ta có thể cài đặt các luật trong middleware Rack::Attack. Các bạn có thể tham khảo ví dụ cơ bản dưới đây:
class Rack::Attack # `Rack::Attack` is configured to use the `Rails.cache` value by default, # but you can override that by setting the `Rack::Attack.cache.store` value Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # Allow all local traffic whitelist('allow-localhost') do |req| '127.0.0.1' == req.ip || '::1' == req.ip end # Allow an IP address to make 5 requests every 5 seconds throttle('req/ip', limit: 5, period: 5) do |req| req.ip end # Send the following response to throttled clients self.throttled_response = ->(env) { retry_after = (env['rack.attack.match_data'] || {})[:period] [ 429, {'Content-Type' => 'application/json', 'Retry-After' => retry_after.to_s}, [{error: "Throttle limit reached. Retry later."}.to_json] ] } end
Để xem tât cả các options để config, bạn vào trang chủ của gem rack-attack để tham khảo thêm.
8. Authentication API
API không lưu giữ cookies hay session nào để có thể xác thực người dùng. Bạn có thể tham khảo về OAuth để có thể thực hiện việc xác định session của người dùng. Tuy nhiên trong hướng dẫn này, mình sẽ không đề cập đến nó, ở đây mình sẽ làm rõ cách hoạt động của nó như thế nào mà k dùng công cụ của bên thứ 3. Cách tốt nhất để thực hiện xác thực một API request là sử dụng HTTP token, mỗi người dùng sẽ có một API key, và nó sẽ được đính kèm trong HTTP Authorization header với mỗi request như sau:
Authorization: Token token="WCZZYjnOQFUYfJIN2ShH1iD24UHo58A6TI"
Đầu tiên chúng ta tạo file migration để thêm thuộc tính api_key và model User
rails g migration AddApiKeyToUsers api_key:string
Thêm hàm tạo api_key cho mỗi user mới được khởi tạo.
class User < ActiveRecord::Base # Assign an API key on create before_create do |user| user.api_key = user.generate_api_key end # Generate a unique API key def generate_api_key loop do token = SecureRandom.base64.tr('+/=', 'Qrt') break token unless User.exists?(api_key: token) end end end
Ở controller , chúng ta có thể thực hiện việc xác thực người dùng bằng method authenticate_or_request_with_http_token:
class ApplicationController < ActionController::Base include ActionController::HttpAuthentication::Token::ControllerMethods # Add a before_action to authenticate all requests. # Move this to subclassed controllers if you only # want to authenticate certain methods. before_action :authenticate protected # Authenticate the user with token based authentication def authenticate authenticate_token || render_unauthorized end def authenticate_token authenticate_with_http_token do |token, options| @current_user = User.find_by(api_key: token) end end def render_unauthorized(realm = "Application") self.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}") render json: 'Bad credentials', status: :unauthorized end end
Bây giờ chúng ta có thể test API bằng lệnh curl như sau:
>> curl -H "Authorization: Token token=USER_TOKEN" http://localhost:3000/user
9. Document cho API
Có một vấn đề ở đây là khi chúng ta xây dựng một REST API server mà không có document cụ thể, thì sẽ gấy ra rất nhiều khó khăn cho các lập trình viên khác khi sử dụng API của chúng ta, cũng như khó khắn trong việc test API.Hiện nay có rất nhiều công cụ mã nguồn mở để giúp cho lập trình viên có thể viết document rõ ràng và có thể test endpoint trực tiếp mà k cần sử dụng đến các công cụ như postman để có thể test API. Hai mã nguồn phổ biến hiện nay là Swagger và API Blue Print, tuy nhiên nó khá là dài nên mình sẽ nói về hai công cụ đó ở bài viết sau, dươi đây là ví dụ về document API sử dụng Swagger mà mình dùng để viết cho project trên.
Như vậy chúng ta có thể thấy rằng để có thể tạo ra một API hoàn chỉnh thì chúng ta cần phải quan tâm đến các vấn đề quan trọng khác như là viết document, bảo vệ API của chúng ta trước các cuộc tấn công, .... Hy vọng bài viết giúp ích nhiều cho các bạn. See you later!