Claude Certification Program · v1.0 · Effective July 2026 · All four tracks open

Home › Study guides › CCDV-F › Domain 2 › Lesson 2.4

CCDV-F · Domain 2 · 33.1% of the exam · Lesson 2.4 · 21 min read

Software engineering foundations: a Claude feature is still software

REST contracts, validated JSON, async calls, branches, CI, code review and refactoring: the engineering that turns a Claude demo into a feature you can ship.

Written against skill 2.4 of the official CCDV-F exam guide (Version 1.0, effective July 2026). An independent resource, not affiliated with Anthropic; the practice questions are written from scratch.

2.4.1 Why a working demo is not yet a feature

Picture a four-person team behind a sales-dashboard web app for small retailers, with a Python back end and a TypeScript front end. Customers keep asking what their charts actually mean. So on Friday, Ines, the back-end developer, writes a fifteen-line script that sends one chart's numbers to Claude and prints a friendly explanation: "Sales rose 18% in March, mostly from two stores." By Tuesday an "Explain this chart" button is live in the staging environment, and things start to go wrong.

While one explanation is being generated, the dashboard stops loading for every other user. A reply that opens with a friendly sentence before its JSON crashes the parser. A slow request times out, the page retries it, and the chart ends up with two saved explanations. And the API key is still pasted into the script, one careless commit away from the repository. None of these is a problem with Claude. Each one is a gap in the ordinary engineering around the call.

From your code's point of view, Claude is a network service: you send an HTTP request carrying JSON, wait a few seconds, and get JSON back, or now and then an error. The engineering that closes those gaps is standard: a clear REST contract, validated JSON and asynchronous code that never blocks on the wait. Every change reaches production through a branch, tests and code review, and old code around the feature gets a careful refactor when it needs reshaping.

Where the engineering lives in one click

BrowserTypeScript, await fetch() and a spinner
Your endpointPython, POST /api/charts/{id}/explanations
Claude APIPOST /v1/messages, seconds of waiting
Validated JSONback to the browser
The model call is one hop in a chain the team owns, and every hop has a contract, a typical failure and a standard fix.

2.4.2 REST: the contract between browser, server and Claude

Here is the first question the team has to settle: who talks to whom, and in what shape? The tempting shortcut is to call Claude straight from the browser. Resist it completely. Any key that reaches the browser can be read by anyone who opens the developer tools. That is why the official TypeScript SDK refuses to run in a browser unless you set an option named dangerouslyAllowBrowser. The browser talks to the team's server, and only the server talks to Claude.

REST (representational state transfer) is the convention both conversations follow. Each thing an API exposes is a resource with its own URL, such as /api/charts/42/explanations. HTTP methods say what to do with it: GET reads, POST creates, PUT replaces, DELETE removes. Every response carries a status code: 2xx for success, 4xx when the caller sent something wrong, 5xx when the server failed. The Claude API is a RESTful API at https://api.anthropic.com. A Messages call is a POST to /v1/messages with a JSON body, your API key and an anthropic-version header, which the SDKs add for you.

The status code decides what your code does next. For the transient failures, the standard answer is exponential backoff: retry after a short pause, and double the pause after each further failure.

Status from the Claude API What it means What your code does
400 invalid_request_error Something in your request is wrong Fix the request; sending it again fails again
401 authentication_error, 403 permission_error The key is bad, or lacks access Fix the credentials; never retry in a loop
413 request_too_large The request is over the size limit Send less
429 rate_limit_error You hit a rate limit Wait as long as the retry-after header says, then retry
500 api_error An unexpected error on Anthropic's side Retry with exponential backoff
529 overloaded_error The API is temporarily overloaded Retry with exponential backoff

Memorise the split in the last column, not every row. The official SDKs already retry connection errors, rate limits and 5xx errors twice by default with backoff, and max_retries changes that.

Retrying raises a second question: is it safe to send this request twice? An operation is idempotent when doing it twice has the same effect as doing it once. GET, PUT and DELETE are idempotent by definition; POST is not. That is where the double explanations came from: the first request finished on the server after the page had given up, so the retry saved a second one. The fix is an idempotency key: a unique id the page creates once per click and sends again with every retry of that click. When the server sees a key it has already handled, it returns the stored result instead of generating a new one.

2.4.3 JSON: parse at the boundary, validate against a schema

JSON (JavaScript Object Notation) is the text format every hop speaks: objects of named fields, arrays, strings, numbers, true, false and null. Parsing turns that text into your language's values, a dict in Python or an object in TypeScript. But parsing only proves the text is well-formed. It says nothing about whether the fields your code needs are present and sensible. That takes a schema, a precise description of the expected shape, checked where data enters your code. Think of it as a customs desk at the border of your code: everything is checked once, on arrival, so nothing inside has to wonder.

The team needs a schema twice. The browser's request must carry a chart id and its data points, checked before any money is spent on a model call. The model's reply matters more. Ines asks Claude for a JSON object with a summary, a trend and a list of caveats, and usually it arrives exactly so. But Claude writes text; it does not run your type checker. A reply can open with a friendly sentence, miss a field, or say "upward" where the front end expects "rising".

The code below uses Pydantic, a popular Python validation library. Look at the class, which is the schema, and at the single call that parses and validates.

from typing import Literal
from pydantic import BaseModel, ValidationError

class ChartExplanation(BaseModel):            # the SCHEMA the front end relies on
    summary: str
    trend: Literal["rising", "falling", "flat", "mixed"]
    caveats: list[str]

def parse_explanation(raw: str) -> ChartExplanation | None:
    try:
        return ChartExplanation.model_validate_json(raw)   # PARSE and VALIDATE together
    except ValidationError as err:            # broken JSON, missing field, unknown trend
        logger.warning("explanation rejected: %s", err)
        return None                           # the endpoint answers 502, never half-valid data

When validation fails, the endpoint can retry the model call once, then answer 502 Bad Gateway, the status for "the service behind me sent something unusable". The API's structured outputs feature can constrain a reply to a JSON schema you supply, but keep the check at your own boundary anyway: it is what protects your contract with the browser. In the browser, a library such as Zod plays the same role for the server's responses.

2.4.4 Async: never block on a slow model call

Now the frozen dashboard. A Claude call takes seconds, because the reply is generated piece by piece, and synchronous code sits still until it arrives. Asynchronous programming lets code pause at a wait without holding up everything else. await marks the pause: while one call waits on the network, the program serves other requests and comes back when the reply lands. Picture a waiter who takes an order to the kitchen and serves other tables meanwhile, instead of standing at the counter until the dish is ready.

Here is the part that trips people up. Python frameworks such as FastAPI run async def handlers on an event loop, one per worker process, and it can switch between requests only at an await. If a handler calls a blocking client instead, the loop is stuck inside that call, and every other request on the worker waits too. One explanation in progress, and the whole dashboard stops loading. The Python SDK offers AsyncAnthropic for exactly this, and the TypeScript SDK's methods already return promises you await.

In the code below, look at three lines: the async client, the await, and asyncio.gather, which runs independent calls at the same time rather than in turn. MODEL and build_prompt stand for the team's own configuration and prompt code.

import asyncio
from anthropic import AsyncAnthropic

client = AsyncAnthropic()  # ASYNC client; reads ANTHROPIC_API_KEY from the environment

async def explain(chart: dict) -> str:
    response = await client.messages.create(      # AWAIT: other requests run meanwhile
        model=MODEL,
        max_tokens=800,
        messages=[{"role": "user", "content": build_prompt(chart)}],  # built per request
    )
    return next(b.text for b in response.content if b.type == "text")

async def explain_dashboard(charts: list[dict]) -> list[str]:
    return await asyncio.gather(*(explain(c) for c in charts))  # CONCURRENT, not in turn

Three habits go with it. Set timeouts: the Python SDK's default is ten minutes, far longer than anyone will watch a spinner. Cap concurrency with a semaphore, a counter that lets only a set number of calls run at once, so forty charts do not trip the rate limit. And build each request's messages inside the handler: a list kept at module level is shared by concurrent requests and can leak one customer's data into another's prompt.

In the browser, fetch is already asynchronous, so Tariq's front-end job is the experience around the wait. He shows a loading state, disables the button while a request is in flight, which also stops double-clicks, and shows a clear message on failure. For work too long for one request, the endpoint can answer 202 Accepted with a job URL the page polls.

2.4.5 Branches, pull requests and CI: the path every change takes

With the feature working, how does it reach production without breaking anything else? Version control with Git is the foundation. Ines works on a branch, a separate line of commits such as feature/explain-chart, so the main branch always holds working code. She commits in small, focused steps, then opens a pull request (PR): a request to merge the branch, with a description reviewers read first.

The pull request is where SDLC integration happens. SDLC stands for software development life cycle, and integrating a Claude feature into it means the feature passes the same gates as any other change. On every push, continuous integration (CI) runs linting, type checks, unit tests and a secret scan. The unit tests swap the Claude client for a stub that returns fixed replies, some valid and some deliberately broken, so they are fast, free and identical on every run. A failure then means the code changed, not the model's wording.

The path every change takes

BRANCHfeature/explain-chart
COMMITsmall steps, no secrets
PULL REQUESTwhat changed and why
CIlint, types, tests, secret scan
REVIEW, then MERGEa person approves
The Claude feature reaches production the same way as any other code: nothing merges until CI is green and a person has approved it.

Just as important is what goes into the repository at all.

Item Commit it? Why
Source code and tests Yes They are the product and its proof
Prompt templates and JSON schemas Yes They change behaviour like code, so they get reviewed like code
Dependency lockfile Yes Every developer and every CI run installs the same versions
.env.example with variable names only Yes Tells teammates which settings to provide
.env, API keys, tokens Never Anyone who can read the repository can spend on your account
Build output, node_modules, virtual environments No Regenerated from source; they only bloat diffs

The key lives in an environment variable, loaded locally from a .env file listed in .gitignore and injected in production by a secret manager. Anthropic's key guidance recommends exactly this setup, and the secret scan in CI as a backstop.

2.4.6 Code review and refactoring, including code Claude wrote

Here is the question that trips teams up: what changes when Claude wrote the code under review? The bar does not. Code review is a second developer checking a change before it merges: is it correct, readable, tested and safe? AI-written code gets exactly that, with extra attention to one weakness. It reads fluently, so its mistakes rarely look like mistakes: a missing edge case, a swallowed exception, a parameter that does not exist. Anthropic's Claude Code guidance calls this the trust-then-verify gap and answers it with evidence such as test output: if you cannot verify it, do not ship it.

Two habits follow. Review in a fresh context: the Writer/Reviewer pattern in the Claude Code best practices has a second session review the code, because it is not biased toward code it just wrote. And keep a person as the approver. An automated reviewer, such as Claude Code's /code-review, reports findings on the diff; Marisol, the team's tech lead, still decides whether it merges.

Refactoring means changing the structure of code without changing what it does. Small-scale refactoring is everyday work. While adding the feature, Ines moves the prompt-building code into its own function, in its own commit, so the reviewer can check the structural change and the behaviour change separately. Large-scale refactoring is a different animal. Bjorn has to reshape the old analytics module behind every chart: thousands of lines, synchronous, full of shared global state.

Small and large refactors need different care

Small-scale one function or file

Rename, extract, inline
Existing tests prove it
Its own commit, beside the feature

Large-scale a module or more

Pin today's behaviour with tests first
Plan, then one small step per PR
Tests green after every step
A small refactor rides on the existing tests in one commit; a large one pins today's behaviour first, then moves in small verified steps.

For a large refactor, build the judge first. Bjorn writes characterization tests that record what the module returns today for realistic inputs. He checks that they fail when he breaks the module on purpose, because a test that cannot fail proves nothing. The tests never change in the same pull request as the code they judge.

Then Claude Code works in steps. In plan mode, where Claude Code only reads and proposes, it maps the module and suggests a sequence of changes. Each step becomes a small pull request that keeps the tests green, so every change stays reviewable and easy to roll back. Mechanical changes across many files can run in parallel sessions, each in its own Git worktree, a separate checkout of the same repository.

2.4.7 The exam traps

Almost every trap in this skill blames the model for an engineering problem, or skips a standard control because the code came from Claude.

  • ✗ Calling the Claude API from the browser, or committing the key "just for now". ✓ Keep the key on the server, in an environment variable or secret manager, and revoke any key that was ever committed.
  • ✗ Retrying every failed request the same way. ✓ Fix the request on a 4xx; retry a 429 after retry-after, and a 5xx or 529 with backoff. Retry only idempotent operations, or add an idempotency key first.
  • ✗ Passing the model's JSON straight to the front end. ✓ Validate it against a schema at the server boundary; parsing alone proves nothing about the fields.
  • ✗ A synchronous client inside an async handler, or awaiting independent calls one by one. ✓ Await the async client, and run independent calls concurrently with a cap.
  • ✗ Merging AI-written code because it looks finished and its own tests pass. ✓ Review it like any code, in a fresh context, with evidence from tests not rewritten alongside it.
  • ✗ Refactoring a whole module in one giant pull request. ✓ Pin behaviour with characterization tests, then move in small steps that each keep them green.

Four tempting fixes, one real one

A bigger modelthe same missing validation
A stronger prompta request, not a guarantee
Retry everythingduplicates and wasted calls
Trust the difflooking right is not proof
Apply the engineering controlcontract, schema, async, tests, review
When a Claude feature misbehaves in production, the fix is usually a standard engineering control at the boundary where it failed, not a change to the model.

2.4.8 Put it together: ship a Claude endpoint the way a team would

You now have every piece, from the REST contract to the refactor. To make them stick, build a small version of the team's endpoint, then break it on purpose.

Much of the exam builds on this engineering. Configuration management (2.6) versions the prompts and settings you just committed, technical fundamentals (5.2) looks closer at the SDKs that wrap the REST API and at websockets, and Claude Code operation (3.1) covers the tool Bjorn used.

Key takeaways

  • ✓ A Claude feature is ordinary software around one slow, sometimes failing network call, and standard engineering is what makes it ready for production.
  • ✓ REST gives resources URLs, actions methods and outcomes status codes: fix a 4xx, retry a 429, 5xx or 529 with backoff, and retry only idempotent operations.
  • ✓ Parsing proves text is JSON and a schema proves it has the right fields, so validate the model's reply at the server boundary before anything trusts it.
  • ✓ Await model calls with an async client, run independent calls concurrently with a cap and timeouts, and keep each request's state inside the request.
  • ✓ Changes reach production through a branch, a pull request, CI with a stubbed Claude client and a secret scan, and a human review; keys and .env files never enter the repository.
  • ✓ AI-written code gets the same review bar as any other code, in a fresh context, with evidence instead of trust.
  • ✓ Refactoring preserves behaviour: pin it with tests first, then change the structure in small, verified steps.

Check your understanding

4 questions written for this lesson, then one from the CCDV-F question bank on the same topic. Every answer option is explained, including the ones you did not pick. Nothing is stored.

51 CCDV-F questions on Domain 2, free

Every question in the bank is tagged to a domain, so you can drill 51 questions on Applications and Integration alone, or sit the full 53-question timed simulator.

Open the CCDV-F question bank → Back to Domain 2 →

The question bank is free. It asks for an account only because the quiz engine has to store answers to score them and show which domains are weak. The questions on this page need nothing.

Sources