Tur Kemal

Framework web bergaya Sinatra: routing, parameter, JSON, dan middleware Kemal.

Daftar isi

Kemal adalah framework web mikro bergaya Sinatra yang dibangun di atas HTTP::Server dari pustaka standar Crystal. Alih-alih menyusun rantai handler secara manual, Kemal menyediakan DSL ringkas berupa macro seperti get untuk mendefinisikan route beserta penangannya dalam beberapa baris. Topik ini membangun satu aplikasi kecil yang memuat routing, parameter URL, parameter query, respons (response) JSON, dan middleware, lengkap dengan pemasangan dependensi serta langkah kompilasinya. Aplikasi hasil akhir berjalan pada port 8952 dan menjawab empat endpoint.

Proyek dan dependensi#

Sebuah proyek Crystal yang menggunakan shard, yakni paket pustaka Crystal, dikenali dari berkas shard.yml pada folder proyek. Berkas ini memuat nama dan versi proyek, ditambah daftar dependencies yang masing-masing menunjuk sumbernya. Kemal diambil dari repositori kemalcr/kemal di GitHub, sehingga satu entri github: kemalcr/kemal sudah cukup. Simpan berkas berikut sebagai shard.yml.

yaml shard.yml
name: aplikasi-kemal
version: "0.1.0"

dependencies:
  kemal:
    github: kemalcr/kemal

crystal: ">= 1.0.0"

Dependensi dipasang dengan shards install. Perintah ini mengunduh setiap shard beserta dependensinya ke folder lib/, lalu menulis shard.lock yang merekam versi persis yang terpasang. Jalankan perintah berikut di folder proyek.

bash
# pasang dependensi proyek
shards install
text Keluaran
I: Resolving dependencies
I: Fetching https://github.com/kemalcr/kemal.git
I: Fetching https://github.com/luislavena/radix.git
I: Fetching https://github.com/crystal-loot/exception_page.git
I: Fetching https://github.com/sija/backtracer.cr.git
I: Installing radix (0.4.1)
I: Installing backtracer (1.2.4)
I: Installing exception_page (0.5.0)
I: Installing kemal (1.15.0)
I: Writing shard.lock

Awalan I: menandai baris informasi dari shards. Keluaran tersebut memperlihatkan bahwa pemasangan kemal 1.15.0 menyertakan shard lain, karena radix digunakan sebagai router, exception_page untuk halaman error, dan backtracer untuk pelacakan error. Versi yang tercetak dapat berbeda mengikuti rilis terbaru yang tersedia saat perintah dijalankan.

Aplikasi contoh#

Kode aplikasi berikut memuat seluruh bagian yang dibahas pada topik ini, dari middleware hingga tiga route. Strukturnya berurutan, yaitu definisi middleware, definisi route, lalu dua baris penutup yang menjalankan server. Simpan berkas tersebut sebagai app.cr di samping shard.yml.

crystal app.cr
require "kemal"
require "json"

# Middleware sederhana: menjawab sendiri permintaan ke /sehat
class Sehat < Kemal::Handler
  def call(context)
    if context.request.path == "/sehat"
      context.response.content_type = "text/plain"
      context.response.print "ok"
      return
    end
    call_next(context)
  end
end

use Sehat.new

get "/" do
  "Selamat datang di aplikasi Kemal"
end

get "/salam/:nama" do |env|
  "Halo, #{env.params.url["nama"]}!"
end

get "/api/penjumlahan" do |env|
  a = env.params.query["a"]?.try(&.to_i) || 0
  b = env.params.query["b"]?.try(&.to_i) || 0
  env.response.content_type = "application/json"
  {"a" => a, "b" => b, "hasil" => a + b}.to_json
end

Kemal.config.port = 8952
Kemal.run

Baris require "kemal" memuat DSL Kemal, sedangkan require "json" menyediakan metode to_json yang digunakan route API. Satu middleware didefinisikan sebagai kelas Sehat dan didaftarkan dengan use, sedangkan tiga route didefinisikan dengan macro get. Dua baris terakhir menetapkan port melalui Kemal.config.port lalu menjalankan server dengan Kemal.run. Bagian berikutnya membedah setiap bagian tersebut satu per satu.

Routing dan parameter URL#

Route didefinisikan dengan macro get yang menerima pola path dan satu blok penangan. Segmen pola yang diawali tanda titik dua menangkap nilai dari URL, sehingga pola /salam/:nama menerima permintaan seperti /salam/Citra dan menyimpan Citra di bawah kunci nama. Blok penangan menerima objek env yang bertipe HTTP::Server::Context, yaitu objek context yang sama dengan yang digunakan HTTP::Server. Nilai yang dikembalikan blok menjadi tubuh respons secara utuh.

crystal Contoh: route dengan parameter URL
# tidak-dijalankan
get "/salam/:nama" do |env|
  "Halo, #{env.params.url["nama"]}!"
end

Parameter URL dibaca dengan env.params.url["nama"]. Kuncinya harus sama persis dengan nama segmen pada pola route, karena kunci yang keliru melempar exception KeyError saat permintaan diproses. Apabila sebuah segmen bersifat opsional, sediakan route terpisah untuk bentuk dengan dan tanpa segmen tersebut, atau gunakan parameter query yang dibahas pada bagian berikutnya.

Parameter query dan respons JSON#

Parameter query dibaca dari env.params.query yang bertipe HTTP::Params, sama seperti query_params pada HTTP::Server. Akses ["a"]? mengembalikan nil apabila kunci tidak ada, dan try(&.to_i) mengonversi string angka menjadi bilangan sambil tetap menangani nilai yang kosong. Kombinasi try dan || seperti ini muncul berulang pada kode Crystal untuk memberi nilai bawaan. Route API berikut menjumlahkan dua bilangan dari parameter query lalu mengembalikan hasilnya.

crystal Contoh: route yang menyajikan JSON
# tidak-dijalankan
get "/api/penjumlahan" do |env|
  a = env.params.query["a"]?.try(&.to_i) || 0
  b = env.params.query["b"]?.try(&.to_i) || 0
  env.response.content_type = "application/json"
  {"a" => a, "b" => b, "hasil" => a + b}.to_json
end

Isi JSON dibuat dari Hash dengan metode to_json dari modul json yang di-require di awal berkas. Sebelum tubuh respons ditulis, content_type ditetapkan ke application/json agar klien menafsirkan jawaban sebagai JSON, bukan sebagai HTML. Tipe nilai pada Hash menentukan bentuk keluarannya, sehingga bilangan pada contoh tercetak tanpa tanda kutip. Urutan kunci pada keluaran mengikuti urutan penulisan kunci di kode.

Middleware Kemal#

Middleware Kemal ditulis sebagai kelas turunan Kemal::Handler yang mendefinisikan metode call(context). Mekanismenya sama dengan middleware HTTP::Server, yaitu call_next(context) meneruskan pekerjaan ke handler berikutnya, sedangkan handler yang tidak memanggil call_next menyelesaikan permintaan sendiri. Middleware didaftarkan dengan use dan dieksekusi sebelum router, sehingga path yang ditangani middleware tidak memerlukan route sama sekali.

Kelas Sehat pada aplikasi contoh memanfaatkan mekanisme tersebut. Permintaan ke /sehat dijawab langsung oleh middleware dengan teks ok, sedangkan semua path lain diteruskan dengan call_next ke router untuk dicocokkan dengan route yang terdaftar. Pola semacam ini lazim digunakan untuk endpoint kesehatan yang dipanggil oleh alat pemantau secara berkala.

crystal Contoh: middleware yang menangani sendiri satu path
# tidak-dijalankan
class Sehat < Kemal::Handler
  def call(context)
    if context.request.path == "/sehat"
      context.response.content_type = "text/plain"
      context.response.print "ok"
      return
    end
    call_next(context)
  end
end

use Sehat.new

Kompilasi dan pengujian#

Aplikasi dikompilasi dengan crystal build. Kompilasi pertama membutuhkan waktu lebih lama karena seluruh kode di folder lib/ disertakan, sedangkan kompilasi berikutnya jauh lebih cepat berkat cache compiler.

bash
# kompilasi aplikasi beserta seluruh dependensinya
crystal build -o app.bin app.cr

Jalankan aplikasi di latar dan uji keempat endpoint dengan curl. Aplikasi mencetak satu baris log berstempel waktu ke stdout untuk setiap permintaan, sehingga stdout diarahkan ke /dev/null pada blok ini agar keluaran yang tampil hanya berisi respons curl. Perintah kill %1 di akhir menghentikan aplikasi.

bash
# jalankan aplikasi di latar, uji empat endpoint, lalu hentikan
./app.bin >/dev/null 2>&1 &
sleep 2
curl -s http://127.0.0.1:8952/; echo
curl -s http://127.0.0.1:8952/salam/Citra; echo
curl -s "http://127.0.0.1:8952/api/penjumlahan?a=12&b=30"; echo
curl -s http://127.0.0.1:8952/sehat; echo
kill %1
text Keluaran
Selamat datang di aplikasi Kemal
Halo, Citra!
{"a":12,"b":30,"hasil":42}
ok

Empat baris tersebut sesuai dengan empat curl yang dijalankan. Route / mengembalikan teks sambutan, route /salam/Citra membuktikan parameter URL terbaca dengan benar, dan route API mengembalikan JSON dengan hasil 42 dari 12 dan 30. Respons ok pada /sehat berasal dari middleware, bukan dari route mana pun, karena tidak ada route yang mendaftarkan path tersebut.

Kesalahan umum#

  • Kompilasi berhenti dengan error can't find file 'kemal'. Dependensi belum dipasang, misalnya karena folder lib/ belum ada di proyek. Jalankan shards install di folder proyek sebelum kompilasi.
  • Aplikasi berhenti segera setelah dijalankan tanpa menerima koneksi. Penyebabnya adalah Kemal.run yang terlupa atau tidak berada di akhir program, sehingga tidak ada server yang berjalan. Pastikan pemanggilan Kemal.run berada pada baris terakhir.
  • Aplikasi gagal berjalan karena port 8952 sudah digunakan. Proses app.bin sebelumnya masih hidup, misalnya karena kill %1 terlupa. Hentikan proses lama atau ubah nilai Kemal.config.port.
  • Permintaan ke route berparameter menjawab status 500. Kunci pada env.params.url tidak cocok dengan nama segmen pola route sehingga KeyError dilempar, dan Kemal mengubah exception tersebut menjadi respons 500. Samakan kunci dengan nama segmen, misalnya nama untuk pola /salam/:nama.

Ringkasan#

  • Kemal dibangun di atas HTTP::Server dan menyediakan macro get untuk mendefinisikan route beserta penanganannya.
  • Parameter URL dibaca dari env.params.url, sedangkan parameter query dari env.params.query dengan akses ["..."]? yang aman terhadap nil.
  • Respons JSON dibuat dengan to_json dari modul json setelah content_type ditetapkan ke application/json.
  • Middleware ditulis sebagai turunan Kemal::Handler, didaftarkan dengan use, dan dapat menyelesaikan permintaan tanpa memanggil call_next.

Referensi#