LangGraph Tutorial (TypeScript): building a RAG pipeline for advanced developers

By Cyprian AaronsUpdated 2026-04-22
langgraphbuilding-a-rag-pipeline-for-advanced-developerstypescript

This tutorial builds a production-shaped RAG pipeline in LangGraph with TypeScript: ingest documents, retrieve relevant chunks, generate an answer, and route the flow with graph state. You’d use this when a plain “retrieve then generate” chain is too brittle and you need explicit control over branching, retries, and state.

What You'll Need

  • Node.js 20+
  • TypeScript 5+
  • npm or pnpm
  • OpenAI API key
  • Packages:
    • @langchain/langgraph
    • @langchain/openai
    • @langchain/core
    • dotenv

Install them:

npm install @langchain/langgraph @langchain/openai @langchain/core dotenv
npm install -D typescript tsx @types/node

Create a .env file:

OPENAI_API_KEY=your_key_here

Step-by-Step

  1. Start by defining the graph state. For RAG, you want to track the user question, retrieved context, the final answer, and whether retrieval was good enough to proceed.
import "dotenv/config";
import { z } from "zod";

export type RagState = {
  question: string;
  context: string[];
  answer: string;
  needsRetrieval: boolean;
};

export const RagStateSchema = z.object({
  question: z.string(),
  context: z.array(z.string()),
  answer: z.string(),
  needsRetrieval: z.boolean(),
});
  1. Next, create your documents and a simple retriever. In production you’d swap this for a vector store, but this keeps the example executable without extra infrastructure.
const docs = [
  "LangGraph is useful for stateful AI workflows with branching and retries.",
  "RAG combines retrieval with generation so answers can use external knowledge.",
  "TypeScript developers can model graph state with typed objects and schemas.",
];

function retrieve(question: string): string[] {
  const q = question.toLowerCase();
  return docs.filter((doc) =>
    q.split(" ").some((term) => term.length > 3 && doc.toLowerCase().includes(term))
  );
}
  1. Now define the graph nodes. The retrieve node populates context, the grade node decides if retrieval was useful, and the answer node calls the model with grounded context.
import { ChatOpenAI } from "@langchain/openai";

const llm = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

async function retrieveNode(state: RagState): Promise<Partial<RagState>> {
  return { context: retrieve(state.question) };
}

async function gradeNode(state: RagState): Promise<Partial<RagState>> {
  return { needsRetrieval: state.context.length === 0 };
}

async function answerNode(state: RagState): Promise<Partial<RagState>> {
  const prompt = [
    "Answer using only the provided context.",
    `Question: ${state.question}`,
    `Context:\n${state.context.join("\n") || "No context found."}`,
    "If context is empty, say you don't have enough information.",
  ].join("\n\n");

  const response = await llm.invoke(prompt);
  return { answer: response.content.toString() };
}
  1. Build the LangGraph workflow. This is where the control flow becomes explicit: retrieve first, decide whether to continue, then generate.
import { Annotation, END, START, StateGraph } from "@langchain/langgraph";

const GraphState = Annotation.Root({
  question: Annotation<string>(),
  context: Annotation<string[]>({
    reducer: (_, update) => update,
    default: () => [],
  }),
  answer: Annotation<string>({
    reducer: (_, update) => update,
    default: () => "",
  }),
  needsRetrieval: Annotation<boolean>({
    reducer: (_, update) => update,
    default: () => false,
  }),
});

const workflow = new StateGraph(GraphState)
  .addNode("retrieve", retrieveNode)
  .addNode("grade", gradeNode)
  .addNode("answer", answerNode)
  .addEdge(START, "retrieve")
  .addEdge("retrieve", "grade")
  .addConditionalEdges("grade", (state) => (state.needsRetrieval ? END : "answer"))
  .addEdge("answer", END);

export const app = workflow.compile();
  1. Run it with a real question. The important part is that the graph returns structured state, so you can inspect retrieval behavior before shipping it into a larger agent system.
async function main() {
  const result = await app.invoke({
    question: "How does LangGraph help with RAG?",
    context: [],
    answer: "",
    needsRetrieval: false,
  });

  
console.log("Question:", result.question);
console.log("Context:", result.context);
console.log("Needs retrieval:", result.needsRetrieval);
console.log("Answer:", result.answer);
}

main().catch(console.error);

Testing It

Run the file with tsx:

npx tsx src/rag.ts

Try two questions:

  • One that matches your local docs, like “What is LangGraph?”
  • One that does not match anything specific

You should see different behavior based on retrieved context. If you later replace retrieve() with a vector store retriever, keep the same graph shape; that’s the point of using LangGraph here.

Next Steps

  • Replace the toy retriever with a real vector store like pgvector or Pinecone.
  • Add a document ingestion graph that chunks PDFs before indexing.
  • Add a retry branch when retrieval returns low-confidence or empty results.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides