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
- Memahami spesifikasi OpenAPI sebagai konteks — tempel YAML dan langsung menghasilkan kode yang sesuai spesifikasi
- Menguasai sistem tipe TypeScript — menghasilkan kode type-safe sejak awal
- 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
| Metrik | Sebelum Claude Code | Sesudah Claude Code |
|---|---|---|
| Waktu definisi skema | 30–45 menit per endpoint | 5–10 menit (termasuk prompt) |
| Tingkat error tipe | Sering di tahap akhir proyek | Hampir nol sejak awal |
| Validasi yang terlewat | Terdeteksi di setiap code review | Kasus edge otomatis tercakup |
| Perbedaan spesifikasi/kode | Usang dalam 2–3 minggu | Selalu 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:
- Buat spesifikasi OpenAPI terlebih dahulu — ini menjadi fondasi semua langkah berikutnya
- Hasilkan skema Zod dari spesifikasi — menulisnya secara manual menyebabkan penyimpangan
- Minta secara eksplisit test happy-path, error-path, dan edge case — tanpa instruksi, hanya happy path yang ditulis
- 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:
Tingkatkan alur kerja Claude Code kamu
50 template prompt yang sudah teruji, siap copy-paste ke Claude Code sekarang juga.
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.
Tentang Penulis
Masa
Engineer yang aktif menggunakan Claude Code. Mengelola claudecode-lab.com, media teknologi 10 bahasa dengan lebih dari 2.000 halaman.
Artikel Terkait
Panduan Lengkap Memulai Claude Code 2026 | 7 Langkah dari Nol hingga Siap Pakai di Dunia Kerja
Panduan pemula lengkap untuk pengguna baru Claude Code. Dari instalasi hingga mengintegrasikannya ke workflow pengembangan nyata — mencakup semua jebakan yang Masa hadapi di awal.
Membuat REST API dengan Claude Code | Panduan Praktis untuk Pemula
Pelajari dasar-dasar REST API bersama Claude Code. Panduan praktis mencakup desain endpoint, validasi, dan penanganan error — dengan kode siap copy-paste.
Claude Code vs Gemini CLI 2026 Perbandingan Mendalam | Apa Bedanya AI Google?
Perbandingan langsung Claude Code dan Gemini CLI oleh engineer DX Masa. Harga, otonomi, jendela konteks, dan ekosistem dianalisis. Termasuk diagram keputusan untuk memilih alat yang tepat.