101 Elixir : Cukup dengan Plug untuk membangun website dinamis yang powerfull (part 1)

surya surakhman
6 min readJun 18, 2018

--

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.exs
Your Mix project was created successfully.
You can use “mix” to compile it, test it, and more:
cd platform
mix test
Run “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)
end
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

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
end
end

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}”)
end
end

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
end
end

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.

--

--