Desain API praktis dengan Claude Code: OpenAPI, test, dan breaking change
Rancang REST API yang andal dengan Claude Code: OpenAPI, mock, test, versioning, security, dan pitfall.
Desain API bukan sekadar memilih URL yang terlihat rapi. Desain API adalah kontrak: apa yang boleh dikirim client, apa yang dikembalikan server, dan bagaimana kegagalan dijelaskan.
Jika kontrak ini kabur, frontend, aplikasi mobile, partner integration, test, dan monitoring akan membuat tafsir sendiri. Semakin lama diperbaiki, semakin mahal biaya perubahan.
Claude Code berguna untuk pekerjaan ini, tetapi jangan pakai hanya sebagai generator kode. Pakai sebagai reviewer desain: minta draf OpenAPI, review endpoint, buat mock dan test, lalu cek apakah perubahan akan merusak client lama.
Gunakan sumber resmi sebagai dasar: OpenAPI Specification, RFC 9110 HTTP Semantics, JSON Schema docs, dan OWASP API Security Top 10. Untuk implementasi lanjutan, baca juga pengembangan API produksi, otomasi test API, dan versioning API.
Apa yang didesain dalam API
API adalah interface untuk program lain. UI manusia menjelaskan diri lewat tombol, teks, dan layout. API menjelaskan diri lewat path, HTTP method, status code, field JSON, schema, contoh, dan response error.
Untuk mulai, tentukan lima hal berikut.
| Keputusan | Arti sederhana | Contoh |
|---|---|---|
| Resource | Kata benda yang diekspos API | orders, customers, invoices |
| Operation | Aksi pada resource | GET, POST, PATCH, DELETE |
| Schema | Bentuk dan aturan JSON | items minimal berisi satu item |
| Error | Cara gagal dijelaskan | 400, 401, 403, 404, 422 dengan detail |
| Compatibility | Cara tidak merusak client | Field wajib baru adalah breaking |
REST tidak perlu dimulai secara akademis. Kebiasaan praktisnya sederhana: URL memakai kata benda, aksi memakai HTTP method. POST /orders lebih jelas daripada POST /orders/create, dan GET /orders/ord_123 lebih mudah diuji daripada GET /getOrder?id=ord_123.
Workflow dengan Claude Code
Jangan meminta Claude Code mendesain, mengimplementasikan, mengetes, dan menulis dokumen sekaligus. Pisahkan menjadi langkah yang bisa direview.
flowchart TD
A["Ringkas aturan bisnis"] --> B["Buat kontrak OpenAPI"]
B --> C["Review HTTP, schema, security"]
C --> D["Generate mock dan API test"]
D --> E["Cek breaking change di CI"]
E --> F["Implementasi, dokumentasi, publikasi"]
OpenAPI adalah kontrak HTTP API yang bisa dibaca mesin. JSON Schema menjelaskan bentuk dan batasan JSON. HTTP status code memberi makna umum untuk sukses dan gagal. Claude Code bisa menghubungkan semuanya, tetapi spesifikasi resmi dan test tetap menjadi sumber kebenaran.
Dalam proyek verifikasi kecil, Masa melihat bahwa meminta daftar endpoint saja menghasilkan awal yang lumayan. Masalah muncul ketika authentication, pagination, idempotency key, dan detail error ditambahkan belakangan. Prompt awal yang menanyakan cara client pulih dari kegagalan menghasilkan desain yang lebih stabil.
Starter kit yang bisa dicopy
Contoh ini tidak memakai package eksternal. Kamu bisa mencoba OpenAPI, mock server, dan breaking change check dalam satu alur.
mkdir api-design-lab
cd api-design-lab
mkdir docs examples
node --version
Buat docs/openapi.yaml. Halaman resmi OpenAPI menampilkan versi terbaru yang dipublikasikan; contoh ini memakai 3.1 karena dukungan tooling masih luas.
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/v1/orders:
post:
summary: Create an order
operationId: createOrder
security:
- bearerAuth: []
parameters:
- name: Idempotency-Key
in: header
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderRequest"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"422":
description: Validation error
content:
application/json:
schema:
$ref: "#/components/schemas/Problem"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
schemas:
CreateOrderRequest:
type: object
required: [customerId, items]
properties:
customerId:
type: string
minLength: 3
items:
type: array
minItems: 1
items:
type: object
required: [sku, quantity]
properties:
sku:
type: string
minLength: 3
quantity:
type: integer
minimum: 1
Order:
type: object
required: [id, status, customerId, total]
properties:
id:
type: string
status:
type: string
enum: [accepted, cancelled]
customerId:
type: string
total:
type: integer
Problem:
type: object
required: [type, title, status, detail]
properties:
type:
type: string
title:
type: string
status:
type: integer
detail:
type: string
errors:
type: array
items:
type: object
Buat examples/mock-server.mjs.
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
function readJson(req) {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => {
body += chunk;
if (body.length > 1_000_000) req.destroy(new Error("Body too large"));
});
req.on("end", () => {
if (!body) return resolve({});
try {
resolve(JSON.parse(body));
} catch (error) {
reject(error);
}
});
req.on("error", reject);
});
}
function send(res, status, body, headers = {}) {
res.writeHead(status, {
"content-type": "application/json; charset=utf-8",
"x-content-type-options": "nosniff",
...headers,
});
res.end(JSON.stringify(body, null, 2));
}
function problem(status, title, detail, errors = []) {
return {
type: "https://example.com/problems/request",
title,
status,
detail,
errors,
};
}
function validateOrder(input) {
const errors = [];
if (typeof input.customerId !== "string" || input.customerId.length < 3) {
errors.push({
path: "customerId",
message: "customerId must be a string with 3+ characters",
});
}
if (!Array.isArray(input.items) || input.items.length === 0) {
errors.push({ path: "items", message: "items must contain at least one item" });
}
for (const [index, item] of (input.items ?? []).entries()) {
if (typeof item.sku !== "string" || item.sku.length < 3) {
errors.push({
path: `items.${index}.sku`,
message: "sku must be a string with 3+ characters",
});
}
if (!Number.isInteger(item.quantity) || item.quantity < 1) {
errors.push({
path: `items.${index}.quantity`,
message: "quantity must be a positive integer",
});
}
}
return errors;
}
const server = createServer(async (req, res) => {
const url = new URL(req.url ?? "/", "http://localhost");
if (req.method === "GET" && url.pathname === "/health") {
return send(res, 200, { ok: true });
}
const customerMatch = url.pathname.match(/^\/v1\/customers\/([a-z0-9-]+)$/);
if (req.method === "GET" && customerMatch) {
return send(res, 200, {
id: customerMatch[1],
name: "Aki Tanaka",
plan: "pro",
});
}
if (req.method === "POST" && url.pathname === "/v1/orders") {
const idempotencyKey = req.headers["idempotency-key"];
if (!idempotencyKey) {
return send(
res,
400,
problem(400, "Missing Idempotency-Key", "POST /v1/orders requires the header.")
);
}
try {
const body = await readJson(req);
const errors = validateOrder(body);
if (errors.length > 0) {
return send(res, 422, problem(422, "Invalid request body", "Fix errors.", errors));
}
return send(
res,
201,
{
id: `ord_${randomUUID()}`,
status: "accepted",
customerId: body.customerId,
total: 4200,
},
{ location: "/v1/orders/example" }
);
} catch {
return send(res, 400, problem(400, "Malformed JSON", "Request body must be JSON."));
}
}
return send(res, 404, problem(404, "Not found", `${req.method} ${url.pathname} is undefined.`));
});
server.listen(3000, () => {
console.log("Mock API running at http://localhost:3000");
});
Jalankan server, lalu panggil dari terminal lain.
node examples/mock-server.mjs
curl -i http://localhost:3000/health
curl -i -X POST http://localhost:3000/v1/orders \
-H "content-type: application/json" \
-H "idempotency-key: demo-001" \
-d '{"customerId":"cus_123","items":[{"sku":"book-1","quantity":2}]}'
curl -i -X POST http://localhost:3000/v1/orders \
-H "content-type: application/json" \
-H "idempotency-key: demo-002" \
-d '{"customerId":"x","items":[]}'
Buat examples/contract-check.mjs. Script ini sengaja gagal.
import assert from "node:assert/strict";
const previous = {
paths: {
"/v1/orders": {
post: {
request: {
required: ["customerId", "items"],
properties: ["customerId", "items", "couponCode"],
},
response: {
required: ["id", "status", "customerId", "total"],
properties: ["id", "status", "customerId", "total"],
},
},
},
},
};
const next = structuredClone(previous);
next.paths["/v1/orders"].post.request.required.push("shippingAddress");
next.paths["/v1/orders"].post.response.properties =
next.paths["/v1/orders"].post.response.properties.filter((name) => name !== "total");
function diffContract(oldSpec, newSpec) {
const breaking = [];
for (const [path, methods] of Object.entries(oldSpec.paths)) {
for (const [method, oldOperation] of Object.entries(methods)) {
const newOperation = newSpec.paths[path]?.[method];
if (!newOperation) {
breaking.push(`${method.toUpperCase()} ${path} was removed`);
continue;
}
const oldRequired = new Set(oldOperation.request.required);
for (const field of newOperation.request.required) {
if (!oldRequired.has(field)) {
breaking.push(`${method.toUpperCase()} ${path} now requires "${field}"`);
}
}
const newResponseFields = new Set(newOperation.response.properties);
for (const field of oldOperation.response.properties) {
if (!newResponseFields.has(field)) {
breaking.push(`${method.toUpperCase()} ${path} removed response "${field}"`);
}
}
}
}
return breaking;
}
const breaking = diffContract(previous, next);
console.log(breaking.join("\n") || "No breaking changes found");
assert.equal(breaking.length, 0, "Breaking API changes detected");
node examples/contract-check.mjs
Gagal adalah hasil yang benar di sini. Script menemukan field request wajib baru dan field response yang dihapus.
Prompt untuk Claude Code
Pisahkan draf, review, contoh, dan cek kompatibilitas.
claude -p "
Buat draf OpenAPI di docs/openapi.yaml untuk API orders e-commerce.
Resources: customers, orders, invoices.
Sertakan summary, operationId, requestBody, responses, examples, dan bearerAuth.
Gunakan OpenAPI 3.1 dan constraint bergaya JSON Schema.
"
claude -p "
Review docs/openapi.yaml sebagai API design reviewer.
Kembalikan Findings berdasarkan severity terlebih dahulu dan jangan edit file dulu.
Cek semantik method/status RFC 9110, schema yang kabur, pagination,
idempotency, authentication, dan risiko umum OWASP API Security.
"
claude -p "
Generate Node.js mock server dan contoh API test dari docs/openapi.yaml.
Cover success, auth failure, validation failure, dan missing resource.
Jaga line panjang di bawah 150 karakter dan tambahkan command di README.
"
claude -p "
Bandingkan docs/openapi.yaml saat ini dengan versi di HEAD.
List breaking changes sebelum memberi saran edit.
Cek path yang dihapus, field wajib baru, field response yang dihapus,
perubahan status code, dan perubahan auth scope.
"
Use case nyata
Use case pertama adalah API orders untuk SaaS. Admin, billing, email, dan export akuntansi membaca Order yang sama. Jika total, mata uang, pajak, dan status cancel tidak jelas, semua integrasi akan membuat aturan sendiri.
Use case kedua adalah API profil mobile. Versi lama aplikasi bisa tetap aktif selama berbulan-bulan. Menghapus field response atau mengubah arti enum dapat merusak client yang belum bisa update.
Use case ketiga adalah API partner B2B. Developer eksternal tidak tahu konvensi internal. Mereka butuh error code stabil, rate limit, panduan retry, sandbox, dan contoh yang bisa dipercaya.
Use case keempat adalah API admin internal. Internal bukan berarti aman. Authorization per object tetap penting: user tidak boleh membaca order tenant lain hanya karena tahu ID.
Pitfall dan failure case
Pitfall umum adalah memasukkan kata kerja ke path: /cancelOrder, /getUserOrders, /updateOrderStatus. Seiring waktu, nama endpoint jadi tidak konsisten. Modelkan resource dulu, lalu gunakan method atau subresource.
Pitfall lain adalah mengembalikan HTTP 200 untuk semua business error. Ini membuat monitoring, SDK, retry, dan client error handling lebih sulit. Pakai 400 untuk input rusak, 401 tanpa auth, 403 forbidden, 404 tidak ada, dan 422 untuk input yang secara makna tidak valid.
Lupa retry safety pada POST juga mahal. Create order atau mulai payment bisa dikirim ulang setelah timeout. Desain Idempotency-Key sejak awal.
Schema yang ambigu menimbulkan bug lambat. Tentukan apakah null berarti menghapus, unknown, atau tidak dikirim. Tulis required fields, minimum length, batas pagination, timezone, dan additional properties.
Versioning, error, schema, security
Versioning bukan hanya menambahkan /v1. Tim harus punya aturan tentang apa yang breaking. Field optional baru biasanya aman; field wajib baru, response field dihapus, status berubah, atau permission diperketat bisa merusak client.
Error yang baik memberi tahu langkah berikutnya. Invalid request saja tidak cukup. Gunakan bentuk stabil seperti type, title, status, detail, dan errors per field bila berguna. Jangan tampilkan stack trace, SQL, atau ID internal di produksi.
Schema harus menulis constraint, bukan cuma contoh. Format ID, minimum length, batas array, default pagination, dan aturan tanggal mengurangi tebakan di client.
Security memisahkan authentication dan authorization. Bearer token tidak cukup jika GET /orders/{id} mengembalikan order tenant lain. Hindari API key di query string, batasi data sensitif, gunakan rate limit, dan simpan audit log.
Monetisasi dan konsultasi
Orang yang mencari desain API biasanya punya proyek nyata: integrasi publik, backend mobile, aturan review tim, atau API partner. Artikel harus menunjukkan workflow yang bisa diterapkan, bukan hanya teori.
ClaudeCodeLab bisa membantu review desain API dengan Claude Code, pembersihan OpenAPI, otomasi test, dan breaking change checks. Tim bisa mulai dari training dan konsultasi; developer individu bisa memakai resource gratis.
Hasil verifikasi
Kode artikel ini saya cek dengan Node v24.14.1. GET /health mengembalikan 200, POST /v1/orders valid mengembalikan 201, dan items kosong mengembalikan 422. contract-check.mjs gagal dengan sengaja dan menampilkan field wajib baru serta field response yang dihapus. Alurnya memberi jalur kecil tetapi nyata dari prompt Claude Code ke OpenAPI, mock, error design, dan compatibility check untuk CI.
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
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.