Tips & Tricks (Updated: 6/3/2026)

REST API Design with Claude Code: Endpoints, Errors, Pagination, Idempotency

Design REST APIs with Claude Code before coding: OpenAPI, errors, pagination, idempotency, and review checks.

REST API Design with Claude Code: Endpoints, Errors, Pagination, Idempotency

A REST API is not finished just because you created a few routes. Before implementation, decide what counts as a resource, which HTTP method each action uses, what the API returns when it fails, how lists are paginated, and how repeated requests are handled. If those choices stay vague, Claude Code can still write code quickly, but frontend, mobile, and partner integrations will each infer a different contract.

This article focuses on using Claude Code to lock the REST API design before implementation. REST stands for Representational State Transfer, but for practical work you can read it as a rule set for operating on resources such as orders, users, and invoices over HTTP. A resource is the thing you handle, a method is the operation, and a status code is the result signal.

For official references, use MDN’s HTTP request methods and HTTP response status codes, plus the OpenAPI Specification. As of June 3, 2026, the OpenAPI latest page resolves to 3.2.0; the sample below uses openapi: 3.1.0 because many linters and generators still handle it more predictably.

For related implementation work, read production API development with Claude Code, Claude Code API testing, and Claude Code code review. If you want reusable templates and checklists, see /products/. If you want to standardize API design reviews across a team, see /training/.

Align The Vocabulary First

Beginners often struggle because REST API design terms sound similar. Before asking Claude Code to implement anything, make sure the team uses these words consistently.

TermPlain explanationExample in this article
ResourceThe noun the API handles; the target of create, read, update, and delete workorders, customers
EndpointA URL plus an HTTP method for entering a resourceGET /v1/orders
MethodThe HTTP verb that says what you want to doGET, POST, PATCH
Status codeA numeric signal for success, bad input, missing auth, and so on200, 201, 400, 404
ValidationChecks that the request has the right shapeEmail format, minimum quantity
IdempotencyA property where repeated identical operations reach the same final stateCreating one order for the same key

Claude Code is good at reading context, but it cannot guarantee the design meaning you had in mind. A request like “build the order creation API” could become /createOrder, /orders/create, or POST /orders. Passing vocabulary and design rules first is the simplest way to improve the generated implementation.

Decide Through Three Use Cases

The same orders API has different design priorities depending on how it is used.

The first use case is B2B SaaS order ingestion. If a partner retries the same nightly CSV job, POST /v1/orders needs an Idempotency-Key so the same request does not create duplicate orders. Retry behavior appears during incident recovery, so idempotency is hard to bolt on later.

The second use case is an admin list API. A staff member may filter by status=paid, move to the next page, and another staff member may add an order at the same time. Plain page=2 can drift. Use cursor pagination with limit and after, and fix the sort order, for example createdAt desc, id desc.

The third use case is a mobile app API. If old app versions stay installed for months, response field names cannot change abruptly. New required fields or a new error envelope should become /v2, while the /v1 contract remains in OpenAPI. Small optional response fields usually do not require a new version.

A fourth common case is an internal service API. It is easier to upgrade all consumers, but monitoring and alerts are often weaker. Even internally, inconsistent status codes and error shapes force every caller to write custom exception handling.

See The Design Flow

Freeze the design in this order before giving the task to Claude Code. It keeps the implementation from drifting while files are being edited.

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"]

Freeze The Endpoint Table Before Coding

The first deliverable is not code. It is an endpoint table. Give this table to Claude Code so routing, OpenAPI, tests, and docs all start from the same contract.

PurposeMethod and pathSuccessImportant rule
List ordersGET /v1/orders?status=paid&limit=20&after=ord_123200 OKCursor pagination with a stable sort
Create orderPOST /v1/orders201 CreatedAccept Idempotency-Key and return Location
Get orderGET /v1/orders/{orderId}200 OKReturn 404 when missing
Update orderPATCH /v1/orders/{orderId}200 OKPartial update; empty patch is 400
Cancel orderPOST /v1/orders/{orderId}/cancel200 OKTreat business state transitions consistently

The usual rule is “nouns in URLs, verbs in methods.” Some business actions, such as canceling an order, are clearer as an explicit sub-action than as a forced DELETE. The key is to write the exception down and keep the same pattern for the same kind of operation.

Make OpenAPI The Design Source

OpenAPI is a machine-readable design document for paths, parameters, requests, and responses. When you tell Claude Code “implement this OpenAPI contract as the source of truth,” you narrow the implementation freedom to the right area.

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"

Save this as openapi.yaml and tell Claude Code not to propose implementation that diverges from it. After generation, a linter such as npx @redocly/cli lint openapi.yaml can check syntax and references. If you generate TypeScript types or clients from OpenAPI, review the human meaning of the endpoint table before generating code.

Standardize Errors And Status Codes

The status code is the outer HTTP signal. The JSON body is the detail that humans and programs read. Returning 200 OK with { "success": false } makes monitoring, SDKs, and retry logic treat the failure as success. Fix a table first: invalid input is 400, missing authentication is 401, insufficient permission is 403, a missing resource is 404, and duplication or idempotency conflict is 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"
  }
}

For beginners: code is the short name a program can branch on, message is the explanation a person reads in logs or UI, and requestId is the identifier support uses to find server logs. Add details only when validation errors need field-level explanations.

Do Not Leave Pagination, Idempotency, Or Versioning Vague

List APIs need pagination from the start. Returning every order may work while data is tiny, but once the table grows, changing the response becomes a breaking change. With cursor pagination, the client passes the previous response’s nextCursor as the next after value.

{
  "data": [
    {
      "id": "ord_100",
      "status": "paid",
      "totalCents": 4800,
      "createdAt": "2026-06-03T09:00:00Z"
    }
  ],
  "pageInfo": {
    "nextCursor": "ord_099",
    "hasMore": true
  }
}

Idempotency prevents repeated requests from creating duplicate effects. POST /orders is especially vulnerable because clients retry after network failures. Store the Idempotency-Key and a hash of the request body for a limited time. If the same key and same body arrive again, return the previous result. If the same key arrives with a different body, return 409.

Versioning gives consumers migration time. Removing a required response field, changing a type, changing the default sort order, or adding a new required request field is a breaking change and should move to /v2. Adding an optional response field usually can stay in /v1.

Prompt Claude Code For Design Review

Use a concrete review prompt before implementation.

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

This makes Claude Code act as a design reviewer before it edits files. Ask for implementation only after you save the reviewed OpenAPI.

Common Failure Cases

Failure case 1 is action-style endpoints. If POST /createOrder, POST /updateOrderStatus, and GET /deleteOrder coexist, the URL no longer tells readers the resource and operation clearly. Prefer POST /v1/orders, PATCH /v1/orders/{orderId}, and DELETE /v1/orders/{orderId}, then document exceptions.

Failure case 2 is inconsistent status codes. If bad input returns 500, missing resources return 200, and successful creation alternates between 200 and 201, clients cannot branch reliably. Standardize success codes as well as error codes.

Failure case 3 is ambiguous pagination. If page=2 does not define whether the order is by creation time or update time, new data can cause duplicates or skipped rows. Document sort order, max limit, and how to detect the next page.

Failure case 4 is hiding validation inside implementation only. If the code has quantity >= 1 but OpenAPI does not, frontend code and generated SDKs cannot reuse the rule. Put constraints in both the specification and the implementation.

Review Checklist

  • Resource names use plural nouns consistently
  • Methods match GET, POST, PATCH, and DELETE semantics
  • Success codes distinguish 200, 201, and 204
  • 400, 401, 403, 404, 409, and 500 errors share one JSON shape
  • List APIs include limit, after, nextCursor, and hasMore
  • Default and maximum limit values are documented
  • Create endpoints cannot duplicate data during retries
  • Criteria for moving breaking changes to /v2 are written down
  • Validation rules appear in OpenAPI
  • The endpoint table and OpenAPI agree before Claude Code receives the task

REST API design is not about making pretty URLs. It is about turning promises to consumers into readable text and machine-readable specifications. Claude Code can increase implementation speed, but humans still need to decide which promises the system must keep.

When Masa tried this on an orders API, putting the OpenAPI file, endpoint table, and error JSON in place before asking Claude Code to implement clearly reduced review rework. Adding Idempotency-Key and cursor pagination during design also avoided later patches for duplicate orders and missing list records.

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

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.