How I Actually Ship with Claude

A senior end-to-end workflow: prototype in chat, build with Claude Code, wrap features behind your own API with evals, and monitor cost and quality in production.

Here is how the pieces fit into one dependable workflow — the way an experienced developer actually ships an AI feature, from idea to production.

1. Prototype the prompt in chat

Before writing any code, iterate on the prompt in a chat window against real examples. Cheap, fast, and it tells you whether the idea is even viable and which model tier you need.

2. Build the feature with Claude Code

Hand the working prompt to Claude Code on a clean branch: build the endpoint, wire the SDK, add the eval harness. Review the diff like a pull request before it lands.

3. Wrap it behind your own API

The browser talks to your endpoint; your server holds the key, rate-limits, validates input, and streams the reply. This is where security and structured output live.

4. Gate on evals

The feature ships behind an eval set that runs in CI. Pin the model ID so upgrades are deliberate. A prompt change that regresses the eval set does not merge.

5. Watch it in production

Log usage, stop_reason, and latency. Track cost per feature and cache-hit rates. Sample real outputs to catch quality drift the eval set missed, and feed the interesting failures back into the eval set.

The loop that keeps it healthy

Production surfaces a new failure → it becomes an eval case → you fix the prompt → the eval set guards against the regression forever. That feedback loop, more than any single trick, is what makes an AI feature stay good after launch.

Example

Example Β· json
{
  "how_i_ship_a_claude_feature": {
    "1_prototype": "Iterate the prompt in chat on real examples; pick the model tier",
    "2_build": "Claude Code on a clean branch builds the endpoint + eval harness; review the diff",
    "3_wrap": "Browser -> my server -> Claude. Key server-side, rate-limited, streamed",
    "4_gate": "Eval set runs in CI with a pinned model ID; regressions don't merge",
    "5_monitor": "Log usage, stop_reason, latency, cache hit rate; sample outputs",
    "6_loop": "Production failure -> new eval case -> prompt fix -> guarded forever"
  }
}

When to use it

  • Prototype a new AI feature in claude.ai chat in an afternoon, then implement it with Claude Code the same day.
  • Gate your AI feature behind a feature flag so you can enable it for beta testers before a full rollout.
  • Monitor token usage and error rates in production dashboards from day one to catch cost spikes before they escalate.

More examples

Prototype in chat, implement with Claude Code

Shows the two-phase workflow: explore the design in chat, then hand the spec to Claude Code for implementation.

Example Β· bash
# Step 1: Prototype in claude.ai chat
# Ask Claude to show you what the API call, system prompt,
# and response parsing should look like

# Step 2: Implement with Claude Code
claude "Build the /api/summarise endpoint we designed in chat:
- POST body: { text: string }
- System prompt: 'Summarise in 3 bullets'
- Model: claude-haiku-4-5, max_tokens: 200
- Return { bullets: string[] }"

Feature-flag the AI endpoint

Guards the AI route behind a feature flag so it can be enabled per user or cohort without a code deploy.

Example Β· javascript
// Feature-flag gate in the API route
export async function POST(req) {
  const user = await getUser(req);
  if (!user.flags.includes("ai_summarise")) {
    return Response.json({ error: "Feature not available" }, { status: 403 });
  }
  // ...call Claude...
}

Monitor cost and error rate in production

Wraps every Claude call with latency, token, and error metrics so production dashboards show usage and cost trends.

Example Β· javascript
async function callClaude(prompt, userId) {
  const start = Date.now();
  try {
    const msg = await client.messages.create({
      model: "claude-haiku-4-5",
      max_tokens: 256,
      messages: [{ role: "user", content: prompt }],
    });
    metrics.record("claude_latency_ms", Date.now() - start, { userId });
    metrics.record("claude_output_tokens", msg.usage.output_tokens, { userId });
    return msg.content[0].text;
  } catch (err) {
    metrics.increment("claude_error", { status: err.status ?? 0, userId });
    throw err;
  }
}

Discussion

  • Be the first to comment on this lesson.