Ship It · From Script to Feature
Add it to a real app
This lesson has a wider time range than the others because the actual duration depends on what application you have at hand. If you have a running web app, the integration takes 25-35 minutes. If you need to scaffold a new route from scratch in a framework you know well, allow 45 minutes.
The exercise
Pick one route in an application you maintain — a webhook handler, an admin endpoint, an internal tool. Add one Claude-powered feature: a summarizer, a classifier, a one-line draft generator. The constraint is that it must live behind a real HTTP endpoint that accepts user input and returns structured JSON. Not a notebook. Not a script you run by hand.
The route contract
The endpoint accepts a JSON body, calls Claude, and returns:
{
"result": "non-empty string from Claude",
"tokens_used": 134,
"model": "claude-opus-4-5"
}
The tokens_used field is response.usage.input_tokens + response.usage.output_tokens. This forces you to read the usage block on every call — good practice that pays off during cost debugging.
A minimal Flask example
# app.py
from flask import Flask, request, jsonify
import anthropic
import os
app = Flask(__name__)
client = anthropic.Anthropic()
@app.route("/summarize", methods=["POST"])
def summarize():
body = request.get_json(silent=True)
if not body or not body.get("text"):
return jsonify({"error": "missing 'text' field"}), 400
text = body["text"]
if len(text) > 10_000:
return jsonify({"error": "text too long"}), 400
try:
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=256,
system="Summarize the following text in two sentences. Plain text only, no markdown.",
messages=[{"role": "user", "content": text}],
)
except anthropic.APIStatusError as e:
return jsonify({"error": "upstream API error", "code": e.status_code}), 502
return jsonify({
"result": response.content[0].text,
"tokens_used": response.usage.input_tokens + response.usage.output_tokens,
"model": response.model,
})
if __name__ == "__main__":
app.run(port=8000)
The one test
# tests/test_summarize.py
import pytest
from app import app as flask_app
@pytest.fixture
def client():
flask_app.config["TESTING"] = True
with flask_app.test_client() as c:
yield c
def test_summarize_returns_result(client):
response = client.post(
"/summarize",
json={"text": "The Anthropic API is an HTTP interface. It accepts JSON and returns JSON."},
content_type="application/json",
)
assert response.status_code == 200
data = response.get_json()
assert isinstance(data["result"], str) and len(data["result"]) > 0
assert isinstance(data["tokens_used"], int) and data["tokens_used"] > 0
def test_missing_text_returns_400(client):
response = client.post("/summarize", json={})
assert response.status_code == 400
The test does not assert what Claude said — that would be brittle because the model's exact wording varies. It asserts the shape: result is a non-empty string, tokens_used is a positive integer. The test fails if the route is broken, the API key is missing from the environment, or the response parsing is wrong.
Handling the API error path correctly
The except anthropic.APIStatusError block returns a 502 — "bad gateway" — rather than a 500. The distinction matters: 500 conventionally means your code is broken; 502 means an upstream dependency failed. Callers, monitoring, and on-call alerts treat these differently.
Do not let the raw Anthropic error message reach the HTTP response body in production. Map it to a generic message and log the real error server-side.
Enable caching before you ship
If your route sends a long system prompt on every request, add the cache marker before you deploy:
system=[{
"type": "text",
"text": "Your system prompt here...",
"cache_control": {"type": "ephemeral"},
}]
A route handling ten requests per minute pays the cache-creation price once every five minutes and the cache-read price on the remaining requests. At scale this is significant.
If you get stuck
ANTHROPIC_API_KEYnot available in tests: the test runner must have the variable in its environment. For local runs,export ANTHROPIC_API_KEY=...before running pytest. For CI, add it as a repository secret.- The test passes locally but fails in CI: the key is likely missing in the CI environment. Check the CI secret configuration.
502in production:anthropic.APIStatusErrorwas raised. Loge.status_codeande.messageserver-side to see which error type triggered it.- Response body is truncated:
max_tokensis too low for the input length. Increase it or add a check that rejects oversized input before calling the API (as the length check in the example does). - Test is brittle because it asserts on exact Claude output: remove the string assertion and replace it with
len(data["result"]) > 0. The test is checking your code, not the model.
Cross-reference
l6 (Prompt caching) shows the exact syntax for the cache marker — enable it before this route goes to production. l4 (Errors, retries, and timeouts) covers the retry logic you may want to wrap around client.messages.create if this endpoint handles high-value requests where transient failures should not surface to users.
Deliverable
A route behind a real HTTP endpoint with a passing test that runs against the live Anthropic API using ANTHROPIC_API_KEY from the environment. The test asserts shape, not exact text. A missing key produces a clear error before any network call is attempted.
Further reading
- "What Agents Actually Need From Your API" — what the response shape, error codes, and operationIds need to look like when the caller is a model rather than a human.
Try these prompts
- Write a minimal Flask route that accepts a JSON body with a "text" field, calls Claude to summarize it, and returns result, tokens_used, and model.
- Write the pytest test that checks the /summarize route returns a non-empty result string and a positive tokens_used integer without asserting exact text.
- The route should return 502 when the Anthropic API fails, not 500. Write the except block that maps anthropic.APIStatusError to a 502 JSON response.
Sign in to keep your place and tick off lessons. Reading stays open either way.