# Add it to a real app

Web page: https://academy.memberpress.ai/courses/api-fundamentals/l8/

Lesson 8 of 9 in [Anthropic API Fundamentals](https://academy.memberpress.ai/courses/api-fundamentals/), 35 min.

Video: https://www.youtube.com/watch?v=UNFyp-EY4Mg

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:

```json
{
  "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**

```python
# 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**

```python
# 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:

```python
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_KEY` not 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.
- `502` in production: `anthropic.APIStatusError` was raised. Log `e.status_code` and `e.message` server-side to see which error type triggered it.
- Response body is truncated: `max_tokens` is 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"](https://sethshoultes.com/blog/what-agents-need.html) — 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.

Next: [Make your endpoint agent-discoverable](https://academy.memberpress.ai/courses/api-fundamentals/l9/)
