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
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)
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
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
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' } }]
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?')
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 })
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
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!
}
})
}
Streaming
const stream = await chain.stream({ input: 'Explain React Server Components' })
for await (const chunk of stream) {
process.stdout.write(chunk.content as string)
}
Observability with LangSmith
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=your-langsmith-key
LANGCHAIN_PROJECT=my-project
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)