Use Cases (Diperbarui: 2/6/2026)

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 praktis dengan Claude Code: OpenAPI, test, dan breaking change

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.

KeputusanArti sederhanaContoh
ResourceKata benda yang diekspos APIorders, customers, invoices
OperationAksi pada resourceGET, POST, PATCH, DELETE
SchemaBentuk dan aturan JSONitems minimal berisi satu item
ErrorCara gagal dijelaskan400, 401, 403, 404, 422 dengan detail
CompatibilityCara tidak merusak clientField 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.

#claude-code #api #rest #openapi #design
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.