I Built a RAG Pipeline in TypeScript Without LangChain — The Whole Thing in 200 Lines A developer built a complete retrieval-augmented generation (RAG) pipeline in TypeScript without using frameworks like LangChain or LlamaIndex, totaling about 200 lines across six files. The pipeline runs entirely on a local laptop, using a local LLM and an in-memory vector store, and the developer explains each stage from scratch, including the data structures and common bugs encountered. Every RAG tutorial I found looked like this: js const chain = RetrievalQAChain.fromLLM model, vectorStore.asRetriever ; const res = await chain.call { query: "what is this document about?" } ; Twelve lines, a Pinecone key, a screenshot of it answering one question about one PDF, and a confident closing paragraph about "production readiness." I read four of them and still couldn't have told you what an embedding actually was, why cosine similarity was the metric everyone used, or what would happen if my documents were 800 pages instead of 8. I could copy the code. I couldn't debug it. So I deleted the frameworks and wrote the whole thing by hand. No LangChain, no LlamaIndex, no hosted vector database, and no cloud LLM — the model runs on my laptop. Six files, a bit over 200 lines of TypeScript, and nothing imported that I can't explain. This post is the whole pipeline, the data structures behind each stage and why they were chosen, the four bugs that cost me the most time, and a debugging method that will save you an afternoon. I'm assuming you write JavaScript or TypeScript, you're comfortable with async / await , arrays, and classes, and you've installed an npm package before. That's it. I am not assuming you know anything about machine learning, vectors, embeddings, or information retrieval. Every one of those is explained from zero as it comes up, and if a line of code does something non-obvious, I explain the line. If you already know what a vector store is, skip to the bug list at the bottom. Strip the acronym away and RAG is one idea: Language models can't read your files. So find the relevant paragraphs yourself, paste them into the prompt, and ask the question. The rest of the pipeline exists to make that sentence practical. Finding the right paragraphs is the hard part. You can't keyword-search your way there, because a user asking "how do I stop duplicate rows" won't use the word "DISTINCT" that appears in your document. Keyword search matches letters, and you need something that matches meaning. That's what the pipeline below does, in five steps: I split those five steps across six files. | File | Job | |---|---| pdf.ts | Pull raw text out of a PDF | chunker.ts | Sliding-window split with overlap | embedder.ts | Call the embedding API, get number arrays back | vectorStore.ts | Hold chunks in memory, cosine similarity search | rag.ts | Orchestrate the four above, call the model | index.ts | CLI entry point | npm init -y npm i pdf-parse@1.1.1 voyageai openai dotenv npm i -D typescript tsx @types/node Two things you need before writing a line: .env as VOYAGE API KEY — the Voyage client picks it up from the environment on its own, so you never pass it explicitly. localhost:1234 and speaks the OpenAI API format, which means the official openai SDK talks to it without modification. No API key, no per-token cost, no internet required.That second choice matters more than it looks. Running the model locally means you can hammer this thing for a whole weekend without watching a billing dashboard, and it forces you to deal with a small context window early, which is where one of the more instructive bugs came from. Note the pinned pdf-parse@1.1.1 . That is not an accident. More on it later. python // src/pdf.ts import pdfParse from "pdf-parse"; import fs from "fs"; export async function readPdf filePath: string : Promise