Use Cases (Updated: 6/2/2026)

Practical API Design with Claude Code: OpenAPI, Tests, and Breaking Change Checks

Design reliable REST APIs with Claude Code: OpenAPI workflow, mock server, tests, versioning, security, and pitfalls.

Practical API Design with Claude Code: OpenAPI, Tests, and Breaking Change Checks

API design is not just naming attractive URLs. It is the agreement between your team and every client about what can be sent, what comes back, and what happens when something fails.

When that agreement is vague, frontend code, mobile apps, partner integrations, tests, and monitoring all invent their own interpretation. The later you repair the contract, the more expensive the repair becomes.

Claude Code is useful here, but only if you use it as a design reviewer, not just as a code generator. Ask it to draft an OpenAPI contract, challenge the endpoint model, generate mocks and tests, and check whether a change breaks existing clients.

Use primary references while working: the OpenAPI Specification, RFC 9110 HTTP Semantics, JSON Schema docs, and the OWASP API Security Top 10. For implementation depth, continue with Production API Development, API Test Automation, and API Versioning.

What API Design Means

An API is a screen for another program. A human-facing screen can explain itself with labels and layout. An API explains itself through paths, HTTP methods, status codes, JSON field names, schemas, examples, and error payloads.

For beginners, API design means deciding five things before implementation goes too far.

DecisionPlain meaningExample
ResourceThe noun the API exposesorders, customers, invoices
OperationWhat happens to that nounGET, POST, PATCH, DELETE
SchemaThe shape and rules of JSONitems must contain at least one item
ErrorHow failures are reported400, 401, 403, 404, 422 with useful details
CompatibilityHow you avoid breaking clientsAdding a required request field is breaking

REST can sound abstract, but the practical habit is simple: use noun-based URLs and let HTTP methods express the action. Prefer POST /orders over POST /orders/create, and prefer GET /orders/ord_123 over GET /getOrder?id=ord_123.

The Claude Code Workflow

The best workflow is staged. Do not ask Claude Code to design, implement, test, and document everything in one breath. Keep each step reviewable.

flowchart TD
  A["Summarize business rules"] --> B["Draft the OpenAPI contract"]
  B --> C["Review HTTP, schema, and security risks"]
  C --> D["Generate mocks and API tests"]
  D --> E["Check breaking changes in CI"]
  E --> F["Implement, document, and sell the API confidently"]

OpenAPI is a machine-readable contract for HTTP APIs. JSON Schema is the vocabulary for describing JSON shapes and constraints. HTTP status codes are shared semantics for success and failure. Claude Code can help connect these pieces, but the official specs and your tests remain the source of truth.

In Masa’s small verification project, asking for endpoint names first produced a decent list. The problem appeared later: authentication, pagination, idempotency, and error detail had to be patched in after the fact. A better prompt asks for recovery behavior and client failure modes at the same time as the URL model.

Copy-Paste Starter Kit

This starter kit avoids external dependencies so you can focus on API design. It gives you an OpenAPI sample, a mock server, and a tiny breaking-change check.

mkdir api-design-lab
cd api-design-lab
mkdir docs examples
node --version

Create docs/openapi.yaml. The official OpenAPI page now points to a newer published spec, but this example uses 3.1 because common tooling still supports it well.

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0
servers:
  - url: https://api.example.com
paths:
  /v1/orders:
    post:
      summary: Create an order
      operationId: createOrder
      security:
        - bearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrderRequest"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Order"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Problem"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  schemas:
    CreateOrderRequest:
      type: object
      required: [customerId, items]
      properties:
        customerId:
          type: string
          minLength: 3
        items:
          type: array
          minItems: 1
          items:
            type: object
            required: [sku, quantity]
            properties:
              sku:
                type: string
                minLength: 3
              quantity:
                type: integer
                minimum: 1
    Order:
      type: object
      required: [id, status, customerId, total]
      properties:
        id:
          type: string
        status:
          type: string
          enum: [accepted, cancelled]
        customerId:
          type: string
        total:
          type: integer
    Problem:
      type: object
      required: [type, title, status, detail]
      properties:
        type:
          type: string
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        errors:
          type: array
          items:
            type: object

Create examples/mock-server.mjs. It runs with Node only.

import { createServer } from "node:http";
import { randomUUID } from "node:crypto";

function readJson(req) {
  return new Promise((resolve, reject) => {
    let body = "";
    req.on("data", (chunk) => {
      body += chunk;
      if (body.length > 1_000_000) req.destroy(new Error("Body too large"));
    });
    req.on("end", () => {
      if (!body) return resolve({});
      try {
        resolve(JSON.parse(body));
      } catch (error) {
        reject(error);
      }
    });
    req.on("error", reject);
  });
}

function send(res, status, body, headers = {}) {
  res.writeHead(status, {
    "content-type": "application/json; charset=utf-8",
    "x-content-type-options": "nosniff",
    ...headers,
  });
  res.end(JSON.stringify(body, null, 2));
}

function problem(status, title, detail, errors = []) {
  return {
    type: "https://example.com/problems/request",
    title,
    status,
    detail,
    errors,
  };
}

function validateOrder(input) {
  const errors = [];
  if (typeof input.customerId !== "string" || input.customerId.length < 3) {
    errors.push({
      path: "customerId",
      message: "customerId must be a string with 3+ characters",
    });
  }
  if (!Array.isArray(input.items) || input.items.length === 0) {
    errors.push({ path: "items", message: "items must contain at least one item" });
  }
  for (const [index, item] of (input.items ?? []).entries()) {
    if (typeof item.sku !== "string" || item.sku.length < 3) {
      errors.push({
        path: `items.${index}.sku`,
        message: "sku must be a string with 3+ characters",
      });
    }
    if (!Number.isInteger(item.quantity) || item.quantity < 1) {
      errors.push({
        path: `items.${index}.quantity`,
        message: "quantity must be a positive integer",
      });
    }
  }
  return errors;
}

const server = createServer(async (req, res) => {
  const url = new URL(req.url ?? "/", "http://localhost");

  if (req.method === "GET" && url.pathname === "/health") {
    return send(res, 200, { ok: true });
  }

  const customerMatch = url.pathname.match(/^\/v1\/customers\/([a-z0-9-]+)$/);
  if (req.method === "GET" && customerMatch) {
    return send(res, 200, {
      id: customerMatch[1],
      name: "Aki Tanaka",
      plan: "pro",
    });
  }

  if (req.method === "POST" && url.pathname === "/v1/orders") {
    const idempotencyKey = req.headers["idempotency-key"];
    if (!idempotencyKey) {
      return send(
        res,
        400,
        problem(400, "Missing Idempotency-Key", "POST /v1/orders requires the header.")
      );
    }

    try {
      const body = await readJson(req);
      const errors = validateOrder(body);
      if (errors.length > 0) {
        return send(res, 422, problem(422, "Invalid request body", "Fix errors.", errors));
      }
      return send(
        res,
        201,
        {
          id: `ord_${randomUUID()}`,
          status: "accepted",
          customerId: body.customerId,
          total: 4200,
        },
        { location: "/v1/orders/example" }
      );
    } catch {
      return send(res, 400, problem(400, "Malformed JSON", "Request body must be JSON."));
    }
  }

  return send(res, 404, problem(404, "Not found", `${req.method} ${url.pathname} is undefined.`));
});

server.listen(3000, () => {
  console.log("Mock API running at http://localhost:3000");
});

Run it, then call it from another terminal.

node examples/mock-server.mjs
curl -i http://localhost:3000/health
curl -i -X POST http://localhost:3000/v1/orders \
  -H "content-type: application/json" \
  -H "idempotency-key: demo-001" \
  -d '{"customerId":"cus_123","items":[{"sku":"book-1","quantity":2}]}'
curl -i -X POST http://localhost:3000/v1/orders \
  -H "content-type: application/json" \
  -H "idempotency-key: demo-002" \
  -d '{"customerId":"x","items":[]}'

Create examples/contract-check.mjs. It intentionally fails so you can see the breaking changes.

import assert from "node:assert/strict";

const previous = {
  paths: {
    "/v1/orders": {
      post: {
        request: {
          required: ["customerId", "items"],
          properties: ["customerId", "items", "couponCode"],
        },
        response: {
          required: ["id", "status", "customerId", "total"],
          properties: ["id", "status", "customerId", "total"],
        },
      },
    },
  },
};

const next = structuredClone(previous);
next.paths["/v1/orders"].post.request.required.push("shippingAddress");
next.paths["/v1/orders"].post.response.properties =
  next.paths["/v1/orders"].post.response.properties.filter((name) => name !== "total");

function diffContract(oldSpec, newSpec) {
  const breaking = [];
  for (const [path, methods] of Object.entries(oldSpec.paths)) {
    for (const [method, oldOperation] of Object.entries(methods)) {
      const newOperation = newSpec.paths[path]?.[method];
      if (!newOperation) {
        breaking.push(`${method.toUpperCase()} ${path} was removed`);
        continue;
      }

      const oldRequired = new Set(oldOperation.request.required);
      for (const field of newOperation.request.required) {
        if (!oldRequired.has(field)) {
          breaking.push(`${method.toUpperCase()} ${path} now requires "${field}"`);
        }
      }

      const newResponseFields = new Set(newOperation.response.properties);
      for (const field of oldOperation.response.properties) {
        if (!newResponseFields.has(field)) {
          breaking.push(`${method.toUpperCase()} ${path} removed response "${field}"`);
        }
      }
    }
  }
  return breaking;
}

const breaking = diffContract(previous, next);
console.log(breaking.join("\n") || "No breaking changes found");
assert.equal(breaking.length, 0, "Breaking API changes detected");
node examples/contract-check.mjs

A failing result is the expected result here. The script catches a new required request field and a removed response field, which are exactly the kind of changes you want CI to block before release.

Prompts for Claude Code

Use separate prompts for drafting, reviewing, generating examples, and checking compatibility. That keeps the work inspectable.

claude -p "
Create an OpenAPI draft at docs/openapi.yaml for an e-commerce orders API.
Resources: customers, orders, invoices.
Include summary, operationId, requestBody, responses, examples, and bearerAuth.
Use OpenAPI 3.1 and JSON Schema style constraints.
"
claude -p "
Review docs/openapi.yaml as an API design reviewer.
Return findings first and do not edit files yet.
Check RFC 9110 method/status semantics, vague schemas, pagination,
idempotency, authentication, and common OWASP API Security risks.
"
claude -p "
Generate a Node.js mock server and API test examples from docs/openapi.yaml.
Cover success, auth failure, validation failure, and missing resource cases.
Keep long lines under 150 characters and add README commands.
"
claude -p "
Compare the current docs/openapi.yaml with HEAD.
List breaking changes before suggesting edits.
Check removed paths, new required fields, removed response fields,
changed status codes, and changed auth scopes.
"

This style treats Claude Code output as review material. That is the right posture for API contracts. Speed matters, but client compatibility matters more.

Realistic Use Cases

The first use case is a SaaS orders API. Admin screens, billing jobs, email notifications, and accounting exports may all consume the same Order model. If total, currency, tax handling, and cancellation states are vague, every downstream integration pays the price.

The second use case is a mobile profile API. Old app versions can remain in the wild for months. Removing a response field or changing an enum meaning can crash clients that cannot be upgraded immediately. Additive changes and deprecation windows matter.

The third use case is a B2B partner API. External developers do not know your internal conventions. They need error codes, rate limits, retry guidance, a sandbox, and stable examples. Without those, support tickets become the documentation.

The fourth use case is an internal admin API. Internal does not mean harmless. Object-level authorization still matters: a user should not fetch another tenant’s order just because they found an ID. This is one of the practical lessons behind OWASP API security guidance.

Pitfalls and Failure Cases

A common failure is stuffing verbs into paths: /cancelOrder, /getUserOrders, and /updateOrderStatus. This grows into overlapping endpoints with inconsistent names. Model resources first, then use methods and subresources where needed, such as POST /orders/{id}/cancellation.

Another failure is returning HTTP 200 for every business error. That makes monitoring, retries, SDK behavior, and client error handling harder. Use status codes to communicate meaning: 400 for malformed input, 401 for missing authentication, 403 for forbidden actions, 404 for unknown resources, and 422 for semantically invalid requests.

Forgetting retry safety on POST is also expensive. Order creation and payment initiation often get retried after a timeout. Design an idempotency key before launch so the server can recognize a repeated attempt.

Schema ambiguity causes slow bugs. Decide whether null means “clear this value” or “unknown.” Define required fields, minimum lengths, page limits, date formats, and additionalProperties behavior instead of relying on examples alone.

Finally, do not treat response cleanup as harmless. Removing a field can be a production incident for a client. Plan a deprecation period, migration notes, contract tests, and a clear versioning policy.

Versioning, Errors, Schema, and Security

Versioning is more than adding /v1 to the path. Teams need a shared rule for what counts as breaking. Adding an optional response field is often safe. Adding a required request field, removing a response field, changing status semantics, tightening auth scopes, or changing the meaning of an enum can break existing clients.

Error design should tell the caller what to do next. Invalid request is not enough. Include a stable shape with type, title, status, detail, and field-level errors where useful. Do not leak stack traces, SQL, internal IDs, or secrets in production responses.

Schema design should encode constraints, not just examples. A schema with only type: string is weak. Document ID formats, minimum lengths, array limits, pagination defaults, and timezone rules so clients can validate before sending.

Security design separates authentication from authorization. Authentication answers “who is this?” Authorization answers “may this actor access this object?” Bearer tokens do not protect you if GET /orders/{id} returns another tenant’s order. Avoid API keys in query strings, limit sensitive response data, rate limit risky operations, and keep audit logs for important actions.

Monetization and Consulting CTA

Readers searching for API design are often close to a real project. They may be preparing a public integration, stabilizing a mobile backend, or writing review rules for a team. This article should not just inform them; it should show that the workflow is concrete enough to hire.

ClaudeCodeLab can help with Claude Code API design reviews, OpenAPI cleanup, test automation, and breaking-change checks. Teams can start with training and consulting. Individual builders can use the free resources and adapt the prompts in this article.

The CTA works because the article includes working code, a failing contract check, official references, and implementation links. A reader can see the difference between generic AI advice and a practical engineering workflow.

What I Verified

For this article, I ran the mock server with Node v24.14.1. GET /health returned 200, a valid POST /v1/orders returned 201, and an empty items array returned 422. The contract check failed intentionally and printed the new required request field plus the removed response field. That gives readers a small but real path from Claude Code prompts to OpenAPI, mocks, errors, and CI-friendly compatibility checks.

#claude-code #api #rest #openapi #design
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.