Skip to main content

Claude API Patterns

Unlike Claude Code (the Agent SDK), using the Anthropic Claude API directly gives you finer-grained control. This chapter covers the patterns you need when building your own AI application.

Claude API vs Agent SDKโ€‹

AspectClaude API (Anthropic)Agent SDK (Claude Code)
Use caseBuilding your own AI appAutomating Claude Code
File accessNone built in (implement it yourself)Built-in tools (Read, Edit, etc.)
ContextYou manage itManaged automatically
Level of controlFull controlLimited
Learning curveHighLow

Basic Messages APIโ€‹

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

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

// Basic call
async function simpleChat(userMessage: string): Promise<string> {
const response = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 4096,
messages: [
{ role: "user", content: userMessage }
]
});

return response.content[0].type === "text"
? response.content[0].text
: "";
}

// With a system prompt
async function expertChat(
systemPrompt: string,
userMessage: string
): Promise<string> {
const response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 8192,
system: systemPrompt,
messages: [
{ role: "user", content: userMessage }
]
});

return response.content[0].type === "text"
? response.content[0].text
: "";
}

Managing Conversation Historyโ€‹

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

const client = new Anthropic();

type Message = {
role: "user" | "assistant";
content: string;
};

class ConversationManager {
private history: Message[] = [];
private systemPrompt: string;

constructor(systemPrompt: string) {
this.systemPrompt = systemPrompt;
}

async chat(userMessage: string): Promise<string> {
this.history.push({ role: "user", content: userMessage });

const response = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 4096,
system: this.systemPrompt,
messages: this.history
});

const assistantMessage = response.content[0].type === "text"
? response.content[0].text
: "";

this.history.push({ role: "assistant", content: assistantMessage });

// If the history grows too long, drop the oldest entries first
if (this.history.length > 20) {
this.history = this.history.slice(-20);
}

return assistantMessage;
}

clearHistory() {
this.history = [];
}
}

// Usage
const assistant = new ConversationManager(
"You are a friendly coding tutor. Answer in English."
);
const reply1 = await assistant.chat("What is a recursive function?");
const reply2 = await assistant.chat("Show me an example with the Fibonacci sequence.");

Streaming Responsesโ€‹

Handle long responses in real time:

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

const client = new Anthropic();

async function streamResponse(prompt: string): Promise<string> {
let fullText = "";

const stream = client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 4096,
messages: [{ role: "user", content: prompt }]
});

// Real-time output
stream.on("text", (text) => {
process.stdout.write(text);
fullText += text;
});

await stream.finalMessage();
console.log(); // newline

return fullText;
}

// Async iterator approach
async function streamWithIterator(prompt: string) {
const stream = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 4096,
stream: true,
messages: [{ role: "user", content: prompt }]
});

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

Tool Useโ€‹

Give Claude a set of functions and let it call them:

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

const client = new Anthropic();

// Tool definitions
const tools: Anthropic.Tool[] = [
{
name: "get_weather",
description: "Gets the current weather for a given city",
input_schema: {
type: "object",
properties: {
city: {
type: "string",
description: "Name of the city to look up (e.g. Seoul, Tokyo)"
},
unit: {
type: "string",
enum: ["celsius", "fahrenheit"],
description: "Temperature unit"
}
},
required: ["city"]
}
},
{
name: "search_database",
description: "Searches an internal database for information",
input_schema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
limit: { type: "number", description: "Maximum number of results" }
},
required: ["query"]
}
}
];

// Tool execution function (real implementation)
async function executeTool(
name: string,
input: Record<string, any>
): Promise<string> {
if (name === "get_weather") {
// In practice, call a weather API
return JSON.stringify({
city: input.city,
temperature: 22,
condition: "clear",
unit: input.unit || "celsius"
});
}
if (name === "search_database") {
// In practice, run a DB query
return JSON.stringify({
results: [`Result 1 for ${input.query}`, `Result 2 for ${input.query}`],
total: 2
});
}
return "Tool not found.";
}

// Tool loop
async function agentLoop(userMessage: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userMessage }
];

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

// Add the response to the history
messages.push({ role: "assistant", content: response.content });

// No tool use โ€” finish
if (response.stop_reason === "end_turn") {
const textBlock = response.content.find(b => b.type === "text");
return textBlock?.type === "text" ? textBlock.text : "";
}

// Handle tool calls
const toolUses = response.content.filter(b => b.type === "tool_use");
const toolResults: Anthropic.ToolResultBlockParam[] = await Promise.all(
toolUses.map(async (toolUse) => {
if (toolUse.type !== "tool_use") return null!;
const result = await executeTool(
toolUse.name,
toolUse.input as Record<string, any>
);
return {
type: "tool_result" as const,
tool_use_id: toolUse.id,
content: result
};
})
);

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

Prompt Cachingโ€‹

Cache long prompts you reuse to cut costs:

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

const client = new Anthropic();

// Cache a long document while answering multiple questions
async function cachedDocumentQA(
document: string,
questions: string[]
): Promise<string[]> {
const answers: string[] = [];

for (const question of questions) {
const response = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 2048,
system: [
{
type: "text",
text: "You are a document analysis expert.",
},
{
type: "text",
text: document,
// @ts-ignore - cache_control is a beta feature
cache_control: { type: "ephemeral" } // Cache the document
}
],
messages: [{ role: "user", content: question }]
});

const text = response.content[0].type === "text"
? response.content[0].text
: "";
answers.push(text);

// Check whether the cache was used
console.log("Cache created:", response.usage.cache_creation_input_tokens ?? 0);
console.log("Cache read:", response.usage.cache_read_input_tokens ?? 0);
}

return answers;
}
Choosing a model
  • claude-opus-4-8: Complex reasoning, long-document analysis (top performance, higher cost)
  • claude-sonnet-5: Balanced performance (suitable for most production cases, released 2026-06-30)
  • claude-haiku-4-5: Fast responses, simple tasks (low cost, high throughput)
Watch out when calling the API directly
  • Rate limits: there are caps on requests per minute and tokens per minute
  • Error handling: you must retry on 429 (rate limit) and 529 (overloaded)
  • Cost tracking: monitor usage.input_tokens and usage.output_tokens