n4nAI

LangChain.js quickstart: your first chat chain in Node

A hands-on langchain.js quickstart node chat chain tutorial: build a runnable Node.js chat chain with LangChain.js, from setup to streaming responses.

n4n Team3 min read567 words

Audio narration

Coming soon — every post will get a voice note here.

This langchain.js quickstart node chat chain gets you from an empty directory to a working conversational loop in under ten minutes. We’ll use the current LangChain.js packages (@langchain/openai, @langchain/core) and Node’s native ESM, then hit a real model, stream tokens, and add memory.

Prerequisites

  • Node.js 18.18+ (or 20+) with npm.
  • An API key from a provider that speaks the OpenAI chat completions API. If you use OpenAI directly, set OPENAI_API_KEY. Any OpenAI-compatible base URL works.
  • Basic comfort with async/await and the terminal.

No TypeScript build step required; we’ll run .mjs files directly with node.

Initialize the project

Create a directory and install the two packages we need:

mkdir lc-chat && cd lc-chat
npm init -y
npm install @langchain/core@^0.3 @langchain/openai@^0.2

We pin minor ranges because LangChain.js ships fast-moving minor versions. The @langchain/openai package wraps the chat completions API and respects a custom baseURL, which matters later.

Build the minimal chat chain

Create chat.mjs. The simplest useful chain is a ChatOpenAI model piped to a PromptTemplate. LangChain.js favors composing RunnableSequence via .pipe():

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0.7,
});

const prompt = PromptTemplate.fromTemplate(
  "You are a terse senior engineer. Answer the question: {question}"
);

const chain = prompt.pipe(model);

const response = await chain.invoke({ question: "What is backpressure in Node streams?" });
console.log(response.content);

Run it:

node chat.mjs

Expected output (truncated):

Backpressure is a mechanism in Node streams that prevents a fast producer from overwhelming a slow consumer by signaling the producer to slow down or pause writing.

That’s the core of a langchain.js quickstart node chat chain: a prompt template piped into a model. The .pipe() method returns a RunnableSequence that is itself a runnable, so you can .invoke(), .stream(), or .batch().

Stream tokens to the terminal

Printing the full response blocks until the model finishes. For chat UX you want tokens as they arrive. Swap invoke for stream:

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";

const model = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0.7 });
const prompt = PromptTemplate.fromTemplate("Explain {topic} in one sentence.");
const chain = prompt.pipe(model);

const stream = await chain.stream({ topic: "event loop" });
process.stdout.write("> ");
for await (const chunk of stream) {
  process.stdout.write(chunk.content);
}
process.stdout.write("\n");

Run node chat.mjs. You’ll see characters appear incrementally:

> The event loop is a Node.js mechanism that handles asynchronous operations by repeatedly polling for and executing queued callbacks without blocking the main thread.

Streaming is first-class in LangChain.js; stream() returns an async iterable of AIMessageChunk objects. Each chunk.content is a string. This is the primitive you’ll build UIs on top of.

Add conversational memory

A single turn isn’t a chain you’d ship. Wire in RunnableWithMessageHistory, the supported path in current LangChain.js. Install the core history helper (it ships inside @langchain/core):

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { InMemoryChatMessageHistory } from "@langchain/core/chat_history";

const model = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0.7 });
const prompt = PromptTemplate.fromTemplate(`Answer as a helpful engineer.

Chat history:
{chat_history}

New question: {input}`);

const chain = prompt.pipe(model);

const store = new Map();
const withHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory: async (sessionId) => {
    if (!store.has(sessionId)) {
      store.set(sessionId, new InMemoryChatMessageHistory());
    }
    return store.get(sessionId);
  },
  inputMessagesKey: "input",
  historyMessagesKey: "chat_history",
});

const config = { configurable: { sessionId: "cli-1" } };
const r1 = await withHistory.invoke({ input: "What is a worker thread?" }, config);
console.log("Bot:", r1.content);

const r2 = await withHistory.invoke({ input: "When would I use one over a cluster?" }, config);
console.log("Bot:", r2.content);

First run output:

Bot: A worker thread is a Node.js feature that lets you run JavaScript in parallel threads sharing memory via SharedArrayBuffer, offloading CPU-heavy tasks from the main event loop.
Bot: Use a worker thread for a single process needing parallel CPU work; use cluster to fork multiple processes for horizontal scaling across CPUs with separate memory.

The second answer references the first because InMemoryChatMessageHistory injected prior messages into chat_history. In production you’d back this with Redis or Postgres.

Make it interactive with readline

A demo script with hardcoded inputs is fine, but a REPL loop shows the real shape. Extend the memory example with Node’s readline:

import readline from "node:readline";
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { InMemoryChatMessageHistory } from "@langchain/core/chat_history";

const model = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0.7 });
const prompt = PromptTemplate.fromTemplate(`Answer as a helpful engineer.

History:
{chat_history}

User: {input}`);
const chain = prompt.pipe(model);
const store = new Map();
const withHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory: async (id) => {
    if (!store.has(id)) store.set(id, new InMemoryChatMessageHistory());
    return store.get(id);
  },
  inputMessagesKey: "input",
  historyMessagesKey: "chat_history",
});

const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = (q) => new Promise((res) => rl.question(q, res));

while (true) {
  const input = await ask("\nyou> ");
  if (input === "exit") break;
  const stream = await withHistory.stream({ input }, { configurable: { sessionId: "cli" } });
  process.stdout.write("bot> ");
  for await (const chunk of stream) process.stdout.write(chunk.content);
  process.stdout.write("\n");
}
rl.close();

Run node repl.mjs and type:

you> What is a stream in Node?
bot> A stream is an abstract interface for working with streaming data, letting you process input or output piece by piece instead of loading everything into memory.
you> Give a concrete use case.
bot> Piping a large file from disk to an HTTP response: you create a read stream and pipe it to the response object, sending chunks to the client without buffering the whole file.

The history injection makes the second turn coherent. That’s a complete langchain.js quickstart node chat chain with state.

Swap providers without rewriting your chain

The ChatOpenAI class only cares about baseURL and apiKey. If you want access to 240+ models behind one OpenAI-compatible endpoint with automatic fallback when a provider is rate-limited, point it at n4n.ai:

const model = new ChatOpenAI({
  model: "anthropic/claude-3.5-sonnet",
  apiKey: process.env.N4N_API_KEY,
  baseURL: "https://api.n4n.ai/v1",
  temperature: 0.7,
});

Everything else—prompt templates, streaming, history—stays identical. That’s the payoff of the LangChain.js runnable abstraction: your chain becomes portable across providers by changing constructor args.

Error handling and timeouts

Models fail. Wrap calls in try/catch and set a timeout via the maxRetries and timeout options:

const model = new ChatOpenAI({
  model: "gpt-4o-mini",
  timeout: 15_000,
  maxRetries: 2,
});

If you’re streaming, handle abort:

const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);
const stream = await chain.stream(input, { signal: controller.signal });

LangChain.js propagates the abort signal to the underlying fetch call. In the REPL loop, you’d wrap the stream consumption in try/catch to avoid crashing on a network error.

Recap and next steps

You now have a runnable, streaming, stateful chat chain in Node with less than 60 lines of code. The pattern—PromptTemplate.pipe(ChatOpenAI) wrapped in RunnableWithMessageHistory—is the backbone for most LLM features: agents, RAG, and tool calling all extend this same runnable contract.

To go further: add tool calling via model.bindTools, or swap the in-memory history for a persistent store. The langchain.js quickstart node chat chain you built here is intentionally minimal; the abstractions are what scale.

Checkpoint: full project structure should look like:

lc-chat/
  package.json
  chat.mjs
  repl.mjs

Run node repl.mjs and you’ll get the interactive two-turn conversation shown earlier. That’s a real, if small, production-shaped component.

Tagslangchainjsnodejsquickstartchat

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All langchain.js for node & typescript posts →