Skip to content

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.

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

Section titled “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

Section titled “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 for placement.

## 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?

Section titled “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.

  2. Lint the contract, then sign it off.

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

    Terminal window
    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.

  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.

    Terminal window
    ! 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 generates requests from docs/openapi.yaml, including invalid ones, and checks every response against the declared schema and status codes:

    Terminal window
    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.

  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?

Section titled “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.

GateCatchesCommand or fileRuns
Spectral lintInconsistent names, missing error responses, undocumented operationsnpx @stoplight/spectral-cli lint docs/openapi.yamlStop hook, CI
Integration testsWrong status codes, broken validation, the 401/403 matrixnpm run test:apiStop hook, CI
Auth-coverage testA route registered without partnerAuth or a scope checktests/routes/auth-coverage.test.tsCI
SchemathesisResponses that do not match the contract; 500s on odd inputuvx schemathesis run … --checks allCI, against a started server
/security-reviewInjection, secrets in logs, missing authorization on a data pathIn the sessionBefore the pull request
Human sign-offWhether the contract is the right productThe docs/openapi.yaml diffPull 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:

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”

Section titled “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:

#!/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:

{
"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.

A reviewer subagent that checks the contract, not the style

Section titled “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:

---
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.

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

Section titled “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.

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 so Claude can check indexes and query plans against the real schema instead of guessing.

Webhooks, rate limits and pagination without surprises

Section titled “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.

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?

Section titled “What breaks when Claude Code writes your API?”
SymptomCauseRecovery
Three different error shapes across endpointsNo single error type in context; each slice invented oneAdd 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 authThe route was added in a later slice and the prompt did not repeat the ruleThe 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 500sTests cover the inputs Claude thought of; the fuzzer sends the ones it did notPaste 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 contractKeep 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.
Claude weakens or deletes a failing testThe prompt rewarded green, not correctSay “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 resourcesContext fills with earlier slices and test outputOne 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 dataBounds were guessed, not taken from the source systemAsk for the bounds as open questions in the contract prompt, and answer them from the partner’s sample payloads before sign-off.

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.

Planning the feature before the contract? Start with feature planning in Claude Code. Working in Codex instead? See API development in Codex.