Software Engineering Foundations for Claude Integration

Learning Objectives

Pre-Reading Check — REST APIs and JSON

1. Which HTTP method and endpoint does an application use to send a message to Claude?

GET https://api.anthropic.com/v1/messages POST https://api.anthropic.com/v1/messages PUT https://api.anthropic.com/v1/completions POST https://api.anthropic.com/v1/chat

2. Because the Messages API is stateless, what must your application do on every conversation turn?

Resend the entire conversation history in the request Reuse a session ID the server stored on the previous turn Open a persistent connection that the server keeps alive Send only the newest user message and let Claude recall the rest

3. Where should the ANTHROPIC_API_KEY live, and which header carries it?

Hardcoded in source; sent in the anthropic-version header In the request body as a model field In an environment variable; sent in the x-api-key header In a cookie; sent in the content-type header

1. REST APIs and JSON

Key Points

Every request to Claude travels over a REST API using standard HTTP methods and URLs, exchanging data as JSON. REST treats everything the server exposes as a resource addressed by a URL, acted on with a handful of universal verbs.

HTTP methodIntentExample in a Claude app
GETRetrieve a resourceFetch the status of a message batch
POSTCreate a resource / submit workSend a message to Claude
PUT / PATCHReplace / update a resourceUpdate a stored configuration
DELETERemove a resourceCancel a batch job

A minimal JSON request is a top-level object with typed fields: model selects the Claude model, max_tokens caps the response length (and cost), and messages is an ordered array of turns, each with a role (user or assistant) and content. The response is likewise JSON — a content array plus metadata such as token usage and a stop reason.

Because REST inherits HTTP's stateless property, each request must carry everything the server needs — which is why you resend the entire conversation history on every turn rather than relying on the server to remember it.

Three headers matter for a basic call:

HeaderPurpose
x-api-keyYour secret API key, which authenticates and authorizes the request
anthropic-versionThe API version (e.g., 2023-06-01), so Anthropic can evolve the API without breaking you
content-typeDeclares the body format, application/json

The API key is a bearer credential: anyone who holds it can spend from your account. It belongs in an environment variable (ANTHROPIC_API_KEY), never hardcoded and never committed to version control.

Figure 6.1: Stateless request/response flow for a Messages API call

sequenceDiagram participant App as Your App participant API as api.anthropic.com/v1/messages App->>API: POST /v1/messages Note right of App: Headers: x-api-key,
anthropic-version, content-type
Body: full messages array (JSON) API-->>App: 200 OK Note left of API: JSON: content array,
token usage, stop_reason Note over App,API: Stateless: every turn resends
the entire conversation history
Animation slot: an animated request/response arrow between "Your App" and "/v1/messages," showing headers and the JSON payload flowing each direction, then the full history growing on each new turn.
Post-Reading Check — REST APIs and JSON

1. Which HTTP method and endpoint does an application use to send a message to Claude?

GET https://api.anthropic.com/v1/messages POST https://api.anthropic.com/v1/messages PUT https://api.anthropic.com/v1/completions POST https://api.anthropic.com/v1/chat

2. Because the Messages API is stateless, what must your application do on every conversation turn?

Resend the entire conversation history in the request Reuse a session ID the server stored on the previous turn Open a persistent connection that the server keeps alive Send only the newest user message and let Claude recall the rest

3. Where should the ANTHROPIC_API_KEY live, and which header carries it?

Hardcoded in source; sent in the anthropic-version header In the request body as a model field In an environment variable; sent in the x-api-key header In a cookie; sent in the content-type header
Pre-Reading Check — Asynchronous Programming

1. What is the primary benefit of using async/await for Claude API calls?

It reduces the number of tokens each request consumes It lets the program do other work while a request is in flight, instead of blocking It guarantees requests never hit rate limits It encrypts the request body automatically

2. Claude streams responses using which mechanism?

Websockets, a bidirectional persistent connection Long polling with repeated GET requests Server-Sent Events (SSE), a unidirectional server-to-client push gRPC bidirectional streaming

3. When firing many concurrent Claude requests, what guidance avoids tripping rate limits?

Launch every request simultaneously in one large burst Ramp traffic up gradually and keep usage patterns consistent Send all requests over a single websocket to bypass limits Disable retries so failed requests do not repeat

4. With SSE streaming, why must code watch for error events rather than only the initial HTTP status?

SSE never returns an HTTP status code at all The status is 200 the moment the stream opens, so errors can occur mid-stream after that success SSE reports all errors as websocket close frames HTTP status codes are stripped by SSE for security reasons

2. Asynchronous Programming

Key Points

A call to Claude is a network round-trip that can take seconds. If your program simply blocks and waits, it wastes time it could spend serving other users or issuing more requests. async/await lets you write non-blocking code that reads like ordinary sequential code: you mark a function async and await where you need the result, freeing the runtime to run other tasks meanwhile. Anthropic's Python SDK provides AsyncAnthropic (built on httpx); TypeScript calls are promise-based, so await is all you need.

The real payoff is concurrency: firing off many requests that overlap in flight rather than running strictly one after another. Since most of each request's duration is spent waiting on the network, that wait overlaps for free — summarizing 20 documents concurrently (e.g., with Python's asyncio.gather) can be dramatically faster than looping sequentially. The catch: concurrency raises the risk of hitting rate limits, the per-minute caps Anthropic enforces. The guidance is to ramp traffic up gradually and keep usage patterns consistent; sudden bursts produce 429 errors and can trip acceleration limits. Effective concurrency means bounding how many requests are in flight, not launching everything at once.

Websockets vs. SSE — a nuance the exam tests

A websocket is a persistent, bidirectional connection where client and server can send messages at any time — well suited to live multiplayer or collaborative editors. It is tempting to assume Claude streaming uses websockets. It does not. Anthropic's SDKs stream via Server-Sent Events (SSE), a simpler, unidirectional mechanism where the server pushes a sequence of events over a single long-lived HTTP response. The client makes one request; the server streams incremental chunks back down that one connection — a one-way flow, not the two-way conversation a websocket is built for.

FeatureWebsocketServer-Sent Events (SSE) — used by Claude
DirectionBidirectional (both send anytime)Unidirectional (server → client)
ProtocolIts own ws:// / wss:// protocolPlain HTTP response
Claude streamingNot usedYes — this is how Claude streams

One consequence of SSE: because the HTTP status is 200 the moment the stream opens, an error can occur after that success code. Mid-stream errors arrive as SSE error events and do not follow the standard status-code error mechanism, so streaming code must watch for error events within the stream, not just the initial response status.

Animation slot: side-by-side comparison — a websocket showing arrows flowing both directions, versus SSE showing a single one-way stream of tokens from server to client, with a mid-stream error event popping up after the initial 200.
Post-Reading Check — Asynchronous Programming

1. What is the primary benefit of using async/await for Claude API calls?

It reduces the number of tokens each request consumes It lets the program do other work while a request is in flight, instead of blocking It guarantees requests never hit rate limits It encrypts the request body automatically

2. Claude streams responses using which mechanism?

Websockets, a bidirectional persistent connection Long polling with repeated GET requests Server-Sent Events (SSE), a unidirectional server-to-client push gRPC bidirectional streaming

3. When firing many concurrent Claude requests, what guidance avoids tripping rate limits?

Launch every request simultaneously in one large burst Ramp traffic up gradually and keep usage patterns consistent Send all requests over a single websocket to bypass limits Disable retries so failed requests do not repeat

4. With SSE streaming, why must code watch for error events rather than only the initial HTTP status?

SSE never returns an HTTP status code at all The status is 200 the moment the stream opens, so errors can occur mid-stream after that success SSE reports all errors as websocket close frames HTTP status codes are stripped by SSE for security reasons
Pre-Reading Check — SDKs That Wrap REST

1. What does a single client.messages.create(...) call do under the hood?

Opens a websocket and waits for a bidirectional handshake Serializes params to JSON, adds required headers, POSTs to /v1/messages, and deserializes a typed Message Stores the conversation on Anthropic's servers so history need not be resent Sends the API key in the request body instead of a header

2. By default, how do the SDKs handle transient failures like connection errors, 429s, and 5xx?

They fail immediately and require you to write all retry logic They retry forever until the request succeeds They retry twice by default with exponential backoff, honoring the retry-after header They retry once immediately with no delay

3. What is the key difference between a 429 and a 529 error?

429 means your account hit its own limit; 529 means Anthropic's servers are overloaded for everyone 429 is a server bug; 529 is a client bug 429 is fatal and non-retryable; 529 is always retried automatically forever They are identical; the number is chosen at random

4. How should you catch errors raised by the SDK?

String-match on the error message text Catch typed exception classes, most-specific first (e.g., RateLimitError before APIStatusError) Parse the raw JSON error object manually every time Catch only a single generic Exception and ignore the type

3. SDKs That Wrap REST

Key Points

You could hand-build every HTTP request, header, and JSON parse yourself. In practice you use a client SDK — a language-specific library that wraps REST behind idiomatic, typed functions. The Python SDK (anthropic) ships both a synchronous Anthropic client and an async AsyncAnthropic, both on httpx, typed via Pydantic. The TypeScript SDK (@anthropic-ai/sdk) auto-reads the key from ANTHROPIC_API_KEY and runs on Node.js, Deno, Bun, and browsers.

The SDK's central trick is to turn the raw POST /v1/messages into a single typed method call. When you invoke client.messages.create(...), the SDK serializes your typed parameters to JSON, adds the required headers, sends the POST, and deserializes the response into a typed Message — everything from Section 1 handled for you. Streaming is likewise abstracted over SSE. Every response also carries a unique request-id (exposed as _request_id), invaluable for debugging.

Figure 6.2: How the SDK wraps the raw REST call

flowchart LR A["Your code: client.messages.create(...)"] --> B["Serialize typed params to JSON body"] B --> C["Add headers: x-api-key,
anthropic-version, content-type"] C --> D["POST https://api.anthropic.com/v1/messages"] D --> E["Deserialize JSON into typed Message object"] E --> F["Typed Message returned to your code"]

Error handling and retries

Claude returns errors as JSON with a top-level error object (a type and message) plus a request_id. The status-code taxonomy is worth memorizing:

StatusError typeMeaning
400invalid_request_errorMalformed request format or content
401authentication_errorAPI key missing, malformed, revoked, or expired
403permission_errorKey lacks permission for the resource
404not_found_errorResource not found
429rate_limit_errorYour account hit a per-minute limit (RPM / ITPM / OTPM)
500api_errorUnexpected internal Anthropic error
529overloaded_errorAPI temporarily overloaded across all users

A distinction the exam loves: 429 vs. 529. A 429 means your account exceeded its own per-minute limits — the fix is to slow your own traffic. A 529 means Anthropic's servers are temporarily at capacity for everyone, independent of your usage — the fix is to back off and retry, possibly with failover. Same "too busy" feeling, different cause and remedy.

The SDKs make much of this automatic: they retry transient failures — connection errors, 429s, and 5xx — using exponential backoff, twice by default, honoring the retry-after header when present. Exponential backoff means each retry waits longer than the last (roughly doubling). If you hand-roll retries, respect retry-after first, then reset-header math, then jittered exponential backoff — the jitter (small random delay) breaks the "thundering herd." Finally, the SDKs raise typed exceptions (a 404 becomes anthropic.NotFoundError); catch these most-specific-first rather than string-matching.

Figure 6.3: SDK retry and exponential-backoff state flow

stateDiagram-v2 [*] --> Sending Sending --> Success: 200 OK Sending --> Transient: connection error / 429 / 5xx Sending --> Fatal: non-retryable (400, 401, 403, 404) Transient --> RetriesLeft: retries remaining? RetriesLeft --> Wait: yes RetriesLeft --> RaiseException: no (retries exhausted) Wait --> Sending: honor retry-after,
else exponential backoff Success --> [*] Fatal --> RaiseException RaiseException --> [*]: typed exception
Animation slot: a request flowing through the retry state machine — a 429 bounces into a "wait" state whose delay visibly doubles each attempt, until it either succeeds or raises a typed exception after retries are exhausted.
Post-Reading Check — SDKs That Wrap REST

1. What does a single client.messages.create(...) call do under the hood?

Opens a websocket and waits for a bidirectional handshake Serializes params to JSON, adds required headers, POSTs to /v1/messages, and deserializes a typed Message Stores the conversation on Anthropic's servers so history need not be resent Sends the API key in the request body instead of a header

2. By default, how do the SDKs handle transient failures like connection errors, 429s, and 5xx?

They fail immediately and require you to write all retry logic They retry forever until the request succeeds They retry twice by default with exponential backoff, honoring the retry-after header They retry once immediately with no delay

3. What is the key difference between a 429 and a 529 error?

429 means your account hit its own limit; 529 means Anthropic's servers are overloaded for everyone 429 is a server bug; 529 is a client bug 429 is fatal and non-retryable; 529 is always retried automatically forever They are identical; the number is chosen at random

4. How should you catch errors raised by the SDK?

String-match on the error message text Catch typed exception classes, most-specific first (e.g., RateLimitError before APIStatusError) Parse the raw JSON error object manually every time Catch only a single generic Exception and ignore the type
Pre-Reading Check — Engineering Practices and the SDLC

1. What does "treat prompts as code" mean for a Claude integration?

Prompts should be generated at runtime and never stored Prompts belong in Git (e.g., a prompts/ directory) so they are diffable, reviewable, and revertible Prompts should be compiled into a binary for security Prompts should be embedded as hardcoded strings scattered through the code

2. What is the non-negotiable version-control rule for the ANTHROPIC_API_KEY?

Commit it so teammates can share one key easily Store it in a public config file for transparency Never commit it — keep it in the environment and .gitignore the .env Encode it in base64 in the README

3. What is the guiding principle of refactoring a Claude integration?

Spread model IDs and retry logic across as many files as possible Isolate the integration behind a thin, well-typed layer that centralizes model IDs, prompts, retry policy, and timeouts Inline every prompt string directly at each call site Avoid abstractions so each call is fully self-contained

4. How can Claude itself participate in code review within a CI pipeline?

By running in non-interactive mode, e.g., feeding a diff to a structured review prompt By automatically merging any PR that passes a single test By storing the API key in the CI logs for auditing It cannot; Claude only works interactively in a chat window

4. Engineering Practices and the SDLC

Key Points

Version control (Git in practice) records the full change history of a codebase. The distinctive move for Claude integrations is to treat prompts as code: prompt templates are development artifacts, so they belong in the repo — typically in a prompts/ directory, organized by feature, one prompt per file. This makes a prompt change reviewable, diffable, revertible, and testable exactly like source code. Concrete habits: write meaningful commit messages (what and why), use branches to explore prompt/model variations, and use Git as a state log with checkpoints so you can cleanly revert an AI-generated change that does not pan out.

A non-negotiable: never commit secrets. Keep ANTHROPIC_API_KEY in environment variables and add .env to .gitignore. The SDKs read the key from the environment by default, which makes this painless.

Code review should cover the LLM-specific concerns alongside the usual ones: correct handling of the full status-code taxonomy (401/403/429/500/529), retry and backoff configuration, timeout handling, no hardcoded secrets, prompt-injection and untrusted-input handling, and cost/token controls such as max_tokens and batching. Claude can also participate: running the SDK or CLI in non-interactive mode lets you wire it into CI pipelines and pre-commit hooks — for example, feeding a diff to a structured security-review prompt to flag vulnerabilities before merge.

Refactoring behind a thin typed layer

Refactoring improves internal structure without changing external behavior. The guiding principle is to isolate the integration behind a thin, well-typed abstraction layer, so model IDs, prompts, retry policy, and timeouts are centralized and swappable. The analogy is a building's electrical panel: route everything through one panel with labeled breakers, so changing a fuse — swapping claude-sonnet-5 for a newer model, tightening a timeout, bumping the retry count — touches one place, not fifty. Small-scale refactoring is local (rename a prompt file, extract a helper); large-scale refactoring restructures whole modules (introduce the central client wrapper, move inline prompts to versioned files, standardize typed-exception handling).

Figure 6.4: SDLC integration pipeline for a Claude feature

flowchart TD A["Plan: choose model, budget max_tokens,
set rate-limit expectations"] --> B["Build: official SDK, prompts in versioned files,
API key in environment"] B --> C["Review: human + Claude-in-CI check
error handling, retries, secrets, injection, cost"] C --> D["Test: exercise typed layer,
Git checkpoints to revert experiments"] D --> E["Maintain: refactor behind thin client layer,
swap models/prompts via central config"] E -.-> A
SDLC stageClaude-specific practice
PlanChoose model, budget tokens (max_tokens), define rate-limit expectations
BuildUse the official SDK; keep prompts in versioned files; keep the API key in the environment
ReviewHuman + Claude-in-CI review of error handling, retries, secrets, prompt injection, cost
TestExercise the typed abstraction layer; use Git checkpoints to revert failed experiments
MaintainRefactor behind the thin client layer; swap models/prompts via the central config
Animation slot: the SDLC loop animating stage by stage, with a "swap model" action rippling from the central client layer to every call site at once, illustrating single-point-of-change maintenance.
Post-Reading Check — Engineering Practices and the SDLC

1. What does "treat prompts as code" mean for a Claude integration?

Prompts should be generated at runtime and never stored Prompts belong in Git (e.g., a prompts/ directory) so they are diffable, reviewable, and revertible Prompts should be compiled into a binary for security Prompts should be embedded as hardcoded strings scattered through the code

2. What is the non-negotiable version-control rule for the ANTHROPIC_API_KEY?

Commit it so teammates can share one key easily Store it in a public config file for transparency Never commit it — keep it in the environment and .gitignore the .env Encode it in base64 in the README

3. What is the guiding principle of refactoring a Claude integration?

Spread model IDs and retry logic across as many files as possible Isolate the integration behind a thin, well-typed layer that centralizes model IDs, prompts, retry policy, and timeouts Inline every prompt string directly at each call site Avoid abstractions so each call is fully self-contained

4. How can Claude itself participate in code review within a CI pipeline?

By running in non-interactive mode, e.g., feeding a diff to a structured review prompt By automatically merging any PR that passes a single test By storing the API key in the CI logs for auditing It cannot; Claude only works interactively in a chat window

Your Progress

Answer Explanations