Skip to content
Agentic AI
Agentic AI7 min read10 views

Claude API in TypeScript: Production Patterns and Best Practices

Production-ready TypeScript patterns for the Claude API. Covers SDK setup, type safety, error handling, streaming, middleware patterns, testing strategies, and deployment best practices for TypeScript applications.

Setting Up the TypeScript SDK

The official Anthropic TypeScript SDK provides full type safety, streaming support, and automatic retries. It is the recommended way to interact with the Claude API from any TypeScript or JavaScript project.

npm install @anthropic-ai/sdk

Basic Client Configuration

import Anthropic from "@anthropic-ai/sdk";

// Basic initialization (reads ANTHROPIC_API_KEY from environment)
const client = new Anthropic();

// Explicit configuration
const client = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
  maxRetries: 3,        // Built-in retry with backoff
  timeout: 120_000,     // 2 minute timeout
});

Type-Safe Message Creation

The SDK provides complete TypeScript types for all API parameters and responses:

flowchart TD
    START["Claude API in TypeScript: Production Patterns and…"] --> A
    A["Setting Up the TypeScript SDK"]
    A --> B
    B["Type-Safe Message Creation"]
    B --> C
    C["Typed Tool Definitions"]
    C --> D
    D["The Agentic Loop Pattern"]
    D --> E
    E["Streaming Pattern"]
    E --> F
    F["Middleware Pattern for Cross-Cutting Co…"]
    F --> G
    G["Testing Patterns"]
    G --> H
    H["Environment Configuration"]
    H --> DONE["Key Takeaways"]
    style START fill:#4f46e5,stroke:#4338ca,color:#fff
    style DONE fill:#059669,stroke:#047857,color:#fff
import Anthropic from "@anthropic-ai/sdk";
import {
  MessageParam,
  ContentBlockParam,
  TextBlock,
  ToolUseBlock,
} from "@anthropic-ai/sdk/resources/messages";

const client = new Anthropic();

// Strongly typed messages
const messages: MessageParam[] = [
  {
    role: "user",
    content: "Explain the SOLID principles with TypeScript examples.",
  },
];

const response = await client.messages.create({
  model: "claude-sonnet-4-5-20250514",
  max_tokens: 4096,
  messages,
});

// Type-safe response handling
for (const block of response.content) {
  if (block.type === "text") {
    // TypeScript knows block is TextBlock here
    console.log(block.text);
  } else if (block.type === "tool_use") {
    // TypeScript knows block is ToolUseBlock here
    console.log(block.name, block.input);
  }
}

Typed Tool Definitions

Use Zod schemas to define tool inputs with runtime validation and TypeScript type inference:

import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

// Define tool input schemas with Zod
const SearchSchema = z.object({
  query: z.string().describe("Search query string"),
  category: z.enum(["docs", "code", "issues"]).optional()
    .describe("Filter by content category"),
  limit: z.number().int().min(1).max(50).default(10)
    .describe("Maximum results to return"),
});

type SearchInput = z.infer<typeof SearchSchema>;

const CreateTicketSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string(),
  priority: z.enum(["low", "medium", "high", "critical"]),
  assignee: z.string().email().optional(),
});

type CreateTicketInput = z.infer<typeof CreateTicketSchema>;

// Convert to Claude tool format
const tools: Anthropic.Tool[] = [
  {
    name: "search",
    description: "Search the knowledge base for relevant documents, code, or issues.",
    input_schema: zodToJsonSchema(SearchSchema) as Anthropic.Tool.InputSchema,
  },
  {
    name: "create_ticket",
    description: "Create a new support ticket in the ticketing system.",
    input_schema: zodToJsonSchema(CreateTicketSchema) as Anthropic.Tool.InputSchema,
  },
];

// Type-safe tool execution
async function executeTool(name: string, input: unknown): Promise<string> {
  switch (name) {
    case "search": {
      const parsed = SearchSchema.parse(input);
      return await performSearch(parsed);
    }
    case "create_ticket": {
      const parsed = CreateTicketSchema.parse(input);
      return await createTicket(parsed);
    }
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}

The Agentic Loop Pattern

import Anthropic from "@anthropic-ai/sdk";

interface AgentConfig {
  model: string;
  maxTokens: number;
  systemPrompt: string;
  tools: Anthropic.Tool[];
  maxIterations: number;
}

interface AgentResult {
  response: string;
  toolCalls: { name: string; input: unknown; result: string }[];
  totalTokens: { input: number; output: number };
  iterations: number;
}

async function runAgent(
  userMessage: string,
  config: AgentConfig,
): Promise<AgentResult> {
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage },
  ];

  const toolCalls: AgentResult["toolCalls"] = [];
  let totalInput = 0;
  let totalOutput = 0;

  for (let i = 0; i < config.maxIterations; i++) {
    const response = await client.messages.create({
      model: config.model,
      max_tokens: config.maxTokens,
      system: config.systemPrompt,
      tools: config.tools,
      messages,
    });

    totalInput += response.usage.input_tokens;
    totalOutput += response.usage.output_tokens;

    if (response.stop_reason === "end_turn") {
      const textContent = response.content
        .filter((b): b is Anthropic.TextBlock => b.type === "text")
        .map((b) => b.text)
        .join("");

      return {
        response: textContent,
        toolCalls,
        totalTokens: { input: totalInput, output: totalOutput },
        iterations: i + 1,
      };
    }

    if (response.stop_reason === "tool_use") {
      const toolResults: Anthropic.ToolResultBlockParam[] = [];

      for (const block of response.content) {
        if (block.type === "tool_use") {
          try {
            const result = await executeTool(block.name, block.input);
            toolCalls.push({ name: block.name, input: block.input, result });
            toolResults.push({
              type: "tool_result",
              tool_use_id: block.id,
              content: result,
            });
          } catch (error) {
            toolResults.push({
              type: "tool_result",
              tool_use_id: block.id,
              content: `Error: ${error instanceof Error ? error.message : String(error)}`,
              is_error: true,
            });
          }
        }
      }

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

  throw new Error(`Agent exceeded max iterations (${config.maxIterations})`);
}

Streaming Pattern

import Anthropic from "@anthropic-ai/sdk";

async function* streamResponse(
  messages: Anthropic.MessageParam[],
  options?: { onToolUse?: (name: string, input: unknown) => void },
): AsyncGenerator<string> {
  const stream = await client.messages.stream({
    model: "claude-sonnet-4-5-20250514",
    max_tokens: 4096,
    messages,
  });

  for await (const event of stream) {
    if (
      event.type === "content_block_delta" &&
      event.delta.type === "text_delta"
    ) {
      yield event.delta.text;
    }
  }
}

// Usage in an Express/Fastify endpoint
app.post("/api/chat", async (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  const messages = req.body.messages;

  for await (const chunk of streamResponse(messages)) {
    res.write(`data: ${JSON.stringify({ text: chunk })}\n\n`);
  }

  res.write("data: [DONE]\n\n");
  res.end();
});

Middleware Pattern for Cross-Cutting Concerns

type MessageCreateParams = Anthropic.MessageCreateParams;
type Message = Anthropic.Message;

type Middleware = (
  params: MessageCreateParams,
  next: (params: MessageCreateParams) => Promise<Message>,
) => Promise<Message>;

class ClaudeClient {
  private client: Anthropic;
  private middlewares: Middleware[] = [];

  constructor() {
    this.client = new Anthropic();
  }

  use(middleware: Middleware): this {
    this.middlewares.push(middleware);
    return this;
  }

  async create(params: MessageCreateParams): Promise<Message> {
    const chain = this.middlewares.reduceRight(
      (next, middleware) => (p: MessageCreateParams) => middleware(p, next),
      (p: MessageCreateParams) => this.client.messages.create(p),
    );
    return chain(params);
  }
}

// Logging middleware
const loggingMiddleware: Middleware = async (params, next) => {
  const start = Date.now();
  console.log(`[Claude] Request: model=${params.model}`);

  const response = await next(params);

  console.log(
    `[Claude] Response: ${response.usage.input_tokens}in/${response.usage.output_tokens}out ` +
    `${Date.now() - start}ms`,
  );
  return response;
};

// Cost tracking middleware
const costMiddleware: Middleware = async (params, next) => {
  const response = await next(params);

  const COSTS: Record<string, { input: number; output: number }> = {
    "claude-sonnet-4-5-20250514": { input: 3, output: 15 },
    "claude-haiku-4-5-20250514": { input: 1, output: 5 },
  };

  const rates = COSTS[params.model] ?? { input: 3, output: 15 };
  const cost =
    (response.usage.input_tokens * rates.input +
      response.usage.output_tokens * rates.output) /
    1_000_000;

  console.log(`[Cost] $${cost.toFixed(6)}`);
  return response;
};

// Usage
const claude = new ClaudeClient();
claude.use(loggingMiddleware).use(costMiddleware);

const response = await claude.create({
  model: "claude-sonnet-4-5-20250514",
  max_tokens: 4096,
  messages: [{ role: "user", content: "Hello" }],
});

Testing Patterns

Mocking the SDK

import { vi, describe, it, expect } from "vitest";
import Anthropic from "@anthropic-ai/sdk";

// Mock the entire SDK
vi.mock("@anthropic-ai/sdk", () => {
  return {
    default: vi.fn().mockImplementation(() => ({
      messages: {
        create: vi.fn(),
        stream: vi.fn(),
      },
    })),
  };
});

describe("ChatService", () => {
  it("should process a simple message", async () => {
    const mockCreate = vi.fn().mockResolvedValue({
      content: [{ type: "text", text: "Hello! How can I help?" }],
      usage: { input_tokens: 10, output_tokens: 8 },
      stop_reason: "end_turn",
    });

    const client = new Anthropic();
    (client.messages.create as any) = mockCreate;

    const service = new ChatService(client);
    const result = await service.chat("Hello");

    expect(result).toBe("Hello! How can I help?");
    expect(mockCreate).toHaveBeenCalledWith(
      expect.objectContaining({
        model: "claude-sonnet-4-5-20250514",
        messages: [{ role: "user", content: "Hello" }],
      }),
    );
  });

  it("should handle tool use responses", async () => {
    const mockCreate = vi
      .fn()
      .mockResolvedValueOnce({
        content: [
          { type: "tool_use", id: "tool_1", name: "search", input: { query: "test" } },
        ],
        stop_reason: "tool_use",
        usage: { input_tokens: 20, output_tokens: 15 },
      })
      .mockResolvedValueOnce({
        content: [{ type: "text", text: "Based on the search results..." }],
        stop_reason: "end_turn",
        usage: { input_tokens: 50, output_tokens: 30 },
      });

    // Test the full tool use loop
    const client = new Anthropic();
    (client.messages.create as any) = mockCreate;

    const result = await runAgent("Search for test", agentConfig);
    expect(result.toolCalls).toHaveLength(1);
    expect(result.toolCalls[0].name).toBe("search");
  });
});

Environment Configuration

// config.ts
import { z } from "zod";

const ConfigSchema = z.object({
  ANTHROPIC_API_KEY: z.string().startsWith("sk-ant-"),
  CLAUDE_MODEL: z.string().default("claude-sonnet-4-5-20250514"),
  CLAUDE_MAX_TOKENS: z.coerce.number().default(4096),
  CLAUDE_MAX_RETRIES: z.coerce.number().default(3),
  CLAUDE_TIMEOUT_MS: z.coerce.number().default(120_000),
});

export const config = ConfigSchema.parse(process.env);

Deployment Considerations

  • Keep the SDK updated: Anthropic releases frequent SDK updates with new features and bug fixes. Pin the major version but allow minor/patch updates
  • Connection pooling: The SDK manages HTTP connections internally. Create one client instance and reuse it across your application
  • Serverless considerations: In AWS Lambda or Vercel Functions, cold starts add 1-2 seconds. Initialize the client outside the handler function so it persists across invocations
  • Memory management: Streaming large responses accumulates strings in memory. For very long outputs, process chunks incrementally rather than concatenating everything
Share
C

Written by

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.

Related Articles You May Like

AI Interview Prep

8 AI System Design Interview Questions Actually Asked at FAANG in 2026

Real AI system design interview questions from Google, Meta, OpenAI, and Anthropic. Covers LLM serving, RAG pipelines, recommendation systems, AI agents, and more — with detailed answer frameworks.

AI Interview Prep

8 LLM & RAG Interview Questions That OpenAI, Anthropic & Google Actually Ask

Real LLM and RAG interview questions from top AI labs in 2026. Covers fine-tuning vs RAG decisions, production RAG pipelines, evaluation, PEFT methods, positional embeddings, and safety guardrails with expert answers.

AI Interview Prep

7 AI Coding Interview Questions From Anthropic, Meta & OpenAI (2026 Edition)

Real AI coding interview questions from Anthropic, Meta, and OpenAI in 2026. Includes implementing attention from scratch, Anthropic's progressive coding screens, Meta's AI-assisted round, and vector search — with solution approaches.

AI Interview Prep

7 Agentic AI & Multi-Agent System Interview Questions for 2026

Real agentic AI and multi-agent system interview questions from Anthropic, OpenAI, and Microsoft in 2026. Covers agent design patterns, memory systems, safety, orchestration frameworks, tool calling, and evaluation.

Learn Agentic AI

MCP Ecosystem Hits 5,000 Servers: Model Context Protocol Production Guide 2026

The MCP ecosystem has grown to 5,000+ servers. This production guide covers building MCP servers, enterprise adoption patterns, the 2026 roadmap, and integration best practices.

AI Interview Prep

6 AI Safety & Alignment Interview Questions From Anthropic & OpenAI (2026)

Real AI safety and alignment interview questions from Anthropic and OpenAI in 2026. Covers alignment challenges, RLHF vs DPO, responsible scaling, red-teaming, safety-first decisions, and autonomous agent oversight.