DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

LangChain.js Complete Guide (2026)

LangChain.js is the TypeScript port of the Python LangChain framework — a set of abstractions for building LLM-powered applications. It's not a model or an API; it's a layer that makes certain patterns easier: chaining prompts, using tools, building RAG pipelines, and managing conversation memory.

When LangChain vs Raw SDK

LangChain adds value when:

  • You're building RAG pipelines with multiple steps (load, split, embed, retrieve, generate)
  • You need to chain multiple LLM calls with intermediate processing
  • You're integrating with vector databases (Pinecone, Chroma, Supabase pgvector)
  • You want structured output with Zod schema validation without wiring it yourself
  • You need conversation memory with different storage backends

Use the raw SDK instead when:

  • You're making a few straightforward API calls
  • Your "agent" is just a loop with tool calling — the Anthropic SDK handles this natively
  • You want to minimize dependencies and bundle size
  • Your team doesn't know LangChain and the abstractions will confuse them

The raw Anthropic SDK + a few utility functions handles 80% of what most apps need. LangChain is the right choice when you're building something genuinely complex.

Installation

LangChain v0.3 split into modular packages. Install only what you need:

# Core (required)
npm install @langchain/core

# Model providers (pick yours)
npm install @langchain/anthropic   # Claude
npm install @langchain/openai      # GPT-4o, embeddings
npm install @langchain/google-genai # Gemini

# For RAG
npm install @langchain/community   # vector stores, loaders
npm install langchain              # high-level chains
Enter fullscreen mode Exit fullscreen mode

Basic Chat

import { ChatAnthropic } from '@langchain/anthropic'
import { HumanMessage, SystemMessage } from '@langchain/core/messages'

const model = new ChatAnthropic({
  model: 'claude-sonnet-4-5',
  temperature: 0,
  maxTokens: 2048
})

const response = await model.invoke([
  new SystemMessage('You are a senior TypeScript developer. Be concise.'),
  new HumanMessage('What is the difference between type and interface in TypeScript?')
])

console.log(response.content)
Enter fullscreen mode Exit fullscreen mode

You can swap ChatAnthropic for ChatOpenAI without changing anything else — that's the value of the abstraction.

Prompt Templates and LCEL

The .pipe() operator is LCEL (LangChain Expression Language) — functional composition:

import { ChatPromptTemplate } from '@langchain/core/prompts'
import { ChatAnthropic } from '@langchain/anthropic'

const model = new ChatAnthropic({ model: 'claude-sonnet-4-5' })

const promptTemplate = ChatPromptTemplate.fromMessages([
  ['system', 'You are a code reviewer specializing in {language}. Be specific about issues.'],
  ['human', 'Review this code:\n\n{code}']
])

// Pipe: template → model
const chain = promptTemplate.pipe(model)

const result = await chain.invoke({
  language: 'TypeScript',
  code: `
    async function getUser(id) {
      const user = await db.query('SELECT * FROM users WHERE id = ' + id)
      return user
    }
  `
})
// → Points out: missing type annotation, SQL injection vulnerability
Enter fullscreen mode Exit fullscreen mode

Structured Output with Zod

Getting reliable JSON from a model without wiring tool calling manually:

import { ChatAnthropic } from '@langchain/anthropic'
import { ChatPromptTemplate } from '@langchain/core/prompts'
import { z } from 'zod'

const model = new ChatAnthropic({ model: 'claude-sonnet-4-5' })

const ReviewSchema = z.object({
  overallScore: z.number().min(1).max(10).describe('Overall code quality score'),
  issues: z.array(z.object({
    severity: z.enum(['critical', 'major', 'minor']),
    description: z.string(),
    suggestion: z.string()
  })),
  approvedForMerge: z.boolean()
})

type Review = z.infer<typeof ReviewSchema>

const structuredModel = model.withStructuredOutput(ReviewSchema)

const prompt = ChatPromptTemplate.fromMessages([
  ['system', 'You are a strict code reviewer. Return a structured review.'],
  ['human', 'Review this TypeScript function:\n\n{code}']
])

const chain = prompt.pipe(structuredModel)

const review: Review = await chain.invoke({
  code: `
    export async function deleteUser(id: string) {
      await db.user.delete({ where: { id } })
    }
  `
})

console.log(review.overallScore)        // 6
console.log(review.approvedForMerge)    // false
console.log(review.issues[0].severity)  // 'critical'
// review is fully typed — TypeScript knows the shape
Enter fullscreen mode Exit fullscreen mode

Tool Use

import { tool } from '@langchain/core/tools'
import { ChatAnthropic } from '@langchain/anthropic'
import { z } from 'zod'

const searchPosts = tool(
  async ({ query, limit = 5 }) => {
    const posts = await db.post.findMany({
      where: { title: { contains: query } },
      take: limit,
      select: { title: true, slug: true }
    })
    return JSON.stringify(posts)
  },
  {
    name: 'search_posts',
    description: 'Search blog posts by keyword',
    schema: z.object({
      query: z.string(),
      limit: z.number().optional()
    })
  }
)

const model = new ChatAnthropic({ model: 'claude-sonnet-4-5' })
const modelWithTools = model.bindTools([searchPosts])

const response = await modelWithTools.invoke('Find posts about TypeScript')

console.log(response.tool_calls)
// [{ name: 'search_posts', args: { query: 'TypeScript' } }]
Enter fullscreen mode Exit fullscreen mode

For a complete agent loop (model → tools → model continues), use LangGraph rather than wiring the loop yourself.

RAG Pipeline

RAG is where LangChain genuinely shines:

import { ChatAnthropic } from '@langchain/anthropic'
import { OpenAIEmbeddings } from '@langchain/openai'
import { MemoryVectorStore } from 'langchain/vectorstores/memory'
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'
import { ChatPromptTemplate } from '@langchain/core/prompts'

// Step 1: Split documents
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 200
})

const docs = await splitter.createDocuments([
  `Prisma is a TypeScript ORM that generates a fully typed client from your schema...`,
  `Drizzle is a lightweight SQL-first ORM that stays close to SQL syntax...`
])

// Step 2: Embed and store
const embeddings = new OpenAIEmbeddings({ model: 'text-embedding-3-small' })
const vectorStore = await MemoryVectorStore.fromDocuments(docs, embeddings)

// Step 3: Retriever
const retriever = vectorStore.asRetriever({ k: 4 })

// Step 4: RAG chain
const model = new ChatAnthropic({ model: 'claude-sonnet-4-5' })

const ragPrompt = ChatPromptTemplate.fromMessages([
  ['system', `Answer using only the context below. Say "I don't know" if the context lacks the answer.

Context: {context}`],
  ['human', '{question}']
])

async function ragQuery(question: string) {
  const relevantDocs = await retriever.invoke(question)
  const context = relevantDocs.map(d => d.pageContent).join('\n\n')
  const chain = ragPrompt.pipe(model)
  const response = await chain.invoke({ context, question })
  return response.content
}

const answer = await ragQuery('What are the differences between Prisma and Drizzle?')
Enter fullscreen mode Exit fullscreen mode

Production RAG with Pinecone

import { PineconeStore } from '@langchain/pinecone'
import { Pinecone } from '@pinecone-database/pinecone'

const pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! })
const index = pinecone.index('my-index')

// Index documents (run once during ingestion)
await PineconeStore.fromDocuments(docs, embeddings, {
  pineconeIndex: index,
  namespace: 'docs-v1'
})

// Query (run on every user request)
const queryStore = await PineconeStore.fromExistingIndex(embeddings, {
  pineconeIndex: index,
  namespace: 'docs-v1'
})

const retriever = queryStore.asRetriever({ k: 5 })
Enter fullscreen mode Exit fullscreen mode

Conversation Memory

Memory keeps conversation history across multiple turns:

import { ChatAnthropic } from '@langchain/anthropic'
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts'
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history'
import { RunnableWithMessageHistory } from '@langchain/core/runnables'

const model = new ChatAnthropic({ model: 'claude-sonnet-4-5' })

const prompt = ChatPromptTemplate.fromMessages([
  ['system', 'You are a helpful coding assistant.'],
  new MessagesPlaceholder('history'),
  ['human', '{input}']
])

const chain = prompt.pipe(model)
const store: Record<string, InMemoryChatMessageHistory> = {}

function getHistory(sessionId: string) {
  if (!store[sessionId]) store[sessionId] = new InMemoryChatMessageHistory()
  return store[sessionId]
}

const chainWithHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory: getHistory,
  inputMessagesKey: 'input',
  historyMessagesKey: 'history'
})

const sessionId = 'user-session-123'

await chainWithHistory.invoke(
  { input: 'I am building a Next.js app with Prisma and PostgreSQL.' },
  { configurable: { sessionId } }
)

// Model remembers the context in subsequent turns
const response = await chainWithHistory.invoke(
  { input: 'What ORM pattern should I use for connection pooling?' },
  { configurable: { sessionId } }
)
// → Recommends the Prisma singleton pattern, knows you're on Next.js
Enter fullscreen mode Exit fullscreen mode

For production, replace InMemoryChatMessageHistory with Upstash Redis:

import { UpstashRedisChatMessageHistory } from '@langchain/community/stores/message/upstash_redis'

function getHistory(sessionId: string) {
  return new UpstashRedisChatMessageHistory({
    sessionId,
    config: {
      url: process.env.UPSTASH_REDIS_REST_URL!,
      token: process.env.UPSTASH_REDIS_REST_TOKEN!
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

Streaming

const stream = await chain.stream({ input: 'Explain React Server Components' })

for await (const chunk of stream) {
  process.stdout.write(chunk.content as string)
}
Enter fullscreen mode Exit fullscreen mode

Observability with LangSmith

LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=your-langsmith-key
LANGCHAIN_PROJECT=my-project
Enter fullscreen mode Exit fullscreen mode

With these env vars set, every LangChain call is automatically traced — full execution tree, every prompt, every retrieval step, tokens used, latency. No code changes needed.

LangChain vs LangGraph vs Raw SDK

Scenario Tool
Single LLM call Raw Anthropic/OpenAI SDK
Tool calling (one round) Raw SDK with tool definitions
Structured output LangChain .withStructuredOutput()
Multi-step prompt chain LangChain LCEL
RAG pipeline LangChain
Stateful agent (loops, branching) LangGraph
Multi-agent workflows LangGraph or OpenAI Agents SDK
Simple chatbot with memory LangChain RunnableWithMessageHistory

LangGraph is LangChain's framework for graph-based agent workflows — built on top of LangChain but handles the stateful loop execution that LangChain alone isn't designed for.

What LangChain Gets Wrong

Abstraction leakage. When things go wrong, debugging requires understanding both your code and LangChain's internals.

Versioning. The @langchain/core, langchain, and community packages have separate versions that can get out of sync. Check compatibility tables when upgrading.

Over-abstraction for simple cases. A straightforward API call wrapped in LCEL is more complex than it needs to be. If your chain has one step, skip LangChain.

Bundle size. langchain + @langchain/community is heavy. Be selective about imports for edge runtimes.

None of these are dealbreakers for the use cases where LangChain shines (RAG, complex chains, production pipelines). But they're reasons to not reach for it by default.


Full article at stacknotice.com/blog/langchain-js-complete-guide-2026

Top comments (0)