Build a RAG App with LangChain and TypeScript
October 18, 2026
Imagine you have a company handbook containing vacation rules, remote work policies, expense requirements, and other information employees need. You want an AI assistant that can answer a question like: How many vacation days do employees receive?
An LLM doesn't automatically have access to that private document. We need to find the relevant policy and provide it to the model when someone asks a question.
That's what we'll build in this tutorial: a company handbook assistant using retrieval augmented generation, or RAG, with LangChain.js and TypeScript.
This is the written companion to my LangChain RAG video tutorial. The complete source code and sample handbook are available on GitHub. We'll use the same models and pipeline as the video, with spelling corrections and basic error handling in the final script.
What does RAG do?
RAG connects a model to information your application retrieves:
- Retrieval: find passages relevant to the user's question.
- Augmentation: include those passages in the model's input as context.
- Generation: ask the model to answer using that context.
Our sample handbook says full-time employees receive 20 paid vacation days per calendar year. Instead of expecting the model to know this policy, our application retrieves the passage containing it.
There are two parts to the flow:
Prepare the handbook:
Text file → Document → Chunks → Embeddings → Vector store
Answer a question:
Question → Retriever → Relevant chunks
↓
Context + Question → Chat model → AnswerThe embedding model helps us search. The chat model writes the answer. They have different jobs.
1. Set up the TypeScript project
You'll need a recent Node.js version supported by your installed LangChain packages, npm, and an OpenAI API key with API billing available. This example runs in a terminal.
Create a project:
mkdir company-handbook-assistant
cd company-handbook-assistant
npm init -y
npm install @langchain/core@1 @langchain/openai@1 @langchain/textsplitters@1 @langchain/classic@1 dotenv
npm install -D typescript tsx @types/node
npm pkg set scripts.dev="tsx src/index.ts"
mkdir src dataWe use @langchain/core for documents, @langchain/textsplitters for chunking, @langchain/openai for embeddings and chat, and @langchain/classic for MemoryVectorStore.
Create a .env file in the project root:
OPENAI_API_KEY=your_openai_api_key_hereYou can create a key in the OpenAI API dashboard. Both model integrations read OPENAI_API_KEY from the environment after dotenv/config loads it.
Add a .gitignore before committing:
node_modules/
.envIf you prefer to start with the exact tutorial repository and its locked dependency versions, use this alternative:
git clone https://github.com/HaiderMalik12/02-langchain-rag-app.git
cd 02-langchain-rag-app
npm ciThen create your own .env. The repository already includes the handbook and script.
2. Add the company handbook
Create data/company-handbook.txt. Here's a small sample you can use immediately:
# Acme Technologies — Employee Handbook
## Vacation Policy
Full-time employees receive 20 paid vacation days per calendar year.
Employees should submit vacation requests at least two weeks before the requested start date.
Employees may carry over up to 5 unused vacation days into the following calendar year.
Vacation requests must be approved by the employee's manager before the vacation begins.
## Remote Work Policy
Employees may work remotely up to three days per week.
Employees must attend important meetings even when working remotely.
## Expenses
Employees can claim reimbursement for approved business expenses.
Expense reports should be submitted within 30 days of the expense.
Receipts are required for expenses of $25 or more.
Travel expenses must be approved by a manager before booking.The full sample handbook also includes working hours, parental leave, learning budgets, equipment, security, and professional conduct. These are fictional policies for the tutorial.
Your project structure should look like this:
company-handbook-assistant/
├── data/
│ └── company-handbook.txt
├── src/
│ └── index.ts
├── .env
├── .gitignore
└── package.json3. Load the text as a LangChain document
Node's readFile can read our text file directly. We then wrap the string in a LangChain Document:
const text = await readFile("./data/company-handbook.txt", "utf-8");
const document = new Document({
pageContent: text,
metadata: { source: "./data/company-handbook.txt" },
});pageContent holds the text. metadata records information about its source, which can help identify where retrieved passages came from later.
We don't need @langchain/community for this plain text example. A PDF or another data source would need a suitable loader or extraction step before entering the same pipeline.
These snippets belong inside the main function shown in the complete script below.
4. Split the document into chunks
A small handbook might fit entirely into a model's context window. Chunking gives us a way to retrieve selected passages as the document collection grows.
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500,
chunkOverlap: 50,
});
const chunks = await splitter.splitDocuments([document]);With the default length measurement, chunkSize is measured in characters. The splitter tries to keep text together at natural boundaries, so chunks can be shorter than 500 characters. chunkOverlap targets some shared text between adjacent chunks to help preserve context at their boundaries; it doesn't guarantee exactly 50 characters of overlap in every case.
You can inspect chunks.map((chunk) => chunk.pageContent) before moving on. The number of chunks depends on the handbook text and splitter settings. See LangChain's recursive text splitter guide for its splitting behavior.
5. Create embeddings and a vector store
An embedding represents text as a vector of numbers. Comparing vectors lets us search for related meaning, rather than requiring the question to repeat a policy's exact wording.
We'll use the same embedding model as the video:
const embeddings = new OpenAIEmbeddings({
model: "text-embedding-3-small",
});
const vectorStore = await MemoryVectorStore.fromDocuments(chunks, embeddings);fromDocuments embeds the chunks and stores their vectors alongside the document content. You don't need a separate manual embedding loop.
For an optional look at a vector, run await embeddings.embedQuery(chunks[0].pageContent) after checking that a chunk exists. That's an extra API call for inspection; the vector store already handles document embedding for our search pipeline.
OpenAI describes embeddings as numerical representations used to measure text relatedness. LangChain's OpenAI embeddings integration connects that model to the vector store.
MemoryVectorStore keeps the index in the running Node.js process. When the script exits, that index is lost. This is useful for learning with a small document; the LangChain memory store documentation describes its in-memory similarity search. In this script, each run builds the index again and makes new embedding requests.
The store runs locally, but the embedding and chat requests go to OpenAI. Use the fictional sample handbook while following along.
6. Retrieve the passage relevant to the question
Turn the vector store into a retriever and ask our vacation question:
const retriever = vectorStore.asRetriever({ k: 1 });
const question = "How many vacation days do employees receive?";
const relevantDocs = await retriever.invoke(question);k: 1 requests the single most similar chunk. It doesn't request one generated answer, and it doesn't guarantee that the retrieved chunk contains the answer.
Under the hood, the question is embedded and compared with the stored chunk vectors. For this example, we expect the vacation policy to be retrieved. Log relevantDocs to check that before involving the chat model.
You can try k: 3 when a question needs information from several passages. Retrieving more text also adds context to the model request, so inspect the results rather than assuming a larger value is always better.
7. Give the context to the chat model
Join the retrieved documents into one context string:
const context = relevantDocs.map((doc) => doc.pageContent).join("\n\n");Then pass the context and question to ChatOpenAI. We'll keep gpt-4o-mini, as used in the video, and tell it to answer only from the supplied context. If the answer isn't there, it should say it doesn't know.
That instruction helps guide the response, but it isn't a guarantee of accuracy. Retrieval can miss a needed passage, and the model can still make mistakes. Inspecting the retrieved text is an essential part of understanding the result.
The complete TypeScript script
Put this in src/index.ts:
import "dotenv/config";
import { readFile } from "node:fs/promises";
import { Document } from "@langchain/core/documents";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
async function main() {
if (!process.env.OPENAI_API_KEY) {
throw new Error("Set OPENAI_API_KEY in your .env file.");
}
const text = await readFile("./data/company-handbook.txt", "utf-8");
if (!text.trim()) {
throw new Error("The company handbook is empty.");
}
const document = new Document({
pageContent: text,
metadata: { source: "./data/company-handbook.txt" },
});
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500,
chunkOverlap: 50,
});
const chunks = await splitter.splitDocuments([document]);
const embeddings = new OpenAIEmbeddings({
model: "text-embedding-3-small",
});
const vectorStore = await MemoryVectorStore.fromDocuments(chunks, embeddings);
console.log(`Stored ${chunks.length} chunks in memory.`);
const retriever = vectorStore.asRetriever({ k: 1 });
const question = "How many vacation days do employees receive?";
const relevantDocs = await retriever.invoke(question);
const context = relevantDocs.map((doc) => doc.pageContent).join("\n\n");
console.log("Retrieved context:\n", context);
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const prompt = `You are an assistant answering questions about the company handbook.
Use only the provided context to answer the question.
If the answer cannot be found in the context, say that you don't know.
Context:
${context}
Question: ${question}`;
const response = await model.invoke(prompt);
console.log("\nAnswer:");
console.log(response.content);
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});Run it from the project root so the relative handbook path resolves correctly:
npm run devAfter the chunk count and retrieved context, expect an answer along these lines:
Answer:
Full-time employees receive 20 paid vacation days per calendar year.The precise wording can vary. Check that it preserves both full-time employees and per calendar year, because those qualifications are part of the policy.
Try a few more questions
Change the question variable and rerun the script:
- “How many days per week can I work remotely?” — the sample policy allows up to three.
- “When should I submit an expense report?” — within 30 days of the expense.
- “What is the company's stock option policy?” — the supplied handbook doesn't contain that policy, so the intended response is that the assistant doesn't know.
That last question is useful: a similarity search can return a chunk even when the document has no answer. The model needs to recognize that the context doesn't establish the requested fact.
If an answer is wrong, start with the logged context. Did retrieval select the right policy? Does the chunk contain the whole rule? Then review the prompt and model response. This helps you distinguish a retrieval problem from an answer generation problem.
Where to go next
You now have the core RAG pipeline: load a document, split it, embed the chunks, retrieve relevant content, and give that content to a chat model.
For a larger application, separate indexing from answering questions so you can reuse stored embeddings. You can also add source references to answers and evaluate the assistant with questions whose expected answers you already know. A real employee assistant would additionally need access controls so retrieval only returns documents that employee is allowed to read.
Watch the full video walkthrough or explore the tutorial repository to follow the original implementation.
If you'd like to go further, my course LangChain with TypeScript: Build AI Apps, RAG & Agents covers LangChain fundamentals, prompt templates, structured outputs, vector stores, tools, agents, and LangGraph through practical examples.