Claude Code로 REST API 설계하기: 엔드포인트, 오류, 페이지네이션, 멱등성
Claude Code로 구현 전 REST API를 설계합니다. OpenAPI, 오류 형식, 페이지네이션, 멱등성 체크리스트 포함.
REST API는 라우트를 몇 개 만들었다고 끝나는 일이 아닙니다. 구현 전에 무엇을 리소스로 볼지, 각 동작에 어떤 HTTP 메서드를 쓸지, 실패 시 무엇을 반환할지, 목록을 어떻게 페이지네이션할지, 같은 요청이 두 번 들어오면 어떻게 처리할지를 정해야 합니다. 이 결정이 흐리면 Claude Code가 코드를 빠르게 작성해도 프런트엔드, 모바일 앱, 외부 연동이 서로 다른 계약을 해석하게 됩니다.
이 글의 목표는 Claude Code로 구현에 들어가기 전에 REST API 설계를 고정하는 것입니다. REST는 Representational State Transfer의 약자이지만, 실무에서는 주문, 사용자, 청구서 같은 리소스를 HTTP 규칙으로 다루는 설계 방식으로 이해하면 충분합니다. 리소스는 다루는 대상, 메서드는 동작, 상태 코드는 결과 신호입니다.
공식 참고 문서는 MDN의 HTTP request methods, HTTP response status codes, 그리고 OpenAPI Specification을 사용합니다. 2026년 6월 3일 기준 OpenAPI latest 페이지는 3.2.0을 가리키지만, 이 예시는 많은 린터와 생성 도구가 더 안정적으로 처리하는 openapi: 3.1.0을 사용합니다.
구현 흐름은 Claude Code로 프로덕션 API 개발, 테스트 관점은 Claude Code API 테스트, 공개 전 검토는 Claude Code 코드 리뷰와 함께 보면 좋습니다. 바로 쓸 수 있는 템플릿과 체크리스트는 /products/에, 팀 단위 API 설계 리뷰 훈련은 /training/에 정리했습니다.
용어를 먼저 맞춘다
초보자가 REST 설계에서 자주 막히는 이유는 용어가 비슷하기 때문입니다. Claude Code에 구현을 맡기기 전에 팀이 다음 단어를 같은 뜻으로 쓰도록 맞춥니다.
| 용어 | 쉬운 설명 | 예시 |
|---|---|---|
| 리소스 | API가 다루는 명사 | orders, customers |
| 엔드포인트 | URL과 HTTP 메서드의 조합 | GET /v1/orders |
| 메서드 | 무엇을 할지 나타내는 HTTP 동사 | GET, POST, PATCH |
| 상태 코드 | 성공, 입력 오류, 인증 누락 등을 숫자로 알리는 신호 | 200, 201, 400, 404 |
| 검증 | 요청이 올바른 형태인지 확인하는 처리 | 이메일 형식, 최소 수량 |
| 멱등성 | 같은 작업을 여러 번 보내도 최종 상태가 같게 유지되는 성질 | 같은 키로 주문 하나만 생성 |
Claude Code는 문맥을 잘 읽지만, 모호한 설계 의도를 보장해서 맞히지는 못합니다. “주문 생성 API를 만들어줘”라는 요청은 /createOrder, /orders/create, POST /orders 중 무엇으로든 이어질 수 있습니다. 먼저 용어와 설계 규칙을 전달하는 것이 구현 품질을 높이는 가장 단순한 방법입니다.
세 가지 유스케이스로 설계를 정한다
같은 주문 API라도 사용 방식에 따라 우선순위가 달라집니다.
첫 번째는 B2B SaaS 주문 연동입니다. 파트너가 같은 야간 CSV 작업을 다시 보내는 경우 POST /v1/orders에는 Idempotency-Key가 필요합니다. 그래야 같은 요청이 중복 주문을 만들지 않습니다. 장애 복구 중 재시도는 흔하므로 멱등성은 나중에 붙이기 어렵습니다.
두 번째는 관리자 목록 API입니다. 담당자가 status=paid로 필터링하고 다음 페이지로 이동하는 동안 다른 담당자가 주문을 추가할 수 있습니다. 단순한 page=2는 결과가 밀릴 수 있습니다. limit과 after를 쓰는 커서 페이지네이션을 사용하고, createdAt desc, id desc처럼 정렬을 고정합니다.
세 번째는 모바일 앱 API입니다. 오래된 앱 버전이 몇 달 동안 남아 있다면 응답 필드명을 갑자기 바꿀 수 없습니다. 새로운 필수 필드나 오류 형식 변경은 /v2로 나누고, /v1 계약은 OpenAPI에 남겨둡니다. 작은 선택 필드 추가는 보통 새 버전이 필요하지 않습니다.
네 번째로 내부 서비스 API도 있습니다. 소비자를 한 번에 업데이트하기 쉽지만 모니터링과 알림이 약해지기 쉽습니다. 내부 API라도 상태 코드와 오류 형식이 제각각이면 호출하는 쪽마다 별도 예외 처리를 쓰게 됩니다.
설계 흐름을 그림으로 본다
Claude Code에 작업을 넘기기 전에 이 순서로 설계를 고정합니다. 그러면 파일을 수정하는 동안 구현이 계약에서 벗어나기 어렵습니다.
flowchart TD
A["Use cases"] --> B["Resources and endpoints"]
B --> C["OpenAPI contract"]
C --> D["Errors, pagination, idempotency"]
D --> E["Claude Code implementation"]
E --> F["Tests and review"]
구현 전에 엔드포인트 표를 고정한다
첫 산출물은 코드가 아니라 엔드포인트 표입니다. 이 표를 Claude Code에 전달하면 라우팅, OpenAPI, 테스트, 문서가 같은 계약에서 출발합니다.
| 목적 | 메서드와 경로 | 성공 | 중요한 규칙 |
|---|---|---|---|
| 주문 목록 | GET /v1/orders?status=paid&limit=20&after=ord_123 | 200 OK | 안정적인 정렬의 커서 페이지네이션 |
| 주문 생성 | POST /v1/orders | 201 Created | Idempotency-Key를 받고 Location 반환 |
| 주문 상세 | GET /v1/orders/{orderId} | 200 OK | 없으면 404 |
| 주문 수정 | PATCH /v1/orders/{orderId} | 200 OK | 부분 수정. 빈 patch는 400 |
| 주문 취소 | POST /v1/orders/{orderId}/cancel | 200 OK | 상태 전이를 일관되게 처리 |
원칙은 “URL에는 명사, 동작은 메서드”입니다. 다만 주문 취소처럼 업무상 상태 전이가 강한 동작은 억지로 DELETE에 맞추기보다 명시적인 하위 동작이 더 읽기 쉬울 수 있습니다. 중요한 것은 예외를 문서화하고 같은 종류의 동작에는 같은 표기를 쓰는 것입니다.
OpenAPI를 설계의 기준으로 삼는다
OpenAPI는 경로, 파라미터, 요청, 응답을 도구가 읽을 수 있는 형태로 쓰는 설계 문서입니다. Claude Code에 “이 OpenAPI 계약을 기준으로 구현하라”고 전달하면 구현의 자유도를 필요한 범위로 줄일 수 있습니다.
openapi: 3.1.0
info:
title: Acme Orders API
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/v1/orders:
get:
operationId: listOrders
summary: List orders
parameters:
- $ref: "#/components/parameters/StatusParam"
- $ref: "#/components/parameters/LimitParam"
- $ref: "#/components/parameters/AfterParam"
responses:
"200":
description: Orders are returned in a stable cursor order.
content:
application/json:
schema:
$ref: "#/components/schemas/OrderListResponse"
"400":
$ref: "#/components/responses/BadRequest"
post:
operationId: createOrder
summary: Create an order
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderRequest"
responses:
"201":
description: Order created.
headers:
Location:
schema:
type: string
description: URL of the created order.
content:
application/json:
schema:
$ref: "#/components/schemas/OrderResponse"
"400":
$ref: "#/components/responses/BadRequest"
"409":
$ref: "#/components/responses/Conflict"
/v1/orders/{orderId}:
get:
operationId: getOrder
summary: Get one order
parameters:
- $ref: "#/components/parameters/OrderId"
responses:
"200":
description: Order found.
content:
application/json:
schema:
$ref: "#/components/schemas/OrderResponse"
"404":
$ref: "#/components/responses/NotFound"
patch:
operationId: updateOrder
summary: Update mutable order fields
parameters:
- $ref: "#/components/parameters/OrderId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateOrderRequest"
responses:
"200":
description: Order updated.
content:
application/json:
schema:
$ref: "#/components/schemas/OrderResponse"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
components:
parameters:
OrderId:
name: orderId
in: path
required: true
schema:
type: string
StatusParam:
name: status
in: query
schema:
type: string
enum: [draft, paid, canceled]
LimitParam:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
AfterParam:
name: after
in: query
schema:
type: string
description: Cursor returned by the previous response.
IdempotencyKey:
name: Idempotency-Key
in: header
required: false
schema:
type: string
minLength: 16
maxLength: 128
schemas:
CreateOrderRequest:
type: object
required: [customerId, items]
properties:
customerId:
type: string
items:
type: array
minItems: 1
items:
type: object
required: [sku, quantity]
properties:
sku:
type: string
quantity:
type: integer
minimum: 1
UpdateOrderRequest:
type: object
minProperties: 1
properties:
status:
type: string
enum: [draft, paid, canceled]
OrderResponse:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/Order"
OrderListResponse:
type: object
required: [data, pageInfo]
properties:
data:
type: array
items:
$ref: "#/components/schemas/Order"
pageInfo:
type: object
required: [nextCursor, hasMore]
properties:
nextCursor:
type: [string, "null"]
hasMore:
type: boolean
Order:
type: object
required: [id, status, totalCents, createdAt]
properties:
id:
type: string
status:
type: string
totalCents:
type: integer
createdAt:
type: string
format: date-time
ErrorResponse:
type: object
required: [error]
properties:
error:
type: object
required: [code, message, requestId]
properties:
code:
type: string
message:
type: string
requestId:
type: string
details:
type: array
items:
type: object
responses:
BadRequest:
description: Request validation failed.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
Conflict:
description: Idempotency key conflicts with a different request.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
NotFound:
description: Resource was not found.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
이 내용을 openapi.yaml로 저장하고 Claude Code에는 이 계약에서 벗어난 구현을 제안하지 말라고 명시합니다. 이후 npx @redocly/cli lint openapi.yaml 같은 린터로 문법과 참조를 확인할 수 있습니다. OpenAPI에서 TypeScript 타입이나 클라이언트를 생성하더라도, 먼저 사람이 엔드포인트 표의 의미를 검토해야 합니다.
오류 형식과 상태 코드를 통일한다
상태 코드는 HTTP 바깥쪽 신호이고, JSON 오류 본문은 사람과 프로그램이 읽는 상세 정보입니다. 200 OK와 함께 { "success": false }를 반환하면 모니터링, SDK, 재시도 로직이 실패를 성공으로 오해합니다. 입력 오류는 400, 인증 없음은 401, 권한 부족은 403, 리소스 없음은 404, 중복이나 멱등성 충돌은 409로 표를 먼저 고정합니다.
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request body has invalid fields.",
"requestId": "req_01J0RESTAPI001",
"details": [
{
"field": "items[0].quantity",
"reason": "must be greater than or equal to 1"
}
]
}
}
{
"error": {
"code": "IDEMPOTENCY_CONFLICT",
"message": "The same Idempotency-Key was used with a different request body.",
"requestId": "req_01J0RESTAPI002"
}
}
{
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order was not found.",
"requestId": "req_01J0RESTAPI003"
}
}
초보자에게 설명하면 code는 프로그램이 분기할 짧은 이름, message는 로그나 화면에서 사람이 읽는 설명, requestId는 서버 로그를 찾기 위한 번호입니다. details는 필드별 검증 오류가 필요할 때만 추가해도 충분합니다.
페이지네이션, 멱등성, 버전을 모호하게 두지 않는다
목록 API는 처음부터 페이지네이션을 정해야 합니다. 데이터가 적을 때는 모든 주문을 반환해도 동작하지만, 나중에 데이터가 늘어나면 응답 변경이 breaking change가 됩니다. 커서 방식에서는 이전 응답의 nextCursor를 다음 요청의 after에 보냅니다.
{
"data": [
{
"id": "ord_100",
"status": "paid",
"totalCents": 4800,
"createdAt": "2026-06-03T09:00:00Z"
}
],
"pageInfo": {
"nextCursor": "ord_099",
"hasMore": true
}
}
멱등성은 반복 요청이 중복 효과를 만들지 않게 하는 설계입니다. 특히 POST /orders는 네트워크 실패 뒤 클라이언트가 재시도하므로 취약합니다. Idempotency-Key와 요청 본문의 해시를 일정 시간 저장합니다. 같은 키와 같은 본문이면 이전 결과를 반환하고, 같은 키에 다른 본문이면 409를 반환합니다.
버전 관리는 소비자에게 마이그레이션 시간을 줍니다. 필수 응답 필드 제거, 타입 변경, 기본 정렬 변경, 새 필수 요청 필드 추가는 breaking change이며 /v2로 옮겨야 합니다. 선택 응답 필드 추가는 보통 /v1에 남아도 됩니다.
Claude Code에 설계 리뷰를 요청한다
구현 전에는 구체적인 리뷰 프롬프트를 사용합니다.
Review this OpenAPI design.
Check:
- Resource names are nouns
- HTTP methods and status codes match MDN semantics
- 400/401/403/404/409/500 errors share the same JSON shape
- List pagination is unambiguous
- POST creation endpoints that need idempotency keys are identified
- No response change breaks the v1 contract
- Return a table of issues to fix before TypeScript implementation and tests
이렇게 하면 Claude Code가 파일을 수정하기 전에 설계 리뷰어로 동작하기 쉽습니다. 검토한 OpenAPI를 저장한 뒤에 구현을 요청합니다.
자주 생기는 실패
첫 번째 실패는 action-style 엔드포인트입니다. POST /createOrder, POST /updateOrderStatus, GET /deleteOrder가 섞이면 URL만으로 리소스와 동작을 읽기 어렵습니다. 기본은 POST /v1/orders, PATCH /v1/orders/{orderId}, DELETE /v1/orders/{orderId}로 맞추고 예외를 문서화합니다.
두 번째 실패는 상태 코드가 흔들리는 것입니다. 입력 오류도 500, 없는 리소스도 200, 생성 성공도 어떤 때는 200 어떤 때는 201이면 클라이언트가 올바르게 분기할 수 없습니다. 오류 코드뿐 아니라 성공 코드도 고정해야 합니다.
세 번째 실패는 페이지네이션이 모호한 것입니다. page=2가 생성일 순인지 수정일 순인지 정해지지 않으면 새 데이터가 들어올 때 중복이나 누락이 생깁니다. 정렬, 최대 limit, 다음 페이지 존재 여부를 OpenAPI에 씁니다.
네 번째 실패는 검증을 구현 안에만 숨기는 것입니다. 코드에만 quantity >= 1이 있고 OpenAPI에는 없다면 프런트엔드와 생성된 SDK가 같은 조건을 재사용할 수 없습니다. 제약은 명세와 구현 양쪽에 있어야 합니다.
리뷰 체크리스트
- 리소스 이름은 복수 명사로 일관된다
- 메서드는
GET,POST,PATCH,DELETE의미에 맞는다 - 성공 코드는
200,201,204를 구분한다 400,401,403,404,409,500오류 JSON 형식이 같다- 목록 API에는
limit,after,nextCursor,hasMore가 있다 limit의 기본값과 최대값이 문서화되어 있다- 생성 API가 재시도에서 중복 데이터를 만들지 않는다
- breaking change를
/v2로 나누는 기준이 적혀 있다 - 검증 규칙이 OpenAPI에 있다
- Claude Code에 넘기기 전에 엔드포인트 표와 OpenAPI가 일치한다
REST API 설계는 예쁜 URL을 만드는 일이 아니라, 소비자와의 약속을 사람이 읽는 글과 도구가 읽는 명세로 바꾸는 일입니다. Claude Code는 구현 속도를 높이지만, 어떤 약속을 지킬지는 사람이 먼저 결정해야 합니다.
Masa의 주문 API에서 실제로 적용해 보니, Claude Code에 구현을 요청하기 전에 OpenAPI, 엔드포인트 표, 오류 JSON을 먼저 고정한 쪽이 리뷰 재작업을 확실히 줄였습니다. 설계 단계에서 Idempotency-Key와 커서 페이지네이션을 넣어 둔 덕분에 중복 주문과 목록 누락을 나중에 덧붙여 고칠 필요도 줄었습니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Claude Code 권한 세이프티 래더: 통제력을 잃지 않고 allow 넓히기
read-only에서 제한 편집, 검증 명령, deploy 확인까지 권한을 단계적으로 넓히는 방법.
Claude Code Small PR Proof Pack: 작은 PR을 리뷰 가능한 상태로 만드는 증거 세트
Claude Code의 작은 PR에 diff, 검증, 공개 URL, CTA 경로, rollback을 붙이는 실무 체크리스트.
Claude Code 커밋 전 리뷰 게이트: diff, 테스트, 공개 URL, CTA 확인
Claude Code 작업을 커밋하기 전에 diff 범위, build, 공개 URL, Gumroad 링크, 상담 CTA, 테스트 누락과 무관한 파일을 확인하는 방법입니다.