← Blog

Claude API Function Calling: A TypeScript Example

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

"Function calling" on Claude is officially called tool use: you describe one or more functions (name, description, JSON Schema input) to Claude, and the model decides when to call them, returning a structured tool_use block instead of plain text. Your code executes the real function and sends the result back so Claude can finish the task.

This article walks through a complete TypeScript example — types, request, response handling, and the follow-up call — so you can wire this into a real application rather than piecing it together from fragments.

How Claude Tool Use Actually Works

The flow has four steps:

  1. You send a request with a tools array describing available functions and their input schemas.
  2. Claude replies with stop_reason: "tool_use" and one or more content blocks of type tool_use, each containing a name and input object matching your schema.
  3. Your application runs the actual function with that input.
  4. You send a new message back to Claude containing a tool_result block with the output, and Claude produces the final answer (or calls another tool).

Nothing is executed on Anthropic's servers — the model only emits structured arguments. Your code is responsible for running the function safely.

Defining Tools with TypeScript Types

Start by modeling the shapes you'll work with. This keeps the tool schema and your function signature in sync.

interface WeatherInput {
  city: string;
  units?: "celsius" | "fahrenheit";
}

interface ToolDefinition {
  name: string;
  description: string;
  input_schema: {
    type: "object";
    properties: Record<string, unknown>;
    required: string[];
  };
}

const getWeatherTool: ToolDefinition = {
  name: "get_weather",
  description: "Get the current weather for a given city.",
  input_schema: {
    type: "object",
    properties: {
      city: { type: "string", description: "City name" },
      units: { type: "string", enum: ["celsius", "fahrenheit"] },
    },
    required: ["city"],
  },
};

The input_schema is standard JSON Schema. Keep descriptions specific — vague descriptions are the most common reason Claude picks the wrong tool or skips one entirely.

Making the Request

interface Message {
  role: "user" | "assistant";
  content: string | ContentBlock[];
}

interface ContentBlock {
  type: "text" | "tool_use" | "tool_result";
  text?: string;
  id?: string;
  name?: string;
  input?: Record<string, unknown>;
  tool_use_id?: string;
  content?: string;
}

interface ClaudeResponse {
  id: string;
  stop_reason: "end_turn" | "tool_use" | "max_tokens";
  content: ContentBlock[];
}

async function callClaude(messages: Message[]): Promise<ClaudeResponse> {
  const res = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-api-key": process.env.ANTHROPIC_API_KEY!,
      "anthropic-version": "2023-06-01",
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      max_tokens: 1024,
      tools: [getWeatherTool],
      messages,
    }),
  });

  return res.json();
}

Handling the tool_use Response

Claude's reply may mix plain text and tool calls in the same content array, so you need to scan for tool_use blocks explicitly.

async function executeWeatherTool(input: WeatherInput): Promise<string> {
  // Replace with a real API call
  return `18°${input.units === "fahrenheit" ? "F" : "C"} and partly cloudy in ${input.city}`;
}

async function runConversation(userText: string): Promise<string> {
  let messages: Message[] = [{ role: "user", content: userText }];

  let response = await callClaude(messages);

  while (response.stop_reason === "tool_use") {
    const toolUseBlocks = response.content.filter(
      (b): b is ContentBlock & { type: "tool_use" } => b.type === "tool_use"
    );

    messages.push({ role: "assistant", content: response.content });

    const toolResults: ContentBlock[] = [];
    for (const block of toolUseBlocks) {
      if (block.name === "get_weather") {
        const result = await executeWeatherTool(block.input as unknown as WeatherInput);
        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id!,
          content: result,
        });
      }
    }

    messages.push({ role: "user", content: toolResults });
    response = await callClaude(messages);
  }

  const finalText = response.content.find((b) => b.type === "text");
  return finalText?.text ?? "";
}

This loop handles the common case where Claude calls a tool, gets the result, and either answers directly or calls another tool. Always loop rather than assuming a single round trip — multi-step tool use is normal, especially with more complex schemas.

Common Pitfalls in TypeScript Implementations

Simplifying the Infrastructure Layer

The TypeScript logic above — request building, response parsing, the tool-result loop — stays the same regardless of which backend serves the model. If you're already relying on a Claude subscription rather than managing raw Anthropic API billing, SubToAPI turns that access into a standard HTTPS endpoint with its own sub_live_... application keys, so the same request/response shapes, including tool use, work without changing your integration code. See the tool use docs and the quickstart for the exact request format, or check pricing if you're evaluating it for a team. There's a free trial at signup.

Testing Your Tool Definitions

Before shipping, test with inputs designed to break the schema:

Logging the raw content array during development — not just the final text — makes it much easier to see why Claude picked (or skipped) a tool.

Questions

Does Claude support parallel function calls in one response? Yes. A single response can contain multiple tool_use blocks. Your code should collect all of them, execute each, and return all corresponding tool_result blocks in the next message before continuing.

Can I use JSON Schema validation libraries with Claude's tool inputs? Yes, and it's recommended. Define your schema once with Zod or a similar library, derive the input_schema from it, and reuse the same schema to validate tool_use.input at runtime before casting to a TypeScript type.

What's the difference between tool use and streaming responses? They're independent features. Tool use defines what Claude can call; streaming controls how the response is delivered token by token. You can combine both, but tool-use content blocks are only finalized once the full block has streamed in — see /docs/streaming for handling partial tool-use JSON correctly.

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 →