Your First LLM API Call in Node.js: Request and Response Anatomy
Make your first Claude API call from Node.js and TypeScript, and learn every part of the request and response: messages, system, max_tokens, stop_reason, usage.
On this page· Setup
Why this matters
Every AI feature in this wiki, from a chat box to a multi-agent research system, is built from the same primitive: one request to the Messages API and one response. If you know exactly what each field does, debugging becomes reading rather than guessing: a truncated answer is a stop_reason, an expensive request is usage, and a confused model is usually a system prompt or a missing message.
Setup#
npm install @anthropic-ai/sdkpnpm add @anthropic-ai/sdkCreate the client once and import it everywhere. With no arguments it reads ANTHROPIC_API_KEY from the environment.
import Anthropic from "@anthropic-ai/sdk";
export const anthropic = new Anthropic({
timeout: 60_000, // milliseconds
maxRetries: 2, // retries 408, 409, 429, 5xx, and connection errors with backoff
});
export const MODEL = "claude-opus-5-5";The request and response, end to end#
Step 1 of 5
build request: model, max_tokens, system, messages
Everything the model needs for this task goes into this one request. The API key is added by the SDK from the environment.
Show text descriptionHide text description
The life of a single request, step by step. Participants, left to right: Your server, Messages API.
- Your server → Your server: build request: model, max_tokens, system, messages · Everything the model needs for this task goes into this one request. The API key is added by the SDK from the environment.
- Your server → Messages API: POST /v1/messages · The SDK sends the request over HTTPS, retrying automatically on rate limits and transient errors.
- Messages API → Messages API: generate tokens until a stop condition · The model writes (and possibly reasons) until it finishes, hits max_tokens, or wants a tool.
- Messages API → Your server: { content: [blocks], stop_reason, usage } · content is a list of typed blocks (text, thinking, tool_use); stop_reason says why generation ended; usage reports input and output tokens.
- Your server → Your server: check stop_reason, read text blocks, log usage · Branch on stop_reason before trusting the text, and record usage for cost tracking.
Here is a complete call that prints every part of the response:
import { anthropic, MODEL } from "./lib/anthropic.ts";
const message = await anthropic.messages.create({
model: MODEL,
max_tokens: 2048,
system: "You are a concise assistant for web developers. Answer in at most three sentences.",
output_config: { effort: "low" },
messages: [{ role: "user", content: "What is the difference between a process and a thread?" }],
});
console.log("id:", message.id);
console.log("model:", message.model);
console.log("stop_reason:", message.stop_reason);
console.log("usage:", message.usage);
console.log("request id (quote it in support tickets):", message._request_id);
for (const block of message.content) {
if (block.type === "text") console.log("\n" + block.text);
}node --env-file=.env.local first-call.tsThe request, field by field#
| Field | Required | What it does |
|---|---|---|
model | Yes | Which model handles the request, for example claude-opus-5-5 |
max_tokens | Yes | The maximum number of output tokens. Generation stops here even mid-sentence |
messages | Yes | The conversation: alternating user and assistant turns, starting with user |
system | No | Instructions that frame the whole conversation: role, rules, output format |
output_config.effort | No | How much reasoning and thoroughness to spend: low to max |
thinking | No | Reasoning settings, for example { type: "adaptive", display: "summarized" } |
tools | No | Functions the model may ask you to run (covered in Tool use) |
metadata.user_id | No | An opaque ID for the end user, which helps the provider detect abuse; never put an email or name here |
stop_sequences | No | Strings that end generation when produced |
A message's content can be a plain string or a list of content blocks, which is how you send images, documents, and tool results:
import { anthropic, MODEL } from "./lib/anthropic.ts";
const message = await anthropic.messages.create({
model: MODEL,
max_tokens: 1024,
messages: [
{
role: "user",
content: [
// Images must be JPEG, PNG, GIF, or WebP. This is a PNG rendering of the React logo.
{
type: "image",
source: { type: "url", url: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/500px-React-icon.svg.png" },
},
{ type: "text", text: "What library does this logo represent? One sentence." },
],
},
],
metadata: { user_id: "user_7f3a" }, // an opaque ID, never personal data
});
console.log(message.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join(""));The response, field by field#
{
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"model": "claude-opus-5-5",
"content": [
{ "type": "thinking", "thinking": "", "signature": "…" },
{ "type": "text", "text": "A process is an isolated program with its own memory…" }
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 42,
"output_tokens": 118,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
}
}contentis a list. Even a simple answer can contain a reasoning block before the text. Never assumecontent[0]is the text; filter bytype.stop_reasontells you why generation ended. Handle it before trusting the text.usageis what you are billed for: uncached input, cache writes and reads, and output (which includes reasoning).
Handle every stop reason#
Hover or tap a component to see what it does.
Show text descriptionHide text description
Code that only reads the text of a response treats a truncated answer, a refusal, and a request for a tool as if they were complete answers. Branch on stop_reason first.
- Response: Every Messages API response says why generation stopped.
- end_turn: The model finished naturally. Read the text blocks.
- max_tokens: The answer hit your output limit and is incomplete. Raise max_tokens, ask for shorter output, or continue the response.
- tool_use: The model is waiting for your code to execute one or more tools and return tool_result blocks.
- refusal: The request was declined. Do not show partial output as an answer; show a clear message instead.
- pause_turn: A long server-side tool turn paused. Append the response and call again to continue.
Wrap the common cases in one helper so every feature handles them the same way:
import type Anthropic from "@anthropic-ai/sdk";
export class IncompleteAnswerError extends Error {}
export class RefusedError extends Error {}
/** The text of a finished answer, or a typed error explaining why there is none. */
export function answerText(message: Anthropic.Message): string {
switch (message.stop_reason) {
case "end_turn":
case "stop_sequence":
return message.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("");
case "max_tokens":
throw new IncompleteAnswerError("The answer was cut off at max_tokens");
case "refusal":
throw new RefusedError("The model declined this request");
case "tool_use":
case "pause_turn":
throw new Error(`stop_reason "${message.stop_reason}" needs the tool loop, not answerText()`);
default:
throw new Error(`Unexpected stop_reason: ${String(message.stop_reason)}`);
}
}Errors and retries#
The SDK throws typed errors: Anthropic.BadRequestError (400: fix the request), AuthenticationError (401: check the key), PermissionDeniedError (403), NotFoundError (404: often a mistyped model ID), RateLimitError (429), InternalServerError (5xx), and APIConnectionError (network). It already retries the transient ones (429, 5xx, connection errors) twice with exponential backoff, so do not wrap every call in your own retry loop; handle the final failure instead. When you report a problem to the provider, include the request ID (message._request_id, or error.requestID on an error).
Common mistakes#
- Reading
content[0].text. The first block may be reasoning or a tool call. Filter bytype === "text". - Setting
max_tokenstoo low. Reasoning counts toward it, so a tight limit can cut answers off. Use a generous limit and steer length with instructions and effort. - Starting
messageswith an assistant turn, or putting instructions in a fake assistant message. Usesystemfor instructions; prefilling the assistant turn is not supported on current models. - Creating a new client per request. Reuse one client; it manages connections and retries.
- Putting personal data in
metadata.user_id. Use an opaque, stable ID such as a hash or your internal user ID.
Try it
Explore the response
- Run
first-call.tswithmax_tokens: 20and observestop_reasonand the cut-off text. Then run it throughanswerText(). - Add
thinking: { type: "adaptive", display: "summarized" }and raise effort tohigh. Compareusage.output_tokenswith the low-effort run. - Change the model ID to
claude-opus-9-9and note which error class is thrown and what its message says.
Check your understanding
Key takeaways
- A request is
model,max_tokens, andmessages, plus optionalsystem, effort, thinking, tools, and metadata. - A response is a list of content blocks, a
stop_reason, andusage; filter blocks by type. - Branch on
stop_reasonbefore trusting text:end_turn,max_tokens,tool_use,refusal,pause_turn. - Reuse one client, rely on its built-in retries, handle typed errors, and log request IDs.
Next steps#
Finished this article? Track it to see your progress on the roadmap.
Related articles
- How LLMs Work: Tokens, Context Windows, Statelessness, and ReasoningA developer's mental model of LLMs: tokens, next-token prediction, the context window, why the API is stateless, and how reasoning works, with runnable code.
- Multi-Turn Chat and Conversation Memory with the Claude APIBuild multi-turn chat in Node.js: keep history, replay content blocks correctly, and control cost with sliding windows, summaries, compaction, and long-term memory.