Siti sedang merapikan daftar tugas toko buku: menambah tugas baru, mengganti isi tugas yang lama, memperbaiki satu field saja, lalu menghapus yang sudah selesai. Setiap aksi itu punya padanan kata kerja HTTP — dan memilih kata kerja yang tepat membuat maksud permintaan terbaca jelas oleh server, rekan tim, dan tooling. Chapter ini menuntaskan kelima metode dengan satu skenario daftar tugas yang sama, plus konsep idempotensi yang menentukan aman atau tidaknya sebuah permintaan diulang.

Contoh kodenya kembali memakai pola server tiruan tertanam: server kecil node:http dengan “database” berupa array di memori, lengkap dengan lima rute metode. Jalankan node crud-tugas.mjs dan keluarannya akan persis seperti yang tercetak di bawah.

Lima Metode dan Tugasnya #

Konvensi industri membagi peran kelima metode dengan tegas:

MetodeTugas lazimRespons sukses yang umum
GETMembaca data, tidak mengubah apa pun200 OK
POSTMembuat sumber daya baru201 Created
PUTMengganti sumber daya secara utuh200 OK
PATCHMemperbarui sebagian field200 OK
DELETEMenghapus sumber daya204 No Content

Kaitannya dengan idempotensi — sifat “dijalankan berkali-kali, hasilnya sama seperti sekali”:

  • GET bersifat safe: membaca saja, tidak mengubah data. GET, PUT, dan DELETE idempoten — diulang tiga kali pun keadaan akhirnya sama.
  • POST tidak idempoten: mengirim POST /tugas tiga kali biasanya membuat tiga data baru.
  • PATCH idempotensinya bergantung implementasi; penggantian nilai tetap (selesai: true) idempoten, operasi relatif (tambah 1) tidak.

Sifat ini bukan teori semata: mesin retry (chapter Retry dan Timeout Strategies) hanya berani mengulang permintaan yang aman diulang. Atauannya sederhana namun tegas — GET, PUT, dan DELETE boleh dicoba lagi begitu saja, sedangkan POST menuntut kehati-hatian tambahan sebelum pengulangan dilakukan.

Satu Berkas, Lima Metode #

Berkas berikut menghidupkan seluruh skenario Siti — dari POST hingga GET pengecekan akhir. Bagian server mengurus lima rute; bagian client menjalankan aksinya berurutan:

// crud-tugas.mjs
// Lima metode HTTP terhadap daftar tugas: GET, POST, PUT, PATCH, DELETE.
// Jalankan di terminal: node crud-tugas.mjs (tanpa internet).

import http from "node:http";

// "Database" tiruan di memori server.
let tugas = [
  { id: 1, judul: "Rapikan rak buku", selesai: false },
  { id: 2, judul: "Cetak label harga", selesai: false },
];
let idBerikut = 3;

// Helper: mengumpulkan body permintaan sampai selesai, lalu menguraikan JSON-nya.
const bacaBody = (req) =>
  new Promise((resolve) => {
    let raw = "";
    req.on("data", (potongan) => (raw += potongan));
    req.on("end", () => resolve(JSON.parse(raw)));
  });

const server = http.createServer(async (req, res) => {
  const kirim = (status, isi) => {
    res.writeHead(status, { "Content-Type": "application/json" });
    res.end(isi === undefined ? "" : JSON.stringify(isi));
  };

  if (req.method === "GET" && req.url === "/tugas") return kirim(200, tugas);

  if (req.method === "POST" && req.url === "/tugas") {
    const baru = { id: idBerikut++, ...(await bacaBody(req)) };
    tugas.push(baru);
    return kirim(201, baru);
  }

  const id = Number(req.url.split("/")[2]);
  const satu = tugas.find((t) => t.id === id);

  if (req.method === "PUT" && satu) {
    const body = await bacaBody(req);
    tugas = tugas.map((t) => (t.id === id ? { ...body, id } : t));
    return kirim(200, tugas.find((t) => t.id === id));
  }
  if (req.method === "PATCH" && satu) {
    Object.assign(satu, await bacaBody(req));
    return kirim(200, satu);
  }
  if (req.method === "DELETE" && satu) {
    tugas = tugas.filter((t) => t.id !== id);
    return kirim(204);
  }
  kirim(404, { pesan: "rute tidak cocok" });
});

server.listen(0, "127.0.0.1", async () => {
  const port = server.address().port;
  const dasar = `http://127.0.0.1:${port}`;

  // POST: menambah data baru.
  const buat = await fetch(`${dasar}/tugas`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ judul: "Hitung kas harian", selesai: false }),
  });
  const baru = await buat.json();
  console.log("POST  ->", buat.status, "| id baru:", baru.id);

  // PUT: mengganti data utuh (field lama yang tidak dikirim ikut hilang).
  const ganti = await fetch(`${dasar}/tugas/2`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ judul: "Cetak label harga baru", selesai: false }),
  });
  const utuh = await ganti.json();
  console.log("PUT   ->", ganti.status, "| field sekarang:", Object.keys(utuh).join(", "));

  // PATCH: memperbarui sebagian field saja.
  const ubah = await fetch(`${dasar}/tugas/2`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ selesai: true }),
  });
  const sebagian = await ubah.json();
  console.log("PATCH ->", ubah.status, "|", sebagian.judul, "selesai:", sebagian.selesai);

  // DELETE: menghapus; respons 204 tanpa body.
  const hapus = await fetch(`${dasar}/tugas/1`, { method: "DELETE" });
  console.log("DELETE->", hapus.status, "| isi body:", JSON.stringify(await hapus.text()));

  // GET: memastikan keadaan akhir daftar.
  const daftar = await (await fetch(`${dasar}/tugas`)).json();
  console.log("GET   ->", daftar.map((t) => `${t.id}:${t.judul}${t.selesai ? " (selesai)" : ""}`).join(" | "));

  server.closeAllConnections();
  server.close();
});

Output yang Dihasilkan:

text
POST  -> 201 | id baru: 3
PUT   -> 200 | field sekarang: judul, selesai, id
PATCH -> 200 | Cetak label harga baru selesai: true
DELETE-> 204 | isi body: ""
GET   -> 2:Cetak label harga baru (selesai) | 3:Hitung kas harian

Mari kita telaah baris-baris pentingnya:

  1. Baris 31 ({ id: idBerikut++, ...(await bacaBody(req)) }): server POST menomori data baru sendiri, lalu menyebarkan isi body di belakangnya. Server yang menentukan id — bukan client — karena server pemegang sumber kebenaran.
  2. Baris 39–42 (rute PUT): data lama diganti seluruhnya oleh { ...body, id }. Bukti terlihat pada keluaran PUT: field yang tersisa hanya judul, selesai, id — isi baru memang tidak memuat apa pun lagi selain itu.
  3. Baris 44–46 (rute PATCH): Object.assign menimpa hanya field yang dikirim. Setelah PATCH, tugas nomor 2 masih bernama “Cetak label harga baru” (warisan PUT) tetapi selesai berubah menjadi true.
  4. Baris 48–50 (rute DELETE): item disaring keluar dari array dan server menjawab 204 No Content. Keluaran DELETE memperlihatkan konsekuensinya: body respons benar-benar string kosong, sehingga kita membacanya dengan text(), bukan json().
  5. Baris 60–64 (POST di sisi client): options fetch berisi method: "POST", header Content-Type: application/json, dan body hasil JSON.stringify — tiga serangkai yang berulang untuk PUT dan PATCH.
  6. Baris 87 (fetch(..., { method: "DELETE" })): permintaan hapus tanpa options tambahan; URL /tugas/1 sudah menyebut sumber daya yang dimaksud.
  7. Baris 91–92 (verifikasi GET): daftar akhir tinggal dua tugas — nomor 1 sudah terhapus, nomor 2 telah berganti judul dan ditandai selesai, nomor 3 adalah buah POST.

Menyusun URL Sumber Daya #

Metode menyatakan aksi; URL menyatakan sumber dayanya. Konvensi REST yang dominan mengatur keduanya menjadi pola yang konsisten:

  • URL koleksi memakai kata jamak: /tugas untuk seluruh daftar.
  • URL item menyertakan identitas: /tugas/2 untuk satu tugas.
  • Aksi mengikuti metode, bukan kata kerja di URL: menghapus tugas 1 ditulis DELETE /tugas/1 — bukan GET /hapusTugas?id=1.

Pola ini yang dipakai berkas contoh: GET /tugas membaca daftar, sedangkan PUT /tugas/2, PATCH /tugas/2, dan DELETE /tugas/1 berbicara ke satu item. Bila aksi tidak masuk akal sebagai pengubahan data — “tandai semua selesai”, misalnya — banyak API menambahkan sub-sumber daya seperti PATCH /tugas/tandai-semua atau mengizinkan PATCH pada koleksi. Penyaringan data tetap tugas query parameter (/buku?genre=sejarah&page=2) — pola yang menjadi pondasi chapter Pagination.

Memilih antara PUT dan PATCH #

Keduanya “mengubah data”, tetapi kontraknya berbeda:

  • PUT menggantikan seluruh isi. Client bertanggung jawab mengirim versi lengkap; field yang terlewat ikut hilang dari data. Cocok ketika client memang memegang bentuk utuh data, misalnya formulir sunting penuh.
  • PATCH menambal sebagian. Client hanya mengirim field yang berubah; field lain tak tersentuh. Cocok untuk aksi ringkas seperti menandai selesai atau mengubah satu kolom.

Kesalahan yang lumayan sering terjadi: memakai PUT dengan body setengah jalan, lalu bingung karena field lain hilang. Anggap PUT seperti mengganti selembar kertas dengan lembar baru, dan PATCH seperti menyunting satu baris di lembar lama.

Metode dan Status yang Cocok #

Pasangan metode–status yang lazim layak dihafal karena muncul di hampir semua API: GET → 200, POST → 201, perubahan data sukses → 200, DELETE → 204. Beberapa API menambahkan Location pada respons 201 yang berisi URL sumber daya baru, dan sebagian menjawab PUT/PATCH dengan 204 ketika tidak ada data balasan yang layak dikirim. Bila metode dikirim tanpa rute yang cocok, server menjawab 405 Method Not Allowed — satu lagi anggota keluarga 4xx yang akan sering kita jumpai di chapter Response Status.

Dengan lima metode ini, hampir semua aksi CRUD sudah terwakili. Selanjutnya kita perlu fasih membaca kode status pada respons — termasuk memahami mengapa fetch tidak menganggap 404 sebagai galat.