Tips & Tricks (Diperbarui: 3/6/2026)

Membuat REST API Pertama dengan Claude Code: CRUD, Validasi, dan Tes

Buat REST API Express pertama dengan Claude Code: CRUD, validasi, desain error, pengujian, dan kode siap jalan.

Membuat REST API Pertama dengan Claude Code: CRUD, Validasi, dan Tes

Saat membuat REST API pertama, bagian tersulit biasanya bukan menulis route Express. Yang lebih sulit adalah memahami apa saja yang membuat API layak dipakai: URL yang jelas, method HTTP yang tepat, validasi input, respons error yang konsisten, dan tes kecil yang membuktikan kode berjalan.

Di panduan ini, kita akan memakai Claude Code untuk membuat Todo API kecil dengan Express. Kita akan membuat proyek, menambahkan endpoint CRUD, memvalidasi JSON, mengembalikan status code yang tepat, lalu menguji API dengan curl dan test runner bawaan Node.js. Kode ini ditujukan untuk Node.js 22/24 LTS atau versi yang lebih baru.

Jika Claude Code masih baru untuk Anda, mulai dari panduan awal Claude Code. Untuk tes API yang lebih dalam, lanjutkan ke Claude Code API testing dan validasi Zod dengan Claude Code.

Istilah yang perlu dipahami

REST API adalah gaya desain untuk mengelola resource melalui HTTP. Di artikel ini, resource-nya adalah todo. Secara sederhana, aplikasi lain memanggil URL, mengirim atau menerima JSON, lalu membaca status code untuk mengetahui hasilnya.

IstilahArti sederhanaContoh
EndpointURL pintu masuk APIGET /todos
MethodKata kerja HTTP untuk aksiGET, POST, PUT, DELETE
Status codeAngka yang menjelaskan hasil200, 201, 400, 404
JSONFormat teks ringan untuk data{ "title": "Learn REST" }
ValidasiMengecek apakah input boleh diterimaMenolak title kosong
IdempotencyRequest yang sama diulang tetap memberi state akhir yang samaMengirim PUT yang sama dua kali

Referensi resmi yang berguna: HTTP request methods di MDN, HTTP status codes di MDN, Express routing, Express error handling, dan Node.js test runner.

API yang akan dibuat

API ini menyimpan Todo di memori. Saat server di-restart, datanya kembali ke awal. Ini sengaja dibuat sederhana agar pemula fokus pada bentuk REST sebelum menambahkan database.

AksiMethod dan URLStatus sukses
Health checkGET /health200 OK
Daftar TodoGET /todos200 OK
Baca satu TodoGET /todos/:id200 OK
Buat TodoPOST /todos201 Created
Ganti TodoPUT /todos/:id200 OK
Hapus TodoDELETE /todos/:id204 No Content

Contoh kecil ini tetap dekat dengan pekerjaan nyata. Kasus pertama adalah API tugas internal untuk review, tiket, atau permintaan operasional. Kasus kedua adalah mock backend untuk mengembangkan UI React, Vue, atau Astro sebelum backend asli siap. Kasus ketiga adalah API kecil untuk kontak atau newsletter, dengan mengganti title menjadi email dan message.

Beri Claude Code kriteria selesai

Tuliskan stack, endpoint, perilaku error, dan cara verifikasi. Prompt seperti “buat API” sering menghasilkan kode yang bisa jalan, tetapi sulit direview desainnya.

Create a beginner Todo REST API with Express 5 and Node.js ES Modules.

Requirements:
- Provide package.json and server.js
- Add GET /health, GET /todos, GET /todos/:id, POST /todos, PUT /todos/:id, DELETE /todos/:id
- Accept JSON input
- title must be 1 to 120 characters; completed must be boolean
- Return JSON errors for 400, 404, and 500
- Keep PUT idempotent: repeating the same request should not change the final resource state
- Include curl requests and a node:test example

Baris idempotency penting. PUT berarti mengganti resource. Jika body yang sama dikirim dua kali, Todo harus berakhir di state yang sama. Jika updatedAt berubah setiap kali walaupun datanya sama, kode tidak lagi cocok dengan penjelasan REST.

Membuat proyek

mkdir claude-rest-api
cd claude-rest-api
npm init -y
npm install express

Ganti package.json:

{
  "name": "claude-rest-api",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "node --watch server.js",
    "start": "node server.js",
    "test": "node --test"
  },
  "dependencies": {
    "express": "^5.0.0"
  }
}

server.js

import express from "express";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";

const PORT = Number(process.env.PORT ?? 3000);

function createHttpError(status, message, details) {
  const error = new Error(message);
  error.status = status;
  error.details = details;
  return error;
}

function hasOwn(object, key) {
  return Object.prototype.hasOwnProperty.call(object, key);
}

function parseId(rawId) {
  const id = Number(rawId);
  return Number.isInteger(id) && id > 0 ? id : null;
}

function validateTodoInput(body, { partial = false } = {}) {
  const errors = [];

  if (!partial || hasOwn(body, "title")) {
    if (typeof body.title !== "string" || body.title.trim().length === 0) {
      errors.push({ field: "title", message: "title is required" });
    } else if (body.title.trim().length > 120) {
      errors.push({ field: "title", message: "title must be 120 characters or fewer" });
    }
  }

  if (!partial || hasOwn(body, "completed")) {
    if (typeof body.completed !== "boolean") {
      errors.push({ field: "completed", message: "completed must be a boolean" });
    }
  }

  return errors;
}

export function createApp() {
  const app = express();

  let nextId = 3;
  const todos = [
    {
      id: 1,
      title: "Read MDN HTTP status docs",
      completed: false,
      updatedAt: "2026-06-03T00:00:00.000Z"
    },
    {
      id: 2,
      title: "Ask Claude Code to review API errors",
      completed: true,
      updatedAt: "2026-06-03T00:00:00.000Z"
    }
  ];

  app.use(express.json({ limit: "32kb" }));

  app.get("/health", (req, res) => {
    res.json({ status: "ok", uptime: process.uptime() });
  });

  app.get("/todos", (req, res) => {
    res.json({ data: todos, count: todos.length });
  });

  app.get("/todos/:id", (req, res, next) => {
    const id = parseId(req.params.id);
    if (!id) return next(createHttpError(400, "id must be a positive integer"));

    const todo = todos.find((item) => item.id === id);
    if (!todo) return next(createHttpError(404, "todo not found"));

    res.json({ data: todo });
  });

  app.post("/todos", (req, res, next) => {
    const errors = validateTodoInput(req.body);
    if (errors.length > 0) {
      return next(createHttpError(400, "invalid request body", errors));
    }

    const todo = {
      id: nextId,
      title: req.body.title.trim(),
      completed: req.body.completed,
      updatedAt: new Date().toISOString()
    };
    nextId += 1;
    todos.push(todo);

    res.status(201).location(`/todos/${todo.id}`).json({ data: todo });
  });

  app.put("/todos/:id", (req, res, next) => {
    const id = parseId(req.params.id);
    if (!id) return next(createHttpError(400, "id must be a positive integer"));

    const errors = validateTodoInput(req.body);
    if (errors.length > 0) {
      return next(createHttpError(400, "invalid request body", errors));
    }

    const index = todos.findIndex((item) => item.id === id);
    if (index === -1) return next(createHttpError(404, "todo not found"));

    const previous = todos[index];
    const nextTodo = {
      ...previous,
      title: req.body.title.trim(),
      completed: req.body.completed
    };

    const changed =
      previous.title !== nextTodo.title || previous.completed !== nextTodo.completed;

    todos[index] = {
      ...nextTodo,
      updatedAt: changed ? new Date().toISOString() : previous.updatedAt
    };

    res.json({ data: todos[index] });
  });

  app.delete("/todos/:id", (req, res, next) => {
    const id = parseId(req.params.id);
    if (!id) return next(createHttpError(400, "id must be a positive integer"));

    const index = todos.findIndex((item) => item.id === id);
    if (index === -1) return next(createHttpError(404, "todo not found"));

    todos.splice(index, 1);
    res.status(204).send();
  });

  app.use((req, res, next) => {
    next(createHttpError(404, `route not found: ${req.method} ${req.originalUrl}`));
  });

  app.use((err, req, res, next) => {
    const status = Number.isInteger(err.status) && err.status >= 400 ? err.status : 500;
    const body = {
      error: {
        status,
        message: status === 500 ? "Internal Server Error" : err.message
      }
    };

    if (err.details) {
      body.error.details = err.details;
    }

    res.status(status).json(body);
  });

  return app;
}

const currentFile = fileURLToPath(import.meta.url);
const startedFile = process.argv[1] ? resolve(process.argv[1]) : "";

if (startedFile === currentFile) {
  createApp().listen(PORT, () => {
    console.log(`REST API listening on http://localhost:${PORT}`);
  });
}

Yang penting bukan hanya route tersedia. API ini memvalidasi input, menyatukan bentuk error JSON, dan memakai status code sesuai konteks.

Menguji dengan curl

npm run dev
curl -i http://localhost:3000/health

curl -i http://localhost:3000/todos

curl -i -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Write API tests","completed":false}'

curl -i -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"","completed":false}'

curl -i -X PUT http://localhost:3000/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Read MDN HTTP status docs","completed":true}'

curl -i -X DELETE http://localhost:3000/todos/2

Periksa hasil sukses dan gagal. API yang hanya berjalan pada jalur ideal belum cukup untuk frontend nyata.

Menambahkan node:test

Buat server.test.js:

import assert from "node:assert/strict";
import test from "node:test";
import { createApp } from "./server.js";

function listen(app) {
  return new Promise((resolve) => {
    const server = app.listen(0, () => resolve(server));
  });
}

async function request(baseUrl, path, options = {}) {
  const response = await fetch(`${baseUrl}${path}`, {
    headers: { "Content-Type": "application/json", ...options.headers },
    ...options
  });
  const text = await response.text();
  return {
    response,
    body: text ? JSON.parse(text) : null
  };
}

test("creates, updates, and deletes a todo", async (t) => {
  const server = await listen(createApp());
  t.after(() => server.close());
  const baseUrl = `http://127.0.0.1:${server.address().port}`;

  const created = await request(baseUrl, "/todos", {
    method: "POST",
    body: JSON.stringify({ title: "Test the API", completed: false })
  });
  assert.equal(created.response.status, 201);
  assert.equal(created.body.data.title, "Test the API");

  const id = created.body.data.id;
  const updated = await request(baseUrl, `/todos/${id}`, {
    method: "PUT",
    body: JSON.stringify({ title: "Test the API", completed: true })
  });
  assert.equal(updated.response.status, 200);
  assert.equal(updated.body.data.completed, true);

  const deleted = await request(baseUrl, `/todos/${id}`, { method: "DELETE" });
  assert.equal(deleted.response.status, 204);
});

test("rejects invalid todo input", async (t) => {
  const server = await listen(createApp());
  t.after(() => server.close());
  const baseUrl = `http://127.0.0.1:${server.address().port}`;

  const result = await request(baseUrl, "/todos", {
    method: "POST",
    body: JSON.stringify({ title: "", completed: false })
  });

  assert.equal(result.response.status, 400);
  assert.equal(result.body.error.details[0].field, "title");
});
npm test

Tesnya kecil, tetapi berguna. Saat nanti meminta Claude Code menambahkan database, OpenAPI, atau autentikasi, minta agar tes ini tetap lulus.

Kesalahan umum

Jangan membuat endpoint seperti /getTodos. Biarkan method membawa kata kerjanya: GET /todos, POST /todos, PUT /todos/:id.

Jangan selalu mengembalikan 200. Input buruk adalah 400, resource tidak ada adalah 404, pembuatan sukses adalah 201, dan delete tanpa body adalah 204.

Jangan mengandalkan validasi frontend saja. API bisa dipanggil langsung, jadi server wajib memvalidasi.

Jangan membocorkan stack trace ke client. Simpan detail di log server, lalu kembalikan JSON yang terkendali.

Jangan merusak idempotency. Jika PUT mengubah timestamp atau nilai acak pada setiap request identik, state akhir tidak lagi stabil.

Langkah berikutnya dan CTA

Tambahkan satu hal setiap kali: database, validasi skema yang lebih kuat, dokumentasi API, lalu autentikasi. Untuk desain error, baca juga Claude Code error handling patterns.

Untuk berlatih dengan proyek sendiri, buka cheat sheet gratis saat menulis prompt Claude Code. Untuk prompt yang bisa dipakai ulang, checklist review, dan template CLAUDE.md, gunakan koleksi produk Claude Code. Untuk onboarding tim, aturan review API, dan desain izin, gunakan pelatihan dan konsultasi Claude Code.

Setelah mencoba isi artikel ini secara langsung, kemajuan terbesar bagi pemula bukanlah kode CRUD itu sendiri. Yang paling membantu adalah melihat sukses dan gagal berdampingan: title kosong mengembalikan 400, ID yang tidak ada mengembalikan 404, dan PUT yang sama berkali-kali tetap menjaga state akhir. REST jadi terasa seperti kontrak antara frontend dan backend, bukan sekadar daftar URL.

#claude-code #rest-api #beginner #typescript #backend #tutorial
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.