← Blog

Claude API TypeScript Client Library Setup Guide

2026-10-04 · 5 min read · SubToAPI Team

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.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →