Server CRUD pada Chapter sebelumnya sudah bekerja, tetapi penanganan kesalahannya masih setengah jalan: body yang bukan JSON membuat JSON.parse gagal dan permintaan menggantung tanpa jawaban. Padahal bagi client, kegagalan adalah informasi sama pentingnya dengan keberhasilan — aplikasi mobile perlu tahu apakah harus memperbaiki input, mengulang permintaan, atau sekadar menampilkan pesan. REST menyediakan dua saluran untuk itu: status code sebagai ringkasan yang bisa dibaca mesin, dan body error sebagai penjelasan yang bisa dibaca manusia. Pada Chapter ini kita memakai keduanya secara disiplin.
Membaca Keluarga Status Code #
Lima keluarga status code membagi tanggung jawab kesalahan dengan tegas:
| Keluarga | Arti | Siapa yang memperbaiki |
|---|---|---|
| 1xx | Informasi proses (jarang dipakai aplikasi biasa) | — |
| 2xx | Permintaan berhasil | — |
| 3xx | Resource pindah atau tersedia di tempat lain | Client mengikuti pengalihan |
| 4xx | Permintaan dari client tidak bisa diproses | Client memperbaiki permintaannya |
| 5xx | Server gagal memproses permintaan yang sehat | Pengelola server memperbaiki kodenya |
Pembagian pada kolom terakhir adalah kunci berpikir kita: 4xx berarti client yang keliru, 5xx berarti server yang keliru. Ketika sebuah aplikasi menerima 429 (terlalu banyak permintaan), aplikasi bisa memperlambat laju pemanggilan; ketika menerima 500, mengulang permintaan yang sama biasanya hanya menghasilkan 500 lagi. Status code yang tepat membuat client bisa bereaksi otomatis tanpa harus mengurai teks.
Memilih Kode yang Tepat #
Dari daftar status code HTTP , delapan kode berikut paling sering kita perlukan:
| Kode | Nama | Dipakai saat |
|---|---|---|
| 400 | Bad Request | Permintaan tidak bisa dibaca sama sekali, misalnya body bukan JSON valid |
| 401 | Unauthorized | Client tidak menyertakan kredensial, atau kredensialnya salah |
| 403 | Forbidden | Client terautentikasi tetapi tidak berhak mengakses resource |
| 404 | Not Found | Resource yang dituju tidak ada |
| 405 | Method Not Allowed | Method tidak didukung pada URL tersebut |
| 409 | Conflict | Permintaan bentrok dengan data yang sudah ada, misalnya id duplikat |
| 422 | Unprocessable Content | Format permintaan benar, tetapi isinya tidak lolos validasi |
| 500 | Internal Server Error | Kesalahan tak terduga di sisi server |
Godaan terbesar pemula adalah menjawab semua kegagalan dengan 400 — padahal 400, 404, dan 422 menceritakan tiga masalah yang berbeda. Kode yang presisi menghemat waktu penelusuran: cukup lihat log, dan kita langsung tahu sisi mana yang harus diperiksa.
Bentuk Error yang Seragam #
Status code memberi tahu kelompok masalah; body error memberi tahu detailnya. Agar client bisa mengurai detail tersebut tanpa rumit, semua error kita memakai satu bentuk:
{
"error": {
"kode": "TIDAK_ADA",
"pesan": "Tugas 999 tidak ditemukan"
}
}Field kode bersifat stabil dan dibaca mesin — client boleh menulis percabangan seperti “bila JSON_TIDAK_VALID, tampilkan form input ulang”. Field pesan bersifat bebas dan dibaca manusia — cocok untuk ditampilkan di antarmuka atau dicatat ke log. Nama field boleh berbeda antar API (banyak API memakai code dan message); yang terpenting adalah satu API memakai satu bentuk untuk semua error, tanpa kecuali.
Penanganan Terpusat dengan try/catch #
Strategi penanganan kita berlapis dua. Lapis pertama ada di dalam handler: kesalahan yang diharapkan — resource tiada, JSON rusak — ditangani dan dijawab dengan kode 4xx yang tepat. Lapis kedua menyelimuti seluruh handler dengan try/catch: kesalahan tak terduga (bug) ditangkap, dicatat ke log server, lalu dijawab 500 dengan pesan umum tanpa membocorkan detail internal. Demo berikut memperlihatkan ketiganya sekaligus — termasuk satu bug yang sengaja ditanam di handler PUT:
import http from 'node:http';
const host = '127.0.0.1';
const tugas = new Map([
[1, { id: 1, title: 'Belajar Node.js', status: 'selesai' }],
]);
let idBerikutnya = 2;
function kirimJson(res, kode, data) {
res.writeHead(kode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
function kirimError(res, kode, kodeError, pesan) {
kirimJson(res, kode, { error: { kode: kodeError, pesan } });
}
async function bacaBodyJson(req) {
const potongan = [];
for await (const data of req) potongan.push(data);
const teks = Buffer.concat(potongan).toString('utf8');
return JSON.parse(teks);
}
const server = http.createServer(async (req, res) => {
try {
await tangani(req, res);
} catch (err) {
console.error('[kesalahan internal]', err.message);
if (!res.headersSent) {
kirimError(res, 500, 'KESALAHAN_INTERNAL', 'Terjadi kesalahan tak terduga di sisi server');
} else {
res.end();
}
}
});
async function tangani(req, res) {
const url = new URL(req.url, `http://${host}`);
const cocok = url.pathname.match(/^\/tugas\/(\d+)$/);
if (cocok && req.method === 'GET') {
const item = tugas.get(Number(cocok[1]));
if (!item) {
return kirimError(res, 404, 'TIDAK_ADA', `Tugas ${cocok[1]} tidak ditemukan`);
}
return kirimJson(res, 200, item);
}
if (cocok && req.method === 'PUT') {
const body = await bacaBodyJson(req);
const lama = tugas.get(Number(cocok[1]));
// Bug: lupa memeriksa keberadaan tugas sebelum membaca field-nya
const diganti = { id: Number(cocok[1]), title: body.title, status: lama.status };
tugas.set(diganti.id, diganti);
return kirimJson(res, 200, diganti);
}
if (!cocok && req.method === 'POST' && url.pathname === '/tugas') {
let body;
try {
body = await bacaBodyJson(req);
} catch {
return kirimError(res, 400, 'JSON_TIDAK_VALID', 'Body bukan JSON yang valid');
}
const id = idBerikutnya++;
const baru = { id, title: body.title, status: body.status ?? 'belum' };
tugas.set(id, baru);
res.writeHead(201, { 'Content-Type': 'application/json', Location: `/tugas/${id}` });
return res.end(JSON.stringify(baru));
}
return kirimError(res, 404, 'TIDAK_ADA', 'Endpoint tidak ditemukan');
}
server.listen(0, host, async () => {
const port = server.address().port;
const base = `http://${host}:${port}`;
const a = await fetch(`${base}/tugas/999`);
console.log(`GET /tugas/999 -> ${a.status}`, JSON.stringify(await a.json()));
const b = await fetch(`${base}/tugas`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{bad json',
});
console.log(`POST /tugas -> ${b.status}`, JSON.stringify(await b.json()));
const c = await fetch(`${base}/tugas/999`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Perbaiki bug ini' }),
});
console.log(`PUT /tugas/999 -> ${c.status}`, JSON.stringify(await c.json()));
server.close();
server.closeAllConnections();
});Output yang Dihasilkan:
GET /tugas/999 -> 404 {"error":{"kode":"TIDAK_ADA","pesan":"Tugas 999 tidak ditemukan"}}
POST /tugas -> 400 {"error":{"kode":"JSON_TIDAK_VALID","pesan":"Body bukan JSON yang valid"}}
PUT /tugas/999 -> 500 {"error":{"kode":"KESALAHAN_INTERNAL","pesan":"Terjadi kesalahan tak terduga di sisi server"}}
[kesalahan internal] Cannot read properties of undefined (reading 'status')Tiga baris pertama output berasal dari client; baris terakhir berasal dari console.error di sisi server yang mengalir lewat stderr — urutannya stabil karena stdout dan stderr ditangkap terpisah lalu digabung. Mari kita bedah:
- Baris 9-16 (
kirimJsondankirimError): Bentuk error seragam didefinisikan sekali dikirimError; setiap handler cukup menyebut kode status, kode error, dan pesan. Tidak ada lagi tempat lain yang bisa membuat bentuk error menyimpang. - Baris 25-36 (pembungkus
try/catch): Seluruh pekerjaan didelegasikan ke fungsitangani. Bilatanganimelempar error, pembungkus mencataterr.messageke log server, lalu mengirim 500 dengan pesan umum. Pemeriksaanres.headersSentmenjaga agar header tidak dikirim dua kali bila kegagalan terjadi di tengah pengiriman response. - Baris 41-48 (GET item): Kesalahan yang diharapkan — tugas tidak ada — dijawab langsung 404 di dalam handler. Inilah lapis pertama.
- Baris 50-57 (PUT dengan bug): Kode membaca
lama.statustanpa memeriksalamaterlebih dahulu. Untuk id yang tidak ada,tugas.get()mengembalikanundefined, ekspresiundefined.statusmelemparTypeError, dan pembungkus mengubahnya menjadi 500 — persis perilaku bug yang tidak tertangani di production. - Baris 59-71 (POST dengan
try/catchlokal):JSON.parseyang gagal ditangkap di dekat sumbernya dan dijawab 400 dengan kodeJSON_TIDAK_VALID. Bandingkan dengan Chapter sebelumnya: permintaan yang sama dahulu menggantung tanpa jawaban. - Baris 80-95 (skenario client): Tiga permintaan sengaja memancing tiga jenis kegagalan — 404 karena resource tiada (salah client), 400 karena format rusak (salah client), dan 500 karena bug (salah server). Client hanya menerima pesan umum untuk kasus terakhir, sementara baris
[kesalahan internal]di log server menyimpan detailCannot read properties of undefined (reading 'status')untuk kita telusuri.
Dengan status code yang presisi dan bentuk error yang seragam, satu jenis kegagalan masih sering kita jumpai: client mengirim JSON yang valid secara format tetapi salah secara isi — judul kosong, status yang tidak dikenal. Chapter Validasi Input menuntaskan lapisan terakhir ini dengan aturan validasi dan status 422.