Tips & Tricks (업데이트: 2026. 6. 3.)

Claude Code로 REST API 설계하기: 엔드포인트, 오류, 페이지네이션, 멱등성

Claude Code로 구현 전 REST API를 설계합니다. OpenAPI, 오류 형식, 페이지네이션, 멱등성 체크리스트 포함.

Claude Code로 REST API 설계하기: 엔드포인트, 오류, 페이지네이션, 멱등성

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는 결과가 밀릴 수 있습니다. limitafter를 쓰는 커서 페이지네이션을 사용하고, 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_123200 OK안정적인 정렬의 커서 페이지네이션
주문 생성POST /v1/orders201 CreatedIdempotency-Key를 받고 Location 반환
주문 상세GET /v1/orders/{orderId}200 OK없으면 404
주문 수정PATCH /v1/orders/{orderId}200 OK부분 수정. 빈 patch는 400
주문 취소POST /v1/orders/{orderId}/cancel200 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와 커서 페이지네이션을 넣어 둔 덕분에 중복 주문과 목록 누락을 나중에 덧붙여 고칠 필요도 줄었습니다.

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

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.