101 Elixir : Cukup dengan Plug untuk membangun website dinamis yang powerfull (part 1)
Kalo kamu datang dari ekosistem NodeJS, kamu tentu tidak asing dengan microframework seperti ExpressJS atau HapiJs. Begitu pun di PHP ada Lumen, di Golang ada Gin, di Ruby ada Rake, di C# ada dotNet microframework. Nah, di Elixir pun ada microframework yang powerfull yaitu Plug. Pada tulisan kali ini saya akan membahas tutorial dasar membangun sebuah dinamic website sederhana menggunakan Plug. Saya berasumsi pembaca sudah mengetahui syntax dasar elixir dan untuk mengikuti tutorial ini kamu perlu menginstall elixir di local mesin kamu. Jika sudah, mari kita mulai!
Initial project
Buat project baru menggunakan Mix dengan nama platform (atau dengan nama apapun sesuai keinginan kamu).
$ mix new platform
kita akan dibuatkan project yang fresh seperti berikut
$ mix new platform
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/platform.ex
* creating test
* creating test/test_helper.exs
* creating test/platform_test.exsYour Mix project was created successfully.
You can use “mix” to compile it, test it, and more:cd platform
mix testRun “mix help” for more commands.
Mix membuatkan kita sebuah project yang terstuktur. Perlu di ingat, semua code yang akan kita buat saat ini akan kita simpan di folder lib/.
Install Dependency
Setelah Mix menyediakan project yang “polos” kita perlu menambah sendiri dependency yang lain nya. Edit file /mix.exs
defp deps do
[
{:cowboy, “~> 1.0.0”},
{:plug, “~> 1.0”},
{:sqlite_ecto, “~> 1.0.0”},
{:ecto, “~> 1.0”}
]
end
Penjelasan dari dependency yang kita gunakan adalah sebagai berikut. Cowboy adalah web server yang untuk Erlang/OTP (tentu bisa digunakan oleh Elixir). Plug adalah microframeworknya, Sqlite_ecto adalah adapter Elixir untuk database SQLite, dan Ecto adalah ORM (Object relation model) untuk Elixir sama seperti active record di Ruby on Rails atau Eloquent di Laravel.
Kemudian jalankan perintah berikut untuk pendapatkan dependency tersebut
$ mix deps.get
Sampai sini instalasi dependency telah selesai.
Plug Server
Pada dasarnya sebuah server bertugas untuk penerima request dan mengembalikan response. Di Plug, request dan response di representasikan menjadi sebuah Connection. Nah, Connection inilah yang dienkapsulasi ke dalam sebuah module yang disebut Plug.Conn. Semua data dari request yang masuk dapat di akses lewat module Plug.Conn tersebut begitu pun response yang kirimkan akan di sertakan ke module Plug.Conn.
Setelah memahami apa itu Plug.Conn, step selanjutnya kita akan mendefinisikan router berdasarkan request yang masuk ke server. Berikut adalah contoh dasar Plug server yang sederhana. Pada file lib/platform.ex
defmodule Platform do
import Plug.Conn def init(options) do
IO.puts “Ready for the requests”
options
end def call( conn, _opts) do
conn
|> put_resp_header("Server", “Plug”)
|> send_resp(200, “Hello from Plug”)
end |> send_resp(200, “Hello from Plug”)
end def server() do
{:ok, pid} = Plug.Adapters.Cowboy.http __MODULE__, []
pid
end
end
Untuk menjalankannya kita harus masuk ke console elixir di dalam direktori platform/ dengan perintah
$ iex -S mix
kemudian eksekusi code yang sudah kita buat
> Platform.server
lalu buka browser dengan url http://localhost:4000. Plug akan menampilkan “Hello from Plug” (note: by default Cowboy menggunakan port 4000)
Penjelasan:
Response yang dikirim dari server berasal dari Plug via Cowboy web server yang kita eksekusi di function send_resp/3
. Module yang dijalankan oleh Cowboy adalah module Platform yang di definisikan dengan __MODULE__
. Setiap module yang akan menjalankan Plug wajib memiliki 2 function yaitu init/1
dan call/2
.
Pada dasarnya function init/1
berisi apapun code yang akan di eksekusi ketika server pertama kali dijalankan, kemudian return dari function init/1
tersebut akan diterima oleh function call/2
sebagai parameter kedua. Parameter pertama berisi conn yang merepresentasikan connection yang dikirim ke server.
Mungkin ada yang bertanya dari mana datangnya function put_resp_header/3
dan send_resp/3
? Function tersebut di ambil dari module Plug.Conn
yang sudah kita import di awal code dengan import Plug.Conn
. Jadi kita bisa saja menuliskan Plug.Conn.send_resp/3
namun sepertinya jadi kurang efisien. Kamu bisa mencari function sesuai kebutuhan kamu di dokumentasi ini
Membuat Routing
Ketika request di terima oleh server, router bertugas untuk menentukan action apa yang akan di lakukan berdasarkan request tersebut. Pada umumnya ada 3 hal yang perlu di cek oleh router yaitu http method, path info dan parameter. Beruntung di elixir kita bisa menggunakan pattern matching untuk memeriksa ke-3 hal tersebut. Berikut adalah contoh routing menggunakan pattern matching. Kita edit lagi file lib/platform.ex
def call( conn, _opts) do
route(conn.method, conn.path_info, conn)
enddef route(“GET”, [], conn) do
conn
|> send_resp(200, “This is Index page”)
enddef route(“GET”, [“users”], conn) do
conn
|> send_resp(200, “This is User page”)
enddef route(“GET”, [“users”, user_id], conn) do
conn
|> send_resp(200, “You are accessing user with id #{user_id}”)
enddef route(_method, _path_info, conn) do
conn
|> send_resp(404, “Page not found”)
end
Pada function call/2
kita menjalankan function route/3 dengan parameter connection method, connection path info dan conn. Dari sini pattern matching akan bekerja dengan menjalankan function route sesuai dengan method dan path_info yang dikirim. Perhatikan kita telah membuat 4 buah function route dengan parameter yang berbeda-beda. Pattern matching secara otomatis akan mencari function dengan parameter yang cocok dan mengeksekusi proses nya.
Jalankan kembali > Platform.server
lalu silahkan coba akses http://localhost:4000
maka server akan mengembalikan text “This is Index page”. Begitu juga jika mengakses http://localhost:4000/users
maka server mengembalikan “This is User page”. Seterusnya kita pun bisa mengakses parameter url yang di lewat connection path_info [“users”, user_id]
Hmmm, tapi bagaimana jika kita mempunyai banyak module Plug? Apakah kita harus menambahkan function init/1
dan call/2
ke semua module itu satu per satu? Jawabannya tentu tidak, kita harus me-refactor code tersebut supaya lebih DRY. Salah satu solusinya dengan membuat sebuah module Macro. Karena macro di eksekusi pada saat compile time, Erlang VM akan selalu menjalankan code yang sama dan juga kita dapat menggunakannya di banyak module.
Buat sebuah module macro di lib/Macros/router.ex
defmodule Platform.Macros.Router do
defmacro __using__(_options) do quote do
def init(options) do
options
end def call(conn, _options) do
route(conn.method, conn.path_info, conn)
end
end end
end
Selanjutnya kita hanya perlu memasangnya di module Platform. lib/platform.ex menjadi seperti ini
defmodule Platform do
use Platform.Macros.Router
import Plug.Conn def route(“GET”, [], conn) do
conn
|> send_resp(200, “This is Index page”)
end def route(“GET”, [“users”], conn) do
conn
|> send_resp(200, “This is User page”)
end def route(“GET”, [“users”, user_id], conn) do
conn
|> send_resp(200, “You are accessing user with id #{user_id}”)
end def route(_method, _path_info, conn) do
conn
|> send_resp(404, “Page not found”)
end def server() do
{:ok, pid} = Plug.Adapters.Cowboy.http __MODULE__, []
pid
endend
Yes! kita sudah berhasil menghilangkan function init/1
dan call/2.
Koneksi antara Router
Sepertinya masih ada yang kurang dari code yang telah kita buat. Kita akan coba me-refactor kembali module Platform dengan memisahkan router untuk Users dan Platform. Buat file baru lib/Router/user.ex lalu pindahkan route untuk Users dari module platform
defmodule Platform.Router.User do
import Plug.Conn
use Platform.Macros.Router def route(“GET”, [“users”], conn) do
conn
|> send_resp(200, “This is User page”)
end def route(“GET”, [“users”, user_id], conn) do
conn
|> send_resp(200, “You are accessing user with id #{user_id}”)
endend
Setelah itu mari kita hubungkan module platform dengan users
defmodule Platform do
use Platform.Macros.Router
import Plug.Conn
alias Platform.Router.User @user_router_option User.init([])
def route(“GET”, [], conn) do
conn
|> send_resp(200, “This is Index page”)
end def route(_method, [“users” | _path], conn) do
User.call(conn, @user_router_option)
end def route(_method, _path_info, conn) do
conn
|> send_resp(404, “Page not found”)
end def server() do
{:ok, pid} = Plug.Adapters.Cowboy.http __MODULE__, []
pid
endend
Yup, kita kembali menggunakan pattern matching untuk me-redirect request sehingga setiap request yang mengakses url /users
akan langsung di alihkan ke router Platform.Router.User.
Oh man, I love pattern matching :)
Benchmark (additional)
Tentu saja saya akan setuju jika ada yang bilang bahwa benchmarking bahasa pemrograman adalah bullsh*t namun tentu masih banyak juga developer yang sangat care dengan benchmark dan hanya mau belajar bahasa baru hanya jika benchmark nya sangat cepat. Tentu itu bukan hal yang salah karena siapa yang mau belajar bahasa yang cuma bisa serve 100 req/second? Oleh karena itu saya membahas benchmark di sini sekedar untuk tambahan saja :).
Pattern matching erlang sudah mengalami banyak pengembangan, contohnya pada kasus routing
yang telah kita buat, Erlang meng-handle pencocokan dengan cara binary search daripada linear search. Dan karena layer antara Plug
dan Cowboy
sangat sangat tipis maka performance nya pun menjadi sangat cepat. Untuk mendapat perbandingan nya, Matthew Rothenberg telah membuat sebuah benchmark test yang hasilnya bisa dilihat di table ini
Dilihat dari table tersebut, Plug bisa dibilang sangat cepat dengan 54948 request per second. Bahkan lebih cepat bila dibandingkan dengan microframework dari Golang yaitu Gin yang mendapat 51483 request per second. Tentu perhitungan ini belum mencakup transaksi dengan database dan lain sebagainya. So, don’t trust benchmark :)
Next
Selanjutnya di part 2, kita akan mencoba menggunakan view engine dari Elixir dan membuat page html sederhana yang nantinya akan kita gunakan untuk menambah dan menampilkan data dari database SQLite.
Semoga tulisan ini bermanfaat.