Most Claude API examples online are Python. If you're a TypeScript developer, you end up mentally translating everything — mapping Dict to Record, figuring out which string literals map to union types, guessing whether the SDK even ships types. This guide skips the translation. Everything here is TypeScript-first, from installation through production Next.js streaming.
Installing the Anthropic SDK
npm install @anthropic-ai/sdk
That's it. TypeScript types are bundled — no @types/ package needed.
India developers: Need an API key without a USD card? AICredits gives you Claude API access with INR / UPI billing. The SDK targets Node.js 18+ and works in Edge Runtime with some caveats (streaming works, but you can't use
process.stdout).
Set your API key in .env.local for Next.js or .env for a standalone Node app:
ANTHROPIC_API_KEY=sk-ant-...
The client reads process.env.ANTHROPIC_API_KEY by default, so you don't have to pass it explicitly — though you can if you're managing multiple keys.
Basic completion with full types
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY, // defaults to env var
});
async function complete(prompt: string): Promise<string> {
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
// TypeScript knows this is ContentBlock[]
const block = message.content[0];
if (block.type === "text") {
return block.text;
}
throw new Error("Unexpected response type");
}
The message.content array is typed as ContentBlock[], which is a discriminated union. You need the block.type === "text" check before TypeScript will let you access block.text — this is intentional. Claude can return tool_use blocks alongside text, so the narrowing forces you to handle both cases.
message.usage gives you { input_tokens, output_tokens } for cost tracking. Log it from day one.
Streaming with async iteration
Non-streaming calls feel slow for anything over a few sentences. Streaming fixes that. The cleanest TypeScript pattern uses async iteration:
async function streamCompletion(prompt: string): Promise<void> {
const stream = await client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
for await (const chunk of stream) {
if (
chunk.type === "content_block_delta" &&
chunk.delta.type === "text_delta"
) {
process.stdout.write(chunk.delta.text);
}
}
const finalMessage = await stream.finalMessage();
console.log("\nUsage:", finalMessage.usage);
}
stream.finalMessage() waits for the stream to complete and returns the full assembled message — useful when you need the complete text plus usage stats after streaming finishes. Don't call it before the for await loop completes, or it'll resolve too early.
There's also stream.text_stream if you just want an async iterator of text strings without dealing with chunk types:
for await (const text of stream.text_stream) {
process.stdout.write(text);
}
Simpler for text-only responses. Use the full chunk iteration when you need to detect stop reasons or handle tool use mid-stream.
Tool use in TypeScript
Function calling is where TypeScript really earns its keep — you get compile-time guarantees on tool definitions and message shapes.
import Anthropic from "@anthropic-ai/sdk";
const tools: Anthropic.Tool[] = [
{
name: "get_weather",
description: "Get current weather for a city",
input_schema: {
type: "object",
properties: {
city: { type: "string", description: "City name" },
units: { type: "string", enum: ["celsius", "fahrenheit"] },
},
required: ["city"],
},
},
];
async function runWithTools(query: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: query },
];
while (true) {
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
tools,
messages,
});
if (response.stop_reason === "end_turn") {
const textBlock = response.content.find((b) => b.type === "text");
return textBlock?.type === "text" ? textBlock.text : "";
}
if (response.stop_reason === "tool_use") {
const toolUse = response.content.find((b) => b.type === "tool_use");
if (!toolUse || toolUse.type !== "tool_use") break;
// Execute the tool
const toolResult = await executeTools(toolUse.name, toolUse.input);
messages.push({ role: "assistant", content: response.content });
messages.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: toolUse.id,
content: JSON.stringify(toolResult),
},
],
});
}
}
return "";
}
The Anthropic.MessageParam type enforces the conversation structure. If you try to push a malformed message, TypeScript catches it at compile time — not at 2am when your production webhook fails.
The toolUse.input is typed as unknown. Cast it to whatever interface your tool expects after validating — Zod works well here.
Next.js API route with streaming (App Router)
Here's a streaming chat endpoint using Next.js App Router:
// app/api/chat/route.ts
import Anthropic from "@anthropic-ai/sdk";
import { NextRequest } from "next/server";
const client = new Anthropic();
export async function POST(req: NextRequest) {
const { message } = await req.json();
const stream = await client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: message }],
});
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
if (
chunk.type === "content_block_delta" &&
chunk.delta.type === "text_delta"
) {
controller.enqueue(encoder.encode(chunk.delta.text));
}
}
controller.close();
},
});
return new Response(readable, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
And consuming it from a React client component:
// app/page.tsx (client component — add "use client" at top)
"use client";
import { useState } from "react";
export default function ChatPage() {
const [output, setOutput] = useState("");
async function sendMessage(message: string) {
setOutput("");
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
setOutput((prev) => prev + decoder.decode(value));
}
}
return (
<div>
<button onClick={() => sendMessage("Explain RAG in one paragraph")}>
Ask Claude
</button>
<pre>{output}</pre>
</div>
);
}
One thing to watch: the response.body!.getReader() non-null assertion. If the API route returns a non-streaming response (e.g., a 400 error), body won't be null but getReader() will give you the error body as a stream. Add a response.ok check before reading:
if (!response.ok) {
const error = await response.text();
throw new Error(`API error: ${error}`);
}
Multi-turn conversation state in React
For a real chat UI, you need to maintain conversation history. Here's the pattern:
"use client";
import { useState } from "react";
interface Message {
role: "user" | "assistant";
content: string;
}
export default function Chat() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
async function send() {
const userMessage: Message = { role: "user", content: input };
const updatedMessages = [...messages, userMessage];
setMessages(updatedMessages);
setInput("");
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: updatedMessages }),
});
let assistantText = "";
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
assistantText += chunk;
setMessages([
...updatedMessages,
{ role: "assistant", content: assistantText },
]);
}
}
return (
<div>
{messages.map((m, i) => (
<div key={i}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={send}>Send</button>
</div>
);
}
Update the API route to accept messages instead of a single message, and pass the full array to client.messages.stream. Claude handles the conversation context — you're just responsible for maintaining the array.
Error handling
The SDK exports typed error classes. Catch them specifically:
import Anthropic from "@anthropic-ai/sdk";
try {
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello" }],
});
} catch (error) {
if (error instanceof Anthropic.APIError) {
console.error(`Status: ${error.status}`); // 429, 500, etc.
console.error(`Message: ${error.message}`);
if (error.status === 429) {
// Rate limited — exponential backoff
await new Promise((r) => setTimeout(r, 2 ** retries * 1000));
}
if (error.status === 529) {
// Claude overloaded — retry after a moment
}
}
throw error;
}
Anthropic.APIError covers HTTP errors from the API. For network failures (no response at all), you'll get a standard Error. The SDK also exports Anthropic.RateLimitError, Anthropic.APIConnectionError, and Anthropic.AuthenticationError as subclasses — useful if you want to handle them separately without checking error.status.
In Next.js API routes, convert these to proper HTTP responses:
catch (error) {
if (error instanceof Anthropic.RateLimitError) {
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
}
if (error instanceof Anthropic.APIError) {
return Response.json({ error: error.message }, { status: 502 });
}
return Response.json({ error: "Internal server error" }, { status: 500 });
}
Validating environment variables at startup
Don't let missing env vars surface as cryptic runtime errors. Validate at app startup:
// lib/env.ts
import { z } from "zod";
const envSchema = z.object({
ANTHROPIC_API_KEY: z.string().min(1),
NODE_ENV: z.enum(["development", "production", "test"]),
});
export const env = envSchema.parse(process.env);
Import env from this file in your Claude client setup. If ANTHROPIC_API_KEY is missing, you get a clear Zod validation error at startup — not a 401 from Anthropic after your first user request.
For Next.js, put server-only env vars in .env.local and never import this file in client components. Use NEXT_PUBLIC_ prefix only for values that are genuinely safe to expose. Your Anthropic API key is never one of them.
What to build next
This covers the core SDK patterns. For more complex setups — streaming agents that call multiple tools in sequence, or real-time UX patterns — the streaming agents guide covers the architecture in depth.
If you're comparing the Anthropic SDK to OpenAI's client, Claude API vs OpenAI API breaks down the differences in message format, tool calling, and streaming behavior.
For tool design specifics — what makes a good tool description, how to structure input_schema for reliable extraction — see how to design tools for AI agents.
The TypeScript SDK is actively maintained and the types are accurate. When in doubt, let your editor's IntelliSense guide you — hover over message.content, inspect Anthropic.Tool, read the discriminated union. The types tell the story.



