Claude API Function Calling: A TypeScript Example
"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:
- You send a request with a
toolsarray describing available functions and their input schemas. - Claude replies with
stop_reason: "tool_use"and one or more content blocks of typetool_use, each containing anameandinputobject matching your schema. - Your application runs the actual function with that input.
- You send a new message back to Claude containing a
tool_resultblock 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
- Loose typing on
input.tool_use.inputarrives asunknown-ish JSON — validate it with a schema library (Zod, io-ts) before casting, since Claude can occasionally produce malformed arguments for edge-case prompts. - Forgetting
tool_use_id. Everytool_resultmust reference the exactidfrom the matchingtool_useblock, or the conversation breaks. - Not handling multiple tool calls in one turn. Claude can request several tools at once; your code needs to resolve all of them before sending results back.
- Blocking on slow functions. If your tool calls a slow external API, run independent calls with
Promise.allrather than sequentially awaiting each one.
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:
- Ambiguous requests where two tools could plausibly apply
- Missing required fields in a prompt (does Claude ask for clarification or guess?)
- Prompts that shouldn't trigger any tool call at all
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.