By Sagar Shankaran, Founder of CallSphere
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.
Key takeaways
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
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
});
The SDK provides complete TypeScript types for all API parameters and responses:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
flowchart LR
USER(["User message"])
LOOP{"messages.create<br/>agent loop"}
THINK["Extended thinking<br/>optional"]
TOOL{"stop_reason<br/>tool_use?"}
EXEC["Execute tool<br/>append tool_result"]
DONE(["stop_reason<br/>end_turn"])
USER --> LOOP --> THINK --> TOOL
TOOL -->|Yes| EXEC --> LOOP
TOOL -->|No| DONE
style LOOP fill:#4f46e5,stroke:#4338ca,color:#fff
style THINK fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
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);
}
}
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}`);
}
}
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})`);
}
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();
});
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" }],
});
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");
});
});
// 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);

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
May 2026's biggest agent-architecture shift: planning, tool selection, and self-correction move inside the model. Framework code shrinks. Here is what changes.
A three-way comparison of Gemini Enterprise, Anthropic managed agents and OpenAI Frontier Platform after Cloud Next 2026 — strengths, gaps, buyer fit.
Anthropic's May 2026 push positions Claude as a vertical platform for financial services. The strategic positioning versus OpenAI and Google.
ServiceNow Project Arc vs Anthropic Managed Agents — runtime, governance, integration, and use cases. The 2026 enterprise autonomous agent comparison.
Anthropic and Moody's announced a data partnership in May 2026 that grounds Claude in audited financial reference data. Why grounding reduces hallucination and what it unlocks.
Anthropic chose not to release Mythos publicly. Inside the dual-use cybersecurity calculus, what restricted release means for enterprises, and the ripple effects.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.