Claude API TypeScript Client Library Setup Guide
Setting up a TypeScript client for the Claude API means wrapping HTTP calls in typed interfaces so you get autocomplete, compile-time checks, and predictable response shapes instead of raw any objects. This guide walks through the setup from an empty project to a typed client that handles requests, streaming, and errors correctly.
If you just want a working endpoint without maintaining your own SDK wrapper, a hosted layer like SubToAPI gives you the same request/response shape with an API key, so the TypeScript types below apply either way — only the base URL and auth header change.
Project setup
Start with a clean TypeScript project targeting Node 18+ (native fetch support):
mkdir claude-client && cd claude-client
npm init -y
npm install typescript tsx --save-dev
npx tsc --init
In tsconfig.json, make sure you're targeting ES2022 or later and have module set to NodeNext or ESNext so top-level fetch and async iterators work without polyfills:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
}
}
Store your API key in an environment variable, never hardcoded:
export ANTHROPIC_API_KEY="sk-ant-..."
Defining the types
The core of a good TypeScript client is modeling the request and response shapes accurately. Here's a minimal but complete set of types for the messages endpoint:
interface MessageParam {
role: "user" | "assistant";
content: string | ContentBlock[];
}
interface ContentBlock {
type: "text" | "tool_use" | "tool_result";
text?: string;
id?: string;
name?: string;
input?: Record<string, unknown>;
}
interface CreateMessageRequest {
model: string;
max_tokens: number;
messages: MessageParam[];
system?: string;
temperature?: number;
stream?: boolean;
tools?: ToolDefinition[];
}
interface ToolDefinition {
name: string;
description: string;
input_schema: Record<string, unknown>;
}
interface MessageResponse {
id: string;
type: "message";
role: "assistant";
content: ContentBlock[];
model: string;
stop_reason: string | null;
usage: {
input_tokens: number;
output_tokens: number;
};
}
These types mirror the standard Claude messages shape, so they work whether you call Anthropic directly or a wrapper service.
Building the client
Wrap fetch in a small class so you're not repeating headers and base URLs everywhere:
class ClaudeClient {
private baseUrl: string;
private apiKey: string;
constructor(apiKey: string, baseUrl = "https://api.anthropic.com/v1") {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
async createMessage(
req: CreateMessageRequest
): Promise<MessageResponse> {
const res = await fetch(`${this.baseUrl}/messages`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": this.apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify(req),
});
if (!res.ok) {
const errorBody = await res.text();
throw new Error(`Claude API error ${res.status}: ${errorBody}`);
}
return res.json() as Promise<MessageResponse>;
}
}
Usage:
const client = new ClaudeClient(process.env.ANTHROPIC_API_KEY!);
const response = await client.createMessage({
model: "claude-opus-4",
max_tokens: 1024,
messages: [{ role: "user", content: "Summarize this paragraph in one sentence." }],
});
console.log(response.content[0].text);
If you're using SubToAPI instead, only the base URL and header name change — the rest of the typed client is identical:
const client = new ClaudeClient(
process.env.SUBTOAPI_KEY!,
"https://api.subtoapi.app/v1"
);
SubToAPI expects Authorization: Bearer $SUBTOAPI_KEY rather than x-api-key, so swap that header if you point your client there. See the quickstart and messages reference for the exact request shape.
Handling streaming responses
Streaming requires switching from res.json() to reading the response body as a stream of server-sent events. Here's a typed async generator that yields text deltas:
async function* streamMessage(
client: ClaudeClient,
req: CreateMessageRequest
): AsyncGenerator<string> {
const res = await fetch(`${client["baseUrl"]}/messages`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": client["apiKey"],
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({ ...req, stream: true }),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const data = JSON.parse(line.replace("data:", "").trim());
if (data.type === "content_block_delta" && data.delta?.text) {
yield data.delta.text;
}
}
}
}
This pattern works the same against SubToAPI's streaming endpoint — details are in the streaming docs.
Adding tool use types
If your app calls tools, extend the request type and add a discriminated union for response content blocks so TypeScript narrows correctly:
type ContentBlockUnion =
| { type: "text"; text: string }
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> };
function isToolUse(
block: ContentBlockUnion
): block is { type: "tool_use"; id: string; name: string; input: Record<string, unknown> } {
return block.type === "tool_use";
}
This lets you loop over response.content and handle tool calls without casting. The tools docs cover the schema format for defining tools on either Anthropic's API or SubToAPI.
Error handling and retries
Wrap calls in retry logic for rate limits and transient failures:
async function withRetry<T>(fn: () => Promise<T>, retries = 3): Promise<T> {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (err) {
if (i === retries - 1) throw err;
await new Promise((r) => setTimeout(r, 2 ** i * 500));
}
}
throw new Error("unreachable");
}
This keeps your typed client resilient without littering retry logic throughout your application code.
Why teams route through a managed layer
Writing this client once is easy. Maintaining it across model version bumps, key rotation, team billing, and usage tracking across multiple apps is the part that gets tedious. SubToAPI exposes the same typed request/response shape behind sub_live_... application keys, with per-key usage metadata and team seats, so you keep this exact TypeScript client and just point it at a different base URL. Check pricing or start a free trial at signup.
questions
Do I need the official Anthropic SDK, or can I write my own TypeScript client? You don't need it. A thin typed wrapper around fetch, like the one above, is often simpler to maintain and easier to adapt to a different base URL or auth header.
What TypeScript version and Node version should I use? Node 18+ and TypeScript 5+ are recommended — Node 18 ships native fetch, and TypeScript 5 handles discriminated unions and satisfies cleanly for response typing.
Can I reuse this client with a different API base URL, like SubToAPI? Yes. The messages and streaming shapes are the same; only the base URL and the auth header (x-api-key vs Authorization: Bearer) need to change.