# Build REST and GraphQL APIs with Claude Code

URL: https://developertoolkit.ai/en/claude-code/build/api-development/
Description: Build REST and GraphQL APIs with Claude Code: contract in plan mode, one resource per slice, and spec, fuzz and auth gates that prove it works.

Claude Code builds REST and GraphQL APIs reliably when the contract comes first: an OpenAPI file or GraphQL schema agreed in plan mode, then one resource at a time implemented against it, tests first. The contract is what a human reviews. Spec linting, schema-conformance fuzzing, an auth-coverage test and a Stop hook prove the handlers match it.

A partner needs twelve endpoints, OAuth2 client credentials, rate limits and signed webhooks in two weeks. You ask Claude Code for "the partner API" and get 2,000 lines in one go: three different error shapes, one route without auth, and an OpenAPI file that describes what you asked for rather than what shipped. This page is for **developers** building API endpoints in the terminal with Claude Code, and for **tech leads** who want every API change in the repository to pass the same gates without a line-by-line review.

**Checked against Claude Code 2.1.283**

Commands and flags were checked on 2026-09-26 against the installed `claude --help` (v2.1.283, the `latest` channel), the Claude Code hooks reference, Spectral CLI 6.16.3 (`@stoplight/spectral-cli`) and Schemathesis 4.28.0. The `stable` channel was on v2.1.274 that day; nothing on this page depends on the difference.

## What you'll walk away with from building APIs in Claude Code

- A contract-first loop: design in plan mode, sign off the spec, implement one resource per slice.
- Five copy-paste prompts: contract design, a test-first resource slice, an auth-coverage test, a GraphQL N+1 fix with a query-count assertion, and signed webhook delivery.
- A gate stack that proves conformance without reading every handler: Spectral, Schemathesis, an auth-coverage test and a Stop hook.
- A read-only reviewer subagent for API contracts that you can commit today.
- The failure modes of agent-written APIs, each with its recovery.

## Pin your API conventions before the first prompt

Claude Code reads `CLAUDE.md` at the start of every session. Put the decisions an API reviewer checks by hand into it, so the agent never has to guess them and every slice uses the same answer. See [how Claude Code's memory files load](/en/claude-code/build/memory-system/) for placement.

```markdown
## API conventions (CLAUDE.md)
- Contract: docs/openapi.yaml is the source of truth. Change it first, in its own commit.
- Stack: Express + TypeScript, Zod for validation, Vitest + supertest for tests.
- Every error response uses ErrorResponse from src/types/api.ts:
  { "error": "UPPER_SNAKE_CODE", "message": string, "requestId": string, "details"?: [...] }
- Middleware order on every route: requestId -> auth -> rateLimit -> validate -> handler.
- Every route under /api/v1/partner/ requires partnerAuth and requireScope(...).
  Exceptions: GET /health and POST /oauth/token. No others.
- Lists use cursor pagination: { data, pagination: { nextCursor, hasMore } }. No offset.
- Strings need maxLength, numbers min/max, arrays maxItems. No unbounded input.
- Never log request bodies, tokens or Authorization headers.
```

The last three rules are the ones a reviewer otherwise has to catch by hand, so write them down.

## How does Claude Code build an API you can trust?

The order matters more than the prompts. Each step produces something you can check before the next one starts.

1. **Design the contract in plan mode.**

   Start with `claude --permission-mode plan`, or type `/plan` in a running session. In plan mode Claude Code reads the codebase and proposes, but does not edit, so you argue about resource names and status codes before any handler exists. The wider method, of which this is the API case, is [spec-driven development](/en/workflows/plan/spec-driven-development/).

   **Copy-paste prompt: design the API contract**

   ```text
   Read CLAUDE.md, src/routes/ and src/types/api.ts. Design the partner API
   as an OpenAPI 3.1 document. The partner must be able to:
   - list products filtered by category, price range and availability
   - create, read and cancel orders on behalf of their customers
   - register webhooks for order.status_changed
   - authenticate with OAuth2 client credentials (scopes: products:read,
     orders:write, webhooks:manage)

   For every operation give: method and path, parameters with bounds,
   request and response schemas, every error status with the ErrorResponse
   shape, the required scope, and the rate-limit tier (standard 100/min,
   elevated 1000/min). Reuse schemas with $ref instead of repeating them.
   List the open questions you had to guess at. Do not write handler code.
   ```

2. **Lint the contract, then sign it off.**

   Save the approved design to `docs/openapi.yaml` and run Spectral on it in the terminal:

   ```bash
   npx @stoplight/spectral-cli lint docs/openapi.yaml --fail-severity warn
   ```

   Spectral needs a ruleset. Create `.spectral.yaml` containing `extends: ["spectral:oas"]` for the built-in OpenAPI rules, then add your own. This contract is the one artifact a human reads in full: resource names, status codes, scopes and the answers to Claude's open questions. Commit it on its own, so every later diff to it is visible in review.

3. **Implement one resource per slice, tests first.**

   Leave plan mode with Shift+Tab, which cycles the permission modes, and implement in auto mode or `acceptEdits`. With v2.1.283 or later (both release channels on 2026-10-12), auto mode is the starting mode for interactive sessions on every plan and provider, so you often land there without choosing it. The exceptions are sessions on models auto mode does not support (for example a default Microsoft Foundry session) and sessions where a settings file sets `disableAutoMode`; if you start in Manual, pick auto or `acceptEdits` with Shift+Tab. Implement products before orders. A slice small enough to finish in one session keeps the context clean and gives you a green checkpoint to return to with `/rewind` if the next slice goes wrong.

   **Copy-paste prompt: one resource slice, tests first**

   ```text
   Implement the products operations from docs/openapi.yaml, nothing else.
   1. Write the supertest integration tests first in tests/routes/products.test.ts:
      for each operation cover 200, 400 (one case per bounded field),
      401 no token, 403 wrong scope, 404 unknown id, and 429 over the limit.
      Run them and show me that they fail.
   2. Then write the Zod schemas in src/schemas/products.schema.ts, derived
      from the OpenAPI schemas, and the routes in src/routes/products.ts,
      following the middleware order in CLAUDE.md.
   3. Run the tests until they pass. Do not edit a test to make it pass;
      if a test is wrong, stop and tell me why.
   4. Finish with the list of files you changed and the test output.
   ```

4. **Exercise the running API.**

   Start the dev server, then check the slice yourself in the same session. Starting a line with `!` runs it directly in shell mode, without Claude interpreting or approving the command. The command and its output land in the context, so Claude can respond to what it shows. Keep real tokens out of these commands, because the output stays in the transcript.

   ```bash
   ! curl -s -w '\n%{http_code}\n' "http://localhost:3000/api/v1/products?minPrice=-1" \
       -H "Authorization: Bearer $API_TEST_TOKEN"
   ```

   A single curl call proves one path. Step 5 proves the rest.

5. **Fuzz the implementation against the contract.**

   [Schemathesis](https://github.com/schemathesis/schemathesis) generates requests from `docs/openapi.yaml`, including invalid ones, and checks every response against the declared schema and status codes:

   ```bash
   uvx schemathesis run docs/openapi.yaml --url http://localhost:3000 \
     --checks all -H "Authorization: Bearer $API_TEST_TOKEN" --max-failures 10
   ```

   Paste the failures back to Claude with "fix the implementation, not the spec, unless the spec is wrong; say which". Every failure is either a handler bug or a contract decision that needs a human. For contract tests between services and a wider API test strategy, see [API test automation](/en/workflows/test/api-testing/).

6. **Review by evidence, not by reading every handler.**

   Run `/code-review` and `/security-review` on the slice, and hand the contract check to the reviewer subagent in the next section. You read the contract diff, the test list and the gate output. You open a handler only where a gate or reviewer points.

## Which gates prove the API works without reading every handler?

Each gate catches a class of bug that a human reviewer misses when skimming 2,000 lines. Run the first four in CI on every pull request; the tech lead owns the gate list and signs off contract changes.

| Gate | Catches | Command or file | Runs |
| --- | --- | --- | --- |
| Spectral lint | Inconsistent names, missing error responses, undocumented operations | `npx @stoplight/spectral-cli lint docs/openapi.yaml` | Stop hook, CI |
| Integration tests | Wrong status codes, broken validation, the 401/403 matrix | `npm run test:api` | Stop hook, CI |
| Auth-coverage test | A route registered without `partnerAuth` or a scope check | `tests/routes/auth-coverage.test.ts` | CI |
| Schemathesis | Responses that do not match the contract; 500s on odd input | `uvx schemathesis run … --checks all` | CI, against a started server |
| `/security-review` | Injection, secrets in logs, missing authorization on a data path | In the session | Before the pull request |
| Human sign-off | Whether the contract is the right product | The `docs/openapi.yaml` diff | Pull request |

The auth-coverage test is the one worth writing on day one, because a route without auth passes every other gate. Make it enumerate the routes the app actually registers, so a new route is covered without anyone remembering to add it:

**Copy-paste prompt: an auth-coverage test that cannot be forgotten**

```text
Write tests/routes/auth-coverage.test.ts. Build the Express app the way
src/app.ts does, walk its router stack, and collect every registered
method + path with the names of its middleware functions. Fail if any
route under /api/v1/partner/ lacks partnerAuth or requireScope, or if a
route outside the allowlist in CLAUDE.md (GET /health, POST /oauth/token)
is unauthenticated. Print the offending routes in the failure message.
Then add a temporary unauthenticated route, show me the test failing,
and remove the route again.
```

The last instruction matters: a gate you have never seen fail is a gate you have not tested.

### Make Claude Code run the gates before it says "done"

A Stop hook runs when Claude tries to finish its turn. If the gates fail, the hook blocks the stop and hands the output back, so "done" means the gates are green. Save this as `.claude/hooks/api-gates.sh` and make it executable:

```bash
#!/usr/bin/env bash
# .claude/hooks/api-gates.sh (Stop)
set -uo pipefail
cd "$CLAUDE_PROJECT_DIR" || exit 0
retrying=$(jq -r '.stop_hook_active')
[[ -z $(git status --porcelain -- docs/openapi.yaml src tests) ]] && exit 0

if out=$(npx @stoplight/spectral-cli lint docs/openapi.yaml --fail-severity warn 2>&1 \
         && npm run --silent test:api 2>&1); then exit 0; fi

if [[ $retrying == "true" ]]; then
  jq -nc '{systemMessage: "API gates still fail after one retry. Stopping for a human."}'
  exit 0
fi
jq -nc --arg r "API gates fail. Fix them before you finish. Last 40 lines:
$(tail -n 40 <<<"$out")" '{decision: "block", reason: $r}'
```

The hook only runs the gates when the spec, `src` or `tests` have uncommitted changes, so if Claude commits mid-turn it passes without checking anything. CI is the backstop for committed work.

Register it in `.claude/settings.json`. The exec form (`command` plus `args`) passes the script path as one argument, so a project path with spaces still works:

```json
{
  "hooks": {
    "Stop": [
      { "hooks": [{ "type": "command", "command": "bash",
                    "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/api-gates.sh"], "timeout": 300 }] }
    ]
  }
}
```

The script reads `stop_hook_active`, which is `true` when Claude is already continuing because of a Stop hook, and gives up after one retry. Claude Code also caps Stop-hook continuations at eight in a row, but a gate that fails twice usually needs a human decision, not more attempts. The `timeout` is in seconds. The pattern and the other hook events are explained in [hooks that enforce your gates](/en/claude-code/automate/hooks-automation/).

### A reviewer subagent that checks the contract, not the style

A subagent reviews in its own context window and reports back a verdict. Give it read-only tools, so it cannot "fix" what it was asked to judge. Save as `.claude/agents/api-contract-reviewer.md`:

```markdown
---
name: api-contract-reviewer
description: Checks that API handlers match docs/openapi.yaml and CLAUDE.md API conventions. Use after implementing or changing an endpoint.
tools: Read, Grep, Glob
---

You review API changes against docs/openapi.yaml and the API conventions in CLAUDE.md.
You never edit files. For every changed route report, with file and line:
- operations in the code that are missing from the spec, and the reverse
- error responses that do not use ErrorResponse
- middleware order that differs from CLAUDE.md
- unbounded inputs (string without maxLength, array without maxItems)
- list endpoints without cursor pagination
End with VERDICT: PASS or VERDICT: FAIL and the count of findings.
```

Treat its verdict as a claim to confirm, not as proof: the gates above are the evidence, and the subagent finds what they do not cover. More patterns are in [custom subagents](/en/claude-code/automate/custom-subagents/).

## How do you build a GraphQL API with the same loop?

The loop is the same with a schema file in place of `docs/openapi.yaml`: write `schema.graphql` in plan mode, sign it off, implement resolvers per type. The failure specific to GraphQL is the N+1 query, which passes every functional test on a small fixture. Make the query count a test assertion, so the fix is proven rather than claimed.

**Copy-paste prompt: fix N+1 resolvers with a query-count test**

```text
Read src/graphql/resolvers/ and the Apollo context setup.
1. Write a test that seeds 20 orders, each with a user and 3 items, runs
   { orders(first: 20) { id user { name } items { product { name } } } }
   and counts the SQL queries through our db client's query event.
   Run it and show me the current count.
2. Add DataLoaders for User, Product and OrderItemsByOrderId, created per
   request in the context (never module-level: that leaks data across users).
3. Assert the query count is at most 4, run the test, and show before and after.
```

Schemathesis also takes a GraphQL schema, either a local `schema.graphql` with `--url` or the endpoint URL itself, so the fuzzing gate carries over. For schema-aware SQL during this work, connect a [database MCP server](/en/ecosystem/mcp/database-mcp/) so Claude can check indexes and query plans against the real schema instead of guessing.

## Webhooks, rate limits and pagination without surprises

These three are where agent-written APIs fail in production rather than in tests, because the bug only shows up under retries, load or large data. Specify the failure behaviour in the prompt, and ask for a test of it.

**Copy-paste prompt: signed webhook delivery with retries**

```text
Implement webhook delivery for order.status_changed, following src/jobs/.
- Payload: { "id": uuid, "event": "order.status_changed", "data": {...},
  "createdAt": ISO-8601 }. The id makes deliveries idempotent for the receiver.
- Sign the raw body with HMAC-SHA256 using the partner's secret; send it as
  X-Webhook-Signature: t=<unix>,v1=<hex>, where the timestamp is part of the
  signed string so a receiver can reject replays older than 5 minutes.
- Queue deliveries; retry on network error or 5xx with backoff at 30 s,
  5 min and 30 min, then mark the delivery failed. Never retry on 4xx.
- Log each attempt (status, duration, attempt number), never the body.
Tests: signature verifies with the secret and fails with a wrong one; a
receiver returning 500 twice then 200 gets exactly 3 attempts; a 410 stops
retries. Show me the test output.
```

For rate limits and pagination, the checks to ask for are specific:

- **Rate limits:** a test that sends limit + 1 requests and asserts `429` with a `Retry-After` header, plus `X-RateLimit-Remaining` on every response. Ask where the counter lives; an in-process counter resets on every deploy and differs per instance.
- **Pagination:** a test that pages through 250 seeded rows with a filter applied and asserts no duplicates and no gaps. Cursor bugs show up only when the sort key has ties, so seed ties on purpose.
- **Performance:** a test that seeds 10,000 rows and records query count and p95 latency for the list endpoint with each filter. A 20-row fixture hides a missing index.

## What breaks when Claude Code writes your API?

| Symptom | Cause | Recovery |
| --- | --- | --- |
| Three different error shapes across endpoints | No single error type in context; each slice invented one | Add the `ErrorResponse` rule to `CLAUDE.md`, then: "grep every `res.status(` and `throw` under src/routes/ and convert each to ErrorResponse; list what you changed." Spectral catches it next time if you add a rule requiring the shared error schema. |
| A route ships without auth | The route was added in a later slice and the prompt did not repeat the rule | The auth-coverage test; if it did not fail, it is not enumerating the real router. Fix the test first, then the route. |
| Tests pass, Schemathesis fails with 500s | Tests cover the inputs Claude thought of; the fuzzer sends the ones it did not | Paste the failing case into the session and ask for a regression test that reproduces it before the fix. |
| Claude edits the spec to match a buggy handler | "Make Schemathesis pass" is satisfiable by weakening the contract | Keep `docs/openapi.yaml` changes in their own commits and require a human approval for that file (a CODEOWNERS entry). Say "fix the implementation, not the spec" in every repair prompt. The general pattern is in [protecting the oracle](/en/workflows/test/protect-the-oracle/). |
| Claude weakens or deletes a failing test | The prompt rewarded green, not correct | Say "do not edit a test to make it pass" (the slice prompt does), and compare the test count before and after in the pull request. |
| The session drifts after several resources | Context fills with earlier slices and test output | One resource per session; start the next with `/clear` or a fresh session. Delegate long test runs to a subagent so only failures come back. |
| Validation rejects real partner data | Bounds were guessed, not taken from the source system | Ask for the bounds as open questions in the contract prompt, and answer them from the partner's sample payloads before sign-off. |

## Where to go next with API development

The cross-tool guide covers the same spec-first method for Codex and Cursor, plus contract testing with Pact and API versioning. Neighbouring Claude Code pages cover the layers beneath and around the API.

- [API development across tools](/en/workflows/build/api-development/) — Spec-first APIs in Claude Code, Codex and Cursor, with contract tests and versioning
- [Database work in Claude Code](/en/claude-code/build/database-work/) — Schemas, migrations and query tuning for the tables behind your endpoints
- [Testing with Claude Code](/en/claude-code/test/testing/) — Test-first loops and coverage that the agent cannot game
- [Documentation from code](/en/claude-code/build/documentation/) — Turn the signed-off contract into partner-facing guides
- [Review automation](/en/claude-code/ship/review-automation/) — Run the reviewer subagent and /code-review on every pull request
- [Reading evidence instead of code](/en/ladder/evidence-not-diffs/) — Why gates and contracts replace line-by-line review

Planning the feature before the contract? Start with [feature planning in Claude Code](/en/claude-code/plan/feature-planning/). Working in Codex instead? See [API development in Codex](/en/codex/build/api-development/).
