| name | langchain-js |
| description | [Applies to: **/*.{js,jsx}] This guide provides opinionated, actionable best practices for building robust and maintainable LLM applications with langchain-js, emphasizing modularity, type safety, observability, and testing. |
| source | cursor_mdc |
langchain-js Best Practices
Building LLM-powered applications with langchain-js requires a disciplined approach to ensure maintainability, performance, and reliability. This guide outlines our team's definitive best practices.
1. Code Organization and Structure
Always prioritize modularity and explicit typing. Break down complex logic into small, reusable components.
1.1. Modular Components & Single Responsibility
Each LangChain component (Agent, Tool, Chain, Model) should reside in its own file or a dedicated module, adhering to the Single Responsibility Principle.
❌ BAD: Monolithic file
import { ChatOpenAI } from "@langchain/openai";
import { createAgent, tool } from "langchain";
import { z } from "zod";
const getStockPrice = tool(async ({ ticker }) => { }, { });
const getNews = tool(async ({ query }) => { }, { });
const model = new ChatOpenAI({ temperature: 0.7 });
const agent = createAgent({
model,
tools: [getStockPrice, getNews],
});
export async function runFinancialAgent(input) {
return agent.invoke(input);
}
✅ GOOD: Modular, reusable components
import { tool } from "langchain";
import { z } from "zod";
export const getStockPriceTool = tool(
async ({ ticker }: { ticker: string }) => {
if (ticker === "AAPL") return "$170.00";
return "Price not found.";
},
{
name: "get_stock_price",
description: "Get the current stock price for a given ticker symbol.",
schema: z.object({
ticker: z.string().describe("The stock ticker symbol (e.g., AAPL)"),
}),
}
);
import { tool } from "langchain";
import { z } from "zod";
export const getNewsTool = tool(
async ({ query }: { query: string }) => {
return `Latest news for ${query}: Market is up!`;
},
{
name: "get_news",
: ,
: z.({
: z.().(),
}),
}
);
{ } ;
{ createAgent } ;
{ getStockPriceTool } ;
{ getNewsTool } ;
model = ({ : });
financialAgent = ({
model,
: [getStockPriceTool, getNewsTool],
});
{ financialAgent } ;
() {
result = financialAgent.({
: [{ : , : }],
});
.(result);
}
();
1.2. Explicit TypeScript Types
Always use explicit types for prompts, models, and tool interfaces. Avoid any.
❌ BAD: Implicit types, no schema validation
const myTool = tool((input) => { }, { name: "my_tool" });
✅ GOOD: Explicit types with Zod schema
import { tool } from "langchain";
import { z } from "zod";
const MyToolInputSchema = z.object({
param1: z.string().describe("Description for param1"),
param2: z.number().optional().describe("Description for param2"),
});
export const myTypedTool = tool(
async (input: z.infer<typeof MyToolInputSchema>) => {
console.log(`Received: ${input.param1}, ${input.param2}`);
return "Processed successfully";
},
{
name: "my_typed_tool",
description: "A tool that takes typed input.",
schema: MyToolInputSchema,
}
);
1.3. Configuration Management
Store API keys, model parameters, and external service URLs in environment variables. Never hardcode secrets.
❌ BAD: Hardcoded API key
const model = new ChatOpenAI({
openAIApiKey: "sk-YOUR_HARDCODED_KEY",
});
✅ GOOD: Environment variables
import "dotenv/config";
export const OPENAI_API_KEY = process.env.OPENAI_API_KEY!;
export const LANGSMITH_API_KEY = process.env.LANGSMITH_API_KEY!;
export const MONGODB_URI = process.env.MONGODB_URI!;
import { ChatOpenAI } from "@langchain/openai";
import { OPENAI_API_KEY } from "../config";
export const chatModel = new ChatOpenAI({
openAIApiKey: OPENAI_API_KEY,
temperature: 0.7,
modelName: "gpt-4o",
});
2. Common Patterns and Anti-patterns
Leverage LangChain's core abstractions effectively.
2.1. Robust Tool Definitions
Always define tools with zod schemas for clear input validation and better agent reasoning.
❌ BAD: Undefined tool schema
const simpleTool = tool((input) => `Processed ${input}`, { name: "simple_tool" });
✅ GOOD: Tool with Zod schema
import { tool } from "langchain";
import { z } from "zod";
export const calculateSumTool = tool(
async ({ a, b }: { a: number; b: number }) => (a + b).toString(),
{
name: "calculate_sum",
description: "Calculates the sum of two numbers.",
schema: z.object({
a: z.number().describe("The first number"),
b: z.number().describe("The second number"),
}),
}
);
2.2. Retrieval-Augmented Generation (RAG)
For knowledge retrieval, use VectorStoreRetriever within a RunnableSequence.
import { MongoDBAtlasVectorSearch } from "@langchain/mongodb";
import { VoyageEmbeddings } from "@langchain/community/embeddings/voyage";
import { MongoClient } from "mongodb";
import { MONGODB_URI } from "../config";
const client = new MongoClient(MONGODB_URI);
const collection = client.db("langchain_db").collection("documents");
export async function getVectorStoreRetriever() {
const embeddings = new VoyageEmbeddings();
const vectorStore = new MongoDBAtlasVectorSearch(embeddings, {
collection,
indexName: "vector_index",
textKey: "content",
embeddingKey: "embedding",
});
return vectorStore.asRetriever();
}
{ } ;
{ } ;
{ , } ;
{ } ;
{ formatDocumentsAsString } ;
{ chatModel } ;
{ getVectorStoreRetriever } ;
= ;
() {
retriever = ();
ragPrompt = .();
.([
.({
: (: { : }) => {
docs = retriever.(input.);
(docs);
},
}),
ragPrompt,
chatModel,
(),
]);
}
{ createRagChain } ;
() {
ragChain = ();
question = ;
answer = ragChain.({ question });
.(answer);
}
();
2.3. Streaming for Responsiveness
Implement streaming for LLM responses to improve user experience, especially for long generations.
import { ChatOpenAI } from "@langchain/openai";
import { OPENAI_API_KEY } from "../config";
export const streamingChatModel = new ChatOpenAI({
openAIApiKey: OPENAI_API_KEY,
temperature: 0.7,
modelName: "gpt-4o",
streaming: true,
});
import { streamingChatModel } from "./models/streamingChatModel";
async function streamResponse() {
const stream = await streamingChatModel.stream("Tell me a long story about a space cat.");
for await (const chunk of stream) {
process.stdout.write(chunk.content);
}
console.log("\n--- Stream Finished ---");
}
streamResponse();
2.4. Error Handling and Retries
Wrap external LLM and API calls with robust retry logic to handle transient failures. Use a library like p-retry.
❌ BAD: No retry logic
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI();
try {
await model.invoke("...");
} catch (error) {
console.error("LLM call failed:", error);
}
✅ GOOD: Retry logic with exponential backoff
import { ChatOpenAI } from "@langchain/openai";
import pRetry from "p-retry";
import { OPENAI_API_KEY } from "../config";
const model = new ChatOpenAI({ openAIApiKey: OPENAI_API_KEY });
async function reliableLLMCall(prompt: string) {
return pRetry(
async () => {
console.log("Attempting LLM call...");
return await model.invoke(prompt);
},
{
retries: 5,
minTimeout: 1000,
maxTimeout: 10000,
onFailedAttempt: (error) => {
console.warn(`Attempt ${error.attemptNumber} failed. Retrying...`);
},
}
);
}
reliableLLMCall("Generate a creative story about a robot chef.")
.then( .(res))
.( .(, err));
3. Performance Considerations
Optimize for speed and resource usage.
3.1. Asynchronous Callbacks
For non-serverless environments, set LANGCHAIN_CALLBACKS_BACKGROUND=true to process LangSmith traces asynchronously, reducing latency. In serverless, ensure it's false or unset for traces to complete before function exit.
export LANGCHAIN_CALLBACKS_BACKGROUND=true
3.2. Efficient Vector Search
When using vector stores, leverage metadata filtering to narrow down search space before vector similarity search, if applicable.
const retriever = vectorStore.asRetriever({
filter: {
source: "internal_docs",
},
});
4. Common Pitfalls and Gotchas
Avoid these common mistakes to prevent headaches.
4.1. Ignoring LangSmith for Observability
Not integrating with LangSmith means flying blind. Every chain, agent step, and retrieval call should be traced.
❌ BAD: No LangSmith setup
✅ GOOD: LangSmith enabled
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=<your-api-key>
export LANGSMITH_PROJECT=<your-project-name>
export LANGSMITH_WORKSPACE_ID=<your-workspace-id>
With these set, LangChain automatically instruments your runs.
4.2. Over-privileged Agents
Agents should operate with the principle of least privilege. Limit tool access and permissions.
❌ BAD: Agent with access to sensitive file system operations
import { tool } from "langchain";
import * as fs from "fs/promises";
import { z } from "zod";
const deleteFileTool = tool(
async ({ path }: { path: string }) => {
await fs.unlink(path);
return `Deleted ${path}`;
},
{
name: "delete_file",
description: "Deletes a file at the given path.",
schema: z.object({ path: z.string() }),
}
);
✅ GOOD: Sandboxed or read-only tools
import { tool } from "langchain";
import * as fs from "fs/promises";
import { z } from "zod";
import path from "path";
const SAFE_DIR = "/app/data/safe_uploads";
const readFileSafeTool = tool(
async ({ filename }: { filename: string }) => {
const filePath = path.join(SAFE_DIR, filename);
if (!filePath.startsWith(SAFE_DIR)) {
throw new Error("Access denied: Cannot read outside safe directory.");
}
return await fs.readFile(filePath, "utf-8");
},
{
name: "read_safe_file",
description: "Reads a file from the designated safe directory.",
schema: z.object({ filename: z.string() }),
}
);
5. Testing Approaches
Treat testing as a first-class concern.
5.1. Unit Testing with FakeLLM / MockChatModel
Isolate and test individual components by mocking LLM responses.
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnableSequence } from "@langchain/core/runnables";
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
export function createGreetingChain(model: BaseChatModel) {
const prompt = PromptTemplate.fromTemplate("Say hello to {name}.");
return RunnableSequence.from([prompt, model, new StringOutputParser()]);
}
import { test, expect, vi } from "vitest";
import { FakeChatModel } from "@langchain/core/utils/testing";
import { createGreetingChain } from "./greetingChain";
test("createGreetingChain generates correct greeting", async () => {
const mockModel = new ({});
mockModel. = vi.( .());
chain = (mockModel);
result = chain.({ : });
(mockModel.).();
(result).();
});
5.2. Integration Testing with LangSmith
Run end-to-end tests against a dedicated sandbox LangSmith project to verify real-world behavior and catch regressions.
import { test, expect } from "vitest";
import { createRagChain } from "./chains/ragChain";
import { Client } from "langsmith";
test("RAG chain answers question correctly with real LLM and retriever", async () => {
const ragChain = await createRagChain();
const question = "What is the capital of France?";
const answer = await ragChain.invoke({ question });
expect(answer).toContain("Paris");
});