Pada Section REST API sebelumnya, kita membangun API lengkap hanya dengan module http bawaan Node.js. Pendekatan tersebut cukup untuk aplikasi kecil karena tidak ada dependency tambahan dan seluruh perilaku server ada di depan mata kita. Namun seiring daftar endpoint bertambah, kita mulai menulis ulang kode yang sama berulang-ulang: pembandingan method dan pathname, pengumpulan potongan body, pengubahan JSON, serta penanganan error yang tersebar di banyak cabang if. Chapter ini membantu kita menimbang kapan framework web seperti Express atau Fastify mulai memberi manfaat nyata, dan kapan module http polos justru pilihan yang lebih tepat.

Bekal dari Module http #

Module http memberi kita fondasi yang lengkap namun mentah: objek req adalah Readable Stream, objek res adalah Writable Stream, dan tidak ada konsep “rute” sama sekali. Semua keputusan routing harus kita tulis sendiri di dalam satu callback createServer. Berkas berikut mengimplementasikan mini API daftar tugas dengan dua endpoint dan satu fallback 404, sepenuhnya tanpa framework.

js
import http from 'node:http';

const host = '127.0.0.1';
const tugas = [
  { id: 1, judul: 'Belajar Node.js' },
  { id: 2, judul: 'Belajar Express' },
];

function kirimJson(res, status, data) {
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
  res.end(JSON.stringify(data));
}

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${host}`);

  if (req.method === 'GET' && url.pathname === '/tugas') {
    kirimJson(res, 200, tugas);
  } else if (req.method === 'POST' && url.pathname === '/tugas') {
    const potongan = [];
    for await (const byte of req) potongan.push(byte);
    const data = JSON.parse(Buffer.concat(potongan).toString('utf8'));
    tugas.push({ id: tugas.length + 1, judul: data.judul });
    kirimJson(res, 201, tugas);
  } else {
    kirimJson(res, 404, { pesan: 'Rute tidak ditemukan' });
  }
});

server.listen(0, host, async () => {
  const base = `http://${host}:${server.address().port}`;
  const r1 = await fetch(`${base}/tugas`);
  console.log('GET /tugas :', r1.status, await r1.text());
  const r2 = await fetch(`${base}/tugas`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ judul: 'Belajar Fastify' }),
  });
  console.log('POST /tugas:', r2.status, await r2.text());
  const r3 = await fetch(`${base}/lain`);
  console.log('GET /lain :', r3.status, await r3.text());
  server.close();
  server.closeAllConnections();
});

Output yang Dihasilkan:

text
GET /tugas : 200 [{"id":1,"judul":"Belajar Node.js"},{"id":2,"judul":"Belajar Express"}]
POST /tugas: 201 [{"id":1,"judul":"Belajar Node.js"},{"id":2,"judul":"Belajar Express"},{"id":3,"judul":"Belajar Fastify"}]
GET /lain : 404 {"pesan":"Rute tidak ditemukan"}

Mari kita amati bagian server pada baris-baris kode di atas:

  1. Baris 8-11 (function kirimJson): helper buatan sendiri untuk mengisi status code, header content-type, dan body JSON. Pada framework, pekerjaan ini sudah tersedia sebagai method bawaan.
  2. Baris 15 (new URL(req.url, ...)): setiap permintaan harus diurai secara manual untuk mendapatkan pathname dan query string.
  3. Baris 17-19 (rantai if/else if): routing diimplementasikan sebagai pembandingan string. Dua kriteria — method dan path — harus diperiksa bersamaan di setiap cabang.
  4. Baris 20-22 (for await + Buffer.concat): body permintaan POST berupa stream, sehingga potongan-potongan byte harus dikumpulkan dulu, digabung, lalu di-parse menjadi JSON.
  5. Baris 25-27 (else): cabang terakhir menangani semua kombinasi method dan path yang tidak dikenali — inilah 404 yang kita rancang sendiri.

Dua endpoint saja sudah menghasilkan 28 baris kode server. Bayangkan jika endpoint bertambah menjadi 30 dengan berbagai parameter, validasi, dan format respons.

Pekerjaan yang Tidak Disediakan Module http #

Kebutuhan aplikasi nyataKondisi dengan module http polos
Routing berparameter seperti /tugas/:idPembandingan string atau regular expression manual di setiap cabang
Pembacaan body JSONKumpulkan chunk, gabungkan Buffer, panggil JSON.parse, bungkus try/catch
Middleware seperti logging dan autentikasiTidak ada mekanisme bawaan; logika lintas rute disalin atau digabung manual
Validasi inputPengecekan tipe dan ukuran field ditulis manual di setiap handler
Penanganan error terpusattry/catch di setiap cabang; format error mudah tidak konsisten
Penyajian berkas statisBaca berkas manual dan atur header content-type sesuai ekstensi

Tabel di atas bukan berarti module http kurang mampu — semua baris di kolom kanan tetap bisa ditulis dan berjalan cepat. Masalahnya muncul pada skala: kode yang sama ditulis berulang di banyak cabang, dan setiap penulis cabang bisa membuat keputusan sedikit berbeda sehingga konsistensi API menurun.

Versi yang Sama dengan Express #

Framework web mengambil alih pekerjaan repetitif tersebut dan menukarnya dengan deklarasi singkat. Bandingkan berkas berikut dengan server-polos.mjs — ketiga permintaan uji menghasilkan status dan body yang persis sama.

js
import express from 'express';

const host = '127.0.0.1';
const app = express();
app.use(express.json());

const tugas = [
  { id: 1, judul: 'Belajar Node.js' },
  { id: 2, judul: 'Belajar Express' },
];

app.get('/tugas', (req, res) => res.json(tugas));

app.post('/tugas', (req, res) => {
  tugas.push({ id: tugas.length + 1, judul: req.body.judul });
  res.status(201).json(tugas);
});

app.use((req, res) => res.status(404).json({ pesan: 'Rute tidak ditemukan' }));

const server = app.listen(0, host, async () => {
  const base = `http://${host}:${server.address().port}`;
  const r1 = await fetch(`${base}/tugas`);
  console.log('GET /tugas :', r1.status, await r1.text());
  const r2 = await fetch(`${base}/tugas`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ judul: 'Belajar Fastify' }),
  });
  console.log('POST /tugas:', r2.status, await r2.text());
  const r3 = await fetch(`${base}/lain`);
  console.log('GET /lain :', r3.status, await r3.text());
  server.close();
  server.closeAllConnections();
});

Output yang Dihasilkan:

text
GET /tugas : 200 [{"id":1,"judul":"Belajar Node.js"},{"id":2,"judul":"Belajar Express"}]
POST /tugas: 201 [{"id":1,"judul":"Belajar Node.js"},{"id":2,"judul":"Belajar Express"},{"id":3,"judul":"Belajar Fastify"}]
GET /lain : 404 {"pesan":"Rute tidak ditemukan"}

Perbedaan pentingnya ada pada 19 baris pertama:

  1. Baris 5 (app.use(express.json())): satu baris menggantikan seluruh rutinitas pengumpulan chunk dan JSON.parse; hasilnya tersedia sebagai req.body.
  2. Baris 12 dan 14 (app.get / app.post): method dan path digabung menjadi satu deklarasi rute, sehingga rantai if/else hilang.
  3. Baris 15 (req.body.judul): body sudah berupa object JavaScript, siap dipakai tanpa Buffer.concat.
  4. Baris 16 (res.status(201).json(...)): status, header, dan serialization JSON ditangani method bawaan yang bisa dirantai.
  5. Baris 19 (app.use tanpa path): middleware tanpa path menjadi penampung semua permintaan yang tidak cocok dengan rute mana pun — fallback 404 dalam satu baris.

Menimbang Keputusan #

Framework memberi manfaat ketika sebagian besar sinyal berikut mulai muncul pada proyek kita:

  • Jumlah endpoint bertambah hingga routing manual menjadi rantai if/else panjang yang sulit dipindai.
  • Kebutuhan lintas rute muncul, misalnya pencatatan log untuk setiap permintaan atau pemeriksaan token sebelum handler dijalankan.
  • Format respons dan error perlu konsisten di seluruh API agar mudah dikonsumsi klien.
  • Anggota tim lebih dari satu orang, sehingga konvensi framework membantu menyamakan gaya kode.

Sebaliknya, framework membawa biaya yang perlu kita sadari: daftar dependencies bertambah dan harus ikut diperbarui saat ada rilis keamanan, ada lapisan abstraksi yang menjauhkan kita dari perilaku mentah HTTP, serta waktu belajar awal untuk anggota tim yang belum familiar. Untuk server kecil — webhook tunggal, health check, atau alat internal satu endpoint — module http polos tetap pilihan yang jujur dan ringan.

PilihanKekuatan utamaBiaya utama
Module http polosTanpa dependency, kendali penuh, cocok untuk server kecilSemua pekerjaan repetitif ditulis sendiri
ExpressEkosistem terbesar, dokumentasi melimpah, API sederhanaPerforma menengah dibanding framework baru
FastifySkema JSON untuk validasi dan serialization cepat, performa tinggiEkosistem lebih muda, konsep plugin perlu dipelajari

Kita akan membedah Express dan Fastify satu per satu pada chapter-chapter berikutnya, lalu menutup dengan panduan memilih. Jika kita memutuskan untuk memakai framework, langkah pertama adalah memahami bentuk paling sederhana dari aplikasi Express — itulah materi Chapter Express: Dasar.

Referensi #