| name | llamaindex-js |
| description | [Applies to: **/*.{js,jsx,ts,tsx}] Definitive guidelines for writing robust, performant, and maintainable `llamaindex-js` applications using modern TypeScript best practices. |
| source | cursor_mdc |
llamaindex-js Best Practices
This document outlines the definitive best practices for developing with llamaindex-js (LlamaIndex.TS). Adhering to these guidelines ensures your applications are type-safe, performant, scalable, and easy to maintain.
1. Embrace TypeScript End-to-End
llamaindex-js is built in TypeScript for a reason. Leverage its type system to prevent runtime errors, improve code clarity, and enable robust refactoring.
✅ GOOD: Explicitly type all llamaindex objects and function parameters.
import { Document, VectorStoreIndex, QueryEngine } from "llamaindex";
import { OpenAIEmbedding } from "llamaindex/embeddings/OpenAIEmbedding";
import { Settings } from "llamaindex/Settings";
Settings.llm = new OpenAI({ model: "gpt-4o-mini" });
Settings.embedModel = new OpenAIEmbedding({ model: "text-embedding-3-small" });
async function buildAndQueryIndex(documents: Document[]): Promise<string> {
const index: VectorStoreIndex = await VectorStoreIndex.fromDocuments(documents);
const queryEngine: QueryEngine = index.asQueryEngine();
const response = await queryEngine.query({ query: "Summarize the key points." });
return response.response;
}
❌ BAD: Using any or omitting types where llamaindex types are available.
async function processData(docs: any[]): Promise<any> {
const index = await VectorStoreIndex.fromDocuments(docs);
const engine = index.asQueryEngine();
const res = await engine.query({ query: "What's up?" });
return res.response;
}
2. Structure Code in Three Distinct Layers
Organize your llamaindex applications into logical layers: Data Ingestion, Index Construction, and Query/Agent Execution. This promotes modularity, testability, and separation of concerns.
2.1. Data Ingestion (Readers/Connectors)
Responsible for loading and transforming raw data into Document objects.
✅ GOOD: Use SimpleDirectoryReader or specific Reader implementations.
import { Document, SimpleDirectoryReader } from "llamaindex";
export async function loadDocumentsFromDirectory(path: string): Promise<Document[]> {
const reader = new SimpleDirectoryReader();
const documents = await reader.loadData(path);
console.log(`Loaded ${documents.length} documents from ${path}`);
return documents;
}
❌ BAD: Manually creating Document objects from raw strings without leveraging readers for common formats.
function createDocumentFromText(text: string): Document {
return new Document({ text: text });
}
2.2. Index Construction
Responsible for taking Document objects, splitting them into Nodes, generating embeddings, and storing them in an Index.
✅ GOOD: Use VectorStoreIndex.fromDocuments for simplicity and IngestionPipeline for advanced control.
import { Document, VectorStoreIndex, IngestionPipeline, SentenceSplitter } from "llamaindex";
import { OpenAIEmbedding } from "llamaindex/embeddings/OpenAIEmbedding";
import { Settings } from "llamaindex/Settings";
export async function buildVectorIndex(documents: Document[]): Promise<VectorStoreIndex> {
const pipeline = new IngestionPipeline({
transformations: [
new SentenceSplitter({ chunkSize: 512, chunkOverlap: 20 }),
Settings.embedModel,
],
});
const nodes = await pipeline.({ documents });
index = .({ nodes });
.();
index;
}
❌ BAD: Manually creating Nodes and embeddings. This is error-prone and bypasses llamaindex's optimized pipeline.
async function manualIndexCreation(documents: Document[]): Promise<VectorStoreIndex> {
const nodes = documents.map(doc => new NodeWith ({ text: doc.text }));
return new VectorStoreIndex({ nodes: nodes });
}
2.3. Query/Agent Execution
Responsible for interacting with the constructed index or agents to answer user queries or perform tasks.
✅ GOOD: Use index.asQueryEngine() for RAG or agent() for agentic workflows.
import { VectorStoreIndex, QueryEngine } from "llamaindex";
import { agent } from "@llamaindex/workflow";
import { OpenAI } from "llamaindex/llm/OpenAI";
import { tool } from "llamaindex";
import { z } from "zod";
export async function queryIndex(index: VectorStoreIndex, query: string): Promise<string> {
const queryEngine: QueryEngine = index.asQueryEngine();
const response = await queryEngine.query({ query });
return response.response;
}
const sumNumbersTool = tool({
name: "sumNumbers",
description: "Use this function to sum two numbers",
parameters: z.object({ a: z.number(), b: z.() }),
: ,
});
(): <> {
chatAgent = ({
: ({ : }),
: ,
: ,
: [sumNumbersTool],
});
result = chatAgent.(message);
result..;
}
❌ BAD: Directly calling the LLM for RAG without a QueryEngine, or building agents from scratch.
import { OpenAI } from "llamaindex/llm/OpenAI";
async function directLLMQuery(query: string): Promise<string> {
const llm = new OpenAI({ model: "gpt-4o-mini" });
const response = await llm.chat({ messages: [{ role: "user", content: query }] });
return response.message.content || "";
}
3. Prefer async/await for All I/O Operations
llamaindex-js is inherently asynchronous. Always use async/await for clarity and error handling, especially for file reads, API calls, and LLM interactions.
✅ GOOD: Structured async/await with proper error handling.
async function executePipeline(): Promise<void> {
try {
const documents = await loadDocumentsFromDirectory("./data");
const index = await buildVectorIndex(documents);
const response = await queryIndex(index, "What is the main theme?");
console.log("Query Response:", response);
} catch (error) {
console.error("Pipeline failed:", error);
}
}
❌ BAD: Chaining .then() and .catch() without async/await for complex flows.
loadDocumentsFromDirectory("./data")
.then(documents => buildVectorIndex(documents))
.then(index => queryIndex(index, "What is the main theme?"))
.then(response => console.log("Query Response:", response))
.catch(error => console.error("Pipeline failed:", error));
4. Leverage LlamaCloud for Production-Grade Pipelines
For heavy-weight document parsing (e.g., PDFs, complex layouts) and managed retrieval, integrate with LlamaCloud services like LlamaParse. This offloads computational burden and improves accuracy.
✅ GOOD: Using LlamaParse for robust document processing.
import { Document, VectorStoreIndex } from "llamaindex";
import { LlamaParseReader } from "llamaindex/readers/LlamaParseReader";
async function parseAndIndexWithLlamaCloud(filePath: string): Promise<VectorStoreIndex> {
const parser = new LlamaParseReader({
resultType: "markdown",
});
const documents: Document[] = await parser.loadData(filePath);
const index = await VectorStoreIndex.fromDocuments(documents);
console.log("Indexed documents parsed by LlamaCloud.");
return index;
}
❌ BAD: Attempting to implement complex document parsing logic locally for production use cases.
import { Document } from "llamaindex";
import fs from "fs/promises";
async function localPdfParse(filePath: string): Promise<Document[]> {
const pdfBuffer = await fs.readFile(filePath);
console.warn("Using local PDF parsing, consider LlamaParse for robustness.");
return [new Document({ text: "Simulated PDF content" })];
}
5. Performance: Batching, Caching, and Streaming
Optimize llamaindex applications for speed and responsiveness.
5.1. Batch Document Processing
When ingesting many documents, process them in batches to manage memory and network requests.
✅ GOOD: Processing documents in chunks.
import { Document, VectorStoreIndex } from "llamaindex";
async function batchIndexDocuments(allDocuments: Document[], batchSize: number = 100): Promise<VectorStoreIndex> {
const index = new VectorStoreIndex();
for (let i = 0; i < allDocuments.length; i += batchSize) {
const batch = allDocuments.slice(i, i + batchSize);
console.log(`Processing batch ${i / batchSize + 1}/${Math.ceil(allDocuments.length / batchSize)}`);
await index.insertAll(batch);
}
return index;
}
5.2. Utilize Ingestion Pipeline Caching
For repeated ingestion of the same data, use the cache option in IngestionPipeline to avoid re-embedding.
✅ GOOD: Configuring a cache for the ingestion pipeline.
import { Document, IngestionPipeline, SentenceSplitter, SimpleNodeParser } from "llamaindex";
import { SimpleCache } from "llamaindex/ingestion/SimpleCache";
import { Settings } from "llamaindex/Settings";
async function buildCachedIndex(documents: Document[]): Promise<void> {
const pipeline = new IngestionPipeline({
transformations: [
new SentenceSplitter(),
new SimpleNodeParser(),
Settings.embedModel,
],
cache: new SimpleCache(),
});
await pipeline.run({ documents });
console.log("Ingestion pipeline ran with caching.");
}
5.3. Implement Streaming for Chat Responses
For interactive chat applications, stream LLM responses to provide immediate feedback to the user.
✅ GOOD: Using stream: true with agents or LLMs.
import { OpenAIAgent } from "llamaindex";
import { createStreamableUI } from "ai/rsc";
async function streamAgentChat(question: string): Promise<JSX.Element> {
const agent = new OpenAIAgent({
});
const responseStream = await agent.chat({
stream: true,
message: question,
});
const uiStream = createStreamableUI(<div>Thinking...</div>);
responseStream.pipeTo(
new WritableStream({
start: () => uiStream.update(""),
write: (chunk) => uiStream.append(chunk.response.delta),
close: () => uiStream.done(),
: .(, err),
}),
).(.);
uiStream.;
}
6. Common Pitfalls and Gotchas
6.1. Avoid Browser Environments
llamaindex-js relies on AsyncLocalStorage-like APIs which are not fully supported in browser environments. Restrict llamaindex code to Node.js, Deno, Bun, Cloudflare Workers, or Next.js Server Components.
✅ GOOD: Deploy llamaindex logic on the server-side (e.g., API routes, serverless functions, RSC).
"use server";
import { runChatAgent } from "@/agents/chatAgent";
export async function serverChatAction(message: string): Promise<string> {
return runChatAgent(message);
}
"use client";
import { serverChatAction } from "@/actions/chat";
import { useState } from "react";
export default function HomePage() {
const [response, setResponse] = useState<string>("");
const handleChat = async (input: string) => {
const result = await serverChatAction(input);
setResponse(result);
};
}
❌ BAD: Directly importing and running llamaindex code in a client-side React component.
"use client";
import { OpenAIAgent } from "llamaindex";
export default function ChatInput() {
const agent = new OpenAIAgent({ });
}
6.2. Securely Manage Environment Variables
Never hardcode API keys or sensitive credentials. Use environment variables and ensure they are loaded correctly for your runtime.
✅ GOOD: Using process.env (Node.js) or equivalent.
import { Settings } from "llamaindex/Settings";
import { OpenAI } from "llamaindex/llm/OpenAI";
if (!process.env.OPENAI_API_KEY) {
throw new Error("OPENAI_API_KEY is not set.");
}
Settings.llm = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
❌ BAD: Hardcoding API keys directly in code.
import { OpenAI } from "llamaindex/llm/OpenAI";
const llm = new OpenAI({ apiKey: "sk-YOUR_HARDCODED_KEY_HERE" });
7. Robust Testing Approaches
Implement a comprehensive testing strategy to ensure the reliability and correctness of your llamaindex applications.
7.1. Unit Tests for Individual Components
Test data loaders, custom tools, and transformation functions in isolation. Mock external dependencies like LLM calls.
✅ GOOD: Mocking LLM responses for unit tests.
import { tool } from "llamaindex";
import { z } from "zod";
import { vi, describe, it, expect } from "vitest";
const sumNumbers = tool({
name: "sumNumbers",
description: "Use this function to sum two numbers",
parameters: z.object({ a: z.number(), b: z.number() }),
execute: ({ a, b }: { a: number; b: number }) => `${a + b}`,
});
describe("sumNumbers tool", () => {
it("should correctly sum two numbers", async () => {
const result = await sumNumbers.execute({ a: 5, b: 3 });
expect(result).toBe("8");
});
});
7.2. Integration Tests for the Full Pipeline
Test the entire RAG pipeline from data ingestion through querying. Use a small, controlled dataset.
✅ GOOD: End-to-end integration test.
import { Document, VectorStoreIndex, SimpleDirectoryReader } from "llamaindex";
import { Settings } from "llamaindex/Settings";
import { MockLLM } from "llamaindex/llm/mock";
import { vi, describe, it, expect, beforeAll } from "vitest";
import fs from "fs/promises";
import path from "path";
describe("RAG Pipeline Integration", () => {
let index: VectorStoreIndex;
const testDataDir = path.join(__dirname, "test_data");
beforeAll(async () => {
await fs.mkdir(testDataDir, { recursive: true });
await fs.writeFile(path.join(testDataDir, "test.txt"), "The capital of France is Paris.");
Settings. = ({
: ({ messages }) => ({
: {
: + messages[messages. - ].,
: ,
},
}),
});
reader = ();
documents = reader.(testDataDir);
index = .(documents);
});
(, () => {
queryEngine = index.();
response = queryEngine.({ : });
(response.).();
});
});
7.3. RAG Evaluation
For production systems, use llamaindex's evaluation modules to measure the quality (faithfulness, relevance, correctness) of your RAG system.
✅ GOOD: Setting up an evaluator.
import { ResponseSynthesizer, ServiceContext } from "llamaindex";
import { FaithfulnessEvaluator } from "llamaindex/evaluation/FaithfulnessEvaluator";
import { MockLLM } from "llamaindex/llm/mock";
async function evaluateRAGResponse(query: string, response: string, sourceNodes: any[]): Promise<void> {
const serviceContext = ServiceContext.fromDefaults({
llm: new MockLLM({
mockFn: async ({ messages }) => ({
message: {
content: "Mock evaluation: " + messages[messages.length - 1].content,
role: "assistant",
},
}),
}),
});
const evaluator = new FaithfulnessEvaluator({ serviceContext });
const evaluationResult = await evaluator.evaluate({
query,
response,
sourceNodes,
});
console.log(, evaluationResult., evaluationResult.);
}