Published Sep 16, 2025 ⦁ Updated Nov 10, 2026 ⦁ 14 min read
AI with JavaScript and Node.js: A Beginner's Guide

AI with JavaScript and Node.js: A Beginner's Guide

You can build AI with JavaScript today without learning Python or machine learning math. Most AI features in a JavaScript app are one HTTPS call from your Node.js server to a hosted model such as GPT, Claude or Gemini, made with the provider's official SDK or with the AI SDK. Two other paths cover the rest: running a small model inside the browser or your Node process with Transformers.js, and running an open model on your own machine through Ollama. Most gen AI JavaScript tutorials stop at a chat demo. This one gives you runnable code for all three paths, plus streaming, structured output, tool calling, real prices and the errors you will hit first.

Monthly npm downloads of JavaScript AI packages
@anthropic-ai/sdk 222.8M
openai 194.7M
ai (AI SDK) 147.4M
@google/genai 108.1M
onnxruntime-node 31.6M
onnxruntime-web 25.9M
@huggingface/transformers 17.1M
langchain 14.1M
ollama 3.9M
@tensorflow/tfjs 2.1M
@mlc-ai/web-llm 625K

Downloads include CI runs and installs as a dependency of other tools, so read them as relative size, not as user counts. Source: npm downloads API, Oct 10 to Nov 8, 2026

AI in JS: three ways to add artificial intelligence to a JavaScript app

A few years ago, JavaScript artificial intelligence meant training small neural networks in the browser with brain.js or TensorFlow.js. Today you rarely need to start there. You pick a pre-trained model and decide where it runs.

Path What runs where Good for What you pay Setup
1. Hosted LLM API Your Node.js server calls OpenAI, Claude or Gemini over HTTPS Chat, summaries, extraction, agents, anything that needs a strong model Per token, per request An API key and about 10 lines of code
2. Model inside your app A model file runs in the browser tab or in your Node process (WebAssembly, WebGPU or native ONNX) Sentiment, embeddings and search, speech to text, small chat models, private data Nothing per request. The user or your server downloads the model once npm install, then a model download on first run
3. Local LLM server Ollama runs an open model on your machine. Your JavaScript talks to it on localhost Developing without a bill, offline tools, data that must not leave the machine Hardware and electricity Install Ollama and pull a multi-gigabyte model
Best for your first AI feature in a web app: Hosted API through the AI SDK
One generateText call works with every major provider, and you can switch models by changing one line. Start with a cheap model such as Claude Haiku 5.5, GPT-6 Luna or a Gemini Flash model.
Best for classification, embeddings and search with no per-request bill: Transformers.js
The same pipeline() code runs in Node.js and in the browser. The models used in this guide are 23 to 268 MB and run on a CPU.
Best for experimenting for free or keeping data on your machine: Ollama
You get a real chat model with streaming and JSON output. The price is a 7 GB download and slower, weaker answers than hosted flagships.
Best for features that should work offline in the browser: Chrome built-in AI or WebLLM
No server at all. Treat it as an enhancement with a server fallback, because it depends on the user's browser and hardware.

You can mix them: a hosted model for chat, plus Transformers.js embeddings for search so each keystroke costs nothing.

Before you start:

  • Node.js 22 or newer. The current openai, @google/genai and ai packages declare Node 22 as their minimum on npm. @anthropic-ai/sdk supports Node 20 and later.
  • ES modules. The examples use import and top-level await. Save files as .mjs, or set "type": "module" in package.json.
  • Keys in environment variables. Put ANTHROPIC_API_KEY=... in a .env file, add it to .gitignore, and load it with Node's built-in flag: node --env-file=.env ask.mjs (Node.js CLI docs).

1. Call an LLM API with fetch

Every provider exposes a plain HTTPS API, so the smallest working version of AI in JavaScript needs no library at all. This is the Claude Messages API called with nothing but fetch. Save it as ask.mjs:

// ask.mjs - run with: ANTHROPIC_API_KEY=sk-ant-... node ask.mjs
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-haiku-5-5",
    max_tokens: 1024,
    messages: [
      { role: "user", content: "Explain JavaScript closures in two sentences." },
    ],
  }),
});

if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);

const data = await res.json();
for (const block of data.content) {
  if (block.type === "text") console.log(block.text);
}
console.log(data.usage); // { input_tokens: ..., output_tokens: ... }

What is going on:

  1. You send a model ID and a list of messages, which is the conversation so far.
  2. max_tokens caps the length of the answer, which also caps what one request can cost.
  3. The reply is JSON. content is an array of blocks, so loop over it and read the text blocks. usage tells you how many tokens you paid for.

The API is stateless. For a chat, keep the messages array yourself and append each user message and model reply before the next request. A long chat gets more expensive with every turn, because you resend the whole history each time.

The if (!res.ok) line matters more than it looks. Run the script with a wrong key and this is the real output:

Error: HTTP 401: {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"},"request_id":"req_..."}

Without that check you would get a confusing TypeError two lines later, when the code tries to loop over a content field that does not exist.

The official SDKs: OpenAI, Anthropic and Google GenAI

Raw fetch is fine for one call. The official SDKs add types, automatic retries, streaming helpers and readable errors. These are the three official AI JavaScript SDKs, with versions from npm as of November 10, 2026:

Package Version Reads this env var License
openai 7.54.0 OPENAI_API_KEY Apache-2.0
@anthropic-ai/sdk 1.7.0 ANTHROPIC_API_KEY Apache-2.0
@google/genai 3.0.0 GOOGLE_API_KEY, or pass apiKey yourself Apache-2.0

OpenAI. The README calls the Responses API the primary API. Chat Completions still works and is described there as "supported indefinitely".

// npm install openai
import OpenAI from "openai";

const client = new OpenAI(); // reads OPENAI_API_KEY

const response = await client.responses.create({
  model: "gpt-6-luna",
  instructions: "You are a concise coding assistant.",
  input: "Are semicolons optional in JavaScript?",
});

console.log(response.output_text);

Anthropic (Claude).

// npm install @anthropic-ai/sdk
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic(); // reads ANTHROPIC_API_KEY

const message = await client.messages.create({
  model: "claude-haiku-5-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Are semicolons optional in JavaScript?" }],
});

for (const block of message.content) {
  if (block.type === "text") console.log(block.text);
}

Google (Gemini). @google/genai is the SDK where, per its README, Google adds new features. Many tutorials still import the older @google/generative-ai package, whose latest npm release is from April 2025.

// npm install @google/genai
import { GoogleGenAI } from "@google/genai";

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

const response = await ai.models.generateContent({
  model: "gemini-3.8-flash",
  contents: "Are semicolons optional in JavaScript?",
});

console.log(response.text);

The model IDs above are on each provider's models page today: OpenAI, Claude, Gemini. IDs change a few times a year, so check those pages instead of copying one from an old post, this one included.

The three SDKs do the same four jobs under different names:

Task openai @anthropic-ai/sdk @google/genai
Text client.responses.create(), read response.output_text client.messages.create(), read text blocks in message.content ai.models.generateContent(), read response.text
Streaming stream: true, then events of type response.output_text.delta client.messages.stream(), then .on("text", ...) or await stream.finalMessage() ai.models.generateContentStream(), read chunk.text
Structured output client.responses.parse() with zodTextFormat(), read output_parsed client.messages.parse() with zodOutputFormat(), read parsed_output config.responseMimeType: "application/json" plus config.responseJsonSchema
Tool calling tools: [{ type: "function", ... }], send back a function_call_output item tools: [{ name, input_schema }], send back a tool_result block config.tools: [{ functionDeclarations }], read response.functionCalls

Sources: OpenAI's structured outputs and function calling guides, Claude's structured outputs docs and the Google GenAI README. If that looks like a lot to remember, the next section is for you.

2. Use the AI SDK (one API for every provider)

The AI SDK is an open source TypeScript library from Vercel (the ai package, version 7.0.166 and Apache-2.0 on npm as of November 2026). It wraps every provider behind the same few functions, so the table above collapses into one way of doing each thing. It is the most downloaded multi-provider library in the chart at the top of this page, and a sensible default for a new project.

npm install ai @ai-sdk/anthropic zod
// generate.ts - run with: ANTHROPIC_API_KEY=sk-ant-... npx tsx generate.ts
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

const { text, usage } = await generateText({
  model: anthropic("claude-haiku-5-5"),
  system: "You write one-line git commit messages.",
  prompt: "Fixed the null check in the login form.",
});

console.log(text);
console.log(usage.inputTokens, usage.outputTokens);

Changing provider is a one-line change: install @ai-sdk/openai and write openai("gpt-6-luna"), or install @ai-sdk/google and write google("gemini-3.8-flash"). The provider packages read ANTHROPIC_API_KEY, OPENAI_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY. Note that the Google one differs from the name Google's own SDK uses.

There is a second way to name a model. A plain string such as "anthropic/claude-sonnet-5.5" goes through Vercel's AI Gateway and reads AI_GATEWAY_API_KEY, per the AI SDK Node.js guide. That gives you one key for many providers.

Streaming responses

LLMs produce text token by token. Streaming shows the answer while it is written instead of making the user stare at a spinner for several seconds. In Node, streamText gives you an async iterable:

import { streamText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

const result = streamText({
  model: anthropic("claude-haiku-5-5"),
  prompt: "Give me three name ideas for a habit-tracking app.",
});

for await (const delta of result.textStream) {
  process.stdout.write(delta);
}

In a web app you stream from a server route to the browser. This is a Next.js App Router route handler. It takes a standard Request and returns a standard Response:

// app/api/ask/route.ts
import { streamText, toTextStream, createTextStreamResponse } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

export async function POST(req: Request) {
  const { prompt } = await req.json();

  const result = streamText({
    model: anthropic("claude-haiku-5-5"),
    prompt: String(prompt).slice(0, 4000),
    maxOutputTokens: 800,
  });

  return createTextStreamResponse({
    stream: toTextStream({ stream: result.stream }),
  });
}

And plain browser JavaScript that reads the stream as it arrives, with no library:

const res = await fetch("/api/ask", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ prompt: "Summarize the plot of Hamlet in 3 lines." }),
});

const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
const output = document.querySelector("#output");

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  output.textContent += value;
}

For a full chat interface in React, Vue or Svelte, the SDK's useChat hook handles message state and streaming for you. If you build with AI coding tools, DevHunt's vibe coding guide covers that workflow.

Structured output: JSON you can trust

Free text is hard to use in code. When you want data (a category, a list of tags, fields pulled from an email), ask for an object that matches a schema. The SDK sends the schema to the model and validates what comes back, as described in the structured data docs:

import { generateText, Output } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

const { output } = await generateText({
  model: anthropic("claude-haiku-5-5"),
  output: Output.object({
    schema: z.object({
      sentiment: z.enum(["positive", "neutral", "negative"]),
      topics: z.array(z.string()).describe("Up to three short topic labels"),
      needsReply: z.boolean(),
    }),
  }),
  prompt:
    "Classify this support ticket: 'The export button has been broken since Monday. Please fix it, we have a deadline.'",
});

console.log(output.sentiment, output.topics, output.needsReply);

output is fully typed, so output.sentiment autocompletes in your editor. Old posts use generateObject for this. In ai 7 that function is marked deprecated, with the note "Use generateText with an output setting instead". Putting structured output on generateText and streamText also lets you combine it with tools in one call.

Tool calling: let the model run your functions

A model cannot look anything up by itself. Tool calling fixes that: you describe a function, the model decides when to call it and with which arguments, your code runs it, and the result goes back to the model. This is the building block of every agent.

import { generateText, tool, isStepCount } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

const { text, steps } = await generateText({
  model: anthropic("claude-haiku-5-5"),
  tools: {
    getPackageVersion: tool({
      description: "Get the latest published version of an npm package",
      inputSchema: z.object({
        name: z.string().describe("The npm package name, for example 'zod'"),
      }),
      execute: async ({ name }) => {
        const res = await fetch(`https://registry.npmjs.org/${name}/latest`);
        if (!res.ok) return { error: `npm returned ${res.status}` };
        const pkg = await res.json();
        return { name: pkg.name, version: pkg.version };
      },
    }),
  },
  stopWhen: isStepCount(5),
  prompt: "What is the latest version of the ai package on npm?",
});

console.log(text);
console.log(`${steps.length} steps`);

Two details trip people up. First, without stopWhen the SDK stops after one step, so you get the tool call but no final answer. stopWhen: isStepCount(5) lets it loop: call the tool, read the result, answer. Second, the description strings are part of your prompt. The model reads them to decide when to call the tool. The tool calling docs cover approvals and error handling.

Treat tool inputs as untrusted. The model writes them, and a user can talk it into writing something hostile. Never pass them straight into a shell command or SQL string.

If you later need retrieval pipelines, long-running agents or memory, that is when a larger JavaScript AI framework such as LangChain.js or Mastra earns its place. For a first feature they add more concepts than you need.

3. AI in Node.js: SDKs, local models and machine learning

Node.js is where most JavaScript AI code runs, because that is where your API keys and data live. Sections 1 and 2 are already Node code. This section covers what else an AI Node.js backend can do: run models in the same process, with no API key and no per-request bill.

The main Node.js AI packages beyond the provider SDKs, with versions from the npm registry on November 10, 2026:

Package Version What it does Runs
ai 7.0.166 One API for hosted LLMs: streaming, structured output, tools, agents Calls hosted models
@huggingface/transformers 4.3.2 Pre-trained models for embeddings, classification, speech, vision, small LLMs In your process, on CPU by default
ollama 0.6.4 Client for a local Ollama server Talks to localhost:11434
onnxruntime-node 1.31.1 Microsoft's runtime for any model exported to ONNX In your process, native code
@tensorflow/tfjs-node 4.22.0 (October 2024) Train and run TensorFlow.js models In your process, native bindings

Local models in Node.js with Transformers.js

Transformers.js is the JavaScript port of Hugging Face's Python library. Its pipeline() function bundles a model with the pre- and post-processing it needs, and it runs on ONNX Runtime under the hood. Sentiment analysis is four lines:

npm install @huggingface/transformers
// sentiment.mjs - run with: node sentiment.mjs
import { pipeline } from "@huggingface/transformers";

const classify = await pipeline(
  "sentiment-analysis",
  "Xenova/distilbert-base-uncased-finetuned-sst-2-english"
);

const reviews = [
  "I love how fast this app is!",
  "The update broke login and support never answered.",
];

console.log(await classify(reviews));

Output from running exactly this on Node.js 26.7 with @huggingface/transformers 4.3.2:

[
  { label: 'POSITIVE', score: 0.9996739029884338 },
  { label: 'NEGATIVE', score: 0.9997520446777344 }
]

The first run downloads the model and caches it on disk. Later runs load it from the cache.

Embeddings are the most useful local model for a web app. An embedding turns text into a list of numbers so that texts with similar meaning get similar numbers. That gives you search that understands "I forgot my login details" and "reset your password" are about the same thing:

// search.mjs - run with: node search.mjs
import { pipeline } from "@huggingface/transformers";

const embed = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", {
  dtype: "q8", // 23 MB quantized file instead of the 90 MB full-precision one
});

const docs = [
  "To reset your password, open Settings and choose Security.",
  "Invoices are emailed on the first day of each month.",
  "You can export your data as CSV from the Reports page.",
];
const query = "I forgot my login details";

const vectors = await embed([query, ...docs], { pooling: "mean", normalize: true });
console.log(vectors.dims); // [4, 384]

const [q, ...rest] = vectors.tolist();
const dot = (a, b) => a.reduce((sum, x, i) => sum + x * b[i], 0);

const ranked = rest
  .map((v, i) => ({ score: dot(q, v).toFixed(3), doc: docs[i] }))
  .sort((a, b) => b.score - a.score);
console.table(ranked);

Real output:

[ 4, 384 ]
┌─────────┬─────────┬──────────────────────────────────────────────────────────────┐
│ (index) │ score   │ doc                                                          │
├─────────┼─────────┼──────────────────────────────────────────────────────────────┤
│ 0       │ '0.503' │ 'To reset your password, open Settings and choose Security.' │
│ 1       │ '0.107' │ 'You can export your data as CSV from the Reports page.'     │
│ 2       │ '0.012' │ 'Invoices are emailed on the first day of each month.'       │
└─────────┴─────────┴──────────────────────────────────────────────────────────────┘

The query shares no keyword with the password document, and it still wins clearly. Because the vectors are normalized, the dot product is the cosine similarity. For more than a few thousand documents, store the vectors in a database with vector search (Postgres with pgvector, for example) instead of an array.

Local LLMs in Node.js with Ollama

Transformers.js is best for small task models. For a general chat model on your own machine, use Ollama. It is a desktop app and server that downloads open models and serves them on http://localhost:11434. The ollama npm package is its official JavaScript client.

  1. Install Ollama from ollama.com/download.
  2. Pull a model: ollama pull gemma4:e2b. The Ollama quickstart uses this one and says the download is about 7.2 GB, with 8 GB of VRAM (or unified memory on a Mac) recommended.
  3. npm install ollama zod and run:
// local.mjs - run with: node local.mjs
import ollama from "ollama";
import * as z from "zod";

// 1. Streaming chat
const stream = await ollama.chat({
  model: "gemma4:e2b",
  messages: [{ role: "user", content: "Explain a JavaScript closure in two sentences." }],
  stream: true,
});
for await (const part of stream) {
  process.stdout.write(part.message.content);
}

// 2. Structured output: pass a JSON schema as `format`
const Ticket = z.object({
  sentiment: z.enum(["positive", "neutral", "negative"]),
  needsReply: z.boolean(),
});
const response = await ollama.chat({
  model: "gemma4:e2b",
  messages: [{ role: "user", content: "Classify: 'Export has been broken since Monday.'" }],
  format: z.toJSONSchema(Ticket),
  options: { num_ctx: 8192 },
});
console.log(Ticket.parse(JSON.parse(response.message.content)));

This example was not run for this guide (Ollama was not installed on the test machine). It is type-checked against ollama 0.6.4 and follows the structured outputs docs. Three things that save an afternoon:

  • ECONNREFUSED 127.0.0.1:11434 is the exact error the script throws when the Ollama server is not running. Open the app, or run ollama serve on Linux.
  • The default context window is small. Ollama's docs give a default of 4K tokens on machines with less than 24 GiB of VRAM (context length). That is the most text the model can see at once, so a long document plus a long chat will not fit. Raise it per request with options: { num_ctx: 8192 }, as above, or set OLLAMA_CONTEXT_LENGTH when you start the server. More context needs more memory.
  • Browser pages on other origins are blocked. Ollama allows cross-origin requests from 127.0.0.1 and 0.0.0.0 by default, and you add more with OLLAMA_ORIGINS.

Ollama also serves OpenAI-compatible endpoints under http://localhost:11434/v1, so OpenAI-style clients can point at it with a changed base URL. You can develop against a local model and deploy against a hosted one.

ONNX Runtime and TensorFlow.js: Node.js machine learning

This is the path for JavaScript machine learning with your own model. If a data scientist trained it in Python with PyTorch or scikit-learn, export it to ONNX and run it in Node with ONNX Runtime. To show the real shape of the API, this script runs the same DistilBERT file that Transformers.js downloaded earlier. Copy model.onnx out of node_modules/@huggingface/transformers/.cache/ and put it next to the script:

// inspect.mjs - npm install onnxruntime-node, then: node inspect.mjs
import * as ort from "onnxruntime-node";

const session = await ort.InferenceSession.create("./model.onnx");
console.log(session.inputNames, session.outputNames);

// int64 inputs take a BigInt64Array; shape is [batch, tokens]
const ids = [101n, 2307n, 102n]; // [CLS] great [SEP]
const feeds = {
  input_ids: new ort.Tensor("int64", BigInt64Array.from(ids), [1, ids.length]),
  attention_mask: new ort.Tensor("int64", BigInt64Array.from(ids.map(() => 1n)), [1, ids.length]),
};
const results = await session.run(feeds);
console.log(results.logits.dims, results.logits.data);
[ 'input_ids', 'attention_mask' ] [ 'logits' ]
[ 1, 2 ] Float32Array(2) [ -4.288115978240967, 4.607375621795654 ]

Two numbers come back: the scores for "negative" and "positive". The second is much higher, so "great" is classified as positive. This is what a pipeline() call hides: turning text into token IDs, building tensors with the right type and shape, and turning raw scores into a label. Input names, types and shapes differ per model, so print session.inputNames first or open the file in Netron.

Classic machine learning using JavaScript means training the model yourself, and TensorFlow.js still does that in the browser and in Node. Be aware of its pace: the latest release of @tensorflow/tfjs and @tensorflow/tfjs-node on npm is 4.22.0 from October 21, 2024, more than two years ago. For new work, training in Python and serving through ONNX Runtime or Transformers.js is the safer route. The latest npm release of brain.js is a 2.0 beta from July 2024.

Is Node.js good for AI?

For building AI features, yes. An LLM call is network I/O, which is what Node's event loop handles well, and streaming a response to thousands of open connections fits it naturally. OpenAI, Anthropic and Google all ship official TypeScript SDKs.

For training large models or heavy numeric work, no. Machine learning with JavaScript lags well behind Python's ecosystem (PyTorch, NumPy, CUDA tooling), and nothing here changes that. Choosing JavaScript for AI makes sense when the work is calling models and building the product around them. Many teams use both languages.

4. Run AI models in the browser

Running a model on the user's device means no per-request cost, no server, and data that stays on the machine. The trade-offs: the user downloads the model first, small on-device models are much weaker than hosted ones, and speed depends on hardware you do not control.

Fast in-browser inference needs WebGPU. Per MDN's compatibility data, it ships in Chrome and Edge since version 113 on Windows, macOS and ChromeOS (Linux came in 144, on recent Intel GPUs only), in Chrome for Android since 121, in Safari since version 26, and partially in Firefox since 141. Firefox for Android has no WebGPU. Check "gpu" in navigator before loading anything large, and fall back to WebAssembly or to your server.

Transformers.js: ready-made models in a few lines

The same library from section 3 runs in the browser, with the same code. Without a bundler you can import it from a CDN, as the README shows:

// inside a script tag with type="module"
import { pipeline } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.2";

// Downloads a small model on first use, then the browser caches it
const classify = await pipeline(
  "sentiment-analysis",
  "Xenova/distilbert-base-uncased-finetuned-sst-2-english"
);

const result = await classify("I love how fast this app is!");
console.log(result); // [{ label: 'POSITIVE', score: 0.99... }]

In the browser it runs on the CPU through WebAssembly by default, with 8-bit quantized weights. Pass { device: "webgpu" } to use the GPU. The README lists the typical dtype choices: "fp32" (the WebGPU default), "fp16", "q8" (the WASM default) and "q4". For anything bigger than a demo, run it in a Web Worker so that loading and inference do not freeze the page.

WebLLM: a chat model inside the browser

WebLLM (@mlc-ai/web-llm, version 0.2.87, Apache-2.0) runs open LLMs fully in the browser on WebGPU, with an OpenAI-style API:

import { CreateMLCEngine } from "@mlc-ai/web-llm";

if (!("gpu" in navigator)) throw new Error("WebGPU is not available in this browser");

const engine = await CreateMLCEngine("Llama-3.2-1B-Instruct-q4f32_1-MLC", {
  initProgressCallback: (p) => console.log(p.text), // download and load progress
});

const chunks = await engine.chat.completions.create({
  messages: [{ role: "user", content: "Write a haiku about JavaScript." }],
  stream: true,
});

let reply = "";
for await (const chunk of chunks) {
  reply += chunk.choices[0]?.delta?.content ?? "";
}
console.log(reply);

Pick the model with care. Version 0.2.87 ships 165 prebuilt model IDs in prebuiltAppConfig.model_list, each with a vram_required_MB value (config source). The 1B Llama model above is listed at about 1.1 GB of VRAM. The 8B model in the README example, Llama-3.1-8B-Instruct-q4f32_1-MLC, is listed at about 6.1 GB. Make the download opt-in, show progress, and run the engine in a Web Worker (CreateWebWorkerMLCEngine) so the page stays responsive.

Chrome built-in AI: Gemini Nano with no model to host

Chrome now ships AI APIs that use a model the browser manages for you (Gemini Nano). You call a JavaScript API, and Chrome handles the download, storage and updates. The status below is from Chrome's built-in AI APIs page as of November 2026:

API On the web In extensions
Translator Chrome 138 Chrome 138
Language Detector Chrome 138 Chrome 138
Summarizer Chrome 138 Chrome 138
Prompt API (general prompts) Chrome 148 Chrome 138
Writer, Rewriter, Proofreader Developer trial Developer trial

The Prompt API is the general one. Always feature-detect, because other browsers do not have it:

const options = {
  expectedInputs: [{ type: "text", languages: ["en"] }],
  expectedOutputs: [{ type: "text", languages: ["en"] }],
};

if (!("LanguageModel" in self)) {
  // Not Chrome, or an older Chrome: call your server route instead
} else if ((await LanguageModel.availability(options)) !== "unavailable") {
  // create() can trigger the model download, so call it after a click
  const session = await LanguageModel.create({
    ...options,
    monitor(m) {
      m.addEventListener("downloadprogress", (e) => {
        console.log(`Downloaded ${e.loaded * 100}%`);
      });
    },
  });

  const stream = session.promptStreaming("Suggest three titles for a post about closures.");
  for await (const chunk of stream) {
    console.log(chunk);
  }
}

This snippet follows Chrome's documentation and was not run for this guide. The hardware limits are strict: desktop only (Windows 10 or 11, macOS 13 or later, Linux, or ChromeOS on Chromebook Plus), at least 22 GB of free disk space, and either a GPU with more than 4 GB of VRAM or a CPU with 16 GB of RAM and 4 cores. Android and iOS are not supported. So treat built-in AI as a progressive enhancement: use it when it is there, and call your server when it is not. The simpler Summarizer API works the same way: Summarizer.create(), then summarizer.summarize(text).

For your own models there is ONNX Runtime Web (onnxruntime-web 1.31.1), the browser twin of the Node runtime from section 3. Transformers.js is built on ONNX Runtime, so use it directly only for a custom ONNX model.

A tiny end-to-end project: explain this error

Here is the smallest complete app that does it right: the key stays on the server, the answer streams, and input and output are capped. You paste an error message and get a short explanation. Two files, no framework.

mkdir explain-error && cd explain-error
npm init -y && npm pkg set type=module
npm install ai @ai-sdk/anthropic
// server.mjs - run with: node --env-file=.env server.mjs
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { streamText, toTextStream, pipeTextStreamToResponse } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

const server = createServer(async (req, res) => {
  if (req.method === "GET" && req.url === "/") {
    res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
    return res.end(await readFile(new URL("./index.html", import.meta.url)));
  }

  if (req.method === "POST" && req.url === "/api/explain") {
    let body = "";
    for await (const chunk of req) body += chunk;

    let error;
    try {
      error = JSON.parse(body).error;
    } catch {}
    if (typeof error !== "string" || !error.trim()) {
      res.writeHead(400, { "content-type": "text/plain" });
      return res.end("Send JSON like { \"error\": \"TypeError: ...\" }");
    }

    const result = streamText({
      model: anthropic("claude-haiku-5-5"),
      system:
        "You explain JavaScript errors to beginners. Say what the error means, " +
        "the most likely cause, and one fix. Stay under 120 words.",
      prompt: error.slice(0, 4000), // cap what one request can cost
      maxOutputTokens: 400,
      onError({ error }) {
        console.error("Model call failed:", error);
      },
    });

    return pipeTextStreamToResponse({
      response: res,
      stream: toTextStream({ stream: result.stream }),
    });
  }

  res.writeHead(404, { "content-type": "text/plain" });
  res.end("Not found");
});

server.listen(3000, () => console.log("Open http://localhost:3000"));
<!-- index.html -->
<!doctype html>
<meta charset="utf-8" />
<title>Explain this error</title>
<form id="form">
  <textarea id="error" rows="5" cols="70" placeholder="Paste an error message"></textarea><br />
  <button>Explain</button>
</form>
<pre id="output" style="white-space: pre-wrap"></pre>

<script type="module">
  const form = document.querySelector("#form");
  const output = document.querySelector("#output");

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    output.textContent = "";

    const res = await fetch("/api/explain", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ error: document.querySelector("#error").value }),
    });
    if (!res.ok) {
      output.textContent = `Request failed: ${res.status} ${await res.text()}`;
      return;
    }

    const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      output.textContent += value;
    }
  });
</script>

Put ANTHROPIC_API_KEY=... in .env, run node --env-file=.env server.mjs and open http://localhost:3000.

What was tested: I ran this server on Node.js 26.7 with the model swapped for the AI SDK's mock model (MockLanguageModelV4 from ai/test, see the testing docs), so no key was needed. The page loaded, a bad request body returned 400, and the stream reader received three separate chunks about 150 ms apart, the delay the mock was set to. Then I ran it with a deliberately invalid key, which showed one behavior worth knowing before you ship:

$ curl -s -X POST http://localhost:3000/api/explain \
    -H 'content-type: application/json' -d '{"error":"TypeError: x"}' -w "status=%{http_code}"
status=200

# server log
Model call failed: APICallError [AI_APICallError]: invalid x-api-key

The browser gets HTTP 200 and an empty body, because a streamed response sends its status line before the model call has finished failing. The real reason is only in the server log, and only because of the onError callback. If your stream comes back empty, look there first.

Before you put this on the public internet, add what a demo skips: rate limiting per user or IP, authentication, and a spending limit in your provider's dashboard.

What AI in JavaScript costs

Hosted models charge per token, separately for input and output, quoted per million tokens. A token is a chunk of text. OpenAI's rule of thumb is that one token is about 4 characters or 0.75 English words (key concepts). Tokenizers differ: Anthropic's models overview says 1 million tokens is roughly 555,000 words on its current tokenizer. To count the tokens in your own prompt, paste it into DevHunt's token counter.

List prices per 1 million tokens, from DevHunt's AI model pricing data (checked against the official pricing pages on November 2, 2026, and spot-checked again on November 10):

Model API ID Input Output
Claude Haiku 5.5 claude-haiku-5-5 $0.10 $0.50
GPT-6 Luna gpt-6-luna $0.10 $0.50
Gemini 3.1 Flash-Lite gemini-3.1-flash-lite $0.25 $1.50
Gemini 3.8 Flash gemini-3.8-flash $0.75 $3.75
GPT-6.1 Terra gpt-6.1-terra $1 $5
Claude Sonnet 5.5 claude-sonnet-5-5 $2 $10
GPT-6.1 Sol gpt-6.1-sol $2 $10
Claude Opus 5.5 claude-opus-5-5 $4 $20
GPT-6 Astra gpt-6-astra $10 $50

Small print from the same sources: the Claude Haiku 5.5 price covers prompts up to 100K tokens, the OpenAI prices cover prompts up to 272K tokens, and Gemini 3.8 Flash is at an introductory rate through December 31, 2026 ($1.50 and $7.50 after that).

To make that concrete, take 1,000 chat replies with 500 input tokens and 500 output tokens each. That is 0.5 million tokens in and 0.5 million out:

Cost of 1,000 chat replies (500 input + 500 output tokens each)
Claude Haiku 5.5 $0.30
GPT-6 Luna $0.30
Gemini 3.1 Flash-Lite $0.88
Gemini 3.8 Flash $2.25
GPT-6.1 Terra $3.00
Claude Sonnet 5.5 $6.00
GPT-6.1 Sol $6.00
Claude Opus 5.5 $12.00
GPT-6 Astra $30.00

List prices, no caching or batch discounts. Source: Computed from list prices in DevHunt's AI model pricing data, checked Nov 2, 2026

That is a 100x spread for the same traffic. One caveat: current models "think" before they answer, and thinking is billed as output. Google's pricing page labels its output price as "including thinking tokens". So real output counts run higher than the visible text. Read the usage object on a few real responses before you estimate a bill.

Ways to keep the bill down:

  • Start with the cheapest model that does the job, and move up only where quality falls short.
  • Cap output (max_tokens or maxOutputTokens) and input length on the server, as the project above does.
  • Trim or summarize old messages in long chats, and cache answers to repeated questions.
  • Move simple tasks (sentiment, tagging, embeddings) to Transformers.js, where a request costs you nothing.

Free tiers. You can learn all of this without paying. Google's pricing page lists Gemini 3.8 Flash and the Flash-Lite models as free of charge on its free tier, and notes that free-tier content is used to improve Google's products, so keep private data out of it. DevHunt's list of free LLM APIs compares the providers that give you a free key, with rate limits and whether a card is needed. Ollama and Transformers.js are free for as long as your hardware holds up.

Common errors and how to fix them

These are the errors a first project runs into. The quoted messages are real output from running this guide's examples with invalid or missing keys on November 10, 2026, or come from the linked official docs.

What you see Cause Fix
401 Incorrect API key provided: sk-... with code invalid_api_key (OpenAI) Wrong, revoked or mistyped key Create a new key. Check for quotes or spaces in .env
401 with "type":"authentication_error","message":"invalid x-api-key" (Claude) Same Same. Check you are not sending another provider's key
400 API key not valid. Please pass a valid API key. with reason API_KEY_INVALID (Gemini) Same. Gemini answers a bad key with 400, not 401 Same. A 400 from Gemini is not always a bad request body
Missing credentials. Please pass an apiKey ... (OpenAI SDK) or Could not resolve authentication method. (Anthropic SDK) The env var is not set in this process Run with node --env-file=.env, or export the variable in the same shell
Unauthenticated request to AI Gateway. (AI SDK) A model passed as a string goes through the gateway, and AI_GATEWAY_API_KEY is not set Set that key, or use a provider package
429 rate_limit_error, or "Rate limit reached for requests" Too many requests or tokens per minute, or credits used up Wait for retry-after, lower concurrency. If it never clears, check billing
529 overloaded_error (Claude), 503 (OpenAI, Gemini) Provider over capacity Retry with backoff
A CORS error in the browser console, or a 401 starting CORS requests are not allowed You called the provider from browser code Call your own server route instead
400 prompt is too long (Claude), or a context length error Input is larger than the model's context window Send less: trim history, summarize, or split the document
Reply stops mid-sentence You hit the output cap. Check stop_reason or finishReason Raise max_tokens, or ask for a shorter answer
404 model not found Typo in the model ID, or the model was retired Copy the ID from the provider's models page
connect ECONNREFUSED 127.0.0.1:11434 Ollama is not running Start the Ollama app or ollama serve
HTTP 200 with an empty stream The model call failed after the response started Read the server log. Add onError to streamText

A few details behind the table:

  • Retries are built in. The OpenAI and Anthropic SDKs retry connection errors, 429s and 5xx responses twice by default with backoff, and maxRetries changes that (OpenAI README, Claude API errors). A 429 that reaches your code has already failed three times.
  • Not every 429 is a rate limit. OpenAI's error codes guide lists separate 429s for exhausted credits and spend limits. Anthropic's rate limit docs say a spend-cap 429 has no retry-after header and keeps failing until access resumes. Retrying those only burns time.
  • Catch typed errors, not strings. Test with instanceof against classes such as OpenAI.RateLimitError or Anthropic.AuthenticationError. Both extend APIError, which has a status field.
  • CORS is the provider protecting you. Anthropic's authentication docs say to call the API from your back end, not from code in your users' browsers. The fix is a server route, not a CORS proxy.

For errors in your own API routes, such as the 400 in the project above, see our guide to handling 400 Bad Request in REST APIs.

How this guide was checked. On November 10, 2026 I read the npm READMEs and registry data for every package named here, plus the linked provider, AI SDK, Ollama and Chrome docs. All SDK examples were type-checked against the listed versions. The Transformers.js and ONNX Runtime examples and the project server were run on Node.js 26.7 on macOS, with output pasted unedited. Hosted-API examples were run only with invalid keys, to capture the errors. The Ollama, WebLLM and Chrome built-in AI examples were not run.

FAQ

Can JavaScript be used for AI?

Yes. JavaScript can call any hosted LLM API, stream the answer into a web page, and run models on the device with Transformers.js, WebLLM, ONNX Runtime or Chrome's built-in AI APIs. Training large models is still mostly done in Python.

How do I build an AI application with JavaScript or Node.js?

Install Node.js 22 or newer, get an API key from a model provider, and run npm install ai @ai-sdk/anthropic (or another provider package). Call generateText or streamText from a server route, keep the key in an environment variable, and have your front end call that route. The end-to-end project above is a complete example in two files.

Is Python or JavaScript better for AI?

Python is better for training models, research and data science, because PyTorch and most ML tooling are Python-first. JavaScript is better for putting AI into web apps: the API calls, the streaming UI and the in-browser inference all happen in JS. JavaScript and AI fit together best at the product layer.

Do I need to know machine learning to add AI to a JavaScript app?

No. Calling an LLM API takes the same skills as calling any other REST API: fetch, JSON and async/await. Knowing how prompts, tokens and context windows work helps far more than math does.

Which JavaScript AI library should I use?

For hosted models, use the AI SDK (ai on npm) if you want one API across providers, or the official openai, @anthropic-ai/sdk or @google/genai package if you use a single provider. For running models yourself, Transformers.js covers most tasks in both Node.js and the browser.

Can I call the OpenAI or Claude API directly from the browser?

Not with a secret key, because anyone can copy it from the page. The OpenAI and Anthropic SDKs block browser use unless you set dangerouslyAllowBrowser: true. Call the API from a server route and have the browser call your route, as in the streaming examples above.

Can I run an LLM in the browser without a server?

Yes, in two ways. WebLLM downloads an open model and runs it on WebGPU. Chrome's Prompt API uses the Gemini Nano model that Chrome manages, on the web since Chrome 148 on supported desktop hardware. Both are far less capable than hosted models, so they suit short, simple tasks.

Can Node.js be used for machine learning?

Yes, for running models. Transformers.js and ONNX Runtime run pre-trained or Python-trained models inside a Node.js process, and TensorFlow.js can train small ones. Think of JavaScript for machine learning as a way to run models, not to build them. For training large models, Python remains the better tool.

Is there a free way to try AI with JS?

Yes. Transformers.js and Ollama run models on your own machine for free, and the Gemini API has a free tier for its Flash models. The JavaScript AI examples in sections 3 and 4 of this guide need no API key at all.