Back end situs Cermati dibuat dengan teknologi Node.js, di atas framework Express.js yang kadang dianggap bleeding edge. Kami menikmati kelebihan dari teknologi ini, yaitu memproses input/output secara asynchronous, sehingga Cermati dapat melayani ribuan request per menitnya. Namun ada kalanya teknologi ini memiliki keterbatasan. Fitur yang biasanya sudah disediakan framework bisa jadi belum ada. Salah satunya adalah mengenai penamaan dalam routes, yang akan dijelaskan pada tulisan ini.
Routing pada Express
Express.js mengizinkan kita mendefinisikan route dengan cara berikut:
router.get('/produk', productListController); router.get('/produk/:title', productDetailController); router.post('/produk/:title', productCreateController); router.get('/produk/:title/syarat', productRequirementController); router.get('/user', userListController); router.get('/user/:id', userDetailController);
Ketika aplikasi yang dibuat menjadi besar, maka halaman routing menjadi panjang. Cara umum yang dilakukan adalah membagi-bagi aplikasi menjadi submodul. Setiap submodul merupakan aplikasi kecil yang dapat berdiri sendiri. Submodul ini kemudian di-mount ke modul utama:
Modul utama:
router.use('/produk', productModule); router.use('/user', userModule);
Modul product:
router.get('/', listController); router.get('/:title', detailController); router.post('/:title', createController); router.get('/:title/syarat', requirementController);
Modul user:
router.get('/', listController); router.get('/:id', detailController);
Bisa saja ada subsubmodul atau subsubsubmodul, meskipun saat ini belum ada kebutuhan untuk itu. Cara ini membuat aplikasi menjadi modular dan terstruktur rapi.
Masalah
Ketika memprogram, terkadang kita bingung apakah yang benar "/produk/:title/syarat", "/produk/syarat/:title", atau "produk/persyaratan/:title"? Akhirnya kita harus melihat kembali apa definisi route-nya, lalu mengetikkannya secara hard coded. Lalu bagaimana ketika kita hendak mengganti pola URL, barangkali menjadi"/syarat-produk/:title" demi SEO? Terdengar cukup merepotkan jika ada banyak end point yang di-hard code.
Solusi
Solusi yang digunakan oleh framework lain seperti Flask dan Django adalah memberi nama pada end point. Misalnya
"product.list": "/produk" "product.detail": "/produk/:title" "product.requirement": "/produk/:title/syarat"
Dengan nama ini, URL generation dapat dilakukan dengan semacam
urlFor('product.list') // '/produk' urlFor('product.detail', {title: 'kartu-kredit'}) // '/produk/kartu-kredit' urlFor('product.requirement', {title: 'kartu-kredit'}) // '/produk/kartu-kredit/syarat'
Kini kita hanya perlu mengingat nama-namanya, seperti kita mengingat nama konstanta pada aplikasi. Mengganti pola URL juga semudah mengganti nilai konstanta, berhubung yang kita gunakan selalu namanya.
Solusi ini kemudian diadopsi ke Node.js. Diharapkan kita dapat memberi label pada definisi routing seperti:
router.get('product.list', '/produk', productListController); router.get('product.detail', '/produk/:title', productDetailController); router.post('product.create', '/produk/:title', productCreateController); router.get('product.requirement', '/produk/:title/syarat', productRequirementController); router.get('user.list', '/user', userListController); router.get('user.detail', '/user/:id', userDetailController);
Atau dalam struktur modular:
Modul utama:
mainRouter.use('product', '/produk', productRouter); mainRouter.use('user', '/user', userRouter);
Modul product:
productRouter.get('list', '/', listController); productRouter.get('title', '/:title', detailController); productRouter.post('create', '/:title', createController); productRouter.get('requirement', '/:title/syarat', requirementController);
Modul user:
userRouter.get('list', '/', listController); userRouter.get('detail', '/:id', detailController);
Implementasi
Terdapat beberapa modul NPM yang dapat memberi nama pada routing. Sayangnya modul yang sudah ada sebatas memetakan suatu nama ke suatu pola URL secara penuh. Kita tidak dapat mendefinisikannya secara sepotong-sepotong seperti yang kita lakukan pada pembagian aplikasi ke submodul. Demi menjaga struktur modular yang telah dimiliki, kami memutuskan untuk mencari solusinya.
Struktur routing sebenarnya dapat dipandang seperti tree:
Jika kita memiliki struktur tree-nya, maka setiap edge-nya dapat diberi label, dan penamaan dari suatu end point adalah gabungan label-label yang ditemui dari root sampai leaf. Masalah berikutnya adalah bagaimana mendapatkan struktur tree ini.
Kami mengamati bahwa modul-modul dan file router pada Express.js diinisialisasi dalam urutan yang mungkin tidak sesuai dengan hierarki routing yang ada. Artinya kita perlu membangun subtree untuk setiap modul secara independen, lalu menggabungkannya pada bagian akhir.
Untuk pembangunan struktur ini, kami mencatat proses "push" dan "pop" yang dilakukan untuk menelusuri subtree yang ada (traversal).
Misalnya untuk modul produk:
Untuk modul user:
Lalu untuk modul utama:
Catatan ini disimpan ke dalam Express instance yang digunakan untuk mendaftarkan routing. Hal ini dilakukan dengan melakukan wrapping pada Express instance dengan library yang akan dibuat. Nilai yang dicatat di-transfer ke pemanggilnya ketika router submodul di-mount ke modul lainnya:
mainRouter.use('product', '/produk', productRouter); mainRouter.use('user', '/user', userRouter);
Pada akhirnya, mainRouter menyimpan struktur "push" dan "pop" pada keseluruhan routing, yang sebenarnya mendeskripsikan struktur tree keseluruhan. Memproses "push" dan "pop" ini secara berurutan sama dengan melakukan pre-order tree traversal, dan kita dapat membangun route table.
Penutup
Dengan adanya route table, pekerjaan sisanya seperti URL generation menjadi mudah. Solusi ini kami tulis dalam bentuk NPM library. Rasanya library ini akan membantu rekan-rekan sesama pengguna Express.js, sehingga kami memutuskan untuk merilisnya sebagai proyek Open Source dengan nama route-label.
Selamat mencoba dan berkontribusi kepada proyek open source ini