Skip to content
AI Full-Stack Wiki

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.

Difficulty: Beginner15 min readUpdated
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/sdk
pnpm add @anthropic-ai/sdk

Create the client once and import it everywhere. With no arguments it reads ANTHROPIC_API_KEY from the environment.

lib/anthropic.ts
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#

One request to the Messages APIYour server sends a POST to the Messages API with the model, an output limit, a system prompt, and messages; the model generates; the response carries content blocks, the stop reason, and usage.Your serverMessages APIbuild request: model,max_tokens, system,messagesPOST /v1/messagesgenerate tokens until astop condition{ content: [blocks],stop_reason, usage }check stop_reason, readtext blocks, log usage

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.

Anatomy of one Messages API call: your Node.js code builds a request with model, max_tokens, system, and messages; the API returns content blocks, a stop_reason, and token usage.
Show text description

The life of a single request, step by step. Participants, left to right: Your server, Messages API.

  1. 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.
  2. Your server → Messages API: POST /v1/messages · The SDK sends the request over HTTPS, retrying automatically on rate limits and transient errors.
  3. 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.
  4. 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.
  5. 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:

first-call.ts
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.ts

The request, field by field#

FieldRequiredWhat it does
modelYesWhich model handles the request, for example claude-opus-5-5
max_tokensYesThe maximum number of output tokens. Generation stops here even mid-sentence
messagesYesThe conversation: alternating user and assistant turns, starting with user
systemNoInstructions that frame the whole conversation: role, rules, output format
output_config.effortNoHow much reasoning and thoroughness to spend: low to max
thinkingNoReasoning settings, for example { type: "adaptive", display: "summarized" }
toolsNoFunctions the model may ask you to run (covered in Tool use)
metadata.user_idNoAn opaque ID for the end user, which helps the provider detect abuse; never put an email or name here
stop_sequencesNoStrings 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:

describe-image.ts
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
  }
}
  • content is a list. Even a simple answer can contain a reasoning block before the text. Never assume content[0] is the text; filter by type.
  • stop_reason tells you why generation ended. Handle it before trusting the text.
  • usage is what you are billed for: uncached input, cache writes and reads, and output (which includes reasoning).

Handle every stop reason#

Handling every stop_reasonEach response ends with a stop reason. end_turn means show the answer; max_tokens means the output was cut off; tool_use means run the requested tools; refusal means show a safe fallback; pause_turn means continue the turn.Responsestop_reasonend_turnuse the answermax_tokenscut off: retry or continuetool_userun tools, send resultsrefusalsafe fallback messagepause_turnsend it back to continue

Hover or tap a component to see what it does.

Handling stop_reason in the Messages API: end_turn, max_tokens, tool_use, refusal, and pause_turn each need different code paths.
Show 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:

lib/answer.ts
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 by type === "text".
  • Setting max_tokens too 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 messages with an assistant turn, or putting instructions in a fake assistant message. Use system for 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

  1. Run first-call.ts with max_tokens: 20 and observe stop_reason and the cut-off text. Then run it through answerText().
  2. Add thinking: { type: "adaptive", display: "summarized" } and raise effort to high. Compare usage.output_tokens with the low-effort run.
  3. Change the model ID to claude-opus-9-9 and note which error class is thrown and what its message says.

Check your understanding

Which fields are required in a Messages API request?
Why should you not read message.content[0].text directly?
The SDK raises a RateLimitError after a call. What has already happened?

Key takeaways

  • A request is model, max_tokens, and messages, plus optional system, effort, thinking, tools, and metadata.
  • A response is a list of content blocks, a stop_reason, and usage; filter blocks by type.
  • Branch on stop_reason before 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.