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โ
| Aspect | Claude API (Anthropic) | Agent SDK (Claude Code) |
|---|---|---|
| Use case | Building your own AI app | Automating Claude Code |
| File access | None built in (implement it yourself) | Built-in tools (Read, Edit, etc.) |
| Context | You manage it | Managed automatically |
| Level of control | Full control | Limited |
| Learning curve | High | Low |
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_tokensandusage.output_tokens
Found an issue on this page? Report it โ