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:

PolaContohMenunjuk ke
Koleksi/tugasSeluruh resource tugas
Item/tugas/42Satu tugas dengan id 42
Sub-koleksi/tugas/42/komentarKomentar milik tugas 42

Agar ketiga pola tersebut konsisten, kita pegang lima aturan penamaan:

  1. Kata benda jamak untuk koleksi: /tugas, bukan /tugas-satu atau /getTugas.
  2. Identifier di path, bukan query: /tugas/42 mengenali satu item; ?status=selesai hanya menyaring koleksi.
  3. Penyarangan maksimal satu-dua tingkat: /tugas/42/komentar masih jelas; /tugas/42/komentar/7/penulis/3/alamat sulit dibaca dan sulit di-cache.
  4. Query parameter untuk menyaring, mengurutkan, dan membagi halaman, bukan untuk mengenali resource.
  5. 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:

MethodURLAksiSuksesIdempoten
GET/tugasDaftar seluruh tugas200Ya
GET/tugas/42Detail satu tugas200Ya
POST/tugasBuat tugas baru201Tidak
PUT/tugas/42Ganti seluruh isi tugas200Ya
PATCH/tugas/42Ubah sebagian field200Tidak selalu
DELETE/tugas/42Hapus tugas204Ya

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:

KodeNamaDipakai saat
200OKGET, PUT, dan PATCH berhasil, response berisi data
201CreatedPOST berhasil menciptakan resource baru
204No ContentDELETE berhasil, response tanpa body
400Bad RequestClient mengirim format yang tidak bisa dibaca (mis. body bukan JSON)
404Not FoundResource yang dituju tidak ada
409ConflictPermintaan bentrok dengan data yang sudah ada
422Unprocessable ContentFormat benar, tetapi isi tidak lolos validasi
500Internal Server ErrorKesalahan 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:

text Blueprint API Manajemen Tugas
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:

js
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:

text
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:

  1. Baris 3-10 (dua Map): Koleksi tugas dan komentar disimpan terpisah; sub-koleksi komentar dipetakan per id tugas induknya, meniru relasi “komentar milik tugas”.
  2. Baris 20-29 (rute koleksi): GET /tugas mengembalikan array dari tugas.values(), sedangkan POST /tugas menjawab 201, mengirim header Location: /tugas/3 sebagai alamat resource baru, lalu mengembalikan representasi tugas yang dibuat. Nilai Location sengaja relatif — client bisa menggabungkannya dengan host apa pun yang dipakainya.
  3. Baris 31-40 (rute item): Regular expression /^\/tugas\/(\d+)$/ menangkap id dari path. Bila tugas.get() mengembalikan undefined, server menjawab 404 dengan bentuk error seragam.
  4. 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/komentar di output. Sub-koleksi kosong adalah kondisi yang sah, bukan error.
  5. Baris 57-78 (skenario uji): Lima permintaan dijalankan berurutan mengikuti blueprint — dua pola GET, satu POST, 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.