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.
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.
| Istilah | Arti sederhana | Contoh |
|---|---|---|
| Endpoint | URL pintu masuk API | GET /todos |
| Method | Kata kerja HTTP untuk aksi | GET, POST, PUT, DELETE |
| Status code | Angka yang menjelaskan hasil | 200, 201, 400, 404 |
| JSON | Format teks ringan untuk data | { "title": "Learn REST" } |
| Validasi | Mengecek apakah input boleh diterima | Menolak title kosong |
| Idempotency | Request yang sama diulang tetap memberi state akhir yang sama | Mengirim 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.
| Aksi | Method dan URL | Status sukses |
|---|---|---|
| Health check | GET /health | 200 OK |
| Daftar Todo | GET /todos | 200 OK |
| Baca satu Todo | GET /todos/:id | 200 OK |
| Buat Todo | POST /todos | 201 Created |
| Ganti Todo | PUT /todos/:id | 200 OK |
| Hapus Todo | DELETE /todos/:id | 204 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.
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.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Permission safety ladder Claude Code: perluas akses tanpa kehilangan kontrol
Naik dari read-only ke edit terbatas, command bukti, dan cek deploy dengan kontrol yang jelas.
Claude Code Small PR Proof Pack: perubahan kecil yang mudah direview
Paket bukti untuk PR Claude Code: diff, check, URL publik, jalur CTA, dan rollback.
Review gate Claude Code sebelum commit: diff, test, URL publik, dan CTA
Cara memakai Claude Code sebelum commit: diff scope, build, URL publik, link Gumroad, CTA konsultasi, missing test, dan file tidak terkait.