LangChain.js is overrated; Build your AI agent with a simple fetch call
If you’re a web developer in 2025, you’ve felt the pressure to integrate AI into your applications. It’s the new frontier — and the first tool everyone points you to is LangChain.js. It’s positioned as the de facto toolkit for working with LLMs, promising to simplify the complex world of AI orchestration.
But a promise of simplicity can hide a mountain of complexity. For many common web development tasks, LangChain is an overcomplicated abstraction. It asks you to learn a vast, opinionated vocabulary of “chains,” “agents,” and “loaders,” hiding the fundamental mechanics of AI interaction behind a wall of jargon. The result? Opaque code that’s hard to debug, a massive dependency footprint, and a frustrating experience when you step outside its patterns.
This article proposes a radical alternative: for tasks as common as Retrieval-Augmented Generation (RAG), you don’t need a heavyweight framework. We’ll prove it by building the same AI agent twice — once with LangChain.js, and once with nothing more than a fetch()
call.
Building a RAG agent with LangChain.js
Let’s start with the conventional approach — using LangChain.js to create a simple question-answer system that uses a small document set as its knowledge base.
First, install the necessary dependencies:
npm install langchain @langchain/openai @langchain/community @langchain/core
This pulls in the core LangChain library, OpenAI integrations, and community utilities — including the in-memory vector store we’ll use to store documents.
Next, initialize the models and load the documents:
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai"; import { MemoryVectorStore } from "@langchain/community/vectorstores/memory"; import { StringOutputParser } from "@langchain/core/output_parsers"; import { PromptTemplate } from "@langchain/core/prompts"; import { RunnableSequence } from "@langchain/core/runnables"; import { formatDocumentsAsString } from "langchain/util/document"; import type { Document } from "@langchain/core/documents"; // Initialize models const model = new ChatOpenAI({}); const embeddings = new OpenAIEmbeddings(); // Create a simple in-memory vector store const vectorStore = new MemoryVectorStore(embeddings); await vectorStore.addDocuments([ { pageContent: "The cat, a small carnivorous mammal, is often kept as a pet." }, { pageContent: "The scientific name for the domestic cat is Felis catus." }, { pageContent: "Cats have excellent night vision, hearing, and a strong sense of smell." }, ] as Array<Document<Record<string, any>>>);
Next, define the logic that connects the components — a retriever, a prompt, and a chain:
const retriever = vectorStore.asRetriever(); const prompt = PromptTemplate.fromTemplate(` Answer the user's question based only on the following context: {context} Question: {question} `); const chain = RunnableSequence.from([ { context: (input) => retriever.invoke(input.question).then(formatDocumentsAsString), question: (input) => input.question, }, prompt, model, new StringOutputParser(), ]); const result = await chain.invoke({ question: "What is the scientific name for a cat?", }); console.log(result); // Expected output: "Felis catus."
It works — and for those familiar with the library, it might even seem elegant. But under the hood, the core mechanics — HTTP calls to the embeddings API, similarity search, prompt assembly — are completely hidden. When something goes wrong, debugging often means spelunking through library internals rather than inspecting a network request.
Building the same RAG agent with fetch()
Now let’s peel back the abstraction. We’ll build the same RAG agent using only the fetch
API — the universal building block of modern web development. This approach gives you total transparency and control.
All you need is a JavaScript runtime that supports fetch
(like Node.js 18+, Deno, or any modern browser) and an OpenAI API key.
1. Define your data and helper function
// Knowledge base const documents = [ "The cat, a small carnivorous mammal, is often kept as a pet.", "The scientific name for the domestic cat is Felis catus.", "Cats have excellent night vision, hearing, and a strong sense of smell.", ]; // Helper for cosine similarity function cosineSimilarity(vecA, vecB) { const dot = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0); const magA = Math.sqrt(vecA.reduce((s, a) => s + a * a, 0)); const magB = Math.sqrt(vecB.reduce((s, b) => s + b * b, 0)); return dot / (magA * magB); }
2. Generate embeddings using OpenAI
async function getEmbedding(text) { const response = await fetch("https://api.openai.com/v1/embeddings", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, }, body: JSON.stringify({ input: text, model: "text-embedding-3-small", }), }); const { data } = await response.json(); return data[0].embedding; }
3. Create an in-memory vector store
const documentEmbeddings = await Promise.all(documents.map(getEmbedding)); function findSimilarDocuments(queryEmbedding, topK = 2) { const matches = documents.map((doc, i) => ({ content: doc, similarity: cosineSimilarity(queryEmbedding, documentEmbeddings[i]), })); return matches.sort((a, b) => b.similarity - a.similarity).slice(0, topK); }
4. Query the LLM directly
async function getAnswerFromLLM(question, context) { const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, }, body: JSON.stringify({ model: "gpt-4o", messages: [ { role: "system", content: `Answer the user's question based only on the following context:n${context}`, }, { role: "user", content: question }, ], }), }); const { choices } = await response.json(); return choices[0].message.content; }
5. Put it all together
const question = "What is the scientific name for a cat?"; console.log(`Question: ${question}`); const queryEmbedding = await getEmbedding(question); const similarDocuments = findSimilarDocuments(queryEmbedding); const context = similarDocuments.map(d => d.content).join("n"); console.log(`nFound Context:n${context}`); const finalAnswer = await getAnswerFromLLM(question, context); console.log(`nFinal Answer:n${finalAnswer}`);
That’s it — a fully functioning RAG agent, built from scratch with a few simple functions and zero dependencies. Every piece is transparent and customizable.
How these two approaches stack up
Here’s how the two approaches stack up:
Aspect | LangChain.js (The Abstraction) | fetch() (First-Principles Approach) |
---|---|---|
Transparency & Debugging | Black Box: Errors often come from deep library internals. | Fully Transparent: Failures are standard HTTP errors, easy to inspect. |
Dependencies & Bundle Size | Heavy: Dozens of packages, slower installs, larger bundles. | Zero Dependencies: Native API, smaller footprint, faster builds. |
Control & Flexibility | Opinionated: Custom logic means fighting abstractions. | Total Control: Every request, header, and prompt is explicit. |
Cognitive Overhead | High: Library-specific terms (Chains, Agents, Runnables). | Low: Plain JavaScript — async/await, JSON, and HTTP. |
Conclusion
LangChain and similar libraries promise an easy on-ramp to AI development. But that convenience often comes at the cost of clarity, control, and performance. For many web applications, these frameworks aren’t simplifying AI — they’re complicating it.
The takeaway is simple: first principles win. Direct API calls using fetch()
are transparent, maintainable, and fast. They teach you transferable skills that don’t disappear with the next framework release. By mastering the basics — prompt design, API interaction, and response handling — you gain the real power to build AI features that are lean, predictable, and yours to control.
The post LangChain.js is overrated; Build your AI agent with a simple <code>fetch</code> call appeared first on LogRocket Blog.
This post first appeared on Read More