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.
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.
# pasang dependensi proyek
shards installI: 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.lockAwalan 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.
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.runBaris 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.
# tidak-dijalankan
get "/salam/:nama" do |env|
"Halo, #{env.params.url["nama"]}!"
endParameter 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.
# 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
endIsi 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.
# 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.newKompilasi 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.
# kompilasi aplikasi beserta seluruh dependensinya
crystal build -o app.bin app.crJalankan 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.
# 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 %1Selamat datang di aplikasi Kemal
Halo, Citra!
{"a":12,"b":30,"hasil":42}
okEmpat 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. Jalankanshards installdi folder proyek sebelum kompilasi. - Aplikasi berhenti segera setelah dijalankan tanpa menerima koneksi. Penyebabnya adalah
Kemal.runyang terlupa atau tidak berada di akhir program, sehingga tidak ada server yang berjalan. Pastikan pemanggilanKemal.runberada pada baris terakhir. - Aplikasi gagal berjalan karena port 8952 sudah digunakan. Proses app.bin sebelumnya masih hidup, misalnya karena
kill %1terlupa. Hentikan proses lama atau ubah nilaiKemal.config.port. - Permintaan ke route berparameter menjawab status 500. Kunci pada
env.params.urltidak cocok dengan nama segmen pola route sehinggaKeyErrordilempar, dan Kemal mengubah exception tersebut menjadi respons 500. Samakan kunci dengan nama segmen, misalnyanamauntuk pola/salam/:nama.
Ringkasan#
- Kemal dibangun di atas HTTP::Server dan menyediakan macro
getuntuk mendefinisikan route beserta penanganannya. - Parameter URL dibaca dari
env.params.url, sedangkan parameter query darienv.params.querydengan akses["..."]?yang aman terhadapnil. - Respons JSON dibuat dengan
to_jsondari modul json setelahcontent_typeditetapkan keapplication/json. - Middleware ditulis sebagai turunan
Kemal::Handler, didaftarkan denganuse, dan dapat menyelesaikan permintaan tanpa memanggilcall_next.
Referensi#
- Kemal — kemalcr — Dokumentasi resmi Kemal yang mencakup panduan memulai, routing, middleware, dan penanganan error.
- Repositori kemalcr/kemal — GitHub — Kode sumber Kemal beserta rilis versinya, termasuk versi 1.15.0 yang dipasang shards pada topik ini.