IAMUVIN

AI & Machine Learning

Building AI Products with Claude API: The Complete 2026 Guide

Uvin Vindula·February 5, 2024·14 min read

Last updated: April 14, 2026

Share

TL;DR

Building AI products with the Claude API comes down to five things: a solid Anthropic SDK setup with proper authentication, streaming responses for real-time UX, tool use for connecting Claude to your domain logic, structured error handling that doesn't break production, and cost controls that stop runaway spend. I use Claude API daily — it powers the EuroParts Lanka AI Part Finder, where customers describe a car problem like "my 2019 Civic makes a grinding noise when braking" and Claude identifies the exact OEM brake pad. This guide covers everything I learned shipping that feature and others to production. All examples are TypeScript + Next.js because that's what I build with. If you're evaluating AI APIs for your product, Claude's combination of reasoning quality, 200K context window, and structured tool use makes it the strongest option available in 2026.


Why Claude API for Production AI Products

I've shipped AI features using OpenAI, Google Gemini, and Claude. I keep coming back to Claude for one reason: it follows complex instructions more reliably than anything else I've tested. When I was building the AI Part Finder for EuroParts Lanka, the system needed to interpret vague customer descriptions ("something rattling under the hood at low speed"), cross-reference them against a parts catalogue of 40,000+ items, and return a specific OEM part number. Claude handled this with a system prompt and tool calling setup that took me two days to build. The equivalent OpenAI implementation took a week of prompt engineering and still hallucinated part numbers more often.

Here's what makes Claude API stand out for product builders in 2026:

Instruction adherence. Claude respects system prompts with near-perfect consistency. When I tell it "never recommend a part unless the OEM number exists in the provided catalogue," it doesn't. With other models, I had to add validation layers to catch hallucinated part numbers.

200K context window. I can feed in an entire product catalogue as context without chunking or RAG for smaller datasets. For the EuroParts build, this meant shipping an MVP in days instead of weeks building a vector search pipeline.

Structured tool use. Claude's function calling is deterministic enough to trust in production. It decides when to call a tool, formats the arguments correctly, and processes the result naturally. No regex parsing of model outputs.

Honest uncertainty. When Claude isn't sure, it says so. This matters for e-commerce — I'd rather show "I couldn't identify the exact part, here are three possibilities" than confidently return the wrong item.

If you're building AI-powered products in 2026, Claude API is where I'd start.


Setup and Authentication

Install the Anthropic SDK and set up your environment. I use the official TypeScript SDK because it has first-class support for streaming, tool use, and type safety.

bash
npm install @anthropic-ai/sdk

Create your API key at console.anthropic.com. Store it as an environment variable — never in code, never committed to git.

bash
# .env.local
ANTHROPIC_API_KEY=sk-ant-api03-xxxxx

Here's the client initialization pattern I use across all my projects:

typescript
// lib/anthropic.ts
import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

export { anthropic };

The SDK reads ANTHROPIC_API_KEY from the environment automatically, so you can also initialize without passing it explicitly. I prefer being explicit because it makes environment issues obvious during debugging.

For Next.js API routes, I create a singleton pattern to avoid re-instantiating on every request:

typescript
// lib/anthropic.ts
import Anthropic from "@anthropic-ai/sdk";

let client: Anthropic | null = null;

export function getAnthropicClient(): Anthropic {
  if (!client) {
    if (!process.env.ANTHROPIC_API_KEY) {
      throw new Error("ANTHROPIC_API_KEY environment variable is not set");
    }
    client = new Anthropic({
      apiKey: process.env.ANTHROPIC_API_KEY,
    });
  }
  return client;
}

This keeps cold starts fast and avoids the overhead of creating a new HTTP client on every invocation.


Building Your First Integration

Let me walk through a real pattern — the kind I use in production. This is a Next.js Route Handler that takes a user question and returns a Claude response:

typescript
// app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getAnthropicClient } from "@/lib/anthropic";

const SYSTEM_PROMPT = `You are a helpful automotive parts assistant. 
When a customer describes a vehicle problem, identify the most likely 
component that needs replacement. Always include the OEM part number 
if available. If you're not confident, say so and suggest the customer 
consult a mechanic.`;

export async function POST(request: NextRequest) {
  const { message } = await request.json();

  if (!message || typeof message !== "string") {
    return NextResponse.json(
      { error: { code: "INVALID_INPUT", message: "Message is required" } },
      { status: 400 }
    );
  }

  const client = getAnthropicClient();

  const response = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 1024,
    system: SYSTEM_PROMPT,
    messages: [{ role: "user", content: message }],
  });

  const text = response.content
    .filter((block) => block.type === "text")
    .map((block) => block.text)
    .join("");

  return NextResponse.json({
    response: text,
    usage: {
      inputTokens: response.usage.input_tokens,
      outputTokens: response.usage.output_tokens,
    },
  });
}

A few things I always do:

  1. Validate input before hitting the API. Every token costs money. Don't send garbage to Claude.
  2. Return usage data. You need this for cost tracking. I log every request's token count to a database.
  3. Use the right model. Claude Sonnet handles 90% of product use cases. I only reach for Opus when I need multi-step reasoning across large contexts.

The model naming convention in 2026 is claude-{tier}-{version}. For most API integrations, claude-sonnet-4-20250514 hits the sweet spot of speed, quality, and cost.


Streaming Responses

Nobody wants to stare at a loading spinner for 8 seconds. Streaming is mandatory for any user-facing AI feature. Here's the pattern I use:

typescript
// app/api/chat/stream/route.ts
import { NextRequest } from "next/server";
import { getAnthropicClient } from "@/lib/anthropic";

export async function POST(request: NextRequest) {
  const { message, systemPrompt } = await request.json();
  const client = getAnthropicClient();

  const stream = await client.messages.stream({
    model: "claude-sonnet-4-20250514",
    max_tokens: 2048,
    system: systemPrompt,
    messages: [{ role: "user", content: message }],
  });

  const encoder = new TextEncoder();

  const readable = new ReadableStream({
    async start(controller) {
      for await (const event of stream) {
        if (
          event.type === "content_block_delta" &&
          event.delta.type === "text_delta"
        ) {
          const chunk = `data: ${JSON.stringify({ text: event.delta.text })}\n\n`;
          controller.enqueue(encoder.encode(chunk));
        }
      }

      const finalMessage = await stream.finalMessage();
      const done = `data: ${JSON.stringify({
        done: true,
        usage: {
          inputTokens: finalMessage.usage.input_tokens,
          outputTokens: finalMessage.usage.output_tokens,
        },
      })}\n\n`;
      controller.enqueue(encoder.encode(done));
      controller.close();
    },
  });

  return new Response(readable, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

On the client side, I consume this with a custom hook:

typescript
// hooks/use-chat-stream.ts
"use client";

import { useState, useCallback } from "react";

interface UseChatStreamReturn {
  response: string;
  isStreaming: boolean;
  send: (message: string) => Promise<void>;
}

export function useChatStream(endpoint: string): UseChatStreamReturn {
  const [response, setResponse] = useState("");
  const [isStreaming, setIsStreaming] = useState(false);

  const send = useCallback(
    async (message: string) => {
      setResponse("");
      setIsStreaming(true);

      const res = await fetch(endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message }),
      });

      const reader = res.body?.getReader();
      const decoder = new TextDecoder();

      if (!reader) throw new Error("No response body");

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value);
        const lines = chunk.split("\n\n").filter(Boolean);

        for (const line of lines) {
          if (!line.startsWith("data: ")) continue;
          const data = JSON.parse(line.slice(6));
          if (data.text) setResponse((prev) => prev + data.text);
          if (data.done) setIsStreaming(false);
        }
      }

      setIsStreaming(false);
    },
    [endpoint]
  );

  return { response, isStreaming, send };
}

The perceived latency drops from 5-8 seconds to under 500ms. Users see the first token almost immediately. This is the single biggest UX improvement you can make in an AI product.


Tool Use and Function Calling

This is where Claude API gets genuinely powerful. Tool use lets Claude decide when to call your functions, pass the right arguments, and incorporate the results into its response. For the EuroParts Part Finder, I defined tools for searching the parts catalogue, checking inventory, and looking up vehicle compatibility.

Here's a simplified version of the pattern:

typescript
// lib/tools.ts
import type { Tool } from "@anthropic-ai/sdk/resources/messages";

export const partFinderTools: Tool[] = [
  {
    name: "search_parts_catalogue",
    description:
      "Search the OEM parts catalogue by keyword, category, or vehicle compatibility. Returns matching parts with OEM numbers, prices, and stock status.",
    input_schema: {
      type: "object" as const,
      properties: {
        query: {
          type: "string",
          description: "Search query — part name, symptom, or OEM number",
        },
        vehicleMake: {
          type: "string",
          description: "Vehicle manufacturer (e.g., Honda, Toyota)",
        },
        vehicleYear: {
          type: "number",
          description: "Model year of the vehicle",
        },
      },
      required: ["query"],
    },
  },
  {
    name: "check_inventory",
    description:
      "Check real-time stock availability for a specific OEM part number.",
    input_schema: {
      type: "object" as const,
      properties: {
        oemNumber: {
          type: "string",
          description: "The OEM part number to check",
        },
      },
      required: ["oemNumber"],
    },
  },
];

The API route handles the tool use loop:

typescript
// app/api/parts/identify/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getAnthropicClient } from "@/lib/anthropic";
import { partFinderTools } from "@/lib/tools";
import { searchParts, checkInventory } from "@/lib/parts-db";
import type { MessageParam } from "@anthropic-ai/sdk/resources/messages";

async function handleToolCall(
  name: string,
  input: Record<string, unknown>
): Promise<string> {
  switch (name) {
    case "search_parts_catalogue":
      return JSON.stringify(
        await searchParts(
          input.query as string,
          input.vehicleMake as string | undefined,
          input.vehicleYear as number | undefined
        )
      );
    case "check_inventory":
      return JSON.stringify(
        await checkInventory(input.oemNumber as string)
      );
    default:
      return JSON.stringify({ error: "Unknown tool" });
  }
}

export async function POST(request: NextRequest) {
  const { customerMessage } = await request.json();
  const client = getAnthropicClient();

  const messages: MessageParam[] = [
    { role: "user", content: customerMessage },
  ];

  let response = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 2048,
    system: `You are an automotive parts specialist for EuroParts Lanka. 
Use the search tool to find parts. Always verify stock before recommending.
Never guess an OEM number — only return numbers from search results.`,
    tools: partFinderTools,
    messages,
  });

  // Tool use loop — Claude may call multiple tools
  while (response.stop_reason === "tool_use") {
    const toolBlocks = response.content.filter(
      (block) => block.type === "tool_use"
    );

    const toolResults = await Promise.all(
      toolBlocks.map(async (block) => ({
        type: "tool_result" as const,
        tool_use_id: block.id,
        content: await handleToolCall(
          block.name,
          block.input as Record<string, unknown>
        ),
      }))
    );

    messages.push({ role: "assistant", content: response.content });
    messages.push({ role: "user", content: toolResults });

    response = await client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 2048,
      system: `You are an automotive parts specialist for EuroParts Lanka.
Use the search tool to find parts. Always verify stock before recommending.
Never guess an OEM number — only return numbers from search results.`,
      tools: partFinderTools,
      messages,
    });
  }

  const text = response.content
    .filter((block) => block.type === "text")
    .map((block) => block.text)
    .join("");

  return NextResponse.json({ recommendation: text });
}

Key things I learned building this:

  • Always run tools in parallel when Claude requests multiple tools at once. The Promise.all above handles this.
  • Constrain tool descriptions tightly. Vague descriptions lead to incorrect tool selection. I rewrote the EuroParts tool descriptions three times before Claude used them reliably.
  • The tool loop must have an exit condition. I cap it at 5 iterations in production to prevent infinite loops from malformed responses.

Error Handling

The Claude API can fail in ways that affect your users. Here's every error I've hit in production and how I handle them:

typescript
// lib/anthropic-safe.ts
import Anthropic from "@anthropic-ai/sdk";
import { getAnthropicClient } from "./anthropic";

interface SafeResponse {
  text: string;
  usage: { inputTokens: number; outputTokens: number };
}

interface SafeError {
  code: string;
  message: string;
  retryable: boolean;
}

type SafeResult =
  | { success: true; data: SafeResponse }
  | { success: false; error: SafeError };

export async function safeChatCompletion(
  systemPrompt: string,
  userMessage: string,
  maxRetries = 2
): Promise<SafeResult> {
  const client = getAnthropicClient();
  let lastError: unknown;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await client.messages.create({
        model: "claude-sonnet-4-20250514",
        max_tokens: 1024,
        system: systemPrompt,
        messages: [{ role: "user", content: userMessage }],
      });

      const text = response.content
        .filter((block) => block.type === "text")
        .map((block) => block.text)
        .join("");

      return {
        success: true,
        data: {
          text,
          usage: {
            inputTokens: response.usage.input_tokens,
            outputTokens: response.usage.output_tokens,
          },
        },
      };
    } catch (error) {
      lastError = error;

      if (error instanceof Anthropic.RateLimitError) {
        const retryAfter = 2 ** attempt * 1000;
        await new Promise((resolve) => setTimeout(resolve, retryAfter));
        continue;
      }

      if (error instanceof Anthropic.APIStatusError) {
        if (error.status === 529) {
          // API overloaded — retry with backoff
          await new Promise((resolve) =>
            setTimeout(resolve, 2 ** attempt * 2000)
          );
          continue;
        }

        return {
          success: false,
          error: {
            code: `API_${error.status}`,
            message: error.message,
            retryable: false,
          },
        };
      }

      if (error instanceof Anthropic.APIConnectionError) {
        // Network issue — retry
        continue;
      }

      return {
        success: false,
        error: {
          code: "UNKNOWN",
          message: "An unexpected error occurred",
          retryable: false,
        },
      };
    }
  }

  return {
    success: false,
    error: {
      code: "MAX_RETRIES",
      message: `Failed after ${maxRetries + 1} attempts`,
      retryable: false,
    },
  };
}

The errors I see most often:

ErrorFrequencyAction
429 Rate LimitWeeklyExponential backoff, then queue
529 OverloadedDuring peak hoursBackoff with longer delay
400 Bad RequestDuring developmentFix the request, don't retry
Network timeoutRareRetry once, then fail gracefully

I log every API error to a dedicated table in Supabase with the request context, timestamp, and error type. This data helped me identify that 80% of my 429 errors happened between 2-4 PM UTC — the overlap of US and EU working hours. I now pre-emptively queue non-urgent requests during that window.


Production Best Practices

After shipping several Claude-powered features, here's what I wish I knew on day one:

Rate limit proactively. Don't rely on Anthropic's rate limits to protect you. Implement your own per-user rate limiting. I use a sliding window counter in Redis — 10 requests per minute per user for the Part Finder, 30 per minute for authenticated API clients.

Cache aggressively. If someone asks "what brake pads fit a 2020 Civic," the answer doesn't change minute to minute. I cache Claude responses keyed on a normalized version of the input. This cut my API spend by 40%.

typescript
// lib/cache.ts
import { createHash } from "crypto";

export function generateCacheKey(
  systemPrompt: string,
  userMessage: string,
  model: string
): string {
  const normalized = `${model}:${systemPrompt}:${userMessage.toLowerCase().trim()}`;
  return createHash("sha256").update(normalized).digest("hex");
}

Set hard spending limits. Anthropic's console lets you set monthly spend caps. Set one. I learned this after a prompt injection in a staging environment generated a loop that burned through $200 in tokens before I noticed.

Monitor response quality. I sample 5% of Claude responses and review them weekly. Quality degrades silently — you won't know unless you look. I tag responses that users report as unhelpful and use them to refine system prompts.

Use structured outputs for machine-consumed responses. When Claude's output feeds into another system (not displayed to a user), I request JSON and validate with Zod:

typescript
import { z } from "zod";

const PartRecommendation = z.object({
  partName: z.string(),
  oemNumber: z.string(),
  confidence: z.enum(["high", "medium", "low"]),
  reasoning: z.string(),
  alternatives: z.array(
    z.object({
      partName: z.string(),
      oemNumber: z.string(),
    })
  ),
});

type PartRecommendation = z.infer<typeof PartRecommendation>;

// In the API call, add to system prompt:
// "Respond ONLY with valid JSON matching this schema: {...}"
// Then validate:
const parsed = PartRecommendation.safeParse(JSON.parse(responseText));
if (!parsed.success) {
  // Handle validation failure — retry or fallback
}

Cost Optimization

Claude API pricing in 2026 for Sonnet: $3 per million input tokens, $15 per million output tokens. Opus is 5x more expensive. Here's how I keep costs sane:

Use the smallest model that works. I ran an experiment with the EuroParts Part Finder: Sonnet correctly identified the right part 94% of the time. Opus hit 97%. For a product that shows alternatives anyway, that 3% didn't justify the 5x cost increase. Sonnet handles all production traffic.

Minimize input tokens. System prompts are sent with every request. I trimmed the EuroParts system prompt from 2,400 tokens to 800 tokens with zero quality loss. That's 1,600 tokens saved per request. At 50,000 requests per month, that's 80 million tokens — $240 saved monthly.

Set sensible `max_tokens`. Don't default to 4096. If your typical response is 200 tokens, set max_tokens to 512. You won't get charged for unused tokens, but a lower cap prevents runaway responses from costing you.

Batch non-urgent requests. I process product description generation in batches during off-peak hours (midnight-6AM UTC). Retry rates are lower and latency is more consistent.

Here's a cost tracking utility I use in every project:

typescript
// lib/cost-tracker.ts
const PRICING = {
  "claude-sonnet-4-20250514": {
    inputPerMillion: 3,
    outputPerMillion: 15,
  },
  "claude-opus-4-20250514": {
    inputPerMillion: 15,
    outputPerMillion: 75,
  },
} as const;

type ModelId = keyof typeof PRICING;

export function calculateCost(
  model: ModelId,
  inputTokens: number,
  outputTokens: number
): number {
  const pricing = PRICING[model];
  const inputCost = (inputTokens / 1_000_000) * pricing.inputPerMillion;
  const outputCost = (outputTokens / 1_000_000) * pricing.outputPerMillion;
  return Math.round((inputCost + outputCost) * 10000) / 10000;
}

My monthly spend for a product serving 50,000 Claude requests: approximately $380. Before optimization, it was $950. The biggest wins came from response caching (40% reduction) and system prompt trimming (25% reduction).


Key Takeaways

  • Start with Claude Sonnet. It handles 90% of product use cases at a fraction of Opus cost. Upgrade only when you measure a quality gap that matters to users.
  • Stream everything user-facing. The perceived latency difference between streaming and waiting is the difference between "this feels fast" and "is it broken?"
  • Tool use replaces prompt engineering hacks. Instead of asking Claude to output structured data and parsing it, define tools that call your actual business logic. It's more reliable and easier to debug.
  • Cache normalized inputs aggressively. Identical questions get identical answers. A SHA-256 hash of the normalized input makes a reliable cache key.
  • Set spending limits before you ship. A staging environment prompt injection loop can burn hundreds of dollars in minutes. Set caps in the Anthropic console and implement per-user rate limits in your application.
  • Monitor quality weekly. Sample responses, track user feedback, and refine system prompts. Model updates can shift behavior — what worked last month might need adjustment.
  • Log every request's token usage. You can't optimize what you don't measure. Store input tokens, output tokens, model, and timestamp for every API call.

*Last updated: April 14, 2026*

Written by Uvin Vindula

Uvin Vindula (IAMUVIN) is a Web3 and AI engineer based in Sri Lanka and the United Kingdom. He built the EuroParts Lanka AI Part Finder using Claude API — a system where customers describe car problems in plain English and get matched to exact OEM parts. He is the author of The Rise of Bitcoin, Director of Blockchain and Software Solutions at Terra Labz, and founder of uvin.lk — Sri Lanka's Bitcoin education platform with 10,000+ learners.

For AI development projects: hello@iamuvin.com Book a call: calendly.com/iamuvin

Working on a Web3 or AI project?

Share
Uvin Vindula

Uvin Vindula

Web3 and AI engineer based in Sri Lanka and the UK. Author of The Rise of Bitcoin. Director of Blockchain and Software Solutions at Terra Labz. Founder of uvin.lk — Sri Lanka's Bitcoin education platform with 10,000+ learners.