MemberPress AI Academy memberpress.ai

Ship It · From Script to Feature

Add it to a real app

Project35 min

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_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

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.