Claude Certification Guides
CCDV-F/Domain 29 min read

Lesson 2.6

Building Conversational Applications

Architecting a chat product on the Messages API: where conversation state lives, how to proxy requests safely, and how to keep long conversations fast and affordable.

A chat application looks simple from the outside, but it forces every Messages API concept to work together: stateless requests, growing history, streaming to a browser, secrets that must never reach the client, and costs that climb with every turn. The exam uses chat scenarios to test whether you can assemble those pieces correctly.

Architecture: the browser never talks to Anthropic

Your API key must stay on a server you control. The browser or mobile app sends the user's message to your backend; the backend appends it to the stored conversation, calls the Messages API, streams the reply back, and persists the assistant turn.

client UI  ->  your backend (holds key, history, auth)  ->  api.anthropic.com

This proxy layer is also where you enforce authentication, rate limiting per user, logging, content checks and prompt templates. Calling the API directly from front-end code exposes the key to anyone who opens developer tools.

Where the history lives

The API is stateless, so the full transcript must be sent on every turn. Store it per conversation in your database as an ordered list of {role, content} objects. On each user message:

def reply(conversation_id: str, user_text: str) -> str:
    history = db.load(conversation_id)                  # list of MessageParam
    history.append({"role": "user", "content": user_text})
    resp = client.messages.create(
        model=MODEL, max_tokens=1024,
        system=SYSTEM_PROMPT, messages=history,
    )
    history.append({"role": "assistant", "content": resp.content})
    db.save(conversation_id, history)
    return "".join(b.text for b in resp.content if b.type == "text")

Store resp.content as the assistant turn, not a flattened string. If the conversation later involves tools, the tool_use blocks must survive in history so that tool_result blocks have something to reference.

System prompt versus first message

Put persona, policies and knowledge that apply to the whole conversation in the top-level system field. Keep it byte-identical from turn to turn so prompt caching can reuse it. Do not inject volatile data such as the current time at the top of the system prompt; it invalidates the cached prefix on every request. Put per-turn context in the latest user message instead.

Streaming to the UI

Chat users expect text to appear as it is generated. On the backend, consume the SDK stream and forward deltas to the client over SSE or WebSockets:

app.post("/chat", async (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  const history = await db.load(req.body.conversationId);
  history.push({ role: "user", content: req.body.text });

  const stream = client.messages.stream({
    model: MODEL, max_tokens: 1024, system: SYSTEM_PROMPT, messages: history,
  });
  stream.on("text", (delta) => res.write(`data: ${JSON.stringify(delta)}\n\n`));
  const final = await stream.finalMessage();
  history.push({ role: "assistant", content: final.content });
  await db.save(req.body.conversationId, history);
  res.end();
});

Persist the assistant turn only after finalMessage() resolves. If the stream fails midway, do not store a partial turn.

Keeping long conversations healthy

Every turn resends everything, so cost and latency grow linearly with history length and eventually the context window overflows. Strategies, in increasing order of sophistication:

  1. Set a turn budget. Keep the system prompt plus the last N turns; drop the oldest pairs.
  2. Token-aware truncation. Use count_tokens to trim history to a target, always removing whole user/assistant pairs so roles still alternate correctly.
  3. Summarise. Periodically ask Claude to summarise the oldest turns into a short paragraph that replaces them, preserving names, decisions and open questions.
  4. Server-side help. Context editing and compaction features can clear stale tool results or summarise automatically on supported models; see lesson 6-3.

Pair any of these with a cache_control breakpoint on the system prompt and on the latest turn, so the stable prefix is read from cache rather than re-processed.

Handling stop reasons in a chat UI

  • end_turn: render the message, enable the input box.
  • max_tokens: show the partial text and offer "continue"; consider a larger max_tokens.
  • tool_use: run the tool server-side, append results, call again before showing anything final; show a "working" indicator.
  • refusal: show a neutral message and log it; do not retry blindly.

Identity and abuse signals

Pass an opaque metadata.user_id (a hash or UUID, never an email) so Anthropic can help detect abuse without receiving personal data. Rate-limit per user on your side so one abusive account cannot consume your organisation's quota.

Exam focus

  • The API key lives only on your backend; the client UI talks to your server, which proxies to the Messages API.
  • Conversation state is yours to store; resend the full history each turn and persist the complete assistant content array.
  • Keep the system prompt stable across turns to benefit from prompt caching; put per-turn data in the user message.
  • Trim or summarise history in whole user/assistant pairs so role alternation is preserved.
  • Stream deltas to the UI but store the assistant turn only after the final message is assembled.
  • metadata.user_id should be an opaque identifier, not personally identifying information.

Check yourself

5 questions

  1. 1.A chat application sends only the latest user message on every request: messages=[{"role": "user", "content": latest_text}] Users complain that Claude forgets everything they said earlier. Why?

  2. 2.After each response, a chat server does: history.push({ role: "assistant", content: response.content[0].text }); The app later adds tools and begins failing on follow-up turns. What is the problem?

  3. 3.A long-running chat begins failing with a 400 error saying the prompt is too long. Which strategy addresses the root cause?

  4. 4.In a customer-support chat app, where should the persona and standing rules ("You are Acme's support assistant; never promise refunds") go, and where should the user's messages go?

  5. 5.A web chat front end runs in the browser. Which component should hold the Anthropic API key and the conversation history?