Membuat API Menggunakan Rails
() translation by (you can also view the original English article)
Zaman sekarang kita sudah terbiasa dengan mengandalkan API (Application Programming Interface). Tidak hanya servis besar seperti Facebook dan Twitter yang mempekerjakan mereka, API sangat populer akibat penyebaran client-side framework seperti React, Angular, dan masih banyak lagi. Ruby juga mengikuti tren ini, dan versi terbaru memiliki fitur untuk Anda membuat API-Only applications
Pada awalnya fungsi ini dikelompokan menjadi beberapa item yang disebut rails-api, tetapi sejak Rails5 di rilis, sekarang termasuk kedalam inti dari framework. Fitur ini bersama dengan ActionCable menjadi salah satu yang ditunggu, dan sekarang kita akan mendiskusikan ini.
Artikel ini mencakup bagaimana cara membuat API Rails application dan menjelaskan bagaimana cara membuat routes dan controllers, di respon oleh JSON format, menambahkan serializers, dan menyiapkan CORS (Cross-Origin Resource Sharing). Anda juga akan belajar tentang beberapa pilihan untuk mengamankan suatu API dan melindunginya dari penyalahgunaan.
Sumber dari artikel ini ada di GitHub.
Membuat API-Only Application
Untuk memulai, jalankan perintah berikut:
1 |
rails new RailsApiDemo --api
|
Perintah diatas akan membuat API-only Rails application baru yang disebut RailsApiDemo
. Jangan lupa dukungan untuk pilihan --api
telah ditambahkan hanya di Rails 5, jadi pastikan Anda memiliki ini atau versi terbaru yang telah terinstall.
Buka Gemfile dan catat ini lebih kecil dari biasa: gems seperti coffee-rails
, turbolinks
, and sass-rails
telah bilang.
Config/application.rb mengandung baris baru:
1 |
config.api_only = true |
Ini berarti Rails akan memuat kumpulan yang lebih kecil dari middleware: sebagai contoh, tidak ada cookie dan sessions yang didukung. Bahkan jika Anda mencoba untuk membuat scaffold,views dan assers itu tidak akan dibuat. Sebeneranya jika Anda melihat views/layouts directory, Anda akan melihat bahwa application.html.erb file telah menghilang juga.
Hal lain yang penting tetapi yang berbeda adalah ApplicationController
diturunkan dari ActionController::API
, bukan ActionController::Base
.
Kurang lebih seperti itu, ini adalah dasar Rails application yang telah Anda lihat berkali-kali. Sekarang mari mulai menambahkan beberapa dari model sehingga kita dapat mengerjakan sesuatu:
1 |
rails g model User name:string |
2 |
rails g model Post title:string body:text user:belongs_to |
3 |
rails db:migrate |
Bukan sesuatu yang menarik yang ada sekarang: kiriman dengan title, dan body yang dimiliki pengguna.
Pastikan suatu asosiasi yang tepat telah disiapkan dan juga mengandung beberapa validasi sederhana:
models/user.rb
1 |
has_many :posts |
2 |
|
3 |
validates :name, presence: true |
models/post.rb
1 |
belongs_to :user |
2 |
|
3 |
validates :title, presence: true |
4 |
validates :body, presence: true |
Menakjubkan! Tahap berikutnya adalah memuat beberapa contoh dari record menjadi table yang baru.
Memuat Demo Data
Cara termudah untuk memuat suatu data adalah dengan memanfaatkan seeds.rb file didalam db directory. Namun, aku pemalas (seperti kebanyak programmer) dan tidak mau memikirkan suatu sample content. Oleh karena itu kenapa kita tidak memanfaatkan kelebihan dari faker gem yang dapat memproduksi data acak dari berbagai: nama, emails, hipster word, "lorem ipsum" text, dan masih banyak lagi.
Gemfile
1 |
group :development do |
2 |
gem 'faker' |
3 |
end |
Install gem:
1 |
bundle install
|
Sekarang modifikasi seeds.rb:
db/seeds.rb
1 |
5.times do |
2 |
user = User.create({name: Faker::Name.name}) |
3 |
user.posts.create({title: Faker::Book.title, body: Faker::Lorem.sentence}) |
4 |
end |
Terakhir, muat data Anda:
1 |
rails db:seed |
Menanggapi dengan JSON
Sekarang, sudah pasti, kita membutuhkan beberapa routes dan controller untuk membuat API. Itu biasa dalam membentuk suatu API's didalam api/
path. Dan juga developers biasanya menyediakan versi dari APIs di dalam sebuah path, contoh api/v1/
. Pada akhirnya jika ada yang berubah dan harus diperkenalkan, Anda dengan mudah membuat namecpace(v2
) dan controller yang terpisah.
Ini adalah bagaimana routes Anda terlihat:
config/routes.rb
1 |
namespace 'api' do |
2 |
namespace 'v1' do |
3 |
resources :posts |
4 |
resources :users |
5 |
end |
6 |
end |
Ini membuat routes seperti:
1 |
api_v1_posts GET /api/v1/posts(.:format) api/v1/posts#index |
2 |
POST /api/v1/posts(.:format) api/v1/posts#create |
3 |
api_v1_post GET /api/v1/posts/:id(.:format) api/v1/posts#show |
Anda juga bisa mencakup
cara selain menggunakan namespace
, tetapi pada dasarnya ini akan melihat pada UsersController
dan PostsController
didalam controllers directory, tidak didalam controllers/api/v1, jadi berhati–hatilah.
Membuat api dengan nested directory v1 didalam sebuah controllers. Bersama dengan controllers Anda:
controllers/api/v1/users_controller.rb
1 |
module Api |
2 |
module V1 |
3 |
class UsersController < ApplicationController |
4 |
end
|
5 |
end
|
6 |
end
|
controllers/api/v1/posts_controller.rb
1 |
module Api |
2 |
module V1 |
3 |
class PostsController < ApplicationController |
4 |
end
|
5 |
end
|
6 |
end
|
Perlu dicatat Anda tidak hanya memerlukan nest di controllers file dibawah api/v1 path, tetapi Anda juga membutuhkan class itu sendiri yang juga memiliki namespace didalam modul Api
dan V1
Pertanyaan berikutnya adalah bagaimana cara untuk merespon secara tepat dengan JSON-formatted data? Di artikel ini kita akan mencoba solusi ini: jBuilder dan active model_serializers gems. Jadi sebelum kita lanjut ke tahap selanjutnya, letakkan mereka di Gemfile:
Gemfile
1 |
gem 'jbuilder', '~> 2.5' |
2 |
gem 'active_model_serializers', '~> 0.10.0' |
Kemudian jalankan:
1 |
bundle install
|
Menggunakan jBuilder Gem
jBuilder adalah suatu gem terkenal yang dibuat oleh tim Rails yang menyediakan DSL sederhana(domain specific language) yang memperbolehkan Anda untuk mendefinisi struktur JSON pada views.
Seharusnya kita menginginkan untuk melihat semua kiriman ketika pengguna menyentuh index
action:
controllers/api/v1/posts_controller.rb
1 |
def index |
2 |
@posts = Post.order('created_at DESC') |
3 |
end |
Yang Anda butuhkan adalah membuat nama view setelah melakukan aksi dengan .json.jbuilder extension. Catat bahwa view harus diletakan pada api/v1 path:
views/api/v1/posts/index.json.jbuilder
1 |
json.array! @posts do |post| |
2 |
json.id post.id |
3 |
json.title post.title |
4 |
json.body post.body |
5 |
end
|
json.array!
traverses the @posts
array. json.id
, json.title
dan json.body
membentuk suatu kunci dengan nama yang sesuai argument sebagai values. Jika Anda masuk http://localhost:3000/api/v1/posts.json, Anda akan melihat ini:
1 |
[
|
2 |
{"id": 1, "title": "Title 1", "body": "Body 1"}, |
3 |
{"id": 2, "title": "Title 2", "body": "Body 2"} |
4 |
]
|
Bagaimana jika kita menginginkan untuk melihat penulias disetiap kiriman? Itu mudah:
1 |
json.array! @posts do |post| |
2 |
json.id post.id |
3 |
json.title post.title |
4 |
json.body post.body |
5 |
json.user do |
6 |
json.id post.user.id |
7 |
json.name post.user.name |
8 |
end
|
9 |
end
|
Ouput akan berubah menjadi:
1 |
[
|
2 |
{"id": 1, "title": "Title 1", "body": "Body 1", "user": {"id": 1, "name": "Username"}} |
3 |
]
|
Konten dari .jbuilder fadalah suatu ruby code, sehingga Anda akan menyadari semua operasi dasar seperti biasa.
Catatan bahwa jbuilder mendukung partials seperti halnya ordinary Rails view, sehingga Anda juga akan bilang:
1 |
json.partial! partial: 'posts/post', collection: @posts, as: :post |
dan kemudian membuat views/api/v1/posts/_post.json.jbuilder file dengan konten sebagai berikut:
1 |
json.id post.id |
2 |
json.title post.title |
3 |
json.body post.body |
4 |
json.user do |
5 |
json.id post.user.id |
6 |
json.name post.user.name |
7 |
end
|
Jadi, seperti yang Anda lihat, jbuilder itu mudah dan sesuai. Sehingga, sebagai alternatif, Anda akan terus dengan serializers, jadi mari kita diskusi mereka di sesi berikutnya.
Menggunakan Serializers
rails_model_serializers gem dibuat oleh tim yang di me-manage rails-api. Sebagai dokumentasi,rails_model_serializers membawa konvesi daripada konfigurasi ke generasi JSON. Pada dasarnya, Anda akan mendefinisikan dimana Anda akan menggunakan saat serialization (JSON generation).
Ini adalah serializer pertama kita:
serializers/post_serializer.rb
1 |
class PostSerializer < ActiveModel::Serializer |
2 |
attributes :id, :title, :body |
3 |
end
|
Di sini bisa kita mengatakan bahwa semua bagian ini harus hadir dalam JSON yang dihasilkan. Sekarang metode seperti to_json
dan as_json
dipanggil selama pada kiriman yang akan menggunakan konfigurasi dan mengembalikan konten yang benar.
Untuk melihat secara langsung, modifikasi index
action seperti ini:
controllers/api/v1/posts_controller.rb
1 |
def index |
2 |
@posts = Post.order('created_at DESC') |
3 |
|
4 |
render json: @posts |
5 |
end |
as_json
akan secara otomatis dipanggil pada @posts
object.
Bagaimana dengan pengguna? Serializers memperbolehkan Anda untuk mengindikasi hubungan, seperti model. Yang lebih lagi, serializers bisa di nested:
serializers/post_serializer.rb
1 |
class PostSerializer < ActiveModel::Serializer |
2 |
attributes :id, :title, :body |
3 |
belongs_to :user |
4 |
|
5 |
class UserSerializer < ActiveModel::Serializer |
6 |
attributes :id, :name |
7 |
end
|
8 |
end
|
Sekarang ketika Anda me-serialisasi kiriman, itu akan otomatis mengandung nested user
key dengan id dan nama. Jika Anda membuatnya terakhir itu akan secara membuat serializer secara terpisah untuk pengguna dengan :id
attribute kecuali:
serializers/post_serializer.rb
1 |
class UserSerializer < ActiveModel::Serializer |
2 |
attributes :name |
3 |
end
|
Kemudian @user.as_json
tidak akan membalikan user's id. Tetap, @post.as_json
akan mengembalikan username dan id, jadi perlu di ingat.
Mengamankan API
Di banyak kasus, kita tidak mau seorang pun melakukan aksi menggunakan API. Jadi mari memberikan pengecekan keamanan sederhana dan memaksa pengguna kita untuk mengirim token mereka ketika membuat atau menghapus kiriman.
Token memiliki daya hidup tak terbatas dan dibuat ketika pengguna melakukan registrasi. Pertama, tambahkan token baru
pada kolom di table user
:
1 |
rails g migration add_token_to_users token:string:index |
Index ini akan menjamin keunikan karena tidak akan ada dua pengguna dengan satu token yang sama:
db/migrate/xyz_add_token_to_users.rb
1 |
add_index :users, :token, unique: true |
Gunakan Migrasi:
1 |
rails db:migrate |
Sekarang tambahkan callback before_save
:
models/user.rb
1 |
before_create -> {self.token = generate_token} |
Metode generate_token
private akan membuat sebuah token yang siklus tak terbatas dan melihat apakah token unik atau tidak. Secepat mungkin ketika token dibuat, mengembalikan:
models/user.rb
1 |
private |
2 |
|
3 |
def generate_token |
4 |
loop do |
5 |
token = SecureRandom.hex |
6 |
return token unless User.exists?({token: token}) |
7 |
end |
8 |
end |
Anda akan menggunakan algoritma untuk membuat token, sebagai contoh seperti MD5 hash dari username dan beberapa salt.
Registrasi Pengguna
Tentu saja, kita juga membutuhkan pengguna untuk mendaftar, karena mereka tidak akan bisa mendapatkan token mereka jika tidak. Saya tidak mau memperkenalkan HTML views pada aplikasi, jadi mari kita menambahkan metode API:
controllers/api/v1/users_controller.rb
1 |
def create |
2 |
@user = User.new(user_params) |
3 |
if @user.save |
4 |
render status: :created |
5 |
else |
6 |
render json: @user.errors, status: :unprocessable_entity |
7 |
end |
8 |
end |
9 |
|
10 |
private |
11 |
|
12 |
def user_params |
13 |
params.require(:user).permit(:name) |
14 |
end |
Ini ide bagus untuk mengembalikan HTTP status code sehingga developers mengerti apa yang terjadi. Sekarang Anda akan membat serializer baru untuk pengguna atau tetap dengan .json.jbuilder file. Saya lebih memilih variasi latter (itu kenapa saya tidak mau menggunakan :json
sebagai metode untuk melakukan render
), tetapi Anda bebas untuk memilih semua itu. Catatan, bahwa, token itu tidak selalu harus di serialisasi, sebagai contoh ketika Anda mengebalikan daftar dari semua pengguna, itu seharusnya bisa membuat aman!
views/api/v1/users/create.json.jbuilder
1 |
json.id @user.id |
2 |
json.name @user.name |
3 |
json.token @user.token |
Tahap berikutnya adalah dengan mencoba apakah semuanya berjalan dengan lancar. Apakah Anda juga akan menggunakan curl
command atau menulis beberapa kode Ruby Karena artikel ini berhubungan dengan Ruby, saya akan memilih opsi coding.
Menguji Registrasi Pengguna
Untuk melakukan HTTP request, kita akan menggunakan Faraday gem, yang menyediakan interface yang sederhana daripada kebanyak adapters (defaultnya adalah Net::HTTP
). Membuat Ruby file yang terpisah, termasuk Faraday, dan mempersiapkan untuk client:
api_client.rb
1 |
require 'faraday' |
2 |
|
3 |
client = Faraday.new(url: 'http://localhost:3000') do |config| |
4 |
config.adapter Faraday.default_adapter |
5 |
end
|
6 |
|
7 |
response = client.post do |req| |
8 |
req.url '/api/v1/users' |
9 |
req.headers['Content-Type'] = 'application/json' |
10 |
req.body = '{ "user": {"name": "test user"} }' |
11 |
end
|
Semua pilihan ini dapat dijelaskan sendiri: seperti kita memilih default adapter, memperisapkan request URL untuk http://localhost:300/api/v1/users, mengubah tipe konten untuk application/json
, dan menyediakan sebuah request.
Respon dari server mengandung JSON, jadi untuk meneruskan saya akan menggunakan Oj gem:
api_client.rb
1 |
require 'oj' |
2 |
|
3 |
# client here...
|
4 |
|
5 |
puts Oj.load(response.body) |
6 |
puts response.status |
Berbeda dengan memecah respon, saya juga memperlihatkan status code yang bermaksud untuk debugging.
Sekarang Anda bisa menjalakan script ini:
1 |
ruby api_client.rb |
dan menyimpan token yang diterima di suatu tempat, kita akan menggunakan ini di sesi berikutnya.
Autentikasi dengan Token
Untuk memastikan autentikasi token, metode authenticate_or_request_with_http_token
bisa digunakan. Ini termasuk bagian dari ActionController::HttpAuthentication::Token::ControllerMethods modul, jadi harus diingat ini termasuk:
controllers/api/v1/posts_controller.rb
1 |
class PostsController < ApplicationController |
2 |
include ActionController::HttpAuthentication::Token::ControllerMethods |
3 |
# ... |
4 |
end
|
Tambahkan before_action
dan metode yang sesuai:
controllers/api/v1/posts_controller.rb
1 |
before_action :authenticate, only: [:create, :destroy] |
2 |
|
3 |
# ... |
4 |
|
5 |
private |
6 |
|
7 |
# ... |
8 |
|
9 |
def authenticate |
10 |
authenticate_or_request_with_http_token do |token, options| |
11 |
@user = User.find_by(token: token) |
12 |
end |
13 |
end |
Sekarang jika token tidak disiapkan atau pengguna dengan token tidak dapat ditemukan, sebuah 401 error akan ditampilkan, Menahan suatu aksi untuk mengeksekusi.
Perlu dicatat bahwa komunikasi antara client dan server harus dibuat melalui HTTPS, jika tidak token bisa dengan mudah dipalsukan. Tentu saja, ini bukanlah solusi yang ideal, dan di banyak kasus lebih baik untuk menggunakan OAuth 2 protocol untuk autentikasi. Paling tidak, ada dua gems yang bisa dengan mudah mendukung proses dari fitur ini: Doorkeeper dan oPRO.
Membuat Posting
Untuk melihat autentikasi kita yang dijalankan, tambahkan untuk membuat
suatu PostsController
:
controllers/api/v1/posts_controller.rb
1 |
def create |
2 |
@post = @user.posts.new(post_params) |
3 |
if @post.save |
4 |
render json: @post, status: :created |
5 |
else |
6 |
render json: @post.errors, status: :unprocessable_entity |
7 |
end |
8 |
end |
Kita bisa mengambil keuntungan dari serializer disini untuk melihat JSON yang tepat. @user
sudah siap didalam before_action
.
Sekarang coba semuanya menggunakan kode yang sederhana:
api_client.rb
1 |
client = Faraday.new(url: 'http://localhost:3000') do |config| |
2 |
config.adapter Faraday.default_adapter |
3 |
config.token_auth('127a74dbec6f156401b236d6cb32db0d') |
4 |
end
|
5 |
|
6 |
response = client.post do |req| |
7 |
req.url '/api/v1/posts' |
8 |
req.headers['Content-Type'] = 'application/json' |
9 |
req.body = '{ "post": {"title": "Title", "body": "Text"} }' |
10 |
end
|
Ubah argument yang dipakai oleh token_auth
dengan token yang diterima pada saat registrasi, dan menjalankan script.
1 |
ruby api_client.rb |
Menghapus Posting
Menghapus sebuah post dilakukan dengan cara yang sama. Menambahkan action destroy
:
controllers/api/v1/posts_controller.rb
1 |
def destroy |
2 |
@post = @user.posts.find_by(params[:id]) |
3 |
if @post |
4 |
@post.destroy |
5 |
else |
6 |
render json: {post: "not found"}, status: :not_found |
7 |
end |
8 |
end |
Kita hanya memperbolehkan pengguna untuk menghapus kiriman yang hanya miliknya Jika kiriman terhapus dengan benar, 204 status code (tidak ada konten) akan di tampilkan. Ini adalah potongan dari kode yang akan dicoba dengan fitur baru ini:
api_client.rb
Gantilah id kiriman dengan angka yang Anda mau.
1 |
response = client.delete do |req| |
2 |
req.url '/api/v1/posts/6' |
3 |
req.headers['Content-Type'] = 'application/json' |
4 |
end
|
Pengaturan CORS
Jika Anda mau menyediakan web service lain untuk mengakses API Anda (dari client-side), kemudian CORS (Cross-Origin Resource Sharing)bisa menyiapkannya.
Pada dasarnya, CORS memperbolehkan suatu web application untuk mengirim AJAX request ke third-party services. Pada dasarnya, CORS memperbolehkan suatu web application untuk mengirim AJAX request ke third-party services. Beruntungnya, ada gem yang disebut rack-cors yang disiapkan sehingga daoat mempermudah kita. Tambahkan ini pada Gemfile:
Gemfile
1 |
gem 'rack-cors' |
Install ini:
1 |
bundle install
|
Dan kemudian sediakan konfigurasi didalam config/initializers/cors.rbfile. Sebenarnya, file ini sudah dibuat untuk Anda dan mengandung contoh penggunaan. Anda juga bisa menemukan contoh dokumentasi yang lengkap pada gem's page.
Konfigurasi berikut ini, sebagai contoh, akan memperbolehkan setiap orang untuk mengakses API Anda menggunakan metode:
config/initializers/cors.rb
1 |
Rails.application.config.middleware.insert_before 0, Rack::Cors do |
2 |
allow do |
3 |
origins '*' |
4 |
|
5 |
resource '/api/*', |
6 |
headers: :any, |
7 |
methods: [:get, :post, :put, :patch, :delete, :options, :head] |
8 |
end |
9 |
end |
Mencegah Penyalahgunaan
Hal terakhir yang saya akan bahas di pedoman ini adalah bagaimana cara melindungi API Anda dari penyalahgunaan dan penolakkan serangan layanan. Ada gem bagus yang disebut rack-attack (dibuat oleh orang dari Kickstarter) yang memperbolehkan Anda untuk mem-blacklist atau whitelist client, mencegah server untuk melebihi request dan masih banyak lagi.
Letakkan ini pada Gemfile:
Gemfile
1 |
gem 'rack-attack' |
Install ini:
1 |
bundle install
|
Dan kita dapat melakukan konfigurasi didalam rack_attack.rb initializer file. Dokumentasi gem menampilkan semua pengaturan yang ada dan bagaimana saran untuk pemakaian. Ini adalah contohnya mengkonfigurasi utntuk membatasi seseorang kecuali kamu untuk mengakses service dan membatasi jumlah request sebanyak 5 request per detik:
config/initializers/rack_attack.rb
1 |
class Rack::Attack |
2 |
safelist('allow from localhost') do |req| |
3 |
# Requests are allowed if the return value is truthy |
4 |
'127.0.0.1' == req.ip || '::1' == req.ip |
5 |
end |
6 |
|
7 |
throttle('req/ip', :limit => 5, :period => 1.second) do |req| |
8 |
req.ip |
9 |
end |
10 |
end |
Hal lain yang perlu dilakukan adalah memasukan RackAttack sebagai middleware:
config/application.rb
1 |
config.middleware.use Rack::Attack |
Kesimpulan
Kita sudah sampai pada akhir artikel ini. Semoga, sekarang Anda lebih merasa percaya diri untuk membuat API dengan Rails! Catatan bahwa ini bukanlah satu-satunya cara, cara lain yang terkenal yang sudah ada sejak lama adalah Grape framework, mungkin Anda tertarik untuk melihat itu juga.
Jangan ragu-ragu untuk mengirimkan pertanyaan Anda jika ada sesuatu yang tidak jelas untuk Anda. Saya mengucapkan terimakasih telah bersama dengan Saya, dan happy coding!