Claude Code로 REST API 초고속 설계·구현·테스트 | OpenAPI 명세서부터 프로덕션까지
Claude Code를 활용해 OpenAPI 명세서 생성부터 프로덕션 TypeScript 코드까지 REST API를 end-to-end로 개발하는 방법을 소개합니다. Hono, zod 유효성 검사, vitest 테스트 자동 생성을 실제 동작하는 코드 예제와 함께 설명합니다.
REST API 설계에 Claude Code가 왜 적합한가
REST API 개발은 반복적인 패턴으로 가득합니다: 엔드포인트 정의, 유효성 검사, 에러 처리, 테스트 — 모두 수작업으로 작성해야 하죠. 이 보일러플레이트를 처리하다 보면 정작 중요한 비즈니스 로직에 집중하기 어렵습니다.
저는 Masa, 사내 시스템 API 현대화 프로젝트를 담당하는 DX 엔지니어입니다. Claude Code 도입 전 엔드포인트당 표준 공수는 “설계 30분 → 구현 2시간 → 테스트 1시간”이었습니다. Claude Code를 전면 도입한 후 설계 10분 → 구현 30분 → 테스트 15분으로 단축됐습니다.
Claude Code가 API 개발에 강한 3가지 이유
- OpenAPI 명세를 컨텍스트로 이해 — YAML을 붙여 넣으면 즉시 명세에 맞는 코드를 생성
- TypeScript 타입 시스템 숙달 — 처음부터 타입 안전한 코드를 작성해 줌
- 다양한 테스트 패턴 숙지 — 정상 케이스, 에러 케이스, 엣지 케이스 테스트를 한 번에 생성
1. Claude Code로 OpenAPI 명세서 자동 생성
프롬프트 예시
다음 요구사항으로 OpenAPI 3.1 명세서(YAML)를 생성해주세요.
리소스: Task (작업 관리)
필드:
- id: UUID
- title: string (1–200자)
- description: string (선택)
- status: "todo" | "in_progress" | "done"
- priority: "low" | "medium" | "high" (기본값: medium)
- assignee_id: UUID (선택)
- due_date: ISO8601 형식 (선택)
- created_at: ISO8601 형식
- updated_at: ISO8601 형식
엔드포인트:
- GET /tasks (페이지네이션 포함 목록)
- GET /tasks/:id
- POST /tasks
- PUT /tasks/:id (전체 업데이트)
- PATCH /tasks/:id (부분 업데이트)
- DELETE /tasks/:id
응답 형식: { data, meta, errors }
에러 코드: 400, 401, 403, 404, 422, 500
인증: Bearer Token (Authorization 헤더)
생성된 OpenAPI YAML (발췌)
openapi: 3.1.0
info:
title: Task Management API
version: 1.0.0
description: 작업 관리 API OpenAPI 명세서
servers:
- url: https://api.example.com/v1
description: 프로덕션
- url: http://localhost:3000/v1
description: 개발
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. 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();
// 미들웨어
app.use("*", cors());
app.use("*", logger());
app.use("*", prettyJSON());
// 라우터 등록
app.route("/v1/tasks", taskRouter);
// 글로벌 에러 핸들러
app.onError((err, c) => {
console.error(err);
return c.json(
{ data: null, meta: null, errors: [{ code: "INTERNAL_ERROR", message: "서버 내부 오류가 발생했습니다" }] },
500
);
});
export default app;
작업 라우터 구현
// 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();
// 작업 목록 조회
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,
});
});
// 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: "작업을 찾을 수 없습니다", field: "id" }] },
404
);
}
return c.json({ data: task, meta: null, errors: null });
});
// 작업 생성
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);
});
// 작업 삭제
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: "작업을 찾을 수 없습니다", field: "id" }] },
404
);
}
return c.json({ data: null, meta: null, errors: null }, 204);
});
3. Zod 유효성 검사 스키마 자동 생성
// 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, "제목은 최소 1자 이상이어야 합니다").max(200, "제목은 200자 이하여야 합니다"),
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>;
도입 전/후 비교
| 지표 | Claude Code 도입 전 | Claude Code 도입 후 |
|---|---|---|
| 스키마 정의 공수 | 엔드포인트당 30–45분 | 5–10분 (프롬프트 작성 포함) |
| 타입 에러 발생률 | 프로젝트 후반에 빈번 | 처음부터 거의 제로 |
| 유효성 검사 누락 | 코드 리뷰마다 지적 | 엣지 케이스 자동 커버 |
| 명세서/코드 불일치 | 2–3주 만에 구식이 됨 | 항상 동기화 (명세에서 생성) |
4. 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: "테스트 작업",
description: "테스트용 작업입니다",
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("정상 케이스: 작업 목록 반환", 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("에러 케이스: per_page 최댓값 초과", 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("정상 케이스: 작업 생성", 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: "새 작업" }),
});
expect(res.status).toBe(201);
});
it("에러 케이스: 제목이 빈 문자열", 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. 에러 처리 패턴
// 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}을(를) 찾을 수 없습니다`, 404),
unauthorized: () => new AppError("UNAUTHORIZED", "인증이 필요합니다", 401),
forbidden: () => new AppError("FORBIDDEN", "이 리소스에 접근할 권한이 없습니다", 403),
conflict: (message: string) => new AppError("CONFLICT", message, 409),
internal: () => new AppError("INTERNAL_ERROR", "서버 내부 오류가 발생했습니다", 500),
};
3가지 자주 빠지는 함정
함정 1: UUID 유효성 검사 누락
Claude Code는 :id 경로 파라미터를 단순 문자열로 처리하는 경우가 있습니다. UUID 형식 검증 없이 잘못된 쿼리가 DB에 도달할 수 있습니다.
// 잘못된 예: 원시 문자열을 DB에 직접 전달
const task = await db.findById(c.req.param("id"));
// 올바른 예: 먼저 UUID 형식 검증
const idSchema = z.string().uuid("유효한 UUID 형식이 아닙니다");
const parsed = idSchema.safeParse(c.req.param("id"));
if (!parsed.success) {
return c.json({ data: null, meta: null, errors: [{ code: "INVALID_ID", message: "ID 형식이 올바르지 않습니다" }] }, 400);
}
const task = await db.findById(parsed.data);
함정 2: Content-Type 헤더 검증 누락
Content-Type: application/json 없이 PUT/POST 요청이 오면, Hono는 body를 빈 객체로 처리합니다. zod 유효성 검사를 통과해 빈 데이터가 저장될 수 있습니다.
// 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이 필요합니다" }] },
415
);
}
}
await next();
});
함정 3: 날짜 필드의 타임존 문제
z.string().datetime()은 UTC 형식만 허용합니다. 프론트엔드에서 2026-04-25T09:00:00+09:00을 보내면 거부됩니다.
// 잘못된 예
due_date: z.string().datetime()
// 올바른 예: 타임존 오프셋 허용
due_date: z.string().datetime({ offset: true }).optional()
정리
REST API 개발에 Claude Code를 통합하면 프로덕션까지의 시간을 크게 단축할 수 있습니다:
- OpenAPI 명세를 먼저 생성 — 이후 모든 단계의 기반이 됩니다
- Zod 스키마는 명세에서 생성 — 직접 작성하면 명세와 코드 간 불일치가 발생합니다
- 정상 케이스·에러 케이스·엣지 케이스 테스트를 명시적으로 요청 — 지시 없이는 정상 케이스만 작성됩니다
- 함정을 프롬프트에 미리 포함 — UUID 검증, Content-Type 확인, 타임존 처리
이 워크플로를 도입한 이후 엔드포인트당 개발 시간이 약 70% 감소했습니다.
단, Claude Code 출력물을 검토 없이 프로덕션에 배포하는 것은 위험합니다. 보안과 비즈니스 로직은 반드시 사람이 검토해야 합니다. Claude Code는 빠른 초안 작성 도구이며, 최종 품질 책임은 개발자에게 있습니다.
관련 글:
Claude Code 워크플로우를 한 단계 업그레이드하세요
지금 바로 Claude Code에 복사해 쓸 수 있는 검증된 프롬프트 템플릿 50선.
무료 PDF: 5분 완성 Claude Code 치트시트
이메일 주소만 등록하시면 A4 한 장짜리 치트시트 PDF를 즉시 보내드립니다.
개인정보는 엄격하게 관리하며 스팸은 보내지 않습니다.
이 글을 작성한 사람
Masa
Claude Code를 적극 활용하는 엔지니어. 10개 언어, 2,000페이지 이상의 테크 미디어 claudecode-lab.com을 운영 중.
관련 글
Claude Code 완벽 입문 가이드 2026 | 제로부터 실무 활용까지 7단계
Claude Code를 처음 사용하는 분들을 위한 완전 입문 가이드. 설치부터 실제 개발 워크플로우에 녹이는 것까지 — Masa가 처음에 겪었던 모든 시행착오를 바탕으로 정리했습니다.
Claude Code로 REST API 만들기 | 초보자를 위한 실전 입문 가이드
Claude Code와 함께 REST API 기초를 배우는 입문 가이드. 엔드포인트 설계부터 유효성 검사, 에러 처리까지 복붙 가능한 코드로 친절하게 설명합니다.
Claude Code vs Gemini CLI 2026 철저 비교 | Google AI는 무엇이 다른가?
DX 엔지니어 Masa가 Claude Code와 Gemini CLI를 직접 사용해 비교. 가격·자율성·컨텍스트 창·에코시스템 분석. 올바른 툴 선택을 위한 결정 플로우차트 포함.