Konsep resource dan method sudah kita pegang; sekarang saatnya merancang API yang nyata. Sepanjang Section ini kita membangun satu API manajemen tugas — API yang sama juga menjadi dasar Proyek REST API Manajemen Tugas di akhir Course. Ukuran keberhasilan sebuah perancangan endpoint adalah keterdugaan: setelah tahu bahwa koleksi tugas berada di /tugas, pembaca harus bisa menebak tanpa membaca dokumentasi bahwa detail satu tugas ada di /tugas/42. Pada Chapter ini kita menyusun aturan URL, memetakan method ke operasi CRUD, dan menyepakati konvensi status code sebelum menulis logika bisnis apa pun.
Tiga Pola URL Dasar #
Hampir semua endpoint REST tersusun dari tiga pola URL berikut:
| Pola | Contoh | Menunjuk ke |
|---|---|---|
| Koleksi | /tugas | Seluruh resource tugas |
| Item | /tugas/42 | Satu tugas dengan id 42 |
| Sub-koleksi | /tugas/42/komentar | Komentar milik tugas 42 |
Agar ketiga pola tersebut konsisten, kita pegang lima aturan penamaan:
- Kata benda jamak untuk koleksi:
/tugas, bukan/tugas-satuatau/getTugas. - Identifier di path, bukan query:
/tugas/42mengenali satu item;?status=selesaihanya menyaring koleksi. - Penyarangan maksimal satu-dua tingkat:
/tugas/42/komentarmasih jelas;/tugas/42/komentar/7/penulis/3/alamatsulit dibaca dan sulit di-cache. - Query parameter untuk menyaring, mengurutkan, dan membagi halaman, bukan untuk mengenali resource.
- Satu kata, satu bentuk: multi-kata memakai kebab-case seperti
/pesanan-reguler, dan konsisten soal garis miring penutup — pilih satu gaya lalu terapkan di seluruh API.
Memetakan Method ke Operasi CRUD #
Setiap kombinasi URL dan method punya satu tugas yang jelas. Tabel berikut menjadi peta lengkap operasi API tugas kita:
| Method | URL | Aksi | Sukses | Idempoten |
|---|---|---|---|---|
GET | /tugas | Daftar seluruh tugas | 200 | Ya |
GET | /tugas/42 | Detail satu tugas | 200 | Ya |
POST | /tugas | Buat tugas baru | 201 | Tidak |
PUT | /tugas/42 | Ganti seluruh isi tugas | 200 | Ya |
PATCH | /tugas/42 | Ubah sebagian field | 200 | Tidak selalu |
DELETE | /tugas/42 | Hapus tugas | 204 | Ya |
Kolom idempoten adalah konsep kunci pada tabel di atas. Sebuah operasi bersifat idempoten
bila dipanggil sekali atau sepuluh kali menghasilkan kondisi akhir yang sama persis di server. POST /tugas tidak idempoten — dua pemanggilan menciptakan dua tugas. DELETE /tugas/42 idempoten — pemanggilan pertama menghapus tugas, pemanggilan kedua mendapati tugas sudah tidak ada dan menjawab 404, tetapi kondisi server tidak berubah lagi. Properti ini penting saat jaringan tidak stabil: client berani mengulang permintaan idempoten tanpa takut menimbulkan efek ganda. Perlu dicatat juga bahwa safe dan idempoten tidak sama: GET keduanya, DELETE hanya idempoten karena tetap mengubah data.
Konvensi Status Code #
Setiap response membawa status code yang memberi tahu client kelompok hasilnya: 2xx sukses, 3xx pindah lokasi, 4xx kesalahan dari sisi client, 5xx kesalahan dari sisi server. Untuk API tugas kita, delapan kode berikut sudah mencukupi:
| Kode | Nama | Dipakai saat |
|---|---|---|
| 200 | OK | GET, PUT, dan PATCH berhasil, response berisi data |
| 201 | Created | POST berhasil menciptakan resource baru |
| 204 | No Content | DELETE berhasil, response tanpa body |
| 400 | Bad Request | Client mengirim format yang tidak bisa dibaca (mis. body bukan JSON) |
| 404 | Not Found | Resource yang dituju tidak ada |
| 409 | Conflict | Permintaan bentrok dengan data yang sudah ada |
| 422 | Unprocessable Content | Format benar, tetapi isi tidak lolos validasi |
| 500 | Internal Server Error | Kesalahan tak terduga di sisi server |
Pemetaan lengkap beserta bentuk error yang seragam dibahas tuntas pada Chapter Status Code dan Penanganan Error; untuk sekarang, tabel di atas cukup sebagai kesepakatan awal.
Blueprint API Manajemen Tugas #
Seluruh keputusan di atas dapat dirangkum menjadi satu blueprint yang menjadi kontrak API kita:
API Manajemen Tugas — blueprint v1
GET /tugas daftar tugas 200
POST /tugas buat tugas baru 201 (+ header Location)
GET /tugas/{id} detail satu tugas 200 / 404
PUT /tugas/{id} ganti seluruh isi 200 / 404
PATCH /tugas/{id} ubah sebagian isi 200 / 404
DELETE /tugas/{id} hapus tugas 204 / 404
GET /tugas/{id}/komentar komentar pada tugas 200 / 404
Query parameter pada GET /tugas:
halaman nomor halaman (default 1)
batas jumlah item per halaman (default 10, maksimum 100)
status saring status: belum | selesai
urut urutkan berdasarkan: id | title | -title
Bentuk error untuk semua status 4xx/5xx:
{ "error": { "kode": "KODE_ERROR", "pesan": "penjelasan singkat" } }Blueprint semacam ini biasanya menjadi halaman pertama dokumentasi API. Client — aplikasi mobile, situs web, atau sekadar skrip otomatisasi — cukup membaca blueprint untuk tahu semua yang bisa dilakukannya.
Menguji Rancangan dengan Server Nyata #
Rancangan di atas baru meyakinkan setelah dijalankan. Server berikut menghidupkan sebagian blueprint — koleksi, item, sub-koleksi, dan POST — dengan routing yang membedakan ketiga pola URL:
import http from 'node:http';
const host = '127.0.0.1';
const tugas = new Map([
[1, { id: 1, title: 'Belajar Node.js', status: 'selesai' }],
[2, { id: 2, title: 'Menulis dokumentasi API', status: 'belum' }],
]);
const komentar = new Map([
[1, [{ id: 1, tugasId: 1, isi: 'Materi routing sangat membantu' }]],
]);
function kirimJson(res, kode, data) {
res.writeHead(kode, { 'Content-Type': 'application/json' });
res.end(data === null ? '' : JSON.stringify(data));
}
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://${host}`);
let cocok = url.pathname.match(/^\/tugas$/);
if (cocok && req.method === 'GET') {
return kirimJson(res, 200, [...tugas.values()]);
}
if (cocok && req.method === 'POST') {
const baru = { id: 3, title: 'Belajar merancang endpoint', status: 'belum' };
tugas.set(baru.id, baru);
res.writeHead(201, { 'Content-Type': 'application/json', Location: '/tugas/3' });
return res.end(JSON.stringify(baru));
}
cocok = url.pathname.match(/^\/tugas\/(\d+)$/);
if (cocok && req.method === 'GET') {
const item = tugas.get(Number(cocok[1]));
if (!item) {
return kirimJson(res, 404, {
error: { kode: 'TIDAK_ADA', pesan: `Tugas ${cocok[1]} tidak ditemukan` },
});
}
return kirimJson(res, 200, item);
}
cocok = url.pathname.match(/^\/tugas\/(\d+)\/komentar$/);
if (cocok && req.method === 'GET') {
if (!tugas.has(Number(cocok[1]))) {
return kirimJson(res, 404, {
error: { kode: 'TIDAK_ADA', pesan: `Tugas ${cocok[1]} tidak ditemukan` },
});
}
return kirimJson(res, 200, komentar.get(Number(cocok[1])) ?? []);
}
return kirimJson(res, 404, {
error: { kode: 'TIDAK_ADA', pesan: 'Endpoint tidak ditemukan' },
});
});
server.listen(0, host, async () => {
const port = server.address().port;
const base = `http://${host}:${port}`;
const a = await fetch(`${base}/tugas`);
console.log(`GET /tugas -> ${a.status}`, JSON.stringify(await a.json()));
const b = await fetch(`${base}/tugas/1`);
console.log(`GET /tugas/1 -> ${b.status}`, JSON.stringify(await b.json()));
const c = await fetch(`${base}/tugas/1/komentar`);
console.log(`GET /tugas/1/komentar -> ${c.status}`, JSON.stringify(await c.json()));
const d = await fetch(`${base}/tugas`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Belajar merancang endpoint' }),
});
console.log(`POST /tugas -> ${d.status}`, JSON.stringify(await d.json()));
const e = await fetch(`${base}/tugas/2/komentar`);
console.log(`GET /tugas/2/komentar -> ${e.status}`, JSON.stringify(await e.json()));
server.close();
server.closeAllConnections();
});Output yang Dihasilkan:
GET /tugas -> 200 [{"id":1,"title":"Belajar Node.js","status":"selesai"},{"id":2,"title":"Menulis dokumentasi API","status":"belum"}]
GET /tugas/1 -> 200 {"id":1,"title":"Belajar Node.js","status":"selesai"}
GET /tugas/1/komentar -> 200 [{"id":1,"tugasId":1,"isi":"Materi routing sangat membantu"}]
POST /tugas -> 201 {"id":3,"title":"Belajar merancang endpoint","status":"belum"}
GET /tugas/2/komentar -> 200 []Mari kita telaah bagian-bagian kuncinya:
- Baris 3-10 (dua
Map): Koleksi tugas dan komentar disimpan terpisah; sub-koleksi komentar dipetakan per id tugas induknya, meniru relasi “komentar milik tugas”. - Baris 20-29 (rute koleksi):
GET /tugasmengembalikan array daritugas.values(), sedangkanPOST /tugasmenjawab 201, mengirim headerLocation: /tugas/3sebagai alamat resource baru, lalu mengembalikan representasi tugas yang dibuat. NilaiLocationsengaja relatif — client bisa menggabungkannya dengan host apa pun yang dipakainya. - Baris 31-40 (rute item): Regular expression
/^\/tugas\/(\d+)$/menangkap id dari path. Bilatugas.get()mengembalikanundefined, server menjawab 404 dengan bentuk error seragam. - Baris 42-49 (rute sub-koleksi): Sebelum mengembalikan komentar, kode memastikan tugas induknya ada — komentar milik tugas yang tidak ada adalah 404, bukan array kosong. Sebaliknya, tugas yang ada tetapi belum punya komentar dijawab 200 dengan array kosong, seperti terlihat pada
GET /tugas/2/komentardi output. Sub-koleksi kosong adalah kondisi yang sah, bukan error. - Baris 57-78 (skenario uji): Lima permintaan dijalankan berurutan mengikuti blueprint — dua pola
GET, satuPOST, lalu sub-koleksi — dan setiap baris output mencantumkan method, path, status, serta body agar pola URL dan status code-nya mudah dibandingkan.
Rancangan sudah teruji di atas meja gambar; tugas kita berikutnya membuatnya benar-benar menyimpan dan mengubah data. Chapter CRUD dengan Penyimpanan di Memori membangun keempat operasi tersebut secara utuh dengan Map sebagai penyimpanan.