Tips & Tricks

Desain, Implementasi & Pengujian REST API Super Cepat dengan Claude Code | Dari Spesifikasi OpenAPI ke Produksi

Pelajari cara mengembangkan REST API dari awal hingga akhir dengan Claude Code: dari pembuatan spesifikasi OpenAPI hingga kode TypeScript siap produksi dengan Hono, validasi zod, dan pembuatan test vitest. Dilengkapi contoh kode yang bisa langsung dijalankan.

Mengapa Claude Code Unggul dalam Desain REST API

Pengembangan REST API penuh dengan pola yang berulang: definisi endpoint, validasi, penanganan error, dan pengujian — semuanya ditulis secara manual. Sulit untuk tetap fokus pada logika bisnis utama sambil mengelola boilerplate tersebut.

Saya Masa, seorang engineer DX yang bertanggung jawab atas modernisasi API sistem internal. Sebelum Claude Code, biaya standar per endpoint saya adalah: “30 menit desain → 2 jam implementasi → 1 jam pengujian”. Setelah mengintegrasikan Claude Code sepenuhnya: 10 menit desain → 30 menit implementasi → 15 menit pengujian.

3 Alasan Claude Code Unggul dalam Pengembangan API

  1. Memahami spesifikasi OpenAPI sebagai konteks — tempel YAML dan langsung menghasilkan kode yang sesuai spesifikasi
  2. Menguasai sistem tipe TypeScript — menghasilkan kode type-safe sejak awal
  3. Mengetahui berbagai pola pengujian — membuat test happy-path, error-path, dan edge case dalam satu proses

1. Membuat Spesifikasi OpenAPI Secara Otomatis dengan Claude Code

Contoh Prompt

Buat spesifikasi OpenAPI 3.1 (YAML) untuk persyaratan berikut.

Sumber daya: Task (manajemen tugas)
Field:
  - id: UUID
  - title: string (1–200 karakter)
  - description: string (opsional)
  - status: "todo" | "in_progress" | "done"
  - priority: "low" | "medium" | "high" (default: medium)
  - assignee_id: UUID (opsional)
  - due_date: format ISO8601 (opsional)
  - created_at: format ISO8601
  - updated_at: format ISO8601

Endpoint:
  - GET /tasks (daftar dengan paginasi)
  - GET /tasks/:id
  - POST /tasks
  - PUT /tasks/:id (pembaruan penuh)
  - PATCH /tasks/:id (pembaruan sebagian)
  - DELETE /tasks/:id

Format respons: { data, meta, errors }
Kode error: 400, 401, 403, 404, 422, 500
Autentikasi: Bearer Token (header Authorization)

YAML OpenAPI yang Dihasilkan (kutipan)

openapi: 3.1.0
info:
  title: Task Management API
  version: 1.0.0
  description: Spesifikasi OpenAPI untuk API Manajemen Tugas

servers:
  - url: https://api.example.com/v1
    description: Produksi
  - url: http://localhost:3000/v1
    description: Pengembangan

components:
  schemas:
    Task:
      type: object
      required: [id, title, status, priority, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 1
          maxLength: 200
        status:
          type: string
          enum: [todo, in_progress, done]
        priority:
          type: string
          enum: [low, medium, high]
          default: medium

2. Implementasi Handler Type-Safe dengan Hono

// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { prettyJSON } from "hono/pretty-json";
import { taskRouter } from "./routes/tasks";

const app = new Hono();

// Middleware
app.use("*", cors());
app.use("*", logger());
app.use("*", prettyJSON());

// Pendaftaran router
app.route("/v1/tasks", taskRouter);

// Global error handler
app.onError((err, c) => {
  console.error(err);
  return c.json(
    { data: null, meta: null, errors: [{ code: "INTERNAL_ERROR", message: "Terjadi kesalahan internal server" }] },
    500
  );
});

export default app;

Implementasi Router Tugas

// src/routes/tasks.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { TaskCreateSchema, TaskUpdateSchema, TaskPatchSchema, TaskQuerySchema } from "../schemas/task";
import { TaskService } from "../services/taskService";

export const taskRouter = new Hono();
const taskService = new TaskService();

// Daftar tugas
taskRouter.get("/", zValidator("query", TaskQuerySchema), async (c) => {
  const query = c.req.valid("query");
  const result = await taskService.findAll(query);
  return c.json({
    data: result.tasks,
    meta: { total: result.total, page: query.page, per_page: query.per_page },
    errors: null,
  });
});

// Ambil tugas berdasarkan ID
taskRouter.get("/:id", async (c) => {
  const id = c.req.param("id");
  const task = await taskService.findById(id);
  if (!task) {
    return c.json(
      { data: null, meta: null, errors: [{ code: "NOT_FOUND", message: "Tugas tidak ditemukan", field: "id" }] },
      404
    );
  }
  return c.json({ data: task, meta: null, errors: null });
});

// Buat tugas
taskRouter.post("/", zValidator("json", TaskCreateSchema), async (c) => {
  const body = c.req.valid("json");
  const task = await taskService.create(body);
  return c.json({ data: task, meta: null, errors: null }, 201);
});

// Hapus tugas
taskRouter.delete("/:id", async (c) => {
  const id = c.req.param("id");
  const deleted = await taskService.delete(id);
  if (!deleted) {
    return c.json(
      { data: null, meta: null, errors: [{ code: "NOT_FOUND", message: "Tugas tidak ditemukan", field: "id" }] },
      404
    );
  }
  return c.json({ data: null, meta: null, errors: null }, 204);
});

3. Pembuatan Skema Validasi Zod Secara Otomatis

// src/schemas/task.ts
import { z } from "zod";

const TaskStatus = z.enum(["todo", "in_progress", "done"]);
const TaskPriority = z.enum(["low", "medium", "high"]);

export const TaskSchema = z.object({
  id: z.string().uuid(),
  title: z.string().min(1, "Judul harus minimal 1 karakter").max(200, "Judul maksimal 200 karakter"),
  description: z.string().nullable(),
  status: TaskStatus,
  priority: TaskPriority,
  assignee_id: z.string().uuid().nullable(),
  due_date: z.string().datetime().nullable(),
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
});

export const TaskCreateSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().optional(),
  status: TaskStatus.default("todo"),
  priority: TaskPriority.default("medium"),
  assignee_id: z.string().uuid().optional(),
  due_date: z.string().datetime().optional(),
});

export const TaskUpdateSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().nullable(),
  status: TaskStatus,
  priority: TaskPriority,
  assignee_id: z.string().uuid().nullable(),
  due_date: z.string().datetime().nullable(),
});

export const TaskPatchSchema = TaskUpdateSchema.partial();

export const TaskQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  per_page: z.coerce.number().int().min(1).max(100).default(20),
  status: TaskStatus.optional(),
  priority: TaskPriority.optional(),
});

export type Task = z.infer<typeof TaskSchema>;
export type TaskCreate = z.infer<typeof TaskCreateSchema>;
export type TaskQuery = z.infer<typeof TaskQuerySchema>;

Perbandingan Sebelum/Sesudah

MetrikSebelum Claude CodeSesudah Claude Code
Waktu definisi skema30–45 menit per endpoint5–10 menit (termasuk prompt)
Tingkat error tipeSering di tahap akhir proyekHampir nol sejak awal
Validasi yang terlewatTerdeteksi di setiap code reviewKasus edge otomatis tercakup
Perbedaan spesifikasi/kodeUsang dalam 2–3 mingguSelalu sinkron (dibuat dari spesifikasi)

4. Pembuatan Test Otomatis dengan Vitest

// src/routes/tasks.test.ts
import { describe, it, expect, vi } from "vitest";
import app from "../index";
import { TaskService } from "../services/taskService";

vi.mock("../services/taskService");

const mockTask = {
  id: "550e8400-e29b-41d4-a716-446655440000",
  title: "Tugas uji coba",
  description: "Sebuah tugas untuk pengujian",
  status: "todo" as const,
  priority: "medium" as const,
  assignee_id: null,
  due_date: null,
  created_at: "2026-04-25T00:00:00.000Z",
  updated_at: "2026-04-25T00:00:00.000Z",
};

describe("GET /v1/tasks", () => {
  it("happy path: mengembalikan daftar tugas", async () => {
    vi.mocked(TaskService.prototype.findAll).mockResolvedValue({ tasks: [mockTask], total: 1 });
    const res = await app.request("/v1/tasks", { headers: { Authorization: "Bearer valid-token" } });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.data).toHaveLength(1);
  });

  it("error: per_page melebihi maksimum", async () => {
    const res = await app.request("/v1/tasks?per_page=101", { headers: { Authorization: "Bearer valid-token" } });
    expect(res.status).toBe(400);
  });
});

describe("POST /v1/tasks", () => {
  it("happy path: membuat tugas", async () => {
    vi.mocked(TaskService.prototype.create).mockResolvedValue(mockTask);
    const res = await app.request("/v1/tasks", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: "Bearer valid-token" },
      body: JSON.stringify({ title: "Tugas baru" }),
    });
    expect(res.status).toBe(201);
  });

  it("error: judul kosong", async () => {
    const res = await app.request("/v1/tasks", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: "Bearer valid-token" },
      body: JSON.stringify({ title: "" }),
    });
    expect(res.status).toBe(400);
  });
});

5. Pola Penanganan Error

// src/lib/errors.ts
export class AppError extends Error {
  constructor(
    public readonly code: string,
    public readonly message: string,
    public readonly statusCode: number,
    public readonly field?: string
  ) {
    super(message);
    this.name = "AppError";
  }
}

export const Errors = {
  notFound: (resource: string) => new AppError("NOT_FOUND", `${resource} tidak ditemukan`, 404),
  unauthorized: () => new AppError("UNAUTHORIZED", "Autentikasi diperlukan", 401),
  forbidden: () => new AppError("FORBIDDEN", "Anda tidak memiliki izin untuk mengakses sumber daya ini", 403),
  conflict: (message: string) => new AppError("CONFLICT", message, 409),
  internal: () => new AppError("INTERNAL_ERROR", "Terjadi kesalahan internal server", 500),
};

3 Jebakan Umum yang Harus Dihindari

Jebakan 1: Validasi UUID yang Hilang

Claude Code terkadang memperlakukan parameter path :id sebagai string biasa. Tanpa validasi format UUID, kueri yang salah bisa sampai ke database.

// SALAH: string mentah langsung dikirim ke DB
const task = await db.findById(c.req.param("id"));

// BENAR: validasi format UUID terlebih dahulu
const idSchema = z.string().uuid("Bukan UUID yang valid");
const parsed = idSchema.safeParse(c.req.param("id"));
if (!parsed.success) {
  return c.json({ data: null, meta: null, errors: [{ code: "INVALID_ID", message: "Format ID tidak valid" }] }, 400);
}
const task = await db.findById(parsed.data);

Jebakan 2: Validasi Content-Type yang Hilang

Untuk permintaan PUT/POST tanpa Content-Type: application/json, Hono memperlakukan body sebagai objek kosong. Validasi zod lolos dan data kosong tersimpan.

// src/middleware/contentType.ts
import { createMiddleware } from "hono/factory";

export const requireJson = createMiddleware(async (c, next) => {
  if (["POST", "PUT", "PATCH"].includes(c.req.method)) {
    const contentType = c.req.header("Content-Type");
    if (!contentType?.includes("application/json")) {
      return c.json(
        { data: null, meta: null, errors: [{ code: "UNSUPPORTED_MEDIA_TYPE", message: "Content-Type: application/json diperlukan" }] },
        415
      );
    }
  }
  await next();
});

Jebakan 3: Masalah Timezone pada Field Tanggal

z.string().datetime() hanya menerima format UTC. Jika frontend mengirim 2026-04-25T16:00:00+07:00, maka akan ditolak.

// SALAH
due_date: z.string().datetime()

// BENAR: izinkan offset timezone
due_date: z.string().datetime({ offset: true }).optional()

Kesimpulan

Mengintegrasikan Claude Code dalam pengembangan REST API mengurangi waktu hingga produksi secara drastis:

  1. Buat spesifikasi OpenAPI terlebih dahulu — ini menjadi fondasi semua langkah berikutnya
  2. Hasilkan skema Zod dari spesifikasi — menulisnya secara manual menyebabkan penyimpangan
  3. Minta secara eksplisit test happy-path, error-path, dan edge case — tanpa instruksi, hanya happy path yang ditulis
  4. Masukkan jebakan umum ke dalam prompt sejak awal — validasi UUID, pengecekan Content-Type, penanganan timezone

Sejak mengadopsi workflow ini, waktu pengembangan per endpoint berkurang sekitar 70%.

Meski begitu, jangan pernah mengirimkan kode Claude Code ke produksi tanpa ditinjau. Keamanan dan logika bisnis selalu membutuhkan pengawasan manusia. Claude Code adalah mesin pembuat draf pertama yang cepat; tanggung jawab kualitas akhir ada pada developer.


Artikel terkait:

#claude-code #rest-api #openapi #typescript #backend

Tingkatkan alur kerja Claude Code kamu

50 template prompt yang sudah teruji, siap copy-paste ke Claude Code sekarang juga.

Gratis

PDF Gratis: Cheatsheet Claude Code dalam 5 Menit

Cukup masukkan emailmu dan kami akan langsung mengirim cheatsheet PDF A4 satu halaman.

Kami menjaga data pribadimu dengan aman dan tidak pernah mengirim spam.

Masa

Tentang Penulis

Masa

Engineer yang aktif menggunakan Claude Code. Mengelola claudecode-lab.com, media teknologi 10 bahasa dengan lebih dari 2.000 halaman.