<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Gauravbytes | Backend Engineer & AI Agent Builder]]></title><description><![CDATA[I build. I break. I document.

Backend engineer & AI agent tinkerer writing about Node.js APIs, SaaS architecture, and the messy reality of shipping products alone.

Follow along as I figure things out in public.]]></description><link>https://gauravbytes.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/616e87a8f1f4c944cc6b49a1/62b66582-c854-4bcb-b2f6-2b2879623cff.png</url><title>Gauravbytes | Backend Engineer &amp; AI Agent Builder</title><link>https://gauravbytes.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 05 Sep 2026 10:57:25 GMT</lastBuildDate><atom:link href="https://gauravbytes.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why Modern Microservices Need Event-Driven Architecture]]></title><description><![CDATA[In traditional backend designs, components communicate primarily using synchronous request-response mechanisms (such as REST APIs or gRPC). Service A explicitly calls Service B and blocks execution wh]]></description><link>https://gauravbytes.dev/why-modern-microservices-need-event-driven-architecture</link><guid isPermaLink="true">https://gauravbytes.dev/why-modern-microservices-need-event-driven-architecture</guid><category><![CDATA[eda]]></category><category><![CDATA[System Design]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[backend]]></category><category><![CDATA[Developer]]></category><category><![CDATA[event-driven-architecture]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 31 Aug 2026 08:33:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/6ac2285d-8059-44d8-aa8c-4755ab0ecf18.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In traditional backend designs, components communicate primarily using synchronous <strong>request-response</strong> mechanisms (such as REST APIs or gRPC). Service A explicitly calls Service B and blocks execution while waiting for B to respond.</p>
<p><strong>Event-Driven Architecture (EDA)</strong> is an architectural paradigm where software components communicate by publishing and consuming state changes called <strong>events</strong>.</p>
<ul>
<li><p>An <strong>Event</strong> represents an immutable record of a past occurrence within the business domain (e.g., <code>UserRegistered</code>, <code>OrderPlaced</code>, <code>PaymentFailed</code>).</p>
</li>
<li><p>Producers emit events without knowing which downstream services will consume them or what actions will be taken.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/3abdb55b-957b-4133-9906-441f89525347.png" alt="" style="display:block;margin:0 auto" />

<h2>Key Components of EDA</h2>
<ol>
<li><p><strong>Event Producer:</strong> The component that detects state changes (e.g., a user signing up) and packages that change into a structured event payload.</p>
</li>
<li><p><strong>Event Router / Broker:</strong> The messaging backbone (such as <strong>Apache Kafka</strong>, <strong>RabbitMQ</strong>, <strong>AWS EventBridge</strong>, or <strong>Redis Streams</strong>) responsible for ingesting, queuing, and routing events to subscribers.</p>
</li>
<li><p><strong>Event Consumer:</strong> Independent worker services or microservices that listen for specific events and perform downstream processing.</p>
</li>
<li><p><strong>Event Payload:</strong> The structured JSON/Avro data containing metadata (event ID, timestamp, schema version) and contextual business attributes.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/cde03739-38bc-4e15-87fe-1bbef0c662b6.png" alt="" style="display:block;margin:0 auto" />

<h2>Where is EDA Helpful? (Real-World Use Cases)</h2>
<ul>
<li><p><strong>E-Commerce Order Workflows:</strong> Processing an order requires multiple side effects (inventory reservation, payment gateway charging, fraud scanning, dispatching emails). EDA handles these side effects asynchronously without delaying the user's checkout response.</p>
</li>
<li><p><strong>Real-Time Analytics &amp; Telemetry:</strong> Ingesting continuous streams of data (clickstream data, IoT sensor metrics, financial market ticks) with minimal latency.</p>
</li>
<li><p><strong>Asynchronous &amp; Heavy Background Jobs:</strong> Offloading computationally intensive tasks (e.g., video rendering, image processing, or PDF report generation) from the main web server thread.</p>
</li>
<li><p><strong>Microservices Integration:</strong> Allowing multiple microservices maintained by distinct engineering teams to react to common domain events without creating tightly coupled dependencies.</p>
</li>
</ul>
<h2>Key Benefits of EDA</h2>
<table>
<thead>
<tr>
<th>Benefit</th>
<th>Architectural Impact</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Loose Decoupling</strong></td>
<td>Producers and consumers operate independently. New consumer services can be attached to the event stream without modifying producer code.</td>
</tr>
<tr>
<td><strong>High Scalability</strong></td>
<td>Consumers can scale horizontally based on workload demands. High traffic spikes are buffered in the broker rather than crashing downstream endpoints.</td>
</tr>
<tr>
<td><strong>Fault Isolation &amp; Resilience</strong></td>
<td>If a consumer service experiences downtime, events are safely retained in the message broker. Once the service recovers, it resumes consumption from the last offset.</td>
</tr>
<tr>
<td><strong>Low Latency / High Responsiveness</strong></td>
<td>User-facing HTTP APIs quickly return <code>202 Accepted</code> after publishing an event, moving heavy side-effects off the critical execution path.</td>
</tr>
</tbody></table>
<h2>Drawbacks and Implementation Challenges</h2>
<ul>
<li><p><strong>Eventual Consistency:</strong> Trading strict single-database ACID transactions for eventual consistency across distributed datastores requires handling race conditions and potential data lag.</p>
</li>
<li><p><strong>Complex Tracing &amp; Debugging:</strong> Troubleshooting failures across asynchronous topics requires distributed tracing frameworks like OpenTelemetry and centralized correlation IDs.</p>
</li>
<li><p><strong>Handling Consumer Failures:</strong> If a consumer encounters an uncaught exception, events can be lost or stuck in infinite retry loops if proper patterns are not established.</p>
</li>
</ul>
<h3>Retries &amp; Dead Letter Queue (DLQ) Pattern</h3>
<p>To ensure high reliability, enterprise implementations combine <strong>Exponential Backoff Retries</strong> with a <strong>Dead Letter Queue (DLQ)</strong>:</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/827694be-cdc2-4b84-8cb0-f690933b233f.png" alt="" style="display:block;margin:0 auto" />

<h3>Production Node.js Implementation Pattern</h3>
<pre><code class="language-javascript">import { EventEmitter } from 'events';

class ResilientEventBus extends EventEmitter {}
const eventBus = new ResilientEventBus();

// Resilient Consumer with Exponential Backoff and DLQ routing
eventBus.on('userRegistered', async (eventPayload) =&gt; {
    const processWithRetry = async (attempt = 1) =&gt; {
        try {
            console.log(`[Attempt ${attempt}] Processing welcome email for: ${eventPayload.email}`);
            
            // Simulating an unreliable third-party API call
            if (Math.random() &lt; 0.6) {
                throw new Error("SMTP Gateway Connection Timeout");
            }
            
            console.log(`[SUCCESS] Email sent to ${eventPayload.email}`);
        } catch (error) {
            console.error(`[ERROR] Attempt ${attempt} failed: ${error.message}`);
            
            if (attempt &lt; 3) {
                const backoffDelay = Math.pow(2, attempt) * 1000;
                console.log(`Retrying in ${backoffDelay}ms...`);
                setTimeout(() =&gt; processWithRetry(attempt + 1), backoffDelay);
            } else {
                console.error(`[CRITICAL] Max retries exhausted for event ID ${eventPayload.eventId}. Forwarding to DLQ.`);
                // Route payload to Dead Letter Queue storage or database for inspection
            }
        }
    };

    await processWithRetry();
});

// Triggering Event Producer
eventBus.emit('userRegistered', {
    eventId: 'evt_994821',
    userId: 'usr_102',
    email: 'gaurav@example.com',
    timestamp: Date.now()
});
</code></pre>
<h2>Which Systems Are Best Suited for EDA?</h2>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/9ed28bd8-19db-4908-a50c-c71f4cbbb2f1.png" alt="" style="display:block;margin:0 auto" />

<h3>System Fit Comparison</h3>
<p><strong>Best Suited For EDA:</strong></p>
<ul>
<li><p>Complex distributed microservices with asynchronous background workloads.</p>
</li>
<li><p>Real-time notification systems, live dashboards, and chat platforms.</p>
</li>
<li><p>Event-driven workflows with high throughput requirements (e.g., Kafka streaming).</p>
</li>
</ul>
<p><strong>Poorly Suited For EDA:</strong></p>
<ul>
<li><p>Simple CRUD web applications with low traffic volumes.</p>
</li>
<li><p>Workflows requiring immediate synchronous confirmation before proceeding (e.g., verifying a 2FA authentication token).</p>
</li>
<li><p>Monolithic applications maintained by single small teams where adding message broker infra increases operational complexity unnecessarily.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to Evaluate RAG Retrieval: A Practical Guide to Precision, Recall, MRR, MAP, and NDCG]]></title><description><![CDATA[You implemented a search algorithm in your RAG system and it's returning data without any error, and you think that's it, you're done. You're wrong.
That mindset works fine for a normal system — an AP]]></description><link>https://gauravbytes.dev/how-to-evaluate-rag-retrieval-a-practical-guide-to-precision-recall-mrr-map-and-ndcg</link><guid isPermaLink="true">https://gauravbytes.dev/how-to-evaluate-rag-retrieval-a-practical-guide-to-precision-recall-mrr-map-and-ndcg</guid><category><![CDATA[RAG ]]></category><category><![CDATA[AI]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[analytics]]></category><category><![CDATA[vector embeddings]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 03 Aug 2026 19:16:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/9903cdde-c02d-4dfd-8b09-c10b453e6960.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You implemented a search algorithm in your RAG system and it's returning data without any error, and you think that's it, you're done. You're wrong.</p>
<p>That mindset works fine for a normal system — an API, a script, a CRUD endpoint. No error, no crash, ship it. But RAG isn't that. Your search can run clean, return a full result set, zero exceptions, 200 all the way — and still be quietly handing your model the wrong chunks. "It didn't crash" tells you nothing about whether it actually found the right stuff. For that you need a different set of metrics, ones built specifically to answer "is my retrieval actually relevant," not "did my code run."</p>
<p>So in this post we're going to break down exactly those metrics, one by one, with real examples:</p>
<ul>
<li><p><strong>Precision@K</strong> — of what you retrieved, how much was actually useful</p>
</li>
<li><p><strong>Recall@K</strong> — of everything relevant out there, how much did you actually find</p>
</li>
<li><p><strong>MRR</strong> — how fast did you find the <em>first</em> relevant chunk</p>
</li>
<li><p><strong>MAP@K</strong> — how good is the ranking across <em>all</em> relevant chunks, not just the first one</p>
</li>
<li><p><strong>NDCG@K</strong> — how close to a perfect ranking, when some chunks are more relevant than others</p>
</li>
</ul>
<p>And once retrieval's covered, we'll also get into how it connects to the generation side — faithfulness, answer relevance — plus how to build a golden dataset and use an LLM as a judge to actually score all this stuff automatically.</p>
<h2>Two different failure modes, one bad answer</h2>
<p>When a RAG system gives a wrong answer, there are two places it could have gone wrong, and conflating them is the single most common mistake in RAG debugging:</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/df452809-dbba-4c5d-b7f0-982657e7dfe5.png" alt="" style="display:block;margin:0 auto" />

<p>A bad answer could mean the retriever never found the right chunk (a <strong>retrieval failure</strong>) — in which case no generation model on earth could have answered correctly, it never saw the information.</p>
<p>Or it could mean retrieval did its job and handed over the right chunk, but the model ignored it, misread it, or made something up anyway (a <strong>generation failure</strong>). These need separate metrics because the fix is completely different: one is a search-tuning problem, the other is a prompting problem.</p>
<p>This article covers only retrieval side metrices in depth.</p>
<h2>The big picture: one ranked result, five ways to score it</h2>
<p>Every metric below is a different way of scoring the exact same thing: a ranked list of retrieved chunks, checked against a hand-labeled set of chunks that are actually relevant.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/f9cead9b-d967-4ad1-9516-ebfa3543849d.png" alt="" style="display:block;margin:0 auto" />

<p>Four of the eight retrieved chunks are relevant, sitting at ranks 2, 4, 5, and 7. Every metric in this post is computed from this one picture — what changes is which ranks it looks at, and whether it cares about order at all.</p>
<h2>Order-unaware metrics: precision@K and recall@K</h2>
<p><strong>Precision@K</strong> — of the K chunks you retrieved, how many are relevant?</p>
<p><strong>Recall@K</strong> — of all relevant chunks that exist, how many did you actually find?</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/15c0bc8a-b51c-4c3c-9c7a-0c64cf4ae680.png" alt="" style="display:block;margin:0 auto" />

<p>Neither metric cares <em>where</em> in the top-5 a relevant chunk landed — only whether it's inside the cutoff. That's their strength (simple, cheap to compute) and their weakness (a system that buries its one good chunk at rank 5 scores the same as one that puts it at rank 1, as long as both make the cutoff).</p>
<p><strong>Recall@K</strong> only ever goes up as K grows. Push K to the size of your whole corpus and recall@K hits 1.0 trivially — you "found" everything because you retrieved everything. Always report recall at a realistic, fixed K (the K your reranker actually forwards to generation), and pair it with precision so a shameless K doesn't hide a bad system.</p>
<h3>What precision@K and recall@K actually tell you</h3>
<p>These two are less about ranking quality and more about two very different questions — one about noise, one about coverage — so it's worth reading them separately rather than as a single combined signal:</p>
<ul>
<li><p><strong>Precision@K is a noise question</strong>: of everything you're handing to the generation model, how much of it is actually useful? Low precision means your context window is full of filler — chunks the model has to read past (or worse, get distracted by) to find the real answer. This matters most when you're feeding a fixed, small K into generation, since every irrelevant chunk in that K is wasted context budget.</p>
</li>
<li><p><strong>Recall@K is a coverage question</strong>: out of everything relevant that exists in your corpus, how much of it did retrieval even manage to surface? Low recall means the answer might not even be <em>possible</em> — no amount of good prompting or reranking can save you if the right chunk never made it into the candidate set at all. This is the metric that catches retrieval-stage blind spots, like an embedding model that consistently misses a certain phrasing of a question.</p>
</li>
<li><p><strong>Reading them together</strong>: high precision + low recall means retrieval is being too conservative — it's confident about the few chunks it returns, but missing others that exist. Low precision + high recall means the opposite — it's casting a wide net and catching everything relevant, but drowning it in noise. Neither number alone tells you which failure mode you're in; you need both.</p>
</li>
</ul>
<p>Because neither metric is order-aware, they're best used as a first-pass health check — "is my candidate set roughly the right shape" — before reaching for MRR, MAP@K, or NDCG@K to judge whether that candidate set is <em>ranked</em> well. A system can have great precision@K and recall@K and still produce a bad user experience if the one relevant chunk out of five is sitting at rank 5 instead of rank 1 — which is exactly the gap the next section covers.</p>
<h2>Order-aware: MRR, the "how fast did we find it" metric</h2>
<p><strong>MRR (Mean Reciprocal Rank)</strong> looks at one thing per query — the rank of the <em>first</em> relevant chunk — and scores it as <code>1 / rank</code>, then averages across queries.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/aee6e17b-ccb5-4fae-a824-31423860348b.png" alt="" style="display:block;margin:0 auto" />

<p><strong>MRR</strong> is <em>order-aware</em> but <em>first-hit-only</em> — for the RRF query above, chunk_12 at rank 2 gives RR = 0.50, and MRR never looks past that point. Whatever happens at ranks 4, 5, and 7 (the other three relevant chunks in this example) is invisible to it. That blind spot is exactly what the next metric fixes.</p>
<h3>What MRR actually tells you</h3>
<p>A single MRR number is a proxy for "how much does the user have to scroll before they hit something useful." That makes it less an abstract IR score and more a direct read on system usability:</p>
<ul>
<li><p><strong>MRR close to 1.0</strong> — your top result is usually the right one. Users (and your generation step) rarely need to look past rank 1.</p>
</li>
<li><p><strong>MRR around 0.5</strong> — the right chunk typically shows up around rank 2, on average. Retrieval is finding it, but it's not confident enough to put it first.</p>
</li>
<li><p><strong>MRR trending low (below ~0.3)</strong> — either the right chunk is buried deep, or it's missing from the top-K entirely for a chunk of your queries (remember, a query with zero relevant hits in the top-K contributes an RR of 0, dragging the average down hard).</p>
</li>
</ul>
<p>Because MRR only credits the <em>first</em> hit, it's the metric to watch specifically when your downstream generation step only reads the first chunk or two before answering, or when you're tuning a reranker and want a fast, cheap signal for "is the single best answer floating to the top." It's a poor fit, though, for judging whether your system surfaces <em>all</em> the relevant context for a question — for that, keep reading.</p>
<h2>MAP@K: the metric that doesn't stop at the first hit</h2>
<p><strong>MAP@K</strong> (Mean Average Precision) computes precision@k at <em>every</em> rank where a relevant chunk shows up, averages those numbers into a per-query score called Average Precision, then means that across all queries.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/af214f4f-94b4-4fba-9198-126e476e940a.png" alt="" style="display:block;margin:0 auto" />

<p>💡 <strong>Key point</strong>: on Query 1, MRR scored 0.50 (it stopped at rank 2) while AP scored 0.54 (it credited all four hits). If a reranker change moved <code>chunk_22</code> from rank 7 up to rank 3, MRR wouldn't move at all — <code>chunk_12</code> is still the first hit at rank 2 — but AP (and MAP@K) would go up, correctly rewarding the improvement. Use MAP@K whenever queries can have more than one relevant chunk, which is the normal case once a corpus has multi-chunk sections</p>
<h3>What MAP@K actually tells you</h3>
<p>MAP@K is a single number that summarizes ranking quality <em>across your entire golden set</em>, in a way that's sensitive to both "did we find the relevant chunks" and "did we rank them near the top." That combination makes it the metric most people report as the headline number when comparing two retrieval systems end-to-end.</p>
<ul>
<li><p><strong>MAP@K close to 1.0</strong> — nearly every relevant chunk for nearly every query is ranked at the very top. This is rare in practice and worth double-checking your golden set isn't too easy.</p>
</li>
<li><p><strong>MAP@K in the 0.5–0.7 range</strong> — a healthy, typical score for a working hybrid search + reranker setup: most queries get their relevant chunks reasonably high, but there's room to improve on the harder queries.</p>
</li>
<li><p><strong>MAP@K trending low</strong> — either relevant chunks are scattered deep in the ranking across many queries, or several queries are missing relevant chunks from the top-K entirely (each contributes an AP of 0, and those zeros drag the mean down fast).</p>
</li>
</ul>
<p>Because MAP@K averages over <em>every</em> relevant chunk per query and <em>every</em> query in the set, it's the metric to reach for when you want one number to track over time as you tune chunking, embeddings, or reranking — a MAP@K that moves up after a change is a much stronger signal than a single query "looking better," since it means the improvement generalized across your whole golden set rather than fixing one query while quietly breaking another.</p>
<h2>NDCG@K: when not all relevant chunks are equally relevant</h2>
<p>Okay so here's the thing every metric so far has been ignoring: relevance isn't actually binary. A chunk isn't just "<strong>relevant</strong>" or "<strong>not relevant</strong>" — some chunks nail the answer, some are kinda-sorta related, and some are just noise that happens to share a keyword.</p>
<p>Precision@K, recall@K, MRR, MAP@K — none of them can tell the difference between "this chunk perfectly answers the question" and "this chunk mentions the topic in passing." <strong>NDCG@K</strong> (Normalized Discounted Cumulative Gain) is the metric built specifically to care about that difference.</p>
<p>To use it, every chunk in your golden set gets a graded relevance score instead of a yes/no — say, 0 (irrelevant), 1 (tangential), 2 (partial answer), 3 (nails it). Here's what the actual math looks like on 3 chunks with relevance scores 3, 2, and 0, shown across three different orderings:</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/12a3e317-9db9-42df-b130-4f44c5053dec.png" alt="" style="display:block;margin:0 auto" />

<p>Let's walk through what's actually happening here, because the log2 stuff looks scarier than it is:</p>
<ol>
<li><p><strong>First, figure out the best possible ranking.</strong> Sort your chunks by relevance, highest first — that's the "Ideal" lane at the top. Compute its DCG (more on that formula in a second), and call that number <strong>IDCG</strong>. It's your ceiling — the best score any ordering could ever get.</p>
</li>
<li><p><strong>Then score whatever ordering retrieval actually gave you.</strong> Same formula, just applied to the real ranking. Each chunk's relevance score gets divided by <code>log2(rank + 1)</code> — so a relevance-3 chunk sitting at rank 1 contributes a full 3.0, but that exact same chunk sitting at rank 3 only contributes 1.5. Same chunk, same "goodness," way less credit — because it's buried where fewer people will actually see it.</p>
</li>
<li><p><strong>Divide actual by ideal.</strong> That's NDCG. In Ordering A, retrieval nailed the ideal ranking exactly, so DCG matches IDCG and NDCG = 1.00 — perfect score. In Ordering B, retrieval buried the single best chunk (relevance 3) all the way at rank 3, so DCG drops to 2.76 and NDCG drops to 0.65.</p>
</li>
</ol>
<p>The whole point: NDCG doesn't just ask "did you find the good stuff," it asks "did you put the <em>best</em> stuff <em>first</em>." Two systems can retrieve the exact same three chunks and still get very different NDCG scores, purely based on the order.</p>
<p>⚠️ <strong>Gotcha</strong>: NDCG needs <em>graded</em> relevance labels (e.g. 0–3) on every golden example, not just relevant/not-relevant. That's real extra annotation work on top of a binary golden set. It's the industry-standard metric for web search ranking, but it's reasonable to treat as future work until the golden dataset has graded labels — don't block a first eval pass on it.</p>
<h3>What NDCG@K actually tells you</h3>
<p>Basically, NDCG is the metric for when "we found the relevant chunk" isn't good enough on its own and you actually care about quality of match, not just presence.</p>
<ul>
<li><p><strong>NDCG@K close to 1.0</strong> — your retrieval isn't just finding relevant stuff, it's putting the <em>best</em> stuff right at the top, close to the ideal ordering. This is what you want feeding into generation, since the model's most likely to lean on whatever's in the first slot or two.</p>
</li>
<li><p><strong>NDCG@K noticeably lower than your MAP@K</strong> — this is the tell that your system is finding relevant chunks (MAP looks fine) but not distinguishing <em>great</em> chunks from <em>okay</em> chunks in how it ranks them. A reranker tuned only on binary relevance can hit this exact gap.</p>
</li>
<li><p><strong>NDCG@K trending low across the board</strong> — either your best chunks are consistently getting buried, or your relevance grading itself needs a second pass (inconsistent grading tanks NDCG fast since it's so sensitive to the relative ordering of scores).</p>
</li>
</ul>
<p>Use it when you've got graded relevance to spend on, and especially when "close enough" chunks genuinely aren't as good as the perfect one — which, if you're citing sources or answering precise technical questions (exactly your case), is basically always.</p>
<h2>Building a golden dataset that doesn't lie to you</h2>
<p>None of the above metrics mean anything without a trustworthy golden set to check against — and the construction choices here matter more than people expect.</p>
<h3>Why the golden dataset matters</h3>
<p>Every single metric in this post — precision@K, recall@K, MRR, MAP@K, NDCG@K, even the faithfulness/relevance judge prompts — is computed <em>relative to</em> the golden set. There's no such thing as "precision" in the abstract; it's always "precision against these specific labeled examples." Which means the golden set isn't a one-off checklist item you build once and forget — it's the ruler you'll keep re-using every time you touch retrieval</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/c2d63060-7a24-4a78-bb79-3d3c72f6ed0d.png" alt="" style="display:block;margin:0 auto" />

<p>Two rules worth holding firm on:</p>
<ul>
<li><p><strong>Hand-label, don't LLM-generate.</strong> LLM-generated Q&amp;A pairs tend to reuse the source chunk's exact phrasing, which inflates retrieval scores relative to how a real user would actually ask. It also introduces self-grading bias if the same model later judges its own generated questions.</p>
</li>
<li><p><strong>Reference real</strong> <code>chunk_id</code><strong>s from the live table</strong>, not paraphrased summaries. This keeps the golden labels valid as ground truth even as embedding models or rerankers get swapped out underneath.</p>
</li>
</ul>
<p>20–30 diverse examples — mixing factual lookup, comparison, "how does X work," and known edge cases (like a section that got split across chunk boundaries) — is enough for a credible first benchmark. More examples matter less than covering different query shapes.</p>
<h2>Summary</h2>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Order-aware?</th>
<th>Needs graded relevance?</th>
<th>Answers</th>
</tr>
</thead>
<tbody><tr>
<td>Precision@K</td>
<td>No</td>
<td>No</td>
<td>Of what I retrieved, how much is useful</td>
</tr>
<tr>
<td>Recall@K</td>
<td>No</td>
<td>No</td>
<td>Of what's out there, how much did I find</td>
</tr>
<tr>
<td>MRR</td>
<td>Yes</td>
<td>No</td>
<td>How high did the <em>first</em> relevant hit rank</td>
</tr>
<tr>
<td>MAP@K</td>
<td>Yes</td>
<td>No</td>
<td>How good is the ranking across <em>all</em> relevant hits</td>
</tr>
<tr>
<td>NDCG@K</td>
<td>Yes</td>
<td>Yes</td>
<td>How close to ideal ranking, weighted by relevance strength</td>
</tr>
</tbody></table>
<p>The practical starting point: precision@K, recall@K, and MRR for a first pass, MAP@K once queries can have multiple relevant chunks, and NDCG@K once the golden set has graded relevance labels rather than binary ones.</p>
]]></content:encoded></item><item><title><![CDATA[RAG Search Algorithms - How Retrieval Actually Works Under the Hood]]></title><description><![CDATA[Every agentic AI system that "looks things up" before answering — a support bot searching a knowledge base, a coding agent searching a codebase, a research assistant searching PDFs — is running a Retr]]></description><link>https://gauravbytes.dev/rag-search-algorithms-how-retrieval-actually-works-under-the-hood</link><guid isPermaLink="true">https://gauravbytes.dev/rag-search-algorithms-how-retrieval-actually-works-under-the-hood</guid><category><![CDATA[AI]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[algorithms]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 20 Jul 2026 09:29:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/118d2519-c74b-429a-b586-4a4ff0448c2a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every agentic AI system that "looks things up" before answering — a support bot searching a knowledge base, a coding agent searching a codebase, a research assistant searching PDFs — is running a <strong>Retrieval-Augmented Generation (RAG)</strong> pipeline underneath. The retrieval step is the part that decides <em>what the model even gets to see</em> before it writes an answer. If retrieval pulls the wrong chunks, no amount of prompt engineering on the generation side will save the output.</p>
<p>Most tutorials treat "search" as a black box: throw text at a vector database, get chunks back. But there are five distinct families of search algorithms in production RAG systems today, each solving a different retrieval failure mode. Picking the right one — or the right combination — is the single highest-leverage decision in a RAG pipeline.</p>
<p>This post walks through each family: how it works, a diagram of the mechanism, the best use case, and a worked example.</p>
<h2>1. Keyword (Lexical) Search — Matching Characters, Not Meaning</h2>
<p>Lexical search treats text as strings. It doesn't understand that "car" and "automobile" mean the same thing — it only knows whether the same tokens appear in the query and the document.</p>
<h3>TF-IDF — The Starting Point</h3>
<p>TF-IDF is the original mathematical approach to lexical relevance. It scores a term in a document using two factors:</p>
<p><strong>term frequency</strong> (how often the word appears in that document) and</p>
<p><strong>inverse document frequency</strong> (how rare the word is across the whole corpus — common words like "the" get down-weighted, rare words get boosted).</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/b5be2ed9-7b96-425d-b4e7-007bf1fe3a47.png" alt="" style="display:block;margin:0 auto" />

<p><strong>The drawback:</strong> TF-IDF has no length normalization. A term appearing 5 times in a 50-word document and 5 times in a 5,000-word document gets scored the same way, even though the first is clearly far more "about" that term. This means very long documents can be unfairly penalized or short documents unfairly boosted, purely as an artifact of length rather than actual relevance.</p>
<p><strong>Best for:</strong> simple keyword scoring where all documents are roughly the same length, or as a teaching baseline. In practice, most production systems have moved past it for the reason above.</p>
<h3>BM25 (Best Matching 25) — Fixing TF-IDF's Length Problem</h3>
<p>BM25 keeps the same core idea as TF-IDF — term frequency weighted by inverse document frequency — but adds <strong>length normalization</strong>, so a document doesn't score higher just because it's longer, and diminishing returns on term frequency, so a word appearing 50 times doesn't score 10x higher than one appearing 5 times. This is why BM25 replaced TF-IDF as the industry-standard lexical algorithm.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/6f451c15-b6cc-4d2f-adb3-8a6ac2bfc717.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Best for:</strong> exact identifiers — part numbers, error codes, SKUs, legal citations, API method names — anything where a semantic model might "helpfully" match a conceptually similar but factually wrong result.</p>
<p><strong>Example:</strong> A support agent searches "error code E4021." A vector search might return docs about "connection timeout errors" in general (semantically close but wrong). BM25 returns only the doc that literally contains "E4021," because it's matching the exact token.</p>
<h2>2. Dense Vector (Semantic) Search — Matching Meaning, Not Characters</h2>
<p>Instead of comparing strings, dense retrieval converts text into <strong>embeddings</strong> — high-dimensional vectors where semantically similar text ends up numerically close together. The retrieval problem then becomes "find the nearest vectors," which is where the index algorithms below come in.</p>
<h3>Exhaustive k-NN — The Naive Baseline</h3>
<p>The simplest possible approach: compare the query vector against <strong>every single vector</strong> in the database and return the closest matches. This is brute-force search.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/3b73dd61-9bb5-419f-a13a-a9796fe2247e.png" alt="" style="display:block;margin:0 auto" />

<p><strong>The drawback:</strong> it's 100% accurate — there's no approximation, so you always get the true nearest neighbors — but the cost scales linearly with the number of vectors. Past roughly hundreds of thousands of vectors, comparing against everything on every query becomes too slow for production latency budgets.</p>
<p><strong>Best for:</strong> small corpora (a few thousand chunks) or as a correctness baseline when validating that a faster, approximate index isn't silently losing relevant results.</p>
<h3>IVF (Inverted File Indexing) — Trading Some Accuracy for Speed</h3>
<p>IVF's fix for exhaustive search's scaling problem is to pre-cluster the vector space (e.g., via k-means) into buckets ahead of time. At query time, instead of scanning every vector, it only searches the nearest cluster(s) to the query — a much smaller subset of the total data.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/0ad9579d-7d9c-45ae-a9af-d61a6563b367.png" alt="" style="display:block;margin:0 auto" />

<p><strong>The drawback:</strong> because it only checks the nearest cluster(s), a true nearest neighbor that happens to sit just across a cluster boundary can be missed. It's faster than exhaustive search, but the accuracy/speed trade-off still isn't ideal for very high-dimensional, high-recall production workloads — which is what pushed the field toward graph-based indexes.</p>
<p><strong>Best for:</strong> very large datasets where memory footprint matters more than squeezing out the last few percentage points of recall — common in on-disk vector indexes.</p>
<h3>HNSW (Hierarchical Navigable Small World)</h3>
<p>HNSW addresses the weaknesses of both approaches above with a different data structure entirely: a multi-layer graph, similar in spirit to a skip list. The top layers are sparse and let the search jump across large distances quickly to find the right neighborhood; lower layers are dense and let it drill down to precise matches — without needing to touch every vector (like exhaustive k-NN) or accept a rigid cluster boundary (like IVF).</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/419d3b7e-6ca8-4106-812d-010f3178d606.png" alt="" style="display:block;margin:0 auto" />

<p>The search starts at node <strong>A</strong> on the sparsest top layer, where only a couple of nodes exist — enough to tell it should head toward the "D" side of the graph rather than search everywhere. It drops down a layer (more nodes now, layer 1), refines its position, and drops down again to the bottom layer where <em>every</em> node lives. By this point it's already standing right next to the true answer, so it only has to check a few close neighbors (C and D) instead of all seven nodes shown here — and in a real index, instead of all vectors in the entire database.</p>
<p>This is why HNSW has become the most widely used ANN algorithm in production vector databases — it gets close to exhaustive-search recall at a small fraction of the latency, and scales to millions of vectors without the boundary-miss problem IVF has.</p>
<p><strong>Best for:</strong> production semantic search at scale — millions of chunks where you need sub-100ms latency and can tolerate "approximate" (not guaranteed 100% exact) nearest neighbors.</p>
<p><strong>Example:</strong> A user asks "how do I get a refund," and the knowledge base document is phrased "requesting reimbursement for a purchase." No shared keywords, but HNSW retrieves it because the embeddings land close together in vector space — at a latency exhaustive k-NN couldn't match at that corpus size.</p>
<h3>Distance Metrics</h3>
<p>The three algorithms above all need a way to define "closeness":</p>
<ul>
<li><p><strong>Cosine similarity</strong> — compares the angle between vectors, ignoring magnitude. Most common default for text embeddings.</p>
</li>
<li><p><strong>Dot product</strong> — factors in magnitude as well as direction; used when embedding magnitude is meaningful (e.g., some recommendation embeddings).</p>
</li>
<li><p><strong>Euclidean distance</strong> — straight-line distance; common in image or spatial embeddings.</p>
</li>
</ul>
<h2>3. Hybrid Search &amp; Fusion — Getting the Best of Both Worlds</h2>
<p>Lexical search misses paraphrases. Vector search misses exact identifiers. Production RAG systems increasingly run both in parallel and fuse the results.</p>
<p><strong>The big picture:</strong> hybrid search isn't one algorithm — it's a pattern. The same query is sent to two independent search systems at once (BM25 and vector search), and the two result lists are then merged into one by a fusion method. The fusion method is the only part that varies — the two most common ones are RRF and weighted scoring, covered below.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/81f587c8-97f4-41f8-a095-a34ae00c59f5.png" alt="" style="display:block;margin:0 auto" />

<p>With that overview in mind, here's how each fusion method actually works.</p>
<h3>RRF (Reciprocal Rank Fusion)</h3>
<p>Vector search returns a decimal similarity score (e.g., 0.82) and BM25 returns an unbounded score (e.g., 14.5) — you can't just add them together, the scales aren't comparable. RRF sidesteps this entirely: it only looks at <strong>rank position</strong> in each result list. A document ranked #1 in either list gets a high reciprocal score (1/1); one ranked #100 gets a tiny score (1/100). Scores from both lists are summed to produce one merged ranking.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/eeba4095-5128-4f83-a7f3-98a2b9f108f8.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Best for:</strong> general-purpose RAG where queries are unpredictable — sometimes users type exact terms, sometimes they paraphrase. RRF requires no score calibration or tuning, which makes it a safe default.</p>
<p><strong>Example:</strong> A query for "PGVector connection pooling" gets exact-match boost from BM25 on "PGVector," while the vector search separately surfaces a doc titled "managing database connections at scale" that never says "PGVector." RRF merges both into one ranked list instead of picking one method and losing the other's catch.</p>
<h3>Weighted Scoring</h3>
<p>Assigns a tunable ratio (e.g., 70% vector, 30% keyword) and computes a blended score directly.</p>
<p><strong>Easy way to think about it:</strong> instead of only caring about <em>rank position</em> like RRF does, weighted scoring uses the actual similarity numbers — but first it decides in advance how much to trust each search method, like a recipe with fixed proportions, and mixes them in that ratio every time.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/bb88b5f0-65c3-4c40-a614-caaf50860758.png" alt="" style="display:block;margin:0 auto" />

<p>Unlike RRF, the raw scores from each method have to be <strong>normalized onto the same 0–1 scale first</strong> (since 0.82 and 14.5 aren't comparable as-is), then each normalized score is multiplied by its fixed weight (0.7 and 0.3 here) and added together. That 70/30 split isn't automatic — someone has to decide it, usually by testing which ratio produces the best results on real queries for that specific domain.</p>
<p><strong>Best for:</strong> domains where you've empirically validated that one signal should dominate — e.g., an e-commerce catalog where exact SKU matches (keyword) should almost always outrank semantic similarity.</p>
<h2>4. Post-Retrieval Algorithms — Filtering the Noise After Retrieval</h2>
<p>Once candidates are retrieved, a second algorithmic pass improves precision before anything reaches the LLM.</p>
<h3>Cross-Encoder Reranking</h3>
<p>Standard vector search (a "bi-encoder") embeds the query and each document <em>independently</em>, then compares vectors — fast, but blind to interactions between query and document. A <strong>cross-encoder reranker</strong> instead feeds the query and each candidate document <em>together</em> into a model that scores relevance directly. It's computationally heavier (can't be pre-computed or indexed), so it's only run on the top candidates from the first retrieval pass, not the whole corpus.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/f3549d2f-b8ec-4f37-870c-383dcd61d33f.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Best for:</strong> any pipeline where context window budget is tight and precision at the top of the list matters more than recall — which is nearly every production RAG system, since LLMs weight earlier context more heavily and irrelevant chunks dilute the answer.</p>
<p><strong>Example:</strong> Initial retrieval returns 50 loosely-related chunks about "database indexing." The reranker re-scores all 50 against the actual query "why is my B-tree index not being used in this query plan" and pushes the 3 genuinely relevant chunks to the top, discarding the rest before they ever hit the LLM.</p>
<h3>Metadata Filtering</h3>
<p>Hard constraints applied before or after the similarity search — e.g., "only search documents where <code>department = engineering</code>" or "only chunks published after 2024." This isn't a ranking algorithm; it's a boundary that the ranking algorithms operate within.</p>
<p><strong>Best for:</strong> multi-tenant systems, access control, or time-sensitive corpora where irrelevant-but-similar content from the wrong tenant or an outdated version must never surface, regardless of similarity score.</p>
<h2>5. Multi-Hop and Graph-Based Search — Answering Across Documents</h2>
<p>Vector and keyword search both operate on isolated chunks. Some questions need information stitched together <em>across</em> multiple documents — that's where graph-based and hypothetical-document approaches come in.</p>
<h3>GraphRAG</h3>
<p>GraphRAG parses unstructured text into a knowledge graph of entities and relationships, then uses graph traversal and community-detection algorithms to answer questions that span multiple disconnected source documents — something flat chunk retrieval structurally can't do well, since no single chunk contains the full answer.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/4df03b14-ba46-4dab-a179-0bf9cf7ff673.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Best for:</strong> questions requiring reasoning across many documents — "how are Team A's incidents connected to the vendor outage last quarter" — where the answer doesn't live in any single chunk.</p>
<p><strong>Example:</strong> Asking "which customers were affected by the same root cause as the March outage" requires connecting an incident report, a customer ticket log, and a vendor postmortem — three separate documents. GraphRAG traverses the entity graph (incident → root cause → affected services → customers) to compile the answer; a single vector search over chunks would never connect all three.</p>
<h2>6. Query Transformation — Rewriting the Question Before Searching</h2>
<p>Every algorithm so far changes <em>how documents are searched</em>. This category is different: it changes <em>what gets searched for</em> — the query itself is rewritten before it ever reaches a retrieval algorithm. This runs at query time only; it doesn't touch or restructure the document corpus at all, which is what separates it from GraphRAG above.</p>
<h3>HyDE (Hypothetical Document Embeddings)</h3>
<p>An LLM first generates a <em>hypothetical, idealized</em> answer to the query. That hypothetical answer — not the raw question — is embedded and used to search the database. The intuition: an answer-shaped piece of text is often closer in vector space to real answer documents than a short, terse question is.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/0d4d2613-35c7-498d-b197-16c629590086.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Best for:</strong> queries that are short, vague, or phrased very differently from how the source documents are written — where a literal embedding of the question undershoots the semantic target.</p>
<h2>Choosing the Right Algorithm: Decision Guide</h2>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/42ca9bd8-06e5-4ad5-bced-e5dc735fa1fa.png" alt="" style="display:block;margin:0 auto" />

<h2>Trade-off Summary Table</h2>
<table>
<thead>
<tr>
<th>Algorithm</th>
<th>Type</th>
<th>Mechanism</th>
<th>Best Use Case</th>
<th>Weakness</th>
<th>Latency at Scale</th>
</tr>
</thead>
<tbody><tr>
<td><strong>BM25</strong></td>
<td>Lexical</td>
<td>Term frequency + inverse doc frequency</td>
<td>Exact IDs, codes, jargon</td>
<td>Misses paraphrases/synonyms</td>
<td>Fast</td>
</tr>
<tr>
<td><strong>TF-IDF</strong></td>
<td>Lexical</td>
<td>Raw term weighting (no length norm)</td>
<td>Legacy keyword systems</td>
<td>No length normalization</td>
<td>Fast</td>
</tr>
<tr>
<td><strong>HNSW</strong></td>
<td>Vector (ANN)</td>
<td>Layered graph traversal</td>
<td>Large-scale semantic search</td>
<td>Approximate, not exact</td>
<td>Fast (sub-100ms typical)</td>
</tr>
<tr>
<td><strong>IVF</strong></td>
<td>Vector (ANN)</td>
<td>Cluster-based partitioning</td>
<td>Very large, memory-constrained corpora</td>
<td>Accuracy depends on cluster count</td>
<td>Fast</td>
</tr>
<tr>
<td><strong>Exhaustive k-NN</strong></td>
<td>Vector (exact)</td>
<td>Brute-force comparison</td>
<td>Small corpora, accuracy baseline</td>
<td>Scales poorly (linear)</td>
<td>Slow at scale</td>
</tr>
<tr>
<td><strong>RRF</strong></td>
<td>Hybrid fusion</td>
<td>Rank-position summing</td>
<td>General-purpose RAG, unpredictable queries</td>
<td>No score calibration, only rank order</td>
<td>Fast</td>
</tr>
<tr>
<td><strong>Weighted Scoring</strong></td>
<td>Hybrid fusion</td>
<td>Tunable score blending</td>
<td>Domains with known signal dominance</td>
<td>Needs manual tuning/validation</td>
<td>Fast</td>
</tr>
<tr>
<td><strong>Cross-Encoder Reranking</strong></td>
<td>Post-retrieval</td>
<td>Joint query+doc scoring</td>
<td>Precision-critical, tight context budgets</td>
<td>Computationally heavy, can't index</td>
<td>Slow (run on top-K only)</td>
</tr>
<tr>
<td><strong>Metadata Filtering</strong></td>
<td>Post-retrieval</td>
<td>Hard constraint boundary</td>
<td>Multi-tenant, access control, freshness</td>
<td>Not a ranking signal by itself</td>
<td>Fast</td>
</tr>
<tr>
<td><strong>GraphRAG</strong></td>
<td>Graph-based</td>
<td>Entity graph + traversal</td>
<td>Multi-hop, cross-document reasoning</td>
<td>Expensive to build/maintain graph</td>
<td>Slow to build, moderate to query</td>
</tr>
<tr>
<td><strong>HyDE</strong></td>
<td>Query transformation</td>
<td>LLM-generated hypothetical answer embedding</td>
<td>Short/vague/mismatched-phrasing queries</td>
<td>Extra LLM call adds latency</td>
<td>Moderate</td>
</tr>
</tbody></table>
<h2>Where This Fits in the Agentic AI Stack</h2>
<p>Retrieval is the layer beneath everything else in an agentic RAG system — before tool selection, before reasoning chains, before final generation. Getting it wrong silently degrades every downstream step, because the agent reasons confidently over whatever context it was given, right or wrong.</p>
]]></content:encoded></item><item><title><![CDATA[pgvector for RAG Search: Schema, Storage, and Retrieval, With Real Results]]></title><description><![CDATA[RAG search has four moving parts: split the document, turn text into vectors, store those vectors somewhere queryable, and pull the right ones back out at query time. This post covers the last two — c]]></description><link>https://gauravbytes.dev/pgvector-for-rag-search-schema-storage-and-retrieval-with-real-results</link><guid isPermaLink="true">https://gauravbytes.dev/pgvector-for-rag-search-schema-storage-and-retrieval-with-real-results</guid><category><![CDATA[agentic AI]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[pgvector]]></category><category><![CDATA[postgres]]></category><category><![CDATA[AI]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 20 Jul 2026 09:27:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/3b559a09-2726-4bba-ac6a-47f941b1fece.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>RAG search has four moving parts: split the document, turn text into vectors, store those vectors somewhere queryable, and pull the right ones back out at query time. This post covers the last two — configuring Postgres to store vectors and structuring retrieval around them</p>
<h2>Introduction</h2>
<p>This post assumes you've already decided how to chunk your documents — if not, see the companion post, "<a href="https://gauravbytes.dev/chunking-strategies-in-langchain-rag-a-deep-dive">Chunking Strategies in RAG: A Deep Dive</a>" which measured fixed-size, recursive, and semantic chunking against real blog content and found recursive chunking the strongest default (100% clean sentence-ending rate vs. 5% for fixed-size, and structure-aware since it respects markdown headers). This post picks up from there: given chunks and a way to embed them, how do you actually get them into Postgres and query them back correctly?</p>
<h2>Big Picture</h2>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/8d9f7840-c83b-4c1b-bd7f-f2845c0a00db.png" alt="" style="display:block;margin:0 auto" />

<p>💡 <strong>Key Point</strong>: <code>embed_chunks()</code> and <code>embed_query()</code> are the same underlying function, called at two different times. This single point of truth matters — if ingestion and query use different embedding logic (different model, different preprocessing, different normalization), the vectors won't be comparable and similarity search silently degrades without throwing any error.</p>
<h2>Step 1: A Single Embedding Function, Used Twice</h2>
<pre><code class="language-python">class EmbeddingService:
    def __init__(self, fitting_corpus):
        self._backend, self.dim = self._load_backend(fitting_corpus)

    def embed_chunk(self, text):
        return self._backend.embed_documents([text])[0]

    def embed_chunks(self, texts):
        return self._backend.embed_documents(texts)

    def embed_query(self, text):
        return self._backend.embed_query(text)
</code></pre>
<p>Wrapping the embedding backend in one class with <code>embed_chunks()</code> and <code>embed_query()</code> methods means the rest of the pipeline never touches the backend directly — it calls this interface. Swapping <code>BAAI/bge-small-en-v1.5</code> for <code>text-embedding-3-small</code> later is a one-line change inside <code>_load_backend()</code>, and nothing in the schema, ingestion, or retrieval code needs to know or care.</p>
<p>⚠️ This only works if <code>self.dim</code> (the embedding dimension) is read from the <em>actual</em> backend after it loads, not hardcoded. Different models produce different dimensions — 384 for BGE-small, 1536 for <code>text-embedding-3-small</code> — and the Postgres schema in the next section is built with this exact number.</p>
<h2>Step 2: Postgres Configuration for Vector Storage</h2>
<p>The only mandatory step is enabling the extension:</p>
<pre><code class="language-sql">CREATE EXTENSION IF NOT EXISTS vector;
</code></pre>
<p>📝 <strong>Terminology</strong>: this needs to run once per database, by a role with sufficient privilege (superuser, or a role granted <code>CREATE</code> on the database). It's a standard extension install — no changes to <code>postgresql.conf</code>, no <code>shared_preload_libraries</code> entry, and no server restart required, unlike some Postgres extensions.</p>
<h2>Step 3: Schema Design</h2>
<pre><code class="language-sql">CREATE TABLE rag_chunks (
    id SERIAL PRIMARY KEY,
    document_id TEXT NOT NULL,
    document_title TEXT NOT NULL,
    chunk_index INT NOT NULL,
    content TEXT NOT NULL,
    embedding VECTOR(384) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE (document_id, chunk_index)
);
</code></pre>
<p>Each column exists for a specific reason tied to how retrieval will use it later:</p>
<table>
<thead>
<tr>
<th>Column</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><code>document_id</code></td>
<td>Traces a chunk back to its source document — needed to cite results or fetch neighboring chunks</td>
</tr>
<tr>
<td><code>chunk_index</code></td>
<td>This chunk's position within its document — enables ordered reconstruction and neighbor lookups</td>
</tr>
<tr>
<td><code>content</code></td>
<td>The raw chunk text — this is what actually gets shown to the user or passed to an LLM, the embedding itself is not human-readable</td>
</tr>
<tr>
<td><code>embedding</code></td>
<td><code>VECTOR(384)</code> — dimension must match <code>EmbeddingService.dim</code> exactly</td>
</tr>
<tr>
<td><code>created_at</code></td>
<td>Standard audit column — useful once you're re-ingesting updated documents and need to know which rows are stale</td>
</tr>
<tr>
<td><code>UNIQUE (document_id, chunk_index)</code></td>
<td>Makes ingestion idempotent — re-running the pipeline on the same document fails loudly on duplicate insert rather than silently doubling the corpus</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/29f66724-82e2-4da9-9fb4-ede067870992.png" alt="" style="display:block;margin:0 auto" />

<h2>Step 4: Ingestion</h2>
<pre><code class="language-python"># -- for each document, chunk it, embed the chunks, insert them
for document in documents: # each document has id and text
    chunks = chunk_text(document.text) # from the chunking
    vectors = embed_chunks(chunks) # single embedding fun

    for chunk_index, (chunk, vector) in enumerate(zip(chunks, vectors)):
        insert_row(
            document_id=document.document_id,
            document_title=document.title,
            chunk_index=chunk_index,
            content=chunk,
            embedding=vector,
        )

    commit()
</code></pre>
<p><strong>Measured result</strong>, running this against 5 real blog posts (<a href="http://gauravbytes.dev">gauravbytes.dev</a>):</p>
<pre><code class="language-plaintext">Ingesting corpus:
  chunking-strategies-in-langchain-rag-a-deep-dive: 37 chunks
  how-i-created-an-mcp-server-for-postgresql-to-power-ai-agents-components-architecture-and-real-testing: 33 chunks
  how-to-pick-the-perfect-database-without-losing-your-mind: 51 chunks
  these-ai-memory-types-decide-whether-your-agent-is-smart-or-useless: 19 chunks
  upgrading-my-postgresql-ai-agent-3-architecture-decisions-i-made-and-why: 38 chunks
Total chunks stored: 178
</code></pre>
<p>Chunk count scales with document length and structure, not a fixed number per document — the <code>database-picking</code> post produced 51 chunks (it's the longest, most heavily <code>##</code>-sectioned post in the corpus) while the <code>AI memory</code> post produced 19 (shorter, flatter structure).</p>
<h2>Step 5: Retrieval</h2>
<pre><code class="language-python">def search(conn, embedder, query, k=3):
    qvec = embedder.embed_query(query)
    with conn.cursor() as cur:
        cur.execute(
            """
            SELECT document_title, chunk_index, content, 1 - (embedding &lt;=&gt; %s::vector) AS similarity
            FROM rag_chunks
            ORDER BY embedding &lt;=&gt; %s::vector
            LIMIT %s;
            """,
            (qvec, qvec, k),
        )
        return cur.fetchall()
</code></pre>
<p><code>&lt;=&gt;</code> is pgvector's cosine distance operator — smaller means more similar, which is why <code>1 - distance</code> is computed separately to display an intuitive similarity score while the raw distance still drives the <code>ORDER BY</code>.</p>
<p><strong>Measured results</strong>, three real queries against the <strong>178</strong> stored chunks:</p>
<pre><code class="language-plaintext">
Query: 'how should I structure memory for an AI agent'
-------------------------------------------------------------

1. similarity=0.819  [# These AI Memory Types Decide Whether Your A... chunk #18]
   If you truly want to build production-grade AI agents…  start with memory architecture first....
2. similarity=0.812  [# These AI Memory Types Decide Whether Your A... chunk #3]
   ## Why Memory Matters in AI Agents  Imagine hiring a human assistant who:  *   forgets your name eve...


Query: 'what commands should be blocked in a database agent for safety'
-------------------------------------------------------------

1. similarity=0.742  [# Upgrading My PostgreSQL AI Agent: 3 Archite... chunk #18]
   Two things worth noting here. First, this goes through `execute_sql()` — the single gateway from Dec...
2. similarity=0.737  [# Upgrading My PostgreSQL AI Agent: 3 Archite... chunk #11]
   The private connection API is about making the right path the only path. A future contributor should...


Query: 'why do I need to clean up log files on a server'
-------------------------------------------------------------

1. similarity=0.696  [# Upgrading My PostgreSQL AI Agent: 3 Archite... chunk #34]
   **The interface abstraction is what matters.** Because `save_session()` and `load_session()` are the...
2. similarity=0.657  [# Upgrading My PostgreSQL AI Agent: 3 Archite... chunk #12]
   ## Decision 2: Schema Caching at Startup...
</code></pre>
<h2>Final Thoughts</h2>
<p>Postgres, with the vector extension enabled, can store embeddings in a normal table and rank them by cosine similarity against a query using nothing more exotic than <code>ORDER BY embedding &lt;=&gt; query_vector</code>. No separate vector database, no new infrastructure to run — a schema, an insert, and a query, all standard SQL plus one operator.</p>
<p>Some of the results still wasn't clearly relevant, even with a real embedding model in place as we can see in the last query result. That's not a reason to distrust <strong>pgvector</strong> or the <strong>embedding</strong> — it's a sign that plain <code>cosine similarity</code> over chunk embeddings is a starting point, not the finished system.</p>
]]></content:encoded></item><item><title><![CDATA[Chunking Strategies in RAG: A Deep Dive]]></title><description><![CDATA[RAG systems live or die on one decision that happens before any embedding is computed: how the source document gets cut into pieces. This post walks through three chunking mechanisms available in Lang]]></description><link>https://gauravbytes.dev/chunking-strategies-in-langchain-rag-a-deep-dive</link><guid isPermaLink="true">https://gauravbytes.dev/chunking-strategies-in-langchain-rag-a-deep-dive</guid><category><![CDATA[agentic AI]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[AI]]></category><category><![CDATA[agents]]></category><category><![CDATA[Python]]></category><category><![CDATA[langchain]]></category><category><![CDATA[chunking]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Tue, 07 Jul 2026 07:27:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/76ee7c30-c461-4efd-af29-3daddf728fb8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>RAG systems live or die on one decision that happens before any embedding is computed: how the source document gets cut into pieces. This post walks through three chunking mechanisms available in LangChain, traces exactly how each one decides where to cut, and reports what happened when all three were run against a real 2,209-word blog post rather than a toy paragraph.</p>
<h2>Introduction</h2>
<p>Three strategies are covered:</p>
<ol>
<li><p><code>CharacterTextSplitter</code> (fixed-size),</p>
</li>
<li><p><code>RecursiveCharacterTextSplitter</code> (recursive), and</p>
</li>
<li><p><code>SemanticChunker</code> (semantic).</p>
</li>
<li><p>They differ in exactly one thing: <strong>what signal they use to decide where a cut belongs.</strong> Fixed-size uses a raw character count. Recursive uses a priority list of structural separators. Semantic uses embedding similarity between sentences.</p>
</li>
</ol>
<blockquote>
<p>⏭️ If you already know what an embedding and cosine similarity are, skip to Big Picture.</p>
</blockquote>
<h2>Background / Prerequisites</h2>
<p>📝 <strong>Terminology — Embedding</strong>: a numeric vector representation of text, produced by a model, positioned in vector space such that semantically similar text produces nearby vectors.</p>
<p>📝 <strong>Terminology — Cosine similarity / distance</strong>: <code>similarity = cos(θ)</code> between two vectors, ranging from -1 to 1 (in practice 0 to 1 for text embeddings). <code>distance = 1 - similarity</code>. Used throughout <code>SemanticChunker</code> to measure how related two pieces of text are.</p>
<p>📝 <strong>Terminology — chunk_overlap</strong>: the number of characters from the end of one chunk repeated at the start of the next, intended to preserve context that would otherwise be severed at a chunk boundary.</p>
<h2>Big Picture: How the Three Strategies Decide Where to Cut</h2>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/914afab4-7e27-413c-afa2-9eaf88fa1dc5.png" alt="" style="display:block;margin:0 auto" />

<p>Each row down this diagram trades one thing for another: fixed-size gives up quality for speed and zero cost; recursive gives up a guarantee for a strong heuristic; semantic gives up a size guarantee entirely in exchange for meaning-awareness, at the cost of needing an embedding model at chunking time.</p>
<h2>Fixed-Size Chunking: <code>CharacterTextSplitter</code></h2>
<pre><code class="language-python">from langchain_text_splitters import CharacterTextSplitter

splitter = CharacterTextSplitter(separator="", chunk_size=500, chunk_overlap=80)
chunks = splitter.split_text(document)
</code></pre>
<p>With <code>separator=""</code>, the splitter performs a hard cut every <code>chunk_size</code> characters, sliding forward by <code>chunk_size - chunk_overlap</code> each time. It has no concept of a word, sentence, or heading — the cut point is purely a character index.</p>
<p><strong>Measured result</strong> <em>(real blog post, "How to Pick the Perfect Database,"</em> <code>chunk_size=500</code><em>,</em> <code>chunk_overlap=80</code><em>)</em>: 22 chunks, mean length 791 chars, only <strong>5% ended on a sentence-terminating character</strong> (<code>.</code>, <code>!</code>, <code>?</code>, <code>:</code>). One chunk boundary landed inside a heading marker, producing <code>"...ETL into an analytics store. ## Graph Databases — The Relationship Specialists A gr..."</code> split directly across two chunks.</p>
<p>⚠️ <strong>Gotcha</strong>: <code>chunk_overlap</code> in <code>CharacterTextSplitter</code> is a raw character count. It does not try to land on a word boundary — the repeated text at a chunk's start can itself begin mid-word.</p>
<h2>Recursive Chunking: <code>RecursiveCharacterTextSplitter</code></h2>
<pre><code class="language-python">from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=100,
    separators=["\n## ", "\n### ", "\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document)
</code></pre>
<h3>Decision Flow</h3>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/54777c5a-55f6-4d5d-bde4-ce8a509c113a.png" alt="" style="display:block;margin:0 auto" />

<p>The default separator list is <code>["\n\n", "\n", ". ", " ", ""]</code>, ordered from largest structural unit to smallest, with <code>""</code> (raw character split) as an unconditional fallback so the algorithm can never fail to produce a chunk under <code>chunk_size</code>.</p>
<p>💡 <strong>Key Point</strong>: reordering this list to match your document's actual structure is the single highest-leverage tuning knob for this splitter. Markdown content with real <code>##</code>/<code>###</code> headers benefits from putting <code>\n##</code> and <code>\n###</code> at the front, ahead of the generic paragraph/sentence defaults.</p>
<p><strong>Measured result</strong> (same document, headers-first separator list, <code>chunk_size=800</code>, <code>chunk_overlap=100</code>): 30 chunks, mean length 511 chars, <strong>100%</strong> ended on a sentence-terminating character — up from 5% for fixed-size on comparable settings.</p>
<p>⚠️ <strong>Gotcha — size ceiling persists</strong>: recursive chunking respects structure but is still size-bound. The document's "Graph Databases" section (heading through the closing sentence of its "Common pitfalls" subsection) measures <strong>1,277 characters</strong>. At <code>chunk_size=700</code>, the splitter is forced to cut through this section regardless of separator ordering. At <code>chunk_size=1300</code>, the section survives as one chunk. This was verified directly, not assumed:</p>
<table>
<thead>
<tr>
<th>chunk_size</th>
<th>Section stays intact?</th>
</tr>
</thead>
<tbody><tr>
<td>700</td>
<td>No</td>
</tr>
<tr>
<td>1300</td>
<td>Yes</td>
</tr>
<tr>
<td>1500</td>
<td>Yes</td>
</tr>
</tbody></table>
<h2>Semantic Chunking: <code>SemanticChunker</code></h2>
<h3>Concept: What "Semantic" Means Here</h3>
<p><code>SemanticChunker</code> has no <code>chunk_size</code> parameter. It instead measures the cosine distance between the embeddings of consecutive sentence groups and cuts wherever that distance exceeds a threshold — i.e., wherever the topic changes.</p>
<h3>Pipeline (Data Flow)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/ce083b0e-cd9e-4809-b8e4-ec3b090ea6f0.png" alt="" style="display:block;margin:0 auto" />

<p>📝 <strong>Terminology —</strong> <code>buffer_size</code>: the number of neighboring sentences folded into each embedded unit. At <code>buffer_size=1</code>, <code>combined_sentence[i]</code> = <code>sentence[i-1] + sentence[i] + sentence[i+1]</code>. This means the distance between position <code>i</code> and <code>i+1</code> compares two overlapping windows that share most of their content — only the sentence falling off one edge and the new one entering the other edge actually differ. This is a deliberate smoothing mechanism: a short, low-information sentence is never embedded in isolation.</p>
<p>⚠️ <strong>Gotcha</strong>: <code>combined_sentence[i]</code> is what gets embedded, not the raw <code>sentence[i]</code>. Confusing the two leads to incorrect assumptions about what the similarity score is actually measuring.</p>
<h3>The Four <code>breakpoint_threshold_type</code> Options</h3>
<table>
<thead>
<tr>
<th>Type</th>
<th>Threshold formula</th>
<th>Sensitive to</th>
<th>Best for</th>
</tr>
</thead>
<tbody><tr>
<td><code>percentile</code> (default)</td>
<td><code>np.percentile(distances, amount)</code></td>
<td>rank, not magnitude</td>
<td>general default, unknown distribution</td>
</tr>
<tr>
<td><code>standard_deviation</code></td>
<td><code>mean(distances) + amount × std(distances)</code></td>
<td>absolute deviation from mean</td>
<td>uniform baseline + one dramatic shift</td>
</tr>
<tr>
<td><code>interquartile</code></td>
<td><code>mean(distances) + amount × (Q3 − Q1)</code></td>
<td>absolute deviation from a <em>robust</em> center</td>
<td>multiple shifts of varying magnitude</td>
</tr>
<tr>
<td><code>gradient</code></td>
<td><code>np.percentile(np.gradient(distances), amount)</code></td>
<td>rate of change, not raw distance</td>
<td>gradual topic drift (legal/scientific text)</td>
</tr>
</tbody></table>
<p><strong>Verified failure mode —</strong> <code>standard_deviation</code> <strong>vs</strong> <code>interquartile</code> on distance sequence <code>[0.05, 0.06, 0.04, 0.90, 0.05, 0.06, 0.35, 0.05, 0.06, 0.04]</code> (two real topic breaks: one dramatic at index 3, one moderate at index 6):</p>
<ul>
<li><p><code>standard_deviation(1)</code>: threshold = 0.426 → breakpoints at <code>[3]</code> only. The dramatic outlier (0.90) inflates the mean and std enough that the moderate break (0.35) falls below threshold and is missed.</p>
</li>
<li><p><code>interquartile(1.5)</code>: threshold = 0.181 (Q1=0.050, Q3=0.060, computed from the tight middle cluster, unaffected by either outlier) → breakpoints at <code>[3, 6]</code>. Both breaks are caught, because IQR's own spread measurement isn't corrupted by the outliers it's trying to detect.</p>
</li>
</ul>
<h3>The Embedding Model Determines Correctness, Not Just Cost</h3>
<p><code>SemanticChunker</code> accepts any object implementing LangChain's <code>Embeddings</code> interface (<code>embed_documents</code>, <code>embed_query</code>). Two were tested:</p>
<p><strong>TF-IDF</strong> (word-overlap statistics, used as a free/offline stand-in):</p>
<pre><code class="language-plaintext">"The cat sat on the mat." vs "A feline was resting on the rug."
  → same meaning, zero shared content words → similarity: 0.000

"The bank raised interest rates today." vs "I sat by the river bank yesterday."
  → different meaning, one shared word ("bank") → similarity: 0.181
</code></pre>
<p>TF-IDF ranks the second, unrelated pair as more similar than the first, meaning-identical pair, purely on shared vocabulary. Run through <code>SemanticChunker</code>, this produces measurable structural noise: on the test document, chunk sizes ranged from <strong>16 to 1,257 characters</strong> (mean 411, std 342) — a single bulleted list item was isolated as its own 16-character chunk, while an entire section was merged into one 1,257-character chunk, because TF-IDF's similarity signal doesn't track actual topic boundaries reliably.</p>
<p><code>HuggingFaceEmbeddings(model_name="BAAI/bge-small-en-v1.5")</code> — a 384-dimension neural embedding model trained to place semantically similar text near each other regardless of shared vocabulary:</p>
<pre><code class="language-python">from langchain_huggingface import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-small-en-v1.5",
    model_kwargs={"device": "cpu"},
    encode_kwargs={"normalize_embeddings": True},
)
</code></pre>
<p>⚠️ <strong>Gotcha — environment dependency</strong>: this requires network access to <code>huggingface.co</code> to download model weights (~130MB, one-time). In a network-restricted environment, this call fails; a working fallback pattern is shown in the referenced script (§8) that catches the download failure and substitutes TF-IDF so the pipeline still runs end-to-end for demonstration purposes.</p>
<h2>Comparison / Trade-offs</h2>
<table>
<thead>
<tr>
<th></th>
<th>Fixed-size</th>
<th>Recursive</th>
<th>Semantic</th>
</tr>
</thead>
<tbody><tr>
<td>Size guarantee</td>
<td>Yes, exact</td>
<td>Yes, upper-bound</td>
<td>None</td>
</tr>
<tr>
<td>Structural awareness</td>
<td>None</td>
<td>Yes, via separator priority</td>
<td>Yes, via meaning</td>
</tr>
<tr>
<td>Cost at chunking time</td>
<td>None</td>
<td>None</td>
<td>Embedding calls (API cost or local inference)</td>
</tr>
<tr>
<td>Measured clean sentence-ending rate</td>
<td>5%</td>
<td>100%</td>
<td>100%</td>
</tr>
<tr>
<td>Chunk size uniformity (CV)</td>
<td>0.05 (most uniform)</td>
<td>0.34</td>
<td>0.83 (least uniform, by design)</td>
</tr>
<tr>
<td>Dependent on embedding model quality</td>
<td>No</td>
<td>No</td>
<td>Yes — critically</td>
</tr>
</tbody></table>
<p>💡 <strong>Key Point</strong>: chunk size uniformity and chunk quality are different axes. Fixed-size had the lowest coefficient of variation (0.05) and the worst quality (5% clean endings) of the three. A tight size distribution is not evidence of good chunking.</p>
<h2>References</h2>
<ul>
<li><p>LangChain text splitters documentation (<code>langchain_text_splitters</code>)</p>
</li>
<li><p>LangChain experimental <code>SemanticChunker</code> source (<code>langchain_experimental</code>)</p>
</li>
<li><p><code>BAAI/bge-small-en-v1.5</code> model card (HuggingFace) for dimension count and intended use</p>
</li>
<li><p>All measured percentages, chunk counts, and character counts in this post were produced by running the scripts in against my own blog post, "<a href="https://gauravbytes.dev/how-to-pick-the-perfect-database-without-losing-your-mind"><em><strong>How to Pick the Perfect Database Without Losing Your Mind</strong></em></a>" (<a href="https://gauravbytes.dev">gauravbytes.dev</a>) — no numbers were estimated or fabricated.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Upgrading My PostgreSQL AI Agent: 3 Architecture Decisions I Made (And Why)]]></title><description><![CDATA[This is Part 2 of an ongoing series on building a production-ready PostgreSQL AI agent using LangChain and Ollama. If you haven't read Part 1, start there — it covers the foundation: connecting to Pos]]></description><link>https://gauravbytes.dev/upgrading-my-postgresql-ai-agent-3-architecture-decisions-i-made-and-why</link><guid isPermaLink="true">https://gauravbytes.dev/upgrading-my-postgresql-ai-agent-3-architecture-decisions-i-made-and-why</guid><category><![CDATA[postgres]]></category><category><![CDATA[AI]]></category><category><![CDATA[ollama]]></category><category><![CDATA[langchain]]></category><category><![CDATA[Python]]></category><category><![CDATA[tools]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Tue, 07 Jul 2026 07:25:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/90b388a2-aefc-4ce9-afd9-1349869d6a6b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This is Part 2 of an ongoing series on building a production-ready PostgreSQL AI agent using LangChain and Ollama. If you haven't read</em> <a href="https://gauravbytes.dev/build-a-postgresql-ai-agent-using-langchain-ollama"><em>Part 1</em></a><em>, start there — it covers the foundation: connecting to PostgreSQL, setting up tools, and getting a working chat loop running locally.</em></p>
<h2>The Prototype Was Working. That Was the Problem.</h2>
<p>After Part 1, I had a working agent. You could type a question in plain English, it would figure out the right SQL, run it, and return results. Impressive enough to demo. Not solid enough to build on.</p>
<p>The more I used it, the more I noticed the cracks:</p>
<ul>
<li><p>The database connection was hardcoded. Switching databases meant editing the source file.</p>
</li>
<li><p>Every time the agent needed to understand a table, it fetched the schema live — adding latency and unnecessary DB hits.</p>
</li>
<li><p>The conversation had no memory. Every session started from zero.</p>
</li>
<li><p>The SQL safety check lived inside the tool itself, meaning there was nothing stopping me (or a future contributor) from adding another path that bypassed it entirely.</p>
</li>
</ul>
<p>None of these are showstoppers at the prototype stage. All of them become real problems the moment you want to hand the tool to someone else, open-source it, or run it against a database that actually matters.</p>
<p>This article covers three architectural decisions I made to address these — what I changed, what I considered, and why I landed where I did.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/595ef207-16b9-409d-9903-b982bd8aa91a.png" alt="" style="display:block;margin:0 auto" />

<h2>Decision 1: A Single SQL Execution Gateway</h2>
<h3>The Problem</h3>
<p>In v1, the <code>execute_sql</code> tool handled everything inline: it validated the query, opened a connection, ran it, and returned the result — all in one function that the LangChain agent called directly.</p>
<p>That works. But it means the safety logic and the execution logic are coupled inside a single tool. If I ever added a second tool — say, one that runs EXPLAIN plans or fetches row counts — I'd have to either duplicate the safety check or remember to call some shared validator manually. Both options are brittle.</p>
<p>The deeper issue: there was no structural guarantee that every SQL statement passed through a single checkpoint. The safety net existed, but it was enforced by convention, not architecture.</p>
<h3>The Decision</h3>
<p>I moved all database interaction behind a single <code>execute_sql()</code> function in <code>db.py</code>. This is the only place in the codebase that runs SQL against the database. Every tool, every command, every path goes through it — no exceptions.</p>
<p>Alongside this, I introduced two custom exceptions:</p>
<pre><code class="language-python">class SQLSafetyError(Exception):
    """Raised when a query violates the read-only safety policy."""
    pass

class SQLExecutionError(Exception):
    """Raised when a valid query fails during execution."""
    pass
</code></pre>
<p>And the connection API became private — a <code>_get_connection()</code> function that nothing outside <code>db.py</code> should call directly.</p>
<pre><code class="language-python">def _get_connection():
    """Private. Use execute_sql() — do not call this directly."""
    return psycopg2.connect(**_load_db_config())
</code></pre>
<h3>Why This Matters</h3>
<p>The separation of <code>SQLSafetyError</code> and <code>SQLExecutionError</code> is deliberate. A blocked query and a failed query are different situations — they should produce different responses to the user and, eventually, different log entries. Collapsing them into a single generic error message loses information that's useful for debugging.</p>
<p>The private connection API is about making the right path the only path. A future contributor shouldn't have to know to call the safety check before running a query — the architecture should make that automatic. If <code>_get_connection()</code> is internal and <code>execute_sql()</code> is the public interface, there's no accidental bypass.</p>
<p>This is the kind of constraint that feels like overhead when you're the only developer. It pays off the moment the codebase has more than one pair of eyes on it.</p>
<h2>Decision 2: Schema Caching at Startup</h2>
<h3>The Problem</h3>
<p>In v1, schema discovery was lazy. When the agent needed to understand a table's structure, it called the <code>get_table_schema</code> tool, which hit the database on demand.</p>
<p>This created a few practical problems:</p>
<ul>
<li><p><strong>Latency.</strong> Every query that involved schema introspection added a round-trip to the database before the actual query ran.</p>
</li>
<li><p><strong>Inconsistency.</strong> The LLM sometimes skipped the schema fetch and guessed column names based on the table name alone. It was usually right. When it was wrong, the error wasn't obvious.</p>
</li>
<li><p><strong>Context fragmentation.</strong> Schema information arrived piecemeal, one table at a time, as the conversation progressed. The agent never had a complete picture of the database upfront.</p>
</li>
</ul>
<h3>The Decision</h3>
<p>The caching implementation is deliberately simple. A module-level variable acts as the cache, populated on the first call and reused on every subsequent one:</p>
<pre><code class="language-python">def get_cached_tables() -&gt; list[str]:
    """Get list of all tables in public schema (cached after first call)."""
    global _schema_cache
    if _schema_cache is not None:
        return _schema_cache
    try:
        result = execute_sql("""
            SELECT table_name FROM information_schema.tables
            WHERE table_schema = 'public'
            ORDER BY table_name
        """)
        _schema_cache = [row[0] for row in result.rows]
    except (SQLSafetyError, SQLExecutionError):
        _schema_cache = []
    return _schema_cache
</code></pre>
<p>Two things worth noting here. First, this goes through <code>execute_sql()</code> — the single gateway from Decision 1 — so the safety check applies even to internal schema queries. Second, if the fetch fails, the cache is set to an empty list rather than left as <code>None</code>. That means a second call won't retry a failed fetch mid-session, which keeps the behavior predictable.</p>
<p>The cached table list feeds into a <code>{schema_context}</code> block that gets injected into the system prompt at startup:</p>
<pre><code class="language-python">"""You are PGChat, an expert PostgreSQL database assistant. You have access to tools to inspect and query the connected database.

Current date and time: {current_time}

Guidelines:
- Use the SCHEMA SNAPSHOT below to answer questions about tables and columns — do NOT call list_tables or get_table_schema unless the user explicitly asks to refresh.
- Prefer short, precise answers. When showing data, summarize it unless the user asks for raw output.
- Never guess table or column names — verify against the schema snapshot.
- When you write SQL, explain what it does briefly.
- If a query fails, analyze the error and suggest fixes.
- Use get_table_sample to preview data when helpful.
- Use search_schema when the user asks "which table has X column?"
- Use run_query to execute SQL queries.

{schema_context}"""
</code></pre>
<p>The guideline <code>do NOT call list_tables or get_table_schema unless the user explicitly asks to refresh</code> is doing real work here. Without it, the LLM will sometimes call those tools anyway out of habit, even when the snapshot has the answer. Explicitly instructing it to trust the snapshot eliminates those redundant tool calls in practice.</p>
<p>A <code>/refresh-schema</code> command lets the user reset <code>_schema_cache</code> to <code>None</code> and trigger a fresh fetch if the schema has changed during a session.</p>
<h3>Why This Matters</h3>
<p>The key insight is that schema information is almost never volatile within a single session. Tables don't get columns added or dropped while you're mid-conversation. Fetching once and injecting as context is both faster and more reliable than fetching on demand.</p>
<p>The system prompt approach has a meaningful side effect: the LLM has the full schema available before it generates its first query. It doesn't have to reason about what tools to call to understand the database — that context is already there. In practice, this reduces the number of tool calls per user question and noticeably improves query accuracy, especially on databases with multiple tables that share similar naming patterns.</p>
<p>The <code>/refresh-schema</code> escape hatch matters for user trust. Schema caching only works if users are confident the snapshot reflects reality. Giving them a command to invalidate it manually means they're never stuck with stale information.</p>
<p>One tradeoff worth acknowledging: for databases with hundreds of tables, injecting the full schema as a system message could push against context window limits. For the current use case this isn't a problem, but it's something to revisit as the tool grows. A reasonable next step would be selectively injecting only the tables relevant to the current query — something worth exploring in a later iteration.</p>
<h2>Decision 3: Session Memory with JSON (For Now)</h2>
<h3>The Problem</h3>
<p>In v1, every conversation started from scratch. The agent had no memory of what you'd asked before, what tables you'd explored, or what the last query returned. For a quick one-off query this is fine. For anything resembling a real workflow — iterating on a report, exploring a dataset across multiple questions — it's a significant limitation.</p>
<h3>The Decision</h3>
<p>I added persistent session memory backed by JSON. At the end of each conversation turn, the session state is written to a <code>.json</code> file. When you start the agent again, it loads that file and resumes where you left off.</p>
<p>The interface is intentionally minimal:</p>
<pre><code class="language-python">def save_session(session: Session, path: str) -&gt; None:
    """Persist session state to disk."""
    ...

def load_session(path: str) -&gt; Session | None:
    """Load a previous session, or return None if none exists."""
    ...
</code></pre>
<h3>Why JSON and Not SQLite or PostgreSQL?</h3>
<p>This is the question I spent the most time on, because it seems obvious to reach for a database when you're already using one.</p>
<p>The case for SQLite: structured queries, better concurrency handling, easier to inspect with standard tooling. The case for PostgreSQL: the agent is already connected to one, so no additional dependency. Both are reasonable. Neither is what I chose.</p>
<p>JSON wins for now for a few specific reasons:</p>
<p><strong>Zero additional dependencies.</strong> The agent already requires Python, psycopg2, LangChain, and Ollama. Adding a SQLite ORM or a second PostgreSQL connection for session storage would increase the setup surface area for something that isn't the core feature. For a CLI tool that someone installs with <code>pip</code>, simpler setup matters.</p>
<p><strong>Human-readable state.</strong> A JSON session file is easy to inspect, copy, share, and debug without any tooling. If something goes wrong with session state, you can open the file in a text editor and understand it immediately.</p>
<p><strong>The interface abstraction is what matters.</strong> Because <code>save_session()</code> and <code>load_session()</code> are the only way the rest of the codebase touches session state, the storage backend can be swapped later without changing anything else. If the JSON approach hits a real limitation — concurrent sessions, large history, search across sessions — migrating to SQLite or PostgreSQL is a one-file change. Building that migration path in now, by keeping the interface clean, costs nothing.</p>
<p>The honest answer is also that JSON is good enough for the current use case. The agent runs as a single-user CLI tool. There's no concurrency to manage, no performance requirement that JSON can't meet, and no schema to maintain. Reaching for a database here would be solving a problem I don't have yet.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/3f06f693-f985-4bfc-bdc6-c6da51f9d02d.png" alt="" style="display:block;margin:0 auto" />

<h2>What's Next</h2>
<p>These three changes move the agent from a prototype to something I'd actually hand to another developer. The architecture is more intentional, the failure modes are clearer, and the session experience is meaningfully better.</p>
<p>The next step is packaging — making the agent installable as a proper CLI tool via <code>pip install postgres-agent</code>, with a clean entry point and configuration handled through environment variables or a config file rather than source edits.</p>
<p>After that, the session storage decision will get revisited. As the tool supports multiple named sessions or shared use cases, JSON will start to show its limits. That's the right time to migrate — not before.</p>
<p>The source code is on GitHub: <a href="https://github.com/icon-gaurav/pgchat">github.com/icon-gaurav/pgchat</a>. If you're building something similar or have thoughts on any of these decisions, I'd like to hear them.</p>
]]></content:encoded></item><item><title><![CDATA[How to Pick the Perfect Database Without Losing Your Mind ]]></title><description><![CDATA[Hey fellow dev — let’s tackle the choice that keeps us up at night: which database should you use? I’ve boiled down the major database archetypes into a practical field guide so you can make the right]]></description><link>https://gauravbytes.dev/how-to-pick-the-perfect-database-without-losing-your-mind</link><guid isPermaLink="true">https://gauravbytes.dev/how-to-pick-the-perfect-database-without-losing-your-mind</guid><category><![CDATA[Databases]]></category><category><![CDATA[System Design]]></category><category><![CDATA[development]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[MySQL]]></category><category><![CDATA[Redis]]></category><category><![CDATA[MongoDB]]></category><category><![CDATA[vector database]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 08 Jun 2026 04:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/49043616-d83e-4a6b-95f6-7be0a0cd96fa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey fellow dev — let’s tackle the choice that keeps us up at night: which database should you use? I’ve boiled down the major database archetypes into a practical field guide so you can make the right call for your next project or system design interview. For each archetype you’ll get: a real-world example, core characteristics, pros/cons, common pitfalls, and concrete product examples.</p>
<h2>Before You Even Look at a Database, Answer These 6 Questions</h2>
<p>Every database choice I've made that turned out well started here. Every bad choice skipped at least one of these.</p>
<ul>
<li><p><strong>What shape is your data? -</strong> Rows and columns with fixed fields? Nested JSON documents? Nodes and edges? Timestamped measurements?</p>
</li>
<li><p><strong>How stable is your schema? -</strong> If your fields change often between records, you'll hate rigid relational schemas. If they're locked in, schema flexibility is a non-issue.</p>
</li>
<li><p><strong>How complex are your relationships? -</strong> Do entities reference each other heavily? Many-to-many? Deep traversals? This is where most teams underestimate.</p>
</li>
<li><p><strong>What are your query patterns? -</strong> Short key lookups vs. aggregations vs. full-text vs. joins — these push you toward completely different architectures.</p>
</li>
<li><p><strong>What's the expected scale? -</strong> A single-region app serving 10K users is a different problem from global OLTP at 1M writes/sec.</p>
</li>
<li><p><strong>Who is operating this thing?</strong> - The best database for your use case is the one your team can actually run well. Don't adopt Cassandra if no one on your team has operated it.</p>
</li>
</ul>
<h3>Quick thumb rule</h3>
<p>If most answers point to ACID + complex relationships → start with <strong>relational DB</strong>.</p>
<p>If you need flexibility in schema + hierarchical documents → consider <strong>document DB.</strong></p>
<p>If you need extreme low-latency key lookups → <strong>key–value store</strong>.</p>
<p>If you need graph traversals → <strong>graph DB.</strong></p>
<p>If you need high-cardinality time series → <strong>time-series DB</strong>.</p>
<h2>Relational Databases (RDBMS) — The Reliable Workhorse</h2>
<p>RDBMS is the foundational technology for systems where <strong>data consistency, reliability, and complex relationships</strong> are critical. Because RDBMS is designed to be highly structured and secure, it is the industry standard for any application where "<strong>getting the data right</strong>" is more important than sheer speed at the expense of accuracy</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/496c5f10-0494-4fe9-927f-72615d77dc17.webp" alt="" style="display:block;margin:0 auto" />

<h3>Characteristics:</h3>
<ul>
<li><p>Structured rows and columns with a defined schema.</p>
</li>
<li><p>Strong ACID guarantees for transactions.</p>
</li>
<li><p>Powerful SQL for joins, aggregation, and complex queries.</p>
</li>
<li><p>Mature tooling for backups, migrations, and analytics.</p>
</li>
</ul>
<h3>Pros:</h3>
<ul>
<li><p>Battle-tested for correctness and consistency.</p>
</li>
<li><p>Great for normalized data and multi-table transactions.</p>
</li>
<li><p>Rich ecosystem (ORMs, monitoring, tooling).</p>
</li>
</ul>
<h3>Cons:</h3>
<ul>
<li><p>Schema migrations can be painful at large scale.</p>
</li>
<li><p>Joins across huge tables can be slow without careful indexing and design.</p>
</li>
<li><p>Vertical scaling limits unless you adopt sharding or distributed SQL.</p>
</li>
</ul>
<p><strong>Market examples:</strong> PostgreSQL, MySQL, Microsoft SQL Server.</p>
<p><strong>Who uses them:</strong> core transactional systems at Shopify, payment systems, many SaaS apps.</p>
<h3>Common pitfalls:</h3>
<ul>
<li><p>Over-normalizing for read-heavy workloads — optimize with materialized views or read replicas.</p>
</li>
<li><p>Ignoring indexing and query plans; assume indexes are free.</p>
</li>
</ul>
<h2>Document Databases — The Flexible Multi-Tool</h2>
<p>Document databases are chosen when your application’s data requirements are evolving rapidly or are too varied to fit neatly into a rigid, table-based schema.</p>
<p>Because they are highly flexible and scale easily, they are the go-to for modern, agile software development. e.g. CMS or user profiles where each user may have different fields.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/b5a1f43e-4395-481c-8f43-a72ffd997109.png" alt="" style="display:block;margin:0 auto" />

<h3>Characteristics:</h3>
<ul>
<li><p>Schema-flexible JSON-like documents (nested structures welcome).</p>
</li>
<li><p>Queryable fields and secondary indexes; supports partial updates.</p>
</li>
<li><p>Good balance between flexible modeling and querying capability.</p>
</li>
</ul>
<h3>Pros:</h3>
<ul>
<li><p>Evolve your data model quickly without rigid migrations.</p>
</li>
<li><p>Excellent for denormalized data and aggregations that map to document shapes.</p>
</li>
<li><p>Often easier to scale horizontally than single-instance RDBMS.</p>
</li>
</ul>
<h3>Cons:</h3>
<ul>
<li><p>Complex joins are either unsupported or expensive (application-level joins).</p>
</li>
<li><p>Risk of inconsistent schemas and duplicated data if not managed.</p>
</li>
<li><p>Transactions historically limited (but many modern engines now support distributed transactions).</p>
</li>
</ul>
<p><strong>Market examples:</strong> MongoDB, Couchbase, Amazon DocumentDB.</p>
<p><strong>Who uses them:</strong> content systems, user profile stores, product catalogs.</p>
<h3>Common pitfalls:</h3>
<ul>
<li><p>Modeling relational semantics as documents without a strategy leads to data redundancy and maintenance headaches.</p>
</li>
<li><p>Uncontrolled document growth (too large documents or too many nested arrays) hurts performance.</p>
</li>
</ul>
<h2>Key–Value Stores — The Blazing-Fast Lookup</h2>
<p>Key-Value databases are your go-to choice when you need lightning-fast performance for simple tasks. Think of them like a giant, <strong>super-organized dictionary</strong> where you can grab any piece of information instantly just by knowing its unique "key."</p>
<p>Highly used in session management and caching</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/5ed9a7d5-b19a-445f-bf16-6f94571e7754.png" alt="" style="display:block;margin:0 auto" />

<h3>Characteristics:</h3>
<ul>
<li><p>Simple API: store and retrieve by primary key.</p>
</li>
<li><p>Extremely low-latency reads and writes.</p>
</li>
<li><p>Minimal structure — values are opaque blobs to the store.</p>
</li>
</ul>
<h3>Pros:</h3>
<ul>
<li><p>Excellent performance at scale; trivially sharded.</p>
</li>
<li><p>Simple to reason about and operate.</p>
</li>
</ul>
<h3>Cons:</h3>
<ul>
<li><p>Limited querying (no secondary indexes or complex queries).</p>
</li>
<li><p>Application must handle consistency and indexing logic.</p>
</li>
</ul>
<p><strong>Market examples:</strong> Redis, Amazon DynamoDB (when used as KV), Memcached.</p>
<h3>Common pitfalls:</h3>
<ul>
<li><p>Using KV where you actually need complex queries or relationships.</p>
</li>
<li><p>Treating cache as primary storage without durable persistence or correct eviction handling.</p>
</li>
</ul>
<h2>Column-Family (Wide-Column) Stores — The High-Throughput Workhorse</h2>
<p>Column-family stores are the perfect choice when you need to handle massive, rapidly growing datasets that would overwhelm a traditional database.</p>
<p>Unlike standard systems that store data row by row, these databases organize information by columns, allowing them to read and write specific data points at incredible speeds.</p>
<p>They are the ideal tool for managing massive workloads like IoT telemetry, web logs, or real-time user activity feeds.</p>
<h3>Characteristics:</h3>
<ul>
<li><p>Data modeled as rows with many sparse columns grouped into families.</p>
</li>
<li><p>Optimized for write-heavy workloads and large-scale partitioning.</p>
</li>
<li><p>Tunable consistency and compaction strategies.</p>
</li>
</ul>
<h3>Pros:</h3>
<ul>
<li><p>High write throughput, designed for horizontal scale.</p>
</li>
<li><p>Efficient for queries that read contiguous ranges or specific columns.</p>
</li>
</ul>
<h3>Cons:</h3>
<ul>
<li><p>Query flexibility is lower than relational databases.</p>
</li>
<li><p>Requires careful data modeling to avoid hot partitions.</p>
</li>
</ul>
<p><strong>Market examples:</strong> Apache Cassandra, ScyllaDB, HBase.</p>
<h3>Common pitfalls:</h3>
<ul>
<li><p>Poor partition key choices causing hotspotting and degraded performance.</p>
</li>
<li><p>Trying to support ad-hoc analytics without ETL into an analytics store.</p>
</li>
</ul>
<h2>Graph Databases — The Relationship Specialists</h2>
<p>A Graph Database stores data as a network of nodes (entities) and edges (relationships), rather than rigid tables. By treating these connections as first-class citizens, it allows for high-performance traversal of complex data paths without the heavy cost of traditional JOIN operations.</p>
<p>These systems are ideal when the relationships between data points are as important as the data itself.</p>
<p>They are the standard for real-time, relationship-heavy tasks like powering recommendation engines, fraud detection, and social network analysis.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/cef938a8-d2db-4ef6-a82d-27f2b610b7fa.png" alt="" style="display:block;margin:0 auto" />

<h3>Characteristics:</h3>
<ul>
<li><p>Native representation of nodes and edges with rich traversal capabilities.</p>
</li>
<li><p>Efficient for deep, variable-length relationship queries.</p>
</li>
<li><p>Query languages: Cypher, Gremlin, or GQL-like syntaxes.</p>
</li>
</ul>
<h3>Pros:</h3>
<ul>
<li><p>Expressive for relationship-heavy domains; traversals are fast.</p>
</li>
<li><p>Intuitive modeling for networks and hierarchies.</p>
</li>
</ul>
<h3>Cons:</h3>
<ul>
<li><p>Not ideal for wide analytical queries or massive ad-hoc aggregations.</p>
</li>
<li><p>Scaling can be more complex than key-value or column stores.</p>
</li>
</ul>
<p><strong>Market examples:</strong> Neo4j, Amazon Neptune, JanusGraph.</p>
<h3>Common pitfalls:</h3>
<ul>
<li><p>Modeling everything as a graph when simpler data models suffice.</p>
</li>
<li><p>Ignoring graph size and traversal complexity — performance can degrade with high-degree nodes.</p>
</li>
</ul>
<h2>Time-Series Databases — Optimized for Temporal Data</h2>
<p>A Time-Series Database is purpose-built to store and query data points indexed by time, such as sensor readings, server logs, or stock market fluctuations.</p>
<p>These systems are essential for applications where tracking trends, patterns, and anomalies over time is critical. They are the industry standard for real-time monitoring, forecasting, and managing complex telemetry data.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/e5dec2c8-8953-400f-acbf-7f59ff45e4d5.webp" alt="" style="display:block;margin:0 auto" />

<h3>Characteristics:</h3>
<ul>
<li><p>Time is a first-class citizen (efficient append, retention, downsampling).</p>
</li>
<li><p>Built-in functions for aggregations over windows and rate calculations.</p>
</li>
<li><p>Often compact storage and compression for high-ingest workloads.</p>
</li>
</ul>
<h3>Pros:</h3>
<ul>
<li><p>Excellent for metric retention, querying recent data, and rollups.</p>
</li>
<li><p>Features like retention policies and continuous aggregations.</p>
</li>
</ul>
<h3>Cons:</h3>
<ul>
<li><p>Not a general-purpose store for arbitrary relational data.</p>
</li>
<li><p>May need separate systems for long-term archival or complex joins.</p>
</li>
</ul>
<p><strong>Market examples:</strong> InfluxDB, TimescaleDB (Postgres extension), Prometheus (metrics).</p>
<h3>Common pitfalls:</h3>
<ul>
<li>Using a TSDB for non-temporal data or trying to join TSDB data with relational transactional data without ETL.</li>
</ul>
<h2>Search Engines / Full-Text Stores — The Queryable Text Engine</h2>
<p>Search Engine Databases are built to <strong>index and retrieve</strong> unstructured text by mapping every word's location, allowing for nearly instant keyword and phrase searches.</p>
<p>Unlike traditional databases, they excel at ranking results by relevance and handling typos through fuzzy matching. They are the industry standard for powering site-wide search bars, log analysis tools, and deep-dive document discovery.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/537465d0-2c5c-4f00-82bd-88238d5bf6c9.webp" alt="" style="display:block;margin:0 auto" />

<h3>Characteristics:</h3>
<ul>
<li><p>Inverted indexes optimized for text search and relevance ranking.</p>
</li>
<li><p>Support for filters, facets, and near-real-time indexing.</p>
</li>
<li><p>Powerful query DSLs for scoring and boosting.</p>
</li>
</ul>
<h3>Pros:</h3>
<ul>
<li><p>Great for full-text search, autocomplete, and ranked results.</p>
</li>
<li><p>Can support analytics over indexed fields.</p>
</li>
</ul>
<h3>Cons:</h3>
<ul>
<li><p>Not designed as a source-of-truth transactional database.</p>
</li>
<li><p>Indexing lag and eventual consistency between primary store and index.</p>
</li>
</ul>
<p><strong>Market examples</strong>: Elasticsearch, OpenSearch, Algolia.</p>
<h3>Common pitfalls:</h3>
<ul>
<li><p>Treating the search index as the primary data store (data loss risk).</p>
</li>
<li><p>Forgetting to reindex when source data schema or semantics change.</p>
</li>
</ul>
<h2>NewSQL / Distributed SQL — SQL with Scale</h2>
<p>NewSQL (Distributed SQL) databases offers both strict ACID consistency and massive horizontal scalability. They automate the distribution of data across cloud clusters, removing the need for manual sharding while maintaining a standard SQL interface.</p>
<p>They are the go-to solution for mission-critical applications that need to grow globally without sacrificing transaction integrity.</p>
<h3>Characteristics:</h3>
<ul>
<li><p>SQL and ACID semantics combined with distributed architecture.</p>
</li>
<li><p>Built-in sharding/replication to scale reads and writes.</p>
</li>
</ul>
<h3>Pros:</h3>
<ul>
<li><p>Familiar SQL model with modern horizontal scaling.</p>
</li>
<li><p>Often simpler to operate than hand-sharded RDBMS clusters.</p>
</li>
</ul>
<h3>Cons:</h3>
<ul>
<li><p>Complexity and operational costs may still be higher than single-node RDBMS.</p>
</li>
<li><p>Some trade-offs in latency or consistency depending on config.</p>
</li>
</ul>
<p><strong>Market examples:</strong> CockroachDB, Google Spanner, YugabyteDB.</p>
<h3>Common pitfalls:</h3>
<ul>
<li><p>Expecting the same latency characteristics as a single-machine RDBMS.</p>
</li>
<li><p>Underestimating the operational model (geo-partitioning, transaction latency).</p>
</li>
</ul>
<h2>Multi-Model Databases — One Engine, Multiple Models</h2>
<p>Multi-Model Databases are versatile platforms built to handle multiple data structures—such as documents, graphs, and relational tables—within a single, unified engine.</p>
<p>Instead of managing separate databases for different data types, this approach allows you to store and query varied structures using one consistent API.</p>
<h3>Characteristics:</h3>
<ul>
<li><p>Support for two or more data models within one engine.</p>
</li>
<li><p>Aims to reduce polyglot persistence complexity.</p>
</li>
</ul>
<h3>Pros:</h3>
<ul>
<li><p>Flexibility; use the right model for each feature without managing multiple systems.</p>
</li>
<li><p>Simplified operational footprint.</p>
</li>
</ul>
<h3>Cons:</h3>
<ul>
<li><p>Each model may not be best-in-class; vendor lock-in risk.</p>
</li>
<li><p>Complexity in modeling and backups across models.</p>
</li>
</ul>
<p><strong>Market examples:</strong> ArangoDB, Cosmos DB (multi-model flavors).</p>
<h3>Common pitfalls:</h3>
<ul>
<li>Believing multi-model removes need for careful modeling and performance testing.</li>
</ul>
<h2>Vector Databases — The AI-powered Semantic Engine</h2>
<p>This database is designed to store and search data as vectors (numerical representations) instead of traditional rows and columns.</p>
<p>AI models convert text, images, or audio into vectors, and the database finds the most similar vectors using semantic meaning rather than exact keyword matches.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/186118fc-71bc-43ed-97ea-122e4fe37140.jpg" alt="" style="display:block;margin:0 auto" />

<h3>Characteristics:</h3>
<ul>
<li><p>Stores high-dimensional vectors (embeddings) generated by ML models.</p>
</li>
<li><p>Optimized for similarity search (Approximate Nearest Neighbor - ANN) rather than exact key matches.</p>
</li>
<li><p>Supports hybrid search (combining metadata filtering with vector similarity).</p>
</li>
</ul>
<h3>Pros :</h3>
<ul>
<li><p>Highly efficient at finding semantically similar items at scale.</p>
</li>
<li><p>Enables advanced AI use cases like LLM memory and RAG.</p>
</li>
<li><p>Flexible support for various distance metrics (cosine, L2, inner product).</p>
</li>
</ul>
<h3>Cons :</h3>
<ul>
<li><p>High memory footprint; vector indexes (like HNSW) can be RAM-intensive.</p>
</li>
<li><p>Query results are probabilistic (approximate) rather than deterministic.</p>
</li>
<li><p>Requires complex pipeline to generate and sync embeddings from source data.</p>
</li>
</ul>
<p><strong>Market examples:</strong> Pinecone, Milvus, Weaviate, Qdrant, pgvector (PostgreSQL extension).</p>
<p><strong>Who uses them:</strong> AI startups, platforms building generative AI features, search infrastructure teams.</p>
<h3>Common pitfalls:</h3>
<ul>
<li><p>Storing massive amounts of raw data in the vector DB, use it for embeddings and store raw data in a traditional DB.</p>
</li>
<li><p>Ignoring the need for data maintenance (embeddings become stale when source content changes).</p>
</li>
<li><p>Choosing the wrong distance metric or index parameters, leading to poor recall/precision.</p>
</li>
</ul>
<h2>How I choose (practical heuristics)</h2>
<ul>
<li><p>Start with the shape of your data and queries:</p>
</li>
<li><p>If queries are heavily relational and transactional → RDBMS or Distributed SQL.</p>
</li>
<li><p>If data is document-like and schema evolves → Document DB.</p>
</li>
<li><p>If you need extreme single-key performance → Key–Value store.</p>
</li>
<li><p>If traversals and relationships are core → Graph DB.</p>
</li>
<li><p>If the workload is metric/time-based → Time-Series DB.</p>
</li>
<li><p>Prioritize correctness first for money/health/safety systems (ACID &gt; scale). You can scale later.</p>
</li>
<li><p>Consider operational burden and team expertise: prefer familiar tools unless scale demands otherwise.</p>
</li>
<li><p>Prototype the hot paths: build and load-test a minimal model of your expected queries and traffic.</p>
</li>
<li><p>Plan for backups, migrations, and observability from day one.</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>Database Type</th>
<th>Best Used For</th>
<th>Example Use Cases</th>
<th>Popular Options</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Relational Database (SQL)</strong></td>
<td>Structured data with relationships and transactions</td>
<td>Banking systems, E-commerce orders, ERP, CRM</td>
<td>PostgreSQL, MySQL, Microsoft SQL Server</td>
</tr>
<tr>
<td><strong>Document Database</strong></td>
<td>Flexible, schema-less JSON data</td>
<td>CMS, Product catalogs, User profiles</td>
<td>MongoDB, Couchbase</td>
</tr>
<tr>
<td><strong>Key-Value Database</strong></td>
<td>Ultra-fast reads and writes</td>
<td>Caching, Sessions, Rate limiting</td>
<td>Redis, Amazon DynamoDB</td>
</tr>
<tr>
<td><strong>Vector Database</strong></td>
<td>Semantic search using AI embeddings</td>
<td>RAG applications, AI chatbots, Recommendation engines</td>
<td>Pinecone, Weaviate, Milvus</td>
</tr>
<tr>
<td><strong>Graph Database</strong></td>
<td>Highly connected data and relationships</td>
<td>Social networks, Fraud detection, Knowledge graphs</td>
<td>Neo4j, Amazon Neptune</td>
</tr>
<tr>
<td><strong>Time-Series Database</strong></td>
<td>Data that changes over time</td>
<td>Monitoring, IoT sensors, Stock market data</td>
<td>InfluxDB, TimescaleDB</td>
</tr>
<tr>
<td><strong>Search Database</strong></td>
<td>Full-text search and analytics</td>
<td>Log analysis, Search engines, Observability platforms</td>
<td>Elasticsearch, OpenSearch</td>
</tr>
<tr>
<td><strong>Columnar Database</strong></td>
<td>Analytical workloads and large-scale reporting</td>
<td>Data warehouses, BI dashboards, Analytics</td>
<td>ClickHouse, Snowflake</td>
</tr>
</tbody></table>
<h2>Quick design patterns</h2>
<ul>
<li><p>Read-heavy with complex joins → RDBMS + read replicas or materialized views.</p>
</li>
<li><p>Flexible user data + frequent reads → Document DB with controlled denormalization.</p>
</li>
<li><p>High ingest telemetry → TSDB or column-family store with downsampling pipeline.</p>
</li>
<li><p>Low-latency session/cache → Redis or managed in-memory KV.</p>
</li>
<li><p>Search plus data store → Primary store (RDBMS/DocDB) + search index (Elasticsearch) with sync strategy.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[These AI Memory Types Decide Whether Your Agent Is Smart or Useless]]></title><description><![CDATA[Everyone is building AI agents right now.
But most of them have one major problem:
They forget everything.
You ask the agent something… then ask a follow-up question… and suddenly it behaves like it h]]></description><link>https://gauravbytes.dev/these-ai-memory-types-decide-whether-your-agent-is-smart-or-useless</link><guid isPermaLink="true">https://gauravbytes.dev/these-ai-memory-types-decide-whether-your-agent-is-smart-or-useless</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[aiagents]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[memory]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 25 May 2026 04:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/f54921a0-c3ad-4550-91a0-37a72fcb5123.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Everyone is building AI agents right now.</p>
<p>But most of them have one major problem:</p>
<p>They forget everything.</p>
<p>You ask the agent something… then ask a follow-up question… and suddenly it behaves like it has amnesia.</p>
<p>That’s because many developers focus only on:</p>
<ul>
<li><p>prompts,</p>
</li>
<li><p>tools,</p>
</li>
<li><p>workflows,</p>
</li>
<li><p>and LLM selection…</p>
</li>
</ul>
<p>…but completely ignore <strong>memory systems</strong>.</p>
<p>The reality is:</p>
<blockquote>
<p>Memory is what transforms an LLM into an actual AI agent.</p>
</blockquote>
<p>Without memory:</p>
<ul>
<li><p>agents feel dumb,</p>
</li>
<li><p>conversations break,</p>
</li>
<li><p>personalization disappears,</p>
</li>
<li><p>and long-running tasks become impossible.</p>
</li>
</ul>
<p>Before implementing any AI agent, you must understand the different memory types, when to use them, and what should (and should not) be stored.</p>
<p>In this article, we’ll break down:</p>
<ul>
<li><p>Short-term memory</p>
</li>
<li><p>Long-term memory</p>
</li>
<li><p>Semantic memory</p>
</li>
<li><p>Episodic memory</p>
</li>
<li><p>Working memory</p>
</li>
<li><p>Retrieval memory And how modern AI systems like OpenAI and Anthropic likely structure memory internally.</p>
</li>
</ul>
<hr />
<h2>Why Memory Matters in AI Agents</h2>
<p>Imagine hiring a human assistant who:</p>
<ul>
<li><p>forgets your name every minute,</p>
</li>
<li><p>never remembers previous tasks,</p>
</li>
<li><p>and resets after every conversation.</p>
</li>
</ul>
<p>That assistant would be useless.</p>
<p>Yet that’s exactly how many AI agents behave today.</p>
<p>Memory enables agents to:</p>
<ul>
<li><p>maintain conversation context,</p>
</li>
<li><p>remember user preferences,</p>
</li>
<li><p>learn from previous interactions,</p>
</li>
<li><p>improve future responses,</p>
</li>
<li><p>and perform long-running autonomous tasks.</p>
</li>
</ul>
<p>This is the difference between:</p>
<ul>
<li><p>a chatbot,</p>
</li>
<li><p>and a real AI assistant.</p>
</li>
</ul>
<hr />
<h2>1. Short-Term Memory (Conversation Memory)</h2>
<h3>What It Is</h3>
<p>Short-term memory stores:</p>
<ul>
<li><p>recent messages,</p>
</li>
<li><p>current task context,</p>
</li>
<li><p>active reasoning steps,</p>
</li>
<li><p>and temporary conversation state.</p>
</li>
</ul>
<p>This memory usually lives inside:</p>
<ul>
<li><p>the context window,</p>
</li>
<li><p>Redis,</p>
</li>
<li><p>in-memory cache,</p>
</li>
<li><p>or session storage.</p>
</li>
</ul>
<h3>Example</h3>
<p>User says:</p>
<blockquote>
<p>“Help me write a LinkedIn post about AI agents.”</p>
</blockquote>
<p>Then later:</p>
<blockquote>
<p>“Make it shorter.”</p>
</blockquote>
<p>The AI needs short-term memory to understand: “it” = the LinkedIn post.</p>
<p>Without it, the agent gets confused.</p>
<h3>Common Mistake</h3>
<p>Many developers dump entire conversations into prompts.</p>
<p>This:</p>
<ul>
<li><p>increases token cost,</p>
</li>
<li><p>slows responses,</p>
</li>
<li><p>and eventually exceeds context limits.</p>
</li>
</ul>
<p>Good agents summarize and compress short-term memory over time.</p>
<hr />
<h2>2. Long-Term Memory (Persistent Memory)</h2>
<h3>What It Is</h3>
<p>Long-term memory stores information across sessions.</p>
<p>This includes:</p>
<ul>
<li><p>user preferences,</p>
</li>
<li><p>goals,</p>
</li>
<li><p>writing style,</p>
</li>
<li><p>past projects,</p>
</li>
<li><p>recurring workflows,</p>
</li>
<li><p>and important facts.</p>
</li>
</ul>
<p>Usually stored in:</p>
<ul>
<li><p>vector databases,</p>
</li>
<li><p>PostgreSQL,</p>
</li>
<li><p>graph databases,</p>
</li>
<li><p>or knowledge stores.</p>
</li>
</ul>
<h3>Example</h3>
<p>If a user always writes:</p>
<ul>
<li><p>backend engineering blogs,</p>
</li>
<li><p>AI tutorials,</p>
</li>
<li><p>and YouTube scripts…</p>
</li>
</ul>
<p>…the agent can remember this and personalize future outputs automatically.</p>
<p>That’s how assistants start feeling “smart.”</p>
<hr />
<h2>3. Semantic Memory</h2>
<h3>What It Is</h3>
<p>Semantic memory stores facts and knowledge.</p>
<p>Think of it like:</p>
<ul>
<li><p>concepts,</p>
</li>
<li><p>relationships,</p>
</li>
<li><p>expertise,</p>
</li>
<li><p>and learned information.</p>
</li>
</ul>
<h3>Example</h3>
<p>The agent remembers:</p>
<ul>
<li><p>LangChain is an AI framework,</p>
</li>
<li><p>PostgreSQL is a database,</p>
</li>
<li><p>Redis is used for caching.</p>
</li>
</ul>
<p>This is knowledge-based memory.</p>
<p>Humans use semantic memory too.</p>
<hr />
<h2>4. Episodic Memory</h2>
<h3>What It Is</h3>
<p>Episodic memory stores experiences and past interactions.</p>
<p>Instead of remembering facts, the agent remembers events.</p>
<h3>Example</h3>
<p>The agent remembers:</p>
<blockquote>
<p>“Last week the user struggled with Docker networking.”</p>
</blockquote>
<p>That historical experience helps future responses.</p>
<p>This is one of the most important memory types for personalization.</p>
<hr />
<h2>5. Working Memory</h2>
<h3>What It Is</h3>
<p>Working memory is temporary reasoning memory.</p>
<p>It exists only while solving a task.</p>
<p>The agent may store:</p>
<ul>
<li><p>intermediate reasoning,</p>
</li>
<li><p>calculations,</p>
</li>
<li><p>plans,</p>
</li>
<li><p>or execution steps.</p>
</li>
</ul>
<p>Once the task finishes, this memory may disappear.</p>
<h3>Example</h3>
<p>While generating SQL:</p>
<ol>
<li><p>Understand schema</p>
</li>
<li><p>Build query</p>
</li>
<li><p>Validate query</p>
</li>
<li><p>Execute safely</p>
</li>
</ol>
<p>The intermediate steps live in working memory.</p>
<hr />
<h2>6. Retrieval Memory (RAG Memory)</h2>
<h3>What It Is</h3>
<p>Instead of storing everything directly in prompts, the agent retrieves relevant information when needed.</p>
<p>This powers:</p>
<ul>
<li><p>RAG systems,</p>
</li>
<li><p>document agents,</p>
</li>
<li><p>and enterprise AI assistants.</p>
</li>
</ul>
<h3>Example</h3>
<p>User asks:</p>
<blockquote>
<p>“Summarize my uploaded PDF.”</p>
</blockquote>
<p>The agent:</p>
<ol>
<li><p>retrieves relevant chunks,</p>
</li>
<li><p>injects them into context,</p>
</li>
<li><p>then answers.</p>
</li>
</ol>
<p>This avoids context overload.</p>
<hr />
<h2>The Real Challenge: What Should Be Stored?</h2>
<p>This is where most AI agent systems fail.</p>
<p>Not everything deserves long-term memory.</p>
<p>Good memory systems decide:</p>
<ul>
<li><p>what to remember,</p>
</li>
<li><p>what to forget,</p>
</li>
<li><p>and what to summarize.</p>
</li>
</ul>
<h3>Good Things to Store</h3>
<ul>
<li><p>User preferences</p>
</li>
<li><p>Long-term goals</p>
</li>
<li><p>Writing style</p>
</li>
<li><p>Repeated workflows</p>
</li>
<li><p>Important project details</p>
</li>
</ul>
<h3>Bad Things to Store</h3>
<ul>
<li><p>Temporary emotions</p>
</li>
<li><p>One-time requests</p>
</li>
<li><p>Sensitive information</p>
</li>
<li><p>Random conversational noise</p>
</li>
</ul>
<hr />
<h2>A Practical AI Agent Memory Architecture</h2>
<p>A production-grade AI agent often looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/ebcaef62-63ef-4c1f-ac14-21db77664094.jpg" alt="" style="display:block;margin:0 auto" />

<p>This layered architecture is what makes modern AI assistants scalable.</p>
<hr />
<h2>How Modern AI Systems Likely Handle Memory</h2>
<p>Companies like OpenAI and Anthropic likely use:</p>
<ul>
<li><p>session memory,</p>
</li>
<li><p>persistent user memory,</p>
</li>
<li><p>retrieval systems,</p>
</li>
<li><p>summarization pipelines,</p>
</li>
<li><p>and memory ranking systems.</p>
</li>
</ul>
<p>The hard part is not storing memory.</p>
<p>The hard part is:</p>
<ul>
<li><p>deciding importance,</p>
</li>
<li><p>retrieval timing,</p>
</li>
<li><p>compression,</p>
</li>
<li><p>and relevance filtering.</p>
</li>
</ul>
<p>Memory engineering is becoming its own field.</p>
<hr />
<h2>Final Thoughts</h2>
<p>Most developers think AI agents are about:</p>
<ul>
<li><p>better prompts,</p>
</li>
<li><p>bigger models,</p>
</li>
<li><p>or more tools.</p>
</li>
</ul>
<p>But memory is what actually creates continuity, intelligence, and personalization.</p>
<p>The future of AI agents won’t belong to the models with the largest context windows.</p>
<p>It will belong to agents that:</p>
<ul>
<li><p>remember intelligently,</p>
</li>
<li><p>forget strategically,</p>
</li>
<li><p>and learn continuously.</p>
</li>
</ul>
<p>If you truly want to build production-grade AI agents…</p>
<p>start with memory architecture first.</p>
]]></content:encoded></item><item><title><![CDATA[The 5 Cron Jobs That Save Backend Servers From Disaster]]></title><description><![CDATA[Most backend systems don’t fail in dramatic ways. They fail quietly—when no one is watching. A missed backup, a full disk, or a stuck background job is often all it takes to turn a stable system into ]]></description><link>https://gauravbytes.dev/5-cron-jobs-that-save-backend-servers-from-disaster</link><guid isPermaLink="true">https://gauravbytes.dev/5-cron-jobs-that-save-backend-servers-from-disaster</guid><category><![CDATA[cronjob]]></category><category><![CDATA[Devops]]></category><category><![CDATA[System Design]]></category><category><![CDATA[automation]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Wed, 20 May 2026 04:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/0c6dfd2c-ebd4-43c2-8e6b-703c56274288.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most backend systems don’t fail in dramatic ways. They fail quietly—when no one is watching. A missed backup, a full disk, or a stuck background job is often all it takes to turn a stable system into a production incident.</p>
<p>That’s why cron jobs matter more than most developers realize. They are the invisible safety net keeping systems alive in production.</p>
<p>Below are 5 cron jobs every backend server should have.</p>
<h2>#1. Automated Backups</h2>
<p>Regularly backing up your data is non-negotiable. This is arguably one of the most critical uses of cron jobs in any backend system.</p>
<p>In production, things rarely break on purpose—they break due to small, unexpected mistakes. A deleted record, a faulty deployment, or even a storage corruption issue can wipe out critical data in seconds.</p>
<p>A backup cron job ensures that no matter what happens, there is always a recent version of your data that can be restored. It quietly runs in the background, taking snapshots of your database and storing them safely in external storage.</p>
<h2>#2. Log Cleanup &amp; Rotation</h2>
<p>Logs are extremely useful until they aren’t. Over time, they quietly grow into massive files that start consuming disk space without any warning.</p>
<p>A log cleanup cron job ensures that old logs are either deleted or compressed regularly. Without this, servers can suddenly run out of storage and crash unexpectedly.</p>
<p>This job is less about performance optimization and more about survival. It keeps your system stable by preventing silent resource exhaustion.</p>
<h2>#3. Server Health Checks</h2>
<p>Your server rarely tells you when it’s about to fail—it just does. That’s why continuous health checks are essential.</p>
<p>This cron job periodically checks CPU usage, memory consumption, disk space, and service availability. If anything crosses safe limits, it triggers alerts before users are affected.</p>
<p>It acts like a silent observer, constantly watching the system so you don’t have to manually inspect it every few hours.</p>
<h2>#4. Cache Cleanup</h2>
<p>Every backend system generates temporary files—uploads, cached responses, processing artifacts, and more. Most of these are never cleaned automatically.</p>
<p>Over time, these files accumulate and slowly degrade system performance or fill up storage completely.</p>
<p>A cleanup cron job ensures that anything temporary truly stays temporary. It removes stale files and keeps the system lean, fast, and predictable.</p>
<h2>#5. Retry Failed Jobs</h2>
<p>Not everything works on the first attempt. API calls fail, webhooks timeout, and background jobs occasionally break due to external dependencies.</p>
<p>Instead of losing that work permanently, a retry cron job reprocesses failed tasks at regular intervals.</p>
<p>This is what makes modern backend systems resilient. It ensures that temporary failures don’t turn into permanent data loss or broken workflows.</p>
<h2>Final Thoughts</h2>
<p>Cron jobs are often overlooked because they don’t feel “core” to the product. But in reality, they are what make production systems stable, reliable, and self-healing.</p>
<p>Most backend failures don’t come from code—they come from missing automation.</p>
<p>And these 5 cron jobs quietly prevent that from happening.</p>
]]></content:encoded></item><item><title><![CDATA[Build a  PostgreSQL AI Agent Using LangChain + Ollama ]]></title><description><![CDATA[🔥 Introduction
What if you could query your database like this:

"Show me top 10 users by revenue"

…and get instant results—without writing SQL?
Welcome to the world of AI-powered database agents.
I]]></description><link>https://gauravbytes.dev/build-a-postgresql-ai-agent-using-langchain-ollama</link><guid isPermaLink="true">https://gauravbytes.dev/build-a-postgresql-ai-agent-using-langchain-ollama</guid><category><![CDATA[AI]]></category><category><![CDATA[langchain]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Python]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[llm]]></category><category><![CDATA[ollama]]></category><category><![CDATA[developers]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 11 May 2026 04:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/bfdaf57d-f520-4f28-80a2-8691650735b5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>🔥 Introduction</h2>
<p>What if you could query your database like this:</p>
<blockquote>
<p><em>"Show me top 10 users by revenue"</em></p>
</blockquote>
<p>…and get instant results—without writing SQL?</p>
<p>Welcome to the world of <strong>AI-powered database agents</strong>.</p>
<p>In this tutorial, you'll learn how to build a <strong>secure PostgreSQL AI agent</strong> using:</p>
<ul>
<li><p>🧩 <strong>LangChain</strong> — for agent orchestration and tool chaining</p>
</li>
<li><p>🦙 <strong>Ollama</strong> — to run a local LLM with zero API cost</p>
</li>
<li><p>🐘 <strong>PostgreSQL</strong> — as the target database</p>
</li>
<li><p>🛡️ <strong>Custom SQL safety guard</strong> — to block destructive queries</p>
</li>
</ul>
<p>By the end, you'll have a production-ready AI database assistant that understands natural language and safely executes SQL queries.</p>
<blockquote>
<p>💻 <strong>Source Code:</strong> <a href="https://github.com/icon-gaurav/postgres-agent">https://github.com/icon-gaurav/postgres-agent</a></p>
</blockquote>
<hr />
<h2>🤖 What is a PostgreSQL AI Agent?</h2>
<p>A <strong>PostgreSQL AI Agent</strong> is an LLM-powered system that:</p>
<ul>
<li><p>Converts natural language → SQL queries</p>
</li>
<li><p>Executes queries on PostgreSQL</p>
</li>
<li><p>Returns structured results</p>
</li>
</ul>
<blockquote>
<p>👉 Think of it as ChatGPT for your database, but controlled and safe.</p>
</blockquote>
<hr />
<h2>⚙️ Tech Stack</h2>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://python.langchain.com">LangChain</a></td>
<td>AI agent orchestration, tool use</td>
</tr>
<tr>
<td><a href="https://ollama.com">Ollama</a></td>
<td>Local LLM inference, no API key required</td>
</tr>
<tr>
<td><a href="https://pypi.org/project/langchain-ollama/">langchain-ollama</a></td>
<td>LangChain ↔ Ollama integration</td>
</tr>
<tr>
<td><a href="https://pypi.org/project/psycopg2/">psycopg2</a></td>
<td>PostgreSQL database adapter for Python</td>
</tr>
<tr>
<td>Python</td>
<td>Core backend runtime</td>
</tr>
</tbody></table>
<hr />
<h2>🧱 Architecture</h2>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/e4f8ba23-4815-415e-8275-175b7f19216d.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>🔌 Step 1: PostgreSQL Connection</h2>
<p>First, configure your database connection using <code>psycopg2</code>:</p>
<pre><code class="language-python">import psycopg2
 
DB_CONFIG = {
    "host": "localhost",
    "port": 5432,
    "database": "postgres",
    "user": "postgres",
    "password": "root",
}
 
def get_connection():
    return psycopg2.connect(**DB_CONFIG)
</code></pre>
<blockquote>
<p>🔐 <strong>Security tip:</strong> In production, load credentials from environment variables or a secrets manager — never hardcode passwords.</p>
</blockquote>
<hr />
<h2>🛠️ Step 2: Create Database Tools</h2>
<p>LangChain agents interact with the world through <strong>tools</strong> — Python functions decorated with <code>@tool</code> that the LLM can invoke by name.</p>
<h3>📋 List Tables Tool</h3>
<pre><code class="language-python">@tool
def list_tables() -&gt; str:
    """List all tables in the database."""
    conn = get_connection()
    try:
        cur = conn.cursor()
        cur.execute("""
            SELECT table_name FROM information_schema.tables
            WHERE table_schema = 'public'
        """)
        tables = [row[0] for row in cur.fetchall()]
        return f"Tables: {', '.join(tables)}" if tables else "No tables found."
    finally:
        conn.close()
</code></pre>
<p>This enables <strong>dynamic schema discovery</strong> — the agent doesn't need hardcoded table names.</p>
<h3>📑 Get Table Schema</h3>
<pre><code class="language-python">@tool
@tool
def get_table_schema(table_name: str) -&gt; str:
    """Get the schema (columns and types) of a specific table."""
    conn = get_connection()
    try:
        cur = conn.cursor()
        cur.execute("""
            SELECT column_name, data_type, is_nullable
            FROM information_schema.columns
            WHERE table_schema = 'public' AND table_name = %s
            ORDER BY ordinal_position
        """, (table_name,))
        columns = cur.fetchall()
        if not columns:
            return f"Table '{table_name}' not found."
        schema = "\n".join([f"  {col[0]} ({col[1]}, nullable={col[2]})" for col in columns])
        return f"Schema for '{table_name}':\n{schema}"
    finally:
        conn.close()
</code></pre>
<p>Allows the agent to understand:</p>
<ul>
<li><p>Column names</p>
</li>
<li><p>Data types</p>
</li>
<li><p>Constraints</p>
</li>
</ul>
<h3>⚡ Execute SQL Tool</h3>
<pre><code class="language-python">@tool
def execute_sql(query: str) -&gt; str:
    """Execute a SQL query against the PostgreSQL database and return results. Use this for SELECT queries."""
    is_safe, reason = validate_read_only_sql(query)
    if not is_safe:
        return f"Safety Guard: Blocked query. {reason}"

    conn = get_connection()
    try:
        cur = conn.cursor()
        cur.execute(query)
        if cur.description:
            columns = [desc[0] for desc in cur.description]
            rows = cur.fetchall()
            if not rows:
                return "Query returned no results."
            result = " | ".join(columns) + "\n"
            result += "\n".join([" | ".join(str(v) for v in row) for row in rows[:50]])
            if len(rows) &gt; 50:
                result += f"\n... ({len(rows)} total rows)"
            return result
        else:
            conn.commit()
            return f"Query executed successfully. Rows affected: {cur.rowcount}"
    except Exception as e:
        conn.rollback()
        return f"SQL Error: {e}"
    finally:
        conn.close()
</code></pre>
<p>This is the <strong>core execution layer</strong>.</p>
<hr />
<h2>Step 3: SQL Safety Guard — Prevent Destructive Queries</h2>
<p>Allowing an LLM to run arbitrary SQL is a critical security risk. The <strong>SQL safety guard</strong> validates every query before execution.</p>
<h3>Read-Only Allowlist</h3>
<p>Only these SQL statement types are permitted:</p>
<table>
<thead>
<tr>
<th>Allowed</th>
<th>Blocked</th>
</tr>
</thead>
<tbody><tr>
<td><code>SELECT</code></td>
<td><code>INSERT</code></td>
</tr>
<tr>
<td><code>WITH</code> (CTEs)</td>
<td><code>UPDATE</code></td>
</tr>
<tr>
<td><code>SHOW</code></td>
<td><code>DELETE</code></td>
</tr>
<tr>
<td><code>EXPLAIN</code></td>
<td><code>DROP</code></td>
</tr>
<tr>
<td>—</td>
<td><code>ALTER</code></td>
</tr>
<tr>
<td>—</td>
<td><code>TRUNCATE</code></td>
</tr>
</tbody></table>
<h3>🧼 Normalize Queries</h3>
<p>We remove:</p>
<ul>
<li><p>Comments</p>
</li>
<li><p>Strings</p>
</li>
<li><p>Hidden injections</p>
</li>
</ul>
<blockquote>
<p>👉 This ensures <strong>safe AI execution</strong> in production environments.</p>
</blockquote>
<hr />
<h2>🧠 Step 4: Setup Ollama (Local LLM)</h2>
<p><a href="https://ollama.com">Ollama</a> lets you download and run large language models entirely on your own machine — no cloud account, no API key, no usage fees.</p>
<blockquote>
<p>📚 <strong>Official Resources:</strong></p>
<ul>
<li><p>🌐 Website: <a href="https://ollama.com">ollama.com</a></p>
</li>
<li><p>📖 Documentation: <a href="https://docs.ollama.com">docs.ollama.com</a></p>
</li>
<li><p>🗂️ Model Library: <a href="https://ollama.com/library">ollama.com/library</a></p>
</li>
<li><p>🐙 GitHub: <a href="https://github.com/ollama/ollama">github.com/ollama/ollama</a></p>
</li>
</ul>
</blockquote>
<hr />
<h3>🔽 Pulling a Model</h3>
<p>Once Ollama is running, pull the model used in this project:</p>
<pre><code class="language-bash">ollama pull qwen2.5:7b
</code></pre>
<p>You can verify it's available with:</p>
<pre><code class="language-bash">ollama list
</code></pre>
<p>Browse all available models at <a href="https://ollama.com/library">ollama.com/library</a>. Some good alternatives for SQL agents:</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Command</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td>Qwen 2.5 7B</td>
<td><code>ollama pull qwen2.5:7b</code></td>
<td>Used in this tutorial</td>
</tr>
<tr>
<td>Llama 3.1 8B</td>
<td><code>ollama pull llama3.1</code></td>
<td>Strong general-purpose model</td>
</tr>
<tr>
<td>DeepSeek-R1 7B</td>
<td><code>ollama pull deepseek-r1</code></td>
<td>Good reasoning ability</td>
</tr>
<tr>
<td>Mistral 7B</td>
<td><code>ollama pull mistral</code></td>
<td>Fast and lightweight</td>
</tr>
</tbody></table>
<hr />
<h3>📦 Installing the LangChain Ollama Package</h3>
<p>The LangChain integration for Ollama lives in the dedicated <a href="https://pypi.org/project/langchain-ollama/"><code>langchain-ollama</code></a> package:</p>
<pre><code class="language-bash">pip install langchain-ollama
</code></pre>
<blockquote>
<p>📖 <strong>Package References:</strong></p>
<ul>
<li><p>📦 PyPI: <a href="https://pypi.org/project/langchain-ollama/">pypi.org/project/langchain-ollama</a></p>
</li>
<li><p>🔗 LangChain Docs: <a href="https://docs.langchain.com/oss/python/integrations/chat/ollama">docs.langchain.com — ChatOllama</a></p>
</li>
<li><p>📐 API Reference: <a href="https://reference.langchain.com/python/langchain-ollama">reference.langchain.com/python/langchain-ollama</a></p>
</li>
</ul>
</blockquote>
<h3>⚙️ Configuring ChatOllama</h3>
<p>Now initialize the LLM in your Python code:</p>
<pre><code class="language-python">from langchain_ollama import ChatOllama
 
llm = ChatOllama(model="qwen2.5:7b", temperature=0)
</code></pre>
<p>Setting <code>temperature=0</code> makes the model deterministic — essential for reliable SQL generation. You can tune other key parameters as needed:</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>model</code></td>
<td>required</td>
<td>Model name from <code>ollama list</code></td>
</tr>
<tr>
<td><code>temperature</code></td>
<td><code>0.8</code></td>
<td>Creativity — use <code>0</code> for SQL tasks</td>
</tr>
<tr>
<td><code>num_predict</code></td>
<td><code>128</code></td>
<td>Max tokens to generate</td>
</tr>
<tr>
<td><code>base_url</code></td>
<td><code>http://localhost:11434</code></td>
<td>Ollama server URL</td>
</tr>
</tbody></table>
<p><strong>Why Ollama?</strong></p>
<ul>
<li><p>✅ No API cost</p>
</li>
<li><p>✅ Runs fully locally — data never leaves your machine</p>
</li>
<li><p>✅ Privacy-friendly — ideal for sensitive database workloads</p>
</li>
<li><p>✅ Fast inference with GPU support</p>
</li>
<li><p>✅ Supports dozens of open-source models</p>
</li>
</ul>
<hr />
<h2>🔗 Step 5: Create LangChain Agent</h2>
<pre><code class="language-python">tools = [list_tables, get_table_schema, execute_sql]
agent = create_agent(llm, tools)
</code></pre>
<p>LangChain allows the AI to:</p>
<ul>
<li><p>Decide which tool to use</p>
</li>
<li><p>Chain multiple steps</p>
</li>
<li><p>Reason dynamically</p>
</li>
</ul>
<hr />
<h2>💬 Step 6: Interactive Chat Loop</h2>
<pre><code class="language-python">while True:
    user_input = input("\nYou: ").strip()
    if user_input.lower() in ("exit", "quit"):
        print("Goodbye!")
        break
    if not user_input:
        continue
</code></pre>
<p>This makes your agent:</p>
<ul>
<li><p>Conversational</p>
</li>
<li><p>Stateful</p>
</li>
<li><p>Easy to debug</p>
</li>
</ul>
<hr />
<h2>🧾Step 7: Debugging &amp; Observability</h2>
<p>Visibility into what the agent is doing is essential for development. This helper function prints each tool call and its result:</p>
<pre><code class="language-python">def print_turn_details(messages: list[BaseMessage]) -&gt; None:
    final_response = ""

    for message in messages:
        if isinstance(message, AIMessage):
            for tool_call in message.tool_calls:
                tool_name = tool_call.get("name", "unknown_tool")
                tool_args = format_tool_payload(tool_call.get("args", {}))
                print(f"\nTool call: {tool_name}({tool_args})")

            content = format_content(message.content).strip()
            if content:
                final_response = content

        elif isinstance(message, ToolMessage):
            tool_name = getattr(message, "name", None) or "tool"
            tool_output = format_content(message.content).strip() or "(no output)"
            print(f"\nTool response [{tool_name}]: {tool_output}")

    if final_response:
        print(f"\nAgent: {final_response}")
    else:
        print("\nAgent: I couldn't generate a response.")
</code></pre>
<p>Shows:</p>
<ul>
<li><p>Tool calls</p>
</li>
<li><p>Tool outputs</p>
</li>
<li><p>Final response</p>
</li>
</ul>
<blockquote>
<p>👉 This is extremely useful for <strong>debugging agent behavior</strong>.</p>
</blockquote>
<hr />
<h2>🧪 Example Queries</h2>
<p>Try asking:</p>
<ul>
<li><p><code>"List all tables"</code></p>
</li>
<li><p><code>"Show schema of users table"</code></p>
</li>
<li><p><code>"Get top 5 users by revenue"</code></p>
</li>
<li><p><code>"How many orders were placed last month?"</code></p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/0865e8a5-269f-4bce-a094-e963a49d5aed.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Real-World Use Cases for a Natural Language Database Agent</h2>
<p>This architecture can power a wide range of applications:</p>
<p><strong>📊 AI-Powered Analytics Dashboards</strong> — let non-technical stakeholders query live data in plain English without learning SQL.</p>
<p><strong>💬 Internal Data Chatbots</strong> — embed in Slack or Microsoft Teams so product and ops teams can self-serve data questions.</p>
<p><strong>🧾 Automated Reporting</strong> — schedule the agent to answer recurring questions and generate daily or weekly reports.</p>
<p><strong>🏢 SaaS Admin Panels</strong> — give your ops team a natural language interface to your product database.</p>
<p><strong>🤖 AI Copilots for Data Teams</strong> — speed up analyst workflows by auto-generating SQL drafts from plain-English specs.</p>
<hr />
<h2>🎯 Conclusion</h2>
<p>You've built more than just a demo. This is a <strong>secure, extensible AI database agent</strong> that can be used in real-world applications.</p>
<p><strong>Key Takeaways:</strong></p>
<ul>
<li><p>LangChain simplifies agent workflows</p>
</li>
<li><p>Ollama enables local LLM execution</p>
</li>
<li><p>SQL safety is critical</p>
</li>
<li><p>Tool-based architecture = powerful AI agents</p>
</li>
</ul>
<p>The core insight: wrapping your database in typed, well-described LangChain tools gives the LLM exactly the context it needs to generate correct SQL — without ever exposing raw database access.</p>
<blockquote>
<p><a href="https://github.com/icon-gaurav/postgres-agent">📦 Full Source Code</a> — complete working implementation from this tutorial</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[The Day I Stopped Building Alone: OpenClaw as My Virtual Team]]></title><description><![CDATA[A candid look at using AI agents to build a SaaS product — what works, what doesn't, and why I'm never going back to building solo.

The Problem With Building Alone
Before I talk about OpenClaw, let m]]></description><link>https://gauravbytes.dev/the-day-i-stopped-building-alone-openclaw-as-my-virtual-team</link><guid isPermaLink="true">https://gauravbytes.dev/the-day-i-stopped-building-alone-openclaw-as-my-virtual-team</guid><category><![CDATA[openclaw]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[Orchestration]]></category><category><![CDATA[solopreneur ]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[SaaS]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 09 Mar 2026 03:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/f151d1ab-a51b-4c1a-8041-87113353e973.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>A candid look at using AI agents to build a SaaS product — what works, what doesn't, and why I'm never going back to building solo.</em></p>
<hr />
<h2><strong>The Problem With Building Alone</strong></h2>
<p>Before I talk about <a href="https://openclaw.ai/">OpenClaw</a>, let me paint the picture.</p>
<p>When you're building a SaaS product by yourself, you're not just a developer. You're the researcher, the architect, the QA engineer, the SEO specialist, and — if you're lucky — the person who actually writes code. Every decision defaults to you. Every skill gap becomes a blocker.</p>
<p>Want to add authentication? Better research OAuth providers, understand security best practices, and implement it right. Need to figure out email deliverability? Time to go down a 3-hour rabbit hole. Building alone means you're constantly context-switching between "figuring things out" and "actually building."</p>
<p>I know because I've been there. Building <a href="https://www.shiftmailer.com/">ShiftMailer</a> — my product for AI-powered email marketing — meant I had to wear all these hats. And honestly? It was exhausting.</p>
<h2><strong>Then Came OpenClaw</strong></h2>
<p>OpenClaw isn't just another AI chatbot. It's an AI agent framework that can actually <em>do</em> things — read files, run commands, search the web, analyze code, and coordinate with other tools. It's like having a team member who doesn't sleep, doesn't complain, and can spin up new skills on demand.</p>
<p>Here's what I learned after using it for ShiftMailer development.</p>
<img src="https://cdn.hashnode.com/uploads/covers/616e87a8f1f4c944cc6b49a1/cb6dbe8b-efc4-4904-a2d8-c1757b40925c.png" alt="" style="display:block;margin:0 auto" />

<h2><strong>What Actually Works</strong></h2>
<h3><strong>1. Research That Goes Beyond Google</strong></h3>
<p>I used to spend hours researching trends, comparing tools, and validating ideas. Now I can ask OpenClaw to:</p>
<ul>
<li><p>Find current trends in email marketing automation</p>
</li>
<li><p>Compare pricing models of competitors</p>
</li>
<li><p>Research technical decisions (like multi-tenancy approaches)</p>
</li>
</ul>
<p>It doesn't just give me links — it synthesizes information and gives me actionable insights. This alone saved me days of scattered research.</p>
<h3><strong>2. Code That I Still Own</strong></h3>
<p>Here's something important: I <em>know</em> how to code. I'm a backend engineer. But that doesn't mean I want to write every boilerplate or debug every edge case alone.</p>
<p>OpenClaw helps me:</p>
<ul>
<li><p><strong>Write faster</strong> — It handles the repetitive stuff so I focus on the interesting parts</p>
</li>
<li><p><strong>Review my code</strong> — Fresh eyes catch bugs I missed</p>
</li>
<li><p><strong>Explore new patterns</strong> — I can ask "how would you approach this with Node.js streams?" and get working examples</p>
</li>
</ul>
<p>It's not replacing my skills. It's amplifying them.</p>
<h3><strong>3. Real-World Analysis</strong></h3>
<p>One surprise: OpenClaw analyzed my website's SEO and gave me specific suggestions. Not generic advice — actual, implementable recommendations based on my content. That's not something I expected an AI agent to do well, but it handled it.</p>
<h2><strong>The Honest Take: What Doesn't Work</strong></h2>
<p>I promised you an honest review, so here it is:</p>
<h3><strong>Orchestration is Hard</strong></h3>
<p>OpenClaw is great at analyzing requirements and using individual tools well. But when it comes to coordinating complex multi-step workflows? Sometimes it struggles. Things that would be trivial for a human — "okay, first do A, then B, but only if C worked" — can get messy.</p>
<p>This means I'm still the conductor. The agent executes well, but I'm the one keeping the orchestra in sync.</p>
<h3><strong>Security: You Gotta Be Careful</strong></h3>
<p>Here's the thing: OpenClaw has access to my files, my environment, my code. That's powerful, but it's also a responsibility.</p>
<p>For now, I'm careful about:</p>
<ul>
<li><p>Not giving agents unrestricted external access</p>
</li>
<li><p>Reviewing code before shipping</p>
</li>
<li><p>Keeping sensitive configs isolated</p>
</li>
</ul>
<p>This isn't a criticism of OpenClaw — it's just smart practice. When you're delegating to an AI, trust but verify.</p>
<h2><strong>The Comparison: Traditional Cofounder vs. AI Agent</strong></h2>
<p>People often ask: "Isn't this like having a cofounder?"</p>
<p>Not really. Here's the difference:</p>
<table>
<thead>
<tr>
<th><strong>Traditional Cofounder</strong></th>
<th><strong>AI Agent (OpenClaw)</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Has fixed skills</td>
<td>Can spin up new skills on demand</td>
</tr>
<tr>
<td>Needs alignment, meetings, context</td>
<td>Instant context, no hand-holding</td>
</tr>
<tr>
<td>Sleeps, has bad days, costs equity</td>
<td>Always available, improves over time</td>
</tr>
<tr>
<td>Human judgment on tough calls</td>
<td>Follows instructions, but needs oversight</td>
</tr>
</tbody></table>
<p>With a traditional cofounder, I'd need to research every aspect myself, find different people for different skills, and coordinate schedules. With OpenClaw, I can say "I need research on X" and get it done — then switch to "help me debug this API" without friction.</p>
<h2><strong>What This Means for Solo Builders</strong></h2>
<p>If you're building alone, here's what I'd tell you:</p>
<p><strong>AI agents aren't magic.</strong> They're tools. And like any tool, they have limits. But if you learn to work with them — to prompt well, to review outputs, to stay in the loop — you can do way more than you could alone.</p>
<p><a href="https://www.shiftmailer.com/">ShiftMailer</a> exists today because I stopped trying to do everything myself. I found a way to leverage AI that works <em>with</em> my skills, not instead of them.</p>
<h2><strong>What's Next</strong></h2>
<p>I'm still figuring out the orchestration piece. I'm still being careful about security. But the gap between "idea" and "working product" has shrunk dramatically.</p>
<p>If you're a solo builder on the fence about AI agents — try it. Start small. See what works for your workflow. You might be surprised what you can ship when you're not alone in the room.</p>
<hr />
<p><em>Have questions about building with AI agents or want to share your story? Let's connect — find me on</em> <a href="https://x.com/gauravk_tweet">X</a> <em>or check out</em> <a href="https://gauravbytes.hashnode.dev"><em>gauravbytes.hashnode.dev</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Building Sequential AI Agents with Vercel AI SDK (Multi-Step LLM Workflows)]]></title><description><![CDATA[Most AI agents today are just a single prompt and a single response.
That approach works—until you need structure, reliability, or production-grade workflows.
In this post, we’ll explore sequential AI agents, how they differ from normal AI agents, an...]]></description><link>https://gauravbytes.dev/building-sequential-ai-agents-with-vercel-ai-sdk-multi-step-llm-workflows</link><guid isPermaLink="true">https://gauravbytes.dev/building-sequential-ai-agents-with-vercel-ai-sdk-multi-step-llm-workflows</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[Vercel]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 19 Jan 2026 09:16:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768766741571/0aca3271-3cef-49b8-8884-8831f0f36fdf.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most AI agents today are just a single prompt and a single response.</p>
<p>That approach works—until you need structure, reliability, or production-grade workflows.</p>
<p>In this post, we’ll explore <strong>sequential AI agents</strong>, how they differ from normal AI agents, and how you can build a multi-step AI workflow using the <strong>Vercel AI SDK</strong>.</p>
<h2 id="heading-what-is-an-ai-agent">What Is an AI Agent?</h2>
<p>An AI agent is a system that:</p>
<ol>
<li><p>Takes an input (user query, event, or data)</p>
</li>
<li><p>Uses an LLM to reason or generate output</p>
</li>
<li><p>Optionally calls tools, APIs, or functions</p>
</li>
<li><p>Returns a result or performs an action</p>
</li>
</ol>
<p>In many applications, this entire process happens <strong>in one step</strong>.</p>
<p>Example:</p>
<blockquote>
<p>User asks: <em>“Write a product update email”</em><br />→ LLM generates the email in a single response</p>
</blockquote>
<p>This works well for simple tasks—but it starts breaking down as complexity grows.</p>
<h2 id="heading-normal-ai-agent-single-step-agent">Normal AI Agent (Single-Step Agent)</h2>
<p>A <strong>normal AI agent</strong> typically follows this flow:</p>
<pre><code class="lang-typescript">Input → LLM → Output
</code></pre>
<h3 id="heading-characteristics">Characteristics</h3>
<ul>
<li><p>Single prompt</p>
</li>
<li><p>Single LLM call</p>
</li>
<li><p>Minimal or no intermediate state</p>
</li>
<li><p>Fast and cheap</p>
</li>
</ul>
<h3 id="heading-example-use-cases">Example Use Cases</h3>
<ul>
<li><p>Chatbots</p>
</li>
<li><p>Text rewriting</p>
</li>
<li><p>Summarization</p>
</li>
<li><p>Simple content generation</p>
</li>
</ul>
<h3 id="heading-limitations">Limitations</h3>
<ul>
<li><p>Hard to enforce structure</p>
</li>
<li><p>No explicit reasoning steps</p>
</li>
<li><p>Poor control over multi-stage workflows</p>
</li>
<li><p>Difficult to debug or extend</p>
</li>
</ul>
<p>When tasks require <strong>planning, validation, transformation, or multiple roles</strong>, a single-step agent becomes fragile.</p>
<h2 id="heading-what-is-a-sequential-ai-agent">What Is a Sequential AI Agent?</h2>
<p>A <strong>sequential AI agent</strong> breaks a task into <strong>multiple ordered steps</strong>, where:</p>
<ul>
<li><p>Each step has a clear responsibility</p>
</li>
<li><p>Output of one step becomes input for the next</p>
</li>
<li><p>Context accumulates across steps</p>
</li>
</ul>
<pre><code class="lang-typescript">Input
  ↓
Step <span class="hljs-number">1</span> (Planner Agent)
  ↓
Step <span class="hljs-number">2</span> (Executor Agent)
  ↓
Step <span class="hljs-number">3</span> (Refiner / Validator Agent)
  ↓
Final Output
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768765239209/5c84630e-268e-4da6-b924-0c1da9df9743.png" alt="Source : https://www.cybage.com/blog/building-intelligent-ai-systems-understanding-agentic-ai-and-design-patterns" class="image--center mx-auto" /></p>
<p>Instead of asking the model to do everything at once, we <strong>guide it through a pipeline</strong>.</p>
<h2 id="heading-normal-agent-vs-sequential-agent">Normal Agent vs Sequential Agent</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Aspect</td><td>Normal Agent</td><td>Sequential Agent</td></tr>
</thead>
<tbody>
<tr>
<td>LLM Calls</td><td>One</td><td>Multiple</td></tr>
<tr>
<td>Structure</td><td>Implicit</td><td>Explicit</td></tr>
<tr>
<td>Control</td><td>Low</td><td>High</td></tr>
<tr>
<td>Debuggability</td><td>Hard</td><td>Easy</td></tr>
<tr>
<td>Cost</td><td>Lower</td><td>Higher</td></tr>
<tr>
<td>Scalability</td><td>Limited</td><td>High</td></tr>
</tbody>
</table>
</div><p>Sequential agents trade <strong>simplicity</strong> for <strong>control and reliability</strong>.</p>
<h2 id="heading-when-are-sequential-ai-agents-beneficial">When Are Sequential AI Agents Beneficial?</h2>
<p>Sequential agents are ideal when:</p>
<h3 id="heading-1-tasks-have-clear-phases">1. Tasks Have Clear Phases</h3>
<p>Example:</p>
<ul>
<li><p>Planning</p>
</li>
<li><p>Writing</p>
</li>
<li><p>Reviewing</p>
</li>
<li><p>Formatting</p>
</li>
</ul>
<h3 id="heading-2-output-must-follow-strict-structure">2. Output Must Follow Strict Structure</h3>
<ul>
<li><p>Emails</p>
</li>
<li><p>Reports</p>
</li>
<li><p>JSON schemas</p>
</li>
<li><p>Code generation</p>
</li>
</ul>
<h3 id="heading-3-different-roles-are-needed">3. Different “Roles” Are Needed</h3>
<ul>
<li><p>Product marketer</p>
</li>
<li><p>Engineer</p>
</li>
<li><p>Editor</p>
</li>
</ul>
<h3 id="heading-4-you-want-deterministic-pipelines">4. You Want Deterministic Pipelines</h3>
<ul>
<li><p>SaaS features</p>
</li>
<li><p>Automations</p>
</li>
<li><p>Multi-tenant systems</p>
</li>
</ul>
<p>This is why sequential agents work extremely well for:</p>
<ul>
<li><p>Product update emails</p>
</li>
<li><p>CRM workflows</p>
</li>
<li><p>Content pipelines</p>
</li>
<li><p>Data extraction and transformation</p>
</li>
</ul>
<h2 id="heading-designing-a-sequential-agent-conceptually">Designing a Sequential Agent (Conceptually)</h2>
<p>Let’s say we want to build a <strong>content generation agent</strong>.</p>
<h3 id="heading-step-1-planner-agent">Step 1: Planner Agent</h3>
<p>Responsibility:</p>
<ul>
<li><p>Analyze the input</p>
</li>
<li><p>Break it into structured sections</p>
</li>
</ul>
<p>Output:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"sections"</span>: [<span class="hljs-string">"Introduction"</span>, <span class="hljs-string">"Key Points"</span>, <span class="hljs-string">"Conclusion"</span>]
}
</code></pre>
<h3 id="heading-step-2-writer-agent">Step 2: Writer Agent</h3>
<p>Responsibility:</p>
<ul>
<li>Generate content for each section</li>
</ul>
<p>Input:</p>
<ul>
<li><p>Original user input</p>
</li>
<li><p>Planner output</p>
</li>
</ul>
<h3 id="heading-step-3-refiner-agent">Step 3: Refiner Agent</h3>
<p>Responsibility:</p>
<ul>
<li><p>Improve tone</p>
</li>
<li><p>Fix grammar</p>
</li>
<li><p>Enforce constraints</p>
</li>
</ul>
<p>Each step is <strong>predictable and replaceable</strong>.</p>
<h2 id="heading-implementing-a-sequential-ai-agent-with-vercel-ai-sdk">Implementing a Sequential AI Agent with Vercel AI SDK</h2>
<h3 id="heading-step-1-create-the-planner-agent">Step 1: Create the Planner Agent</h3>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { generateText } <span class="hljs-keyword">from</span> <span class="hljs-string">"ai"</span>;
<span class="hljs-keyword">import</span> { openai } <span class="hljs-keyword">from</span> <span class="hljs-string">"@ai-sdk/openai"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">plannerAgent</span>(<span class="hljs-params">input: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> generateText({
    model: openai(<span class="hljs-string">"gpt-4.1"</span>),
    prompt: <span class="hljs-string">`Analyze the input and create a structured plan.\n\nInput: <span class="hljs-subst">${input}</span>`</span>,
  });

  <span class="hljs-keyword">return</span> result.text;
}
</code></pre>
<h3 id="heading-step-2-create-the-writer-agent">Step 2: Create the Writer Agent</h3>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">writerAgent</span>(<span class="hljs-params">plan: <span class="hljs-built_in">string</span>, input: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> generateText({
    model: openai(<span class="hljs-string">"gpt-4.1"</span>),
    prompt: <span class="hljs-string">`Using the following plan, write detailed content.\n\nPlan:\n<span class="hljs-subst">${plan}</span>\n\nInput:\n<span class="hljs-subst">${input}</span>`</span>,
  });

  <span class="hljs-keyword">return</span> result.text;
}
</code></pre>
<h3 id="heading-step-3-create-the-refiner-agent">Step 3: Create the Refiner Agent</h3>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">refinerAgent</span>(<span class="hljs-params">content: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> generateText({
    model: openai(<span class="hljs-string">"gpt-4.1"</span>),
    prompt: <span class="hljs-string">`Refine the following content for clarity and tone.\n\n<span class="hljs-subst">${content}</span>`</span>,
  });

  <span class="hljs-keyword">return</span> result.text;
}
</code></pre>
<h3 id="heading-step-4-orchestrate-the-sequential-flow">Step 4: Orchestrate the Sequential Flow</h3>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sequentialAgent</span>(<span class="hljs-params">input: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">const</span> plan = <span class="hljs-keyword">await</span> plannerAgent(input);
  <span class="hljs-keyword">const</span> draft = <span class="hljs-keyword">await</span> writerAgent(plan, input);
  <span class="hljs-keyword">const</span> finalOutput = <span class="hljs-keyword">await</span> refinerAgent(draft);

  <span class="hljs-keyword">return</span> finalOutput;
}
</code></pre>
<p>This orchestration is the <strong>heart of a sequential agent</strong>.</p>
<h2 id="heading-benefits-of-this-approach">Benefits of This Approach</h2>
<ul>
<li><p>Clear separation of responsibilities</p>
</li>
<li><p>Easier debugging (inspect each step)</p>
</li>
<li><p>Reusable agents</p>
</li>
<li><p>Better output consistency</p>
</li>
<li><p>Safer production usage</p>
</li>
</ul>
<p>This is especially useful when building <strong>AI-powered SaaS features</strong>, not demos.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Sequential AI agents represent a shift from <em>“ask the model to do everything”</em> to <em>“designing AI workflows.”</em></p>
<p>With the <a target="_blank" href="https://vercel.com/docs/ai-sdk">Vercel AI SDK</a>, building these workflows feels natural and maintainable.</p>
<p>If you’re building:</p>
<ul>
<li><p>AI-first products</p>
</li>
<li><p>Content pipelines</p>
</li>
<li><p>Internal tooling</p>
</li>
</ul>
<p>…sequential agents will give you <strong>control, clarity, and confidence</strong>.</p>
<p>If you’re interested, the next step could be:</p>
<ul>
<li><p>Adding validation agents</p>
</li>
<li><p>Parallel agents</p>
</li>
<li><p>Streaming intermediate steps</p>
</li>
<li><p>Persisting agent state</p>
</li>
</ul>
<p>That’s where AI engineering starts to feel like real software engineering 🚀</p>
]]></content:encoded></item><item><title><![CDATA[🛍️ Building an AI-Powered E-commerce Chatbot Using Vercel AI SDK and Gemini]]></title><description><![CDATA[E-commerce is rapidly transforming — and AI-powered shopping assistants are becoming the new default buying experience.Think about it:

Instead of browsing 20 product pages, users simply ask:“Show me ]]></description><link>https://gauravbytes.dev/building-an-ai-powered-e-commerce-chatbot-using-vercel-ai-sdk-and-gemini</link><guid isPermaLink="true">https://gauravbytes.dev/building-an-ai-powered-e-commerce-chatbot-using-vercel-ai-sdk-and-gemini</guid><category><![CDATA[AI]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[gemini]]></category><category><![CDATA[llm]]></category><category><![CDATA[ecommerce]]></category><category><![CDATA[chatbot]]></category><category><![CDATA[Vercel]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Fri, 28 Nov 2025 18:18:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764186329330/a6c58ccb-8946-4e3d-a530-e28c43084d3c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>E-commerce is rapidly transforming — and AI-powered shopping assistants are becoming the new default buying experience.<br />Think about it:</p>
<ul>
<li><p>Instead of browsing <strong>20 product pages</strong>, users simply ask:<br /><em>“Show me red shoes under ₹1500.”</em></p>
</li>
<li><p>Instead of navigating a 5-step checkout, they say:<br /><em>“Add the second one to my cart and checkout.”</em></p>
</li>
</ul>
<p>To explore this future, I built a <strong>fully functional AI E-Commerce Chatbot</strong> using <strong>Vercel AI SDK + Gemini</strong>, equipped with tools like:</p>
<ul>
<li><p><strong>Fetch Catalog</strong></p>
</li>
<li><p><strong>Add to Cart</strong></p>
</li>
<li><p><strong>Checkout Cart</strong></p>
</li>
</ul>
<p>And a <strong>UI that responds with card-style product previews</strong>, add-to-cart confirmation messages, and interactive checkout prompts.</p>
<p>This blog is a <strong>complete to-do guide</strong> to help developers build the same chatbot from scratch.</p>
<p>Let’s start building it</p>
<h2>🧩 Define Tools for Gemini</h2>
<p>Gemini supports <strong>function calling</strong>, so we define our tools. We will create functions that accept some parameters and return a result based on them. Here, we are using an API call to achieve this, but you can implement any business logic in these functions.</p>
<p><strong>Catalog tool</strong></p>
<pre><code class="language-typescript">export async function fetchCatalog({query, maxPrice}: { query: string; maxPrice: number }) {
    const res = await fetch(
        `${process.env.NEXT_PUBLIC_BASE_URL}/api/catalog?query=${query}&amp;maxPrice=${maxPrice}`
    );
    if (!res.ok) throw new Error("Failed to fetch catalog");
    return res.json();
}
</code></pre>
<p><strong>Cart tool</strong></p>
<pre><code class="language-typescript">export async function addToCart(productId: string, quantity: number) {
    const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/cart`, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({product_id: productId, qty: quantity}),
    });
    if (!res.ok) throw new Error("Failed to add to cart");
    return res.json();
}
</code></pre>
<p><strong>Checkout Tool</strong></p>
<pre><code class="language-typescript">export async function checkoutCart() {
    const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/checkout`, {
        method: "POST",
    });
    if (!res.ok) throw new Error("Failed to checkout");
    return res.json();
}
</code></pre>
<h1><strong>🛠️ Create the Tool-Enabled Chat API</strong></h1>
<p>This is the heart of the chatbot.</p>
<h3>Define the prompt for our AI agent</h3>
<pre><code class="language-typescript">const SYSTEM_PROMPT = `
You are a shopping assistant AI integrated with tools.

You can help the user browse products, add them to a cart, and checkout.

### Available tools:
1. **fetchCatalog()**
   - Retrieves all products from the catalog.
   - Always call this tool to see what products are available before suggesting items.

2. **addToCart(productId: string)**
   - Adds the specified product to the user's cart.
   - Call this when the user asks to "add", "buy", or "I want this".

3. **checkoutCart()**
   - Processes the order and simulates checkout.
   - Only call this if the user explicitly says "checkout", "place order", or "buy now".

---

### Rules of behavior:
- Never invent products. Only use the results returned by 'fetchCatalog'.
- Always confirm product details (name, price, stock) when suggesting items.
- If the user is vague (e.g., "show me something cool"), fetch the catalog and then suggest a few options.
- Be conversational and friendly, but clearly indicate actions you’re taking.
- After a successful checkout, thank the user and end the flow.

---

### Examples:

**User:** "Show me headphones under $150"  
**Assistant reasoning:** Call 'fetchCatalog', filter results by category + price, then present matching products.  

**User:** "Yes, add the headphones to my cart"  
**Assistant reasoning:** Call 'addToCart(productId)' for that item then show the current cart.  

**User:** "Checkout now"  
**Assistant reasoning:** Call 'checkoutCart' and return the order confirmation.
`
</code></pre>
<h3><strong>API endpoint to use our chatbot</strong></h3>
<pre><code class="language-typescript">import {convertToModelMessages, streamText, UIMessage} from "ai";
import {addToCart, checkoutCart, fetchCatalog} from "@/lib/tools";
import {google} from "@ai-sdk/google";
import {NextRequest, NextResponse} from "next/server";
import {z} from 'zod';

export const runtime = "edge";
const gemini = google("models/gemini-2.5-flash-lite");


export async function POST(req: NextRequest) {
    try {
        const {messages}: { messages: UIMessage[] } = await req.json()

        const result = await streamText({
            model: gemini,
            system: SYSTEM_PROMPT,
            messages: convertToModelMessages(messages),
            tools: {
                fetchCatalog: {
                    description: "Retrieves all products from the catalog.",
                    inputSchema: z.object({
                        query: z.string().optional().default(""),
                        maxPrice: z.number().optional().default(1000)
                    }),
                    execute: async ({query, maxPrice}) =&gt; {
                        return await fetchCatalog({query, maxPrice});
                    }
                },
                addToCart: {
                    description: "Adds the specified product to the user's cart.",
                    inputSchema: z.object({
                        productId: z.string(),
                        quantity: z.number().optional().default(1)
                    }),
                    execute: async ({productId, quantity}) =&gt; {
                        return await addToCart(productId, quantity);
                    }
                },
                checkoutCart: {
                    description: "Process checkout and initiate payment",
                    inputSchema: z.object({}),
                    execute: async () =&gt; {
                        return await checkoutCart();
                    }
                }
            }
        });


        return result.toUIMessageStreamResponse({
            onError: errorHandler
        });
    } catch (err) {
        console.error("Error : ", err);
        return NextResponse.json({error: "Internal server error"}, {status: 500});
    }
}

function errorHandler(error: unknown) {
    if (error == null) {
        return 'unknown error';
    }

    if (typeof error === 'string') {
        return error;
    }

    if (error instanceof Error) {
        return error.message;
    }

    return JSON.stringify(error);
}
</code></pre>
<h1><strong>🎨 Build UI With Card Components</strong></h1>
<p>When the LLM returns tool results, your UI displays them as <strong>cards</strong> so we will implement these components in react</p>
<h3><strong>🟥 Product Card</strong></h3>
<pre><code class="language-typescript">// components/ProductCard.tsx
"use client";

import {useState} from "react";

export default function ProductCard({product, addToCart}: { product: any, addToCart: any }) {
    const [adding, setAdding] = useState(false);
    const [added, setAdded] = useState(false);

    const handleAddToCart = async () =&gt; {
        setAdding(true);
        addToCart(product?.id, product?.name)
        setAdded(true)
        setAdding(false);
    };

    return (
        &lt;div className="border rounded-xl p-4 shadow-md flex flex-col items-center gap-2 bg-white"&gt;
            &lt;img
                src={product.image}
                alt={product.name}
                className="w-32 h-32 object-cover rounded-lg"
            /&gt;
            &lt;h3 className="text-lg font-semibold"&gt;{product.name}&lt;/h3&gt;
            &lt;p className="text-gray-600"&gt;₹{product?.price}&lt;/p&gt;
            &lt;button
                onClick={handleAddToCart}
                disabled={adding || added}
                className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
            &gt;
                {adding ? "Adding..." : added ? "Added" : "Add to Cart"}
            &lt;/button&gt;
        &lt;/div&gt;
    );
}
</code></pre>
<h3><strong>🟦 Cart Card</strong></h3>
<pre><code class="language-typescript">"use client";
import {CartItem} from "@/lib/cartStore";
export default function Cart({items = []}: { items?: any }) {
    const total = items?.reduce((sum:number, i:CartItem) =&gt; sum + i.price * i.qty, 0);
    return (
        &lt;div className="p-4 bg-white rounded-xl shadow-md w-[320px]"&gt;
            &lt;h2 className="text-lg font-bold mb-3"&gt;🛒 Your Cart&lt;/h2&gt;

            {items.length === 0 &amp;&amp; &lt;p className="text-gray-500"&gt;Cart is empty&lt;/p&gt;}

            &lt;ul className="space-y-3"&gt;
                {items.map((item:CartItem) =&gt; (
                    &lt;li
                        key={item.productId}
                        className="flex items-center justify-between gap-2 border-b pb-2"
                    &gt;
                        &lt;div className="flex items-center gap-2"&gt;
                            {item.image &amp;&amp; (
                                &lt;img
                                    src={item.image}
                                    alt={item.name}
                                    className="w-10 h-10 rounded"
                                /&gt;
                            )}
                            &lt;div&gt;
                                &lt;p className="font-medium"&gt;{item.name}&lt;/p&gt;
                                &lt;p className="text-sm text-gray-500"&gt;
                                    ₹{item?.price} × {item.qty} =  ${item?.price * item.qty}
                                &lt;/p&gt;
                            &lt;/div&gt;

                        &lt;/div&gt;

                    &lt;/li&gt;
                ))}
            &lt;/ul&gt;

            {items.length &gt; 0 &amp;&amp; (
                &lt;div className="pt-3 flex justify-between font-bold"&gt;
                    &lt;span&gt;Total -&amp;nbsp;&lt;/span&gt;
                    &lt;span&gt;₹{total ? total.toFixed(2) : "0.00"}&lt;/span&gt;
                &lt;/div&gt;
            )}
        &lt;/div&gt;
    );
}
</code></pre>
<h3><strong>🟩 Checkout Card</strong></h3>
<pre><code class="language-typescript">"use client";
import { useState } from "react";
import {OrderItem} from "@/lib/orderStore";

export default function Checkout({order}: { order: OrderItem | null }) {
    const [step, setStep] = useState&lt;"details" | "payment" | "success"&gt;("details");
    // mock "processing payment"
    const handlePayment = () =&gt; {
        setStep("payment");
        setTimeout(() =&gt; setStep("success"), 2000);
    };

    return (
        &lt;div className="p-6 max-w-md mx-auto bg-white rounded-xl shadow-lg"&gt;
            &lt;h2 className="text-xl font-bold mb-4"&gt;Checkout&lt;/h2&gt;

            {step === "details" &amp;&amp; (
                &lt;div&gt;
                    &lt;h3 className="font-semibold mb-2"&gt;Shipping Info&lt;/h3&gt;
                    &lt;form className="space-y-3"&gt;
                        &lt;input
                            type="text"
                            placeholder="Full Name"
                            className="w-full border rounded px-3 py-2"
                        /&gt;
                        &lt;input
                            type="text"
                            placeholder="Address"
                            className="w-full border rounded px-3 py-2"
                        /&gt;
                        &lt;input
                            type="text"
                            placeholder="City"
                            className="w-full border rounded px-3 py-2"
                        /&gt;
                        &lt;input
                            type="text"
                            placeholder="ZIP Code"
                            className="w-full border rounded px-3 py-2"
                        /&gt;
                    &lt;/form&gt;

                    &lt;div className="mt-4 border-t pt-3"&gt;
                        &lt;p className="flex justify-between"&gt;
                            &lt;span className="font-medium"&gt;Total&lt;/span&gt;
                            &lt;span&gt;₹{order?.totalAmount}&lt;/span&gt;
                        &lt;/p&gt;
                    &lt;/div&gt;

                    &lt;button
                        onClick={handlePayment}
                        className="mt-4 w-full bg-blue-500 text-white py-2 rounded"
                    &gt;
                        Proceed to Payment
                    &lt;/button&gt;
                &lt;/div&gt;
            )}

            {step === "payment" &amp;&amp; (
                &lt;div className="text-center"&gt;
                    &lt;p className="text-gray-600"&gt;Processing Payment...&lt;/p&gt;
                    &lt;div className="mt-3 animate-spin rounded-full h-10 w-10 border-4 border-blue-500 border-t-transparent mx-auto"&gt;&lt;/div&gt;
                &lt;/div&gt;
            )}

            {step === "success" &amp;&amp; (
                &lt;div className="text-center"&gt;
                    &lt;h3 className="text-green-600 font-bold text-lg"&gt;✅ Payment Successful!&lt;/h3&gt;
                    &lt;p className="mt-2 text-gray-600"&gt;
                        Thank you for your purchase. Your order will be shipped soon.
                    &lt;/p&gt;
                &lt;/div&gt;
            )}
        &lt;/div&gt;
    );
}
</code></pre>
<h1><strong>💬Integrate Tool Responses in UI</strong></h1>
<p>Your chat page processes three types of messages:</p>
<ul>
<li><p>normal model text</p>
</li>
<li><p>tool calls</p>
</li>
<li><p>tool responses</p>
</li>
</ul>
<pre><code class="language-typescript">// app/page.tsx
"use client";

import {useChat} from "@ai-sdk/react";
import {DefaultChatTransport} from "ai";
import {useState} from "react";
import {Bot, User} from "lucide-react";
import ProductCard from "@/components/ProductCard";
import Cart from "@/components/CartCard";
import Checkout from "@/components/Checkout";
import {OrderItem} from "@/lib/orderStore";

export default function Home() {
    const {messages, sendMessage, status} = useChat({
        transport: new DefaultChatTransport({
            api: '/api/chat',
        }),
    });
    const [input, setInput] = useState('');

    return (
        &lt;main className="flex flex-col items-center pt-4 min-h-screen bg-white"&gt;
            &lt;div className="w-[70vw] bg-white rounded-lg p-6"&gt;
                &lt;h1 className="text-2xl font-bold mb-4 text-center"&gt;
                    🛍️ Shopping Assistant
                &lt;/h1&gt;

                {/* Chat messages */}
                &lt;div className="space-y-4 h-[calc(100vh-200px)] w-full overflow-y-auto p-4 rounded-lg mb-4"&gt;
                    {messages.map((m) =&gt; (
                        &lt;div
                            key={m.id}
                            className={`flex flex-start items-start gap-3 items-center`}
                        &gt;
                            {m.role === "user" ? (
                                &lt;div className="flex items-center gap-2"&gt;
                                    &lt;div className="bg-blue-500 text-white p-2 rounded-full"&gt;
                                        &lt;User className="w-5 h-5"/&gt;
                                    &lt;/div&gt;
                                &lt;/div&gt;) : (
                                &lt;div className="flex items-center gap-2"&gt;
                                    &lt;div className="bg-gray-600 text-white p-2 rounded-full"&gt;
                                        &lt;Bot className="w-5 h-5"/&gt;
                                    &lt;/div&gt;
                                &lt;/div&gt;
                            )}
                            &lt;div
                                className={`w-full flex py-[5px] px-[10px] rounded-[10px] ${m.role === "user" ? "bg-blue-100" : "bg-gray-200"}`}&gt;
                                {m.parts.map((part, index) =&gt; {
                                        switch (part?.type) {
                                            case 'step-start':
                                                // show step boundaries as horizontal lines:
                                                return index &gt; 0 ? (
                                                    &lt;div key={index} className="text-gray-500"&gt;
                                                        &lt;hr className="my-2 border-gray-300"/&gt;
                                                    &lt;/div&gt;
                                                ) : null;
                                            case 'tool-fetchCatalog':
                                                switch (part?.state) {
                                                    case 'input-streaming':
                                                        return &lt;span className="italic text-gray-600"
                                                                     key={`tool-${index}`}&gt; 🛍️ Fetching products… &lt;/span&gt;;
                                                    case 'input-available':
                                                        return &lt;span className="italic text-gray-600"
                                                                     key={`tool-${index}`}&gt; 🛍️ Fetching products… &lt;/span&gt;;
                                                    case 'output-available':
                                                        let products = part.output as any

                                                        return products?.length &gt; 0 ?
                                                           &lt;div className={"flex flex-wrap gap-4"} key={"products-list"}&gt;
                                                                {products?.map((product: any) =&gt; {
                                                                    return &lt;ProductCard product={product} addToCart={async (productId:string, productName:string) =&gt; {
                                                                        await sendMessage({text: `Add ${productName} to my cart`})
                                                                    }}
                                                                                        key={product.id}/&gt;
                                                                })}
                                                                    &lt;/div&gt;
                                                            :
                                                            &lt;div key={`tool-${index}`} className="text-gray-600"&gt;No
                                                                products found.&lt;/div&gt;
                                                    case 'output-error':
                                                        return &lt;div key={`tool-${index}`}&gt;Error: {part.errorText}&lt;/div&gt;;
                                                }
                                                break;
                                            case 'tool-addToCart':
                                                switch (part?.state) {
                                                    case 'input-streaming':
                                                        return &lt;span className="italic text-gray-600"
                                                                     key={`tool-${index}`}&gt;
                                                            ➕ Adding item to cart…
                                                        &lt;/span&gt;;
                                                    case 'input-available':
                                                        return &lt;span className="italic text-gray-600"
                                                                     key={`tool-${index}`}&gt;
                                                            ➕ Adding item to cart…
                                                        &lt;/span&gt;;
                                                    case 'output-available':
                                                        return &lt;Cart key={`tool-${index}}` } items={part.output as any[]}/&gt;;
                                                    case 'output-error':
                                                        return &lt;div key={`tool-${index}`}&gt;Error: {part.errorText}&lt;/div&gt;;
                                                }
                                                break;
                                            case 'tool-checkoutCart':
                                                const order = part?.output as OrderItem
                                                switch (part?.state) {
                                                    case 'input-streaming':
                                                        return &lt;span className="italic text-gray-600"
                                                                     key={`tool-${index}`}&gt;💳 Processing checkout…&lt;/span&gt;;
                                                    case 'input-available':
                                                        return &lt;span className="italic text-gray-600"
                                                                     key={`tool-${index}`}&gt;💳 Processing checkout…&lt;/span&gt;;
                                                    case 'output-available':
                                                        return &lt;Checkout order={order} key={`tool-${index}`}/&gt;;
                                                    case 'output-error':
                                                        return &lt;div key={`tool-${index}`}&gt;Error: {part.errorText}&lt;/div&gt;;
                                                }
                                                break;

                                            case 'text':
                                                return &lt;span key={index}&gt;{part.text}&lt;/span&gt;;
                                        }
                                    }
                                )}
                            &lt;/div&gt;
                        &lt;/div&gt;
                    ))}
                    {status === 'submitted' &amp;&amp; &lt;div className="text-gray-500"&gt;Thinking...&lt;/div&gt;}
                &lt;/div&gt;

                {/* Input box */}
                &lt;form onSubmit={e =&gt; {
                    e.preventDefault();
                    if (input.trim()) {
                        sendMessage({text: input});
                        setInput('');
                    }
                }} className="flex gap-2"&gt;
                    &lt;input
                        className="flex-1 border border-gray-300 shadow-lg rounded-lg px-4 py-2 focus:outline-none focus:ring-1 focus:ring-gray-400"
                        value={input}
                        disabled={status !== 'ready'}
                        placeholder="Ask about products... e.g. 'Show me shoes under 200 rupees'"
                        onChange={e =&gt; setInput(e.target.value)}
                    /&gt;
                    &lt;button
                        type="submit"
                        className="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 shadow-lg disabled:opacity-50"
                        disabled={status !== 'ready' || !input.trim()}
                    &gt;
                        Ask
                    &lt;/button&gt;
                &lt;/form&gt;
            &lt;/div&gt;
        &lt;/main&gt;
    );
}
</code></pre>
<h1>Result</h1>
<h3>List all products</h3>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764181143368/b75816ea-bef9-4f19-ae3e-58634c79a442.png" alt="" style="display:block;margin:0 auto" />

<h3>Filter products based on price</h3>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764181216423/77f04e4c-fc33-4b5b-8ea3-450ccb617dce.png" alt="" style="display:block;margin:0 auto" />

<h3>Add to cart using text</h3>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764181277059/f8455632-e95e-402e-94e3-ee95b693ac15.png" alt="" style="display:block;margin:0 auto" />

<h3>Checkout feature using text that allows user to pay using preferred method</h3>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764185232878/b0c99f37-b8bf-4ccb-a035-2872a8857a4b.png" alt="" style="display:block;margin:0 auto" />

<h1>🎯 <strong>What We Just Built</strong></h1>
<p>With <strong>Gemini tool calling + Vercel AI SDK + Next.js</strong>, you now have an:</p>
<ul>
<li><p>AI-powered <strong>shopping assistant</strong></p>
</li>
<li><p>Interactive <strong>product catalog chatbot</strong></p>
</li>
<li><p>Cart-enabled <strong>conversational checkout</strong></p>
</li>
<li><p>Real-time <strong>streaming chat UI</strong></p>
</li>
<li><p>Modern e-commerce experience using <strong>React card components</strong></p>
</li>
</ul>
<h1>🚀 <strong>Next Steps</strong></h1>
<p>Add more advanced features:</p>
<ul>
<li><p>🔍 Vector search (Supabase, Pinecone)</p>
</li>
<li><p>🔧 Tool-based filtering (by price, brand, ratings)</p>
</li>
<li><p>👤 Personalized recommendations</p>
</li>
<li><p>💳 Real checkout integration</p>
</li>
<li><p>📦 Track order status</p>
</li>
</ul>
<h1>🏁 <strong>Conclusion</strong></h1>
<p>Using <strong>Vercel AI SDK + Google Gemini</strong>, we've created a fully interactive <strong>e-commerce chatbot</strong> that transforms the shopping experience. This innovation allows users to discover products, add items to their cart, and complete the checkout process all within a conversational interface. This development is a significant advancement in the tech world, offering a seamless and engaging way to shop online.</p>
<p>Here is the live version: <a href="https://shopping-assistant-kappa.vercel.app/">DEMO</a></p>
<p>Source code - <a href="https://github.com/icon-gaurav/ecom-chatbot">Github</a></p>
]]></content:encoded></item><item><title><![CDATA[Building an Ethereum dApp with Next.js, Wagmi, and MetaMask]]></title><description><![CDATA[Over the last few years, decentralized applications (dApps) have become one of the most exciting areas in Web3. Instead of relying on centralized servers, dApps interact directly with blockchains—allowing users to own their assets, sign transactions,...]]></description><link>https://gauravbytes.dev/building-an-ethereum-dapp-with-nextjs-wagmi-and-metamask</link><guid isPermaLink="true">https://gauravbytes.dev/building-an-ethereum-dapp-with-nextjs-wagmi-and-metamask</guid><category><![CDATA[dapps]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[Metamask]]></category><category><![CDATA[Web3]]></category><category><![CDATA[defi]]></category><category><![CDATA[Blockchain]]></category><category><![CDATA[#wagmi]]></category><category><![CDATA[crypto]]></category><category><![CDATA[sepolia]]></category><category><![CDATA[Smart Contracts]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 25 Aug 2025 11:04:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755811467976/1cadd7ee-e625-43ea-8927-0acac2e8c02b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Over the last few years, decentralized applications (dApps) have become one of the most exciting areas in Web3. Instead of relying on centralized servers, dApps interact directly with blockchains—allowing users to <strong>own their assets, sign transactions, and engage with trustless systems</strong>.</p>
<p>In this tutorial, we’ll walk through building a simple Ethereum dApp using <strong>Next.js, Wagmi, and MetaMask</strong>. Our application will let users:</p>
<ul>
<li><p>Connect their MetaMask wallet</p>
</li>
<li><p>Send ETH to another address on a testnet (Sepolia)</p>
</li>
<li><p>View their recent transactions in a paginated format</p>
</li>
</ul>
<p>This project is designed as a starting point for anyone who wants to dive into Ethereum development and understand how wallet connections, transactions, and blockchain interactions work in practice.</p>
<p>We’ll use <strong>MetaMask</strong> as the crypto wallet for connecting and signing transactions, <strong>Wagmi</strong> (a React hooks library) for interacting with Ethereum, and <strong>Next.js</strong> for building the frontend of our dApp.</p>
<p>By the end of this guide, you’ll have a fully functional Ethereum dApp running on a testnet—ready to extend into something bigger, like token transfers or DeFi features.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we dive into building the dApp, here’s what you’ll need to get started:</p>
<h3 id="heading-1-basic-knowledge">1. Basic Knowledge</h3>
<ul>
<li><p>Familiarity with <strong>React</strong> and <strong>Next.js</strong> fundamentals</p>
</li>
<li><p>A basic understanding of <strong>Ethereum</strong> and how transactions work</p>
</li>
<li><p>Some exposure to <strong>JavaScript/TypeScript</strong></p>
</li>
</ul>
<h3 id="heading-2-installed-tools">2. Installed Tools</h3>
<ul>
<li><p><strong>Node.js &amp; npm/yarn</strong> – To run the Next.js project</p>
</li>
<li><p><strong>MetaMask</strong> – Installed as a browser extension or installed on mobile (for connecting and approving transactions)</p>
</li>
<li><p><strong>VS Code (or any IDE)</strong> – To write and manage your project code</p>
</li>
</ul>
<h3 id="heading-3-ethereum-test-network-setup">3. Ethereum Test Network Setup</h3>
<p>We’ll use the <strong>Sepolia testnet</strong> for this project. That way, you don’t spend real ETH while testing.</p>
<p>👉 Steps:</p>
<ol>
<li><p>Install <strong>MetaMask</strong> if you haven’t already.</p>
</li>
<li><p>Switch your MetaMask network to <strong>Sepolia Test Network</strong>.</p>
</li>
<li><p>Get some free test ETH from a <strong>Sepolia Faucet</strong> (just Google <em>Sepolia faucet</em> and request test ETH).</p>
</li>
</ol>
<p>💡 <strong>Note:</strong> If you’re having trouble getting test ETH, you can also reach out to me via [<a target="_blank" href="mailto:icon.gaurav806@gmail.com">email</a>], and I can transfer some Sepolia ETH to your wallet for testing.</p>
<h3 id="heading-4-libraries-well-use">4. Libraries We’ll Use</h3>
<ul>
<li><p><strong>Next.js</strong> – React framework for building the frontend</p>
</li>
<li><p><strong>Wagmi</strong> – React hooks for Ethereum (easy wallet connection + transaction handling)</p>
</li>
<li><p><strong>Viem</strong> – A low-level Ethereum library used under the hood by Wagmi</p>
</li>
<li><p><strong>Tailwind CSS</strong> – For styling (optional, but makes UI much easier)</p>
</li>
</ul>
<p>Once you have these prerequisites in place, you’ll be ready to start coding your dApp 🚀</p>
<h2 id="heading-project-setup">🚀 Project Setup</h2>
<p>Let’s set up our Ethereum dApp from scratch. We’ll be using <strong>Next.js</strong> for the frontend, <strong>Wagmi</strong> for wallet interactions, and <strong>MetaMask</strong> as our crypto wallet provider.</p>
<h3 id="heading-1-create-a-nextjs-app">1. Create a Next.js App</h3>
<p>First, create a new Next.js project using the official CLI:</p>
<pre><code class="lang-bash">npx create-next-app eth-dapp
<span class="hljs-built_in">cd</span> eth-dapp
</code></pre>
<h3 id="heading-2-install-dependencies">2. Install Dependencies</h3>
<p>We’ll need <strong>Wagmi</strong>, <strong>viem</strong>, and <strong>ethers</strong> to connect with Ethereum.</p>
<pre><code class="lang-bash">npm install wagmi viem ethers
</code></pre>
<p>If you want pretty logging (optional), install <strong>pino-pretty</strong>:</p>
<pre><code class="lang-bash">npm install pino-pretty
</code></pre>
<h3 id="heading-3-configure-wagmi">3. Configure Wagmi</h3>
<p>Inside your project, set up the Wagmi client in a <code>Web3Provider.tsx</code> file. This will allow us to connect to Ethereum testnets like <strong>Sepolia</strong>.</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>
<span class="hljs-keyword">import</span> {createConfig, http, WagmiProvider} <span class="hljs-keyword">from</span> <span class="hljs-string">'wagmi'</span>
<span class="hljs-keyword">import</span> {mainnet, sepolia} <span class="hljs-keyword">from</span> <span class="hljs-string">'wagmi/chains'</span>
<span class="hljs-keyword">import</span> {QueryClient, QueryClientProvider} <span class="hljs-keyword">from</span> <span class="hljs-string">"@tanstack/react-query"</span>;
<span class="hljs-keyword">import</span> {metaMask} <span class="hljs-keyword">from</span> <span class="hljs-string">"@wagmi/connectors"</span>;
<span class="hljs-keyword">import</span> {injected} <span class="hljs-keyword">from</span> <span class="hljs-string">"wagmi/connectors"</span>;

<span class="hljs-keyword">const</span> queryClient = <span class="hljs-keyword">new</span> QueryClient();
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = createConfig({
    chains: [mainnet, sepolia],
    connectors: [
        metaMask(),
        injected(),
    ],
    transports: {
        [mainnet.id]: http(),
        [sepolia.id]: http(),
    },
})

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Web3Provider</span>(<span class="hljs-params">{children}: { children: React.ReactNode }</span>) </span>{
    <span class="hljs-keyword">return</span> &lt;WagmiProvider config={config}&gt;
        &lt;QueryClientProvider client={queryClient}&gt;
            {children}
        &lt;<span class="hljs-regexp">/QueryClientProvider&gt;&lt;/</span>WagmiProvider&gt;;
}
</code></pre>
<p>Now wrap your app in this provider (<code>app/layout.tsx</code> in Next.js 13+):</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {Web3Provider} <span class="hljs-keyword">from</span> <span class="hljs-string">"@/lib/web3/Web3Provider"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayout</span>(<span class="hljs-params">{ children }: { children: React.ReactNode }</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;html lang=<span class="hljs-string">"en"</span>&gt;
      &lt;body&gt;
            &lt;Web3Provider&gt;{children}&lt;/Web3Provider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<h3 id="heading-4-install-metamask">4. Install MetaMask</h3>
<p>Make sure you have the <a target="_blank" href="https://metamask.io/">MetaMask extension</a> installed in your browser or in your phone.</p>
<ul>
<li><p>Switch to the <strong>Sepolia Testnet</strong> in MetaMask.</p>
</li>
<li><p>Request some test ETH from a <a target="_blank" href="https://sepoliafaucet.com/">Sepolia Faucet</a>.</p>
</li>
</ul>
<p>💡 <em>If you’re unable to get test ETH, feel free to email me at</em> <strong><em>[</em></strong><a target="_blank" href="mailto:icon.gaurav806@gmail.com">icon.gaurav806@gmail.com</a><strong><em>]</em></strong> <em>and I can transfer some to your wallet for testing.</em></p>
<p>👉 That’s our base setup. Next, we’ll build the <strong>Wallet Connection component</strong> so users can connect their MetaMask wallet to the dApp.</p>
<h2 id="heading-connecting-your-wallet">🔗 Connecting Your Wallet</h2>
<p>Now that our project is set up with <strong>Next.js</strong> and <strong>Wagmi</strong>, let’s add a <strong>Wallet Connect button</strong> so users can link their MetaMask wallet to the dApp.</p>
<h3 id="heading-1-create-a-wallet-connect-component">1. Create a Wallet Connect Component</h3>
<p>Inside <code>components/WalletConnect.tsx</code>, add the following:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>
<span class="hljs-keyword">import</span> {useAccount, useConnect, useDisconnect} <span class="hljs-keyword">from</span> <span class="hljs-string">"wagmi"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">WalletConnect</span>(<span class="hljs-params">{ onAddWallet }: {
    onAddWallet: (address: <span class="hljs-built_in">string</span>) =&gt; <span class="hljs-built_in">void</span>
}</span>) </span>{
    <span class="hljs-keyword">const</span> {connectAsync, connectors, status , reset, error} = useConnect();
    <span class="hljs-keyword">const</span> { disconnect } = useDisconnect();
    <span class="hljs-keyword">const</span> handleConnect = <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">try</span> {
            disconnect(); <span class="hljs-comment">// Disconnect any existing connection</span>
            <span class="hljs-keyword">const</span> connector = connectors[<span class="hljs-number">0</span>]; <span class="hljs-comment">// Assuming the first connector is the one we want</span>
            <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> connectAsync({ connector });
            onAddWallet(data?.accounts?.[<span class="hljs-number">0</span>]);
        } <span class="hljs-keyword">catch</span> (error) {
            reset()
            <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to connect wallet:"</span>, error);
            <span class="hljs-built_in">console</span>.log(error)
        }
    }


    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">"max-w-md mx-auto mt-10 p-6 bg-white rounded-2xl shadow-md"</span>&gt;
            &lt;h2 className=<span class="hljs-string">"text-xl font-semibold text-gray-800 mb-4"</span>&gt;Add Wallet&lt;/h2&gt;

            &lt;div className=<span class="hljs-string">"text-center"</span>&gt;
                &lt;button
                    onClick={handleConnect}
                    className=<span class="hljs-string">"w-full py-2 px-4 bg-green-600 text-white rounded-lg hover:bg-green-700 transition"</span>
                &gt;
                    {status === <span class="hljs-string">'pending'</span>? <span class="hljs-string">'Connecting...'</span> :<span class="hljs-string">'Connect Using External Wallet'</span>}
                &lt;/button&gt;

                {error &amp;&amp; &lt;p className=<span class="hljs-string">"text-red-600 mt-2"</span>&gt;{error.message}&lt;/p&gt;}
            &lt;/div&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<h3 id="heading-2-add-it-to-the-homepage">2. Add It to the Homepage</h3>
<p>Open <code>app/page.tsx</code> and include the WalletConnect component:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> WalletConnect <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/WalletConnect"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"flex flex-col items-center justify-center px-6"</span>&gt;

            {<span class="hljs-comment">/* ===== Hero Section with Wallet Connect ===== */</span>}
            &lt;section className=<span class="hljs-string">"text-center mt-20 max-w-3xl"</span>&gt;
                &lt;h1 className=<span class="hljs-string">"text-5xl font-extrabold text-gray-900 mb-6"</span>&gt;
                    Manage Your Crypto <span class="hljs-keyword">with</span> Ease 🚀
                &lt;/h1&gt;
                &lt;p className=<span class="hljs-string">"text-lg text-gray-600 mb-8"</span>&gt;
                    Create wallets, send &amp; receive Ethereum, and track transactions —
                    all <span class="hljs-keyword">in</span> one secure, easy-to-use app.
                &lt;/p&gt;

                {<span class="hljs-comment">/* Connect CTA */</span>}
                &lt;div className=<span class="hljs-string">"mt-6"</span>&gt;
                    &lt;WalletConnect onAddWallet={<span class="hljs-function">() =&gt;</span> redirect(<span class="hljs-string">'/dashboard'</span>)}/&gt;
                    &lt;p className=<span class="hljs-string">"text-sm text-gray-500 mt-3"</span>&gt;
                        Don’t have a wallet yet? Install{<span class="hljs-string">" "</span>}
                        &lt;Link
                            href=<span class="hljs-string">"https://metamask.io/download/"</span>
                            target=<span class="hljs-string">"_blank"</span>
                            rel=<span class="hljs-string">"noopener noreferrer"</span>
                            className=<span class="hljs-string">"text-blue-600 underline"</span>
                        &gt;
                            MetaMask
                        &lt;/Link&gt;{<span class="hljs-string">" "</span>}
                        to get started.
                    &lt;/p&gt;
                &lt;/div&gt;
            &lt;/section&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h3 id="heading-3-test-it">3. Test It</h3>
<ul>
<li>Run your dev server:</li>
</ul>
<pre><code class="lang-bash">npm run dev
</code></pre>
<ul>
<li><p>Open <a target="_blank" href="http://localhost:3000"><code>http://localhost:3000</code></a>.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755808946974/b257da30-a2a7-472e-939d-c042843bf1f9.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<ul>
<li><p>Click <strong>Connect Wallet</strong> → MetaMask should pop up asking for approval.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755808983904/ba423a13-dee3-421d-91eb-47a90177118c.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Once connected, you’ll see get redirected to dashboard page.</p>
</li>
</ul>
<p>✅ At this stage, your dApp can connect and disconnect a wallet.<br />Next, we’ll extend this by <strong>fetching account details and showing balances</strong>.</p>
<h2 id="heading-fetching-wallet-details-amp-balances">💰 Fetching Wallet Details &amp; Balances</h2>
<p>Once users connect their wallet, we can show them their <strong>Ethereum address</strong>, <strong>balance</strong>, and even a quick copy button. This helps them confirm they are on the right account before sending transactions.</p>
<h3 id="heading-1-fetch-account-details-with-wagmi">1. Fetch Account Details with Wagmi</h3>
<p>Wagmi provides hooks like <code>useAccount</code> and <code>useBalance</code> to make this easy. Inside <code>app/dashboard/page.tsx</code>, add the following:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> {useAccount, useBalance} <span class="hljs-keyword">from</span> <span class="hljs-string">"wagmi"</span>;
<span class="hljs-keyword">import</span> {useState} <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> {Copy, Send, Wallet, ExternalLink, X} <span class="hljs-keyword">from</span> <span class="hljs-string">"lucide-react"</span>;
<span class="hljs-keyword">import</span> {redirect} <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;
<span class="hljs-keyword">import</span> SendTransaction <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/SendTransaction"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DashboardPage</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> {address, isConnected} = useAccount();
    <span class="hljs-keyword">const</span> {data: balance} = useBalance({address});


    <span class="hljs-keyword">const</span> handleCopy = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (address) {
            navigator.clipboard.writeText(address);
            alert(<span class="hljs-string">"Wallet address copied!"</span>);
        }
    };

    <span class="hljs-keyword">if</span> (!isConnected) {
        <span class="hljs-keyword">return</span> (
            &lt;div className=<span class="hljs-string">"flex items-center justify-center min-h-screen p-4"</span>&gt;
                &lt;div className=<span class="hljs-string">"p-6 border rounded-lg shadow-md text-center w-full max-w-sm"</span>&gt;
                    &lt;p className=<span class="hljs-string">"text-lg font-semibold"</span>&gt;No wallet connected&lt;/p&gt;
                    &lt;p className=<span class="hljs-string">"text-sm text-gray-500 mt-2"</span>&gt;
                        Please connect your wallet to view your dashboard.
                    &lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        );
    }

    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">"p-4 sm:p-8 space-y-6 sm:space-y-8"</span>&gt;
            &lt;h1 className=<span class="hljs-string">"text-2xl sm:text-3xl font-bold"</span>&gt;Dashboard&lt;/h1&gt;

            {<span class="hljs-comment">/* Wallet Info */</span>}
            &lt;div className=<span class="hljs-string">"border rounded-lg shadow-md p-4 sm:p-6 flex items-center justify-between gap-4"</span>&gt;
                &lt;div className=<span class="hljs-string">"flex items-center gap-4 w-full"</span>&gt;
                    &lt;Wallet className=<span class="hljs-string">"w-8 h-8 sm:w-10 sm:h-10 text-blue-600"</span>/&gt;
                    &lt;div className=<span class="hljs-string">"min-w-0 flex-1"</span>&gt;
                        &lt;p className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;Connected Wallet&lt;/p&gt;
                        &lt;p className=<span class="hljs-string">"font-semibold text-sm sm:text-base break-all "</span>&gt;{address}&lt;/p&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
                &lt;button
                    onClick={handleCopy}
                    className=<span class="hljs-string">"p-2 border rounded-lg hover:bg-gray-100 flex-shrink-0"</span>
                &gt;
                    &lt;Copy className=<span class="hljs-string">"w-4 h-4"</span>/&gt;
                &lt;/button&gt;
            &lt;/div&gt;


            {<span class="hljs-comment">/* Balance Info */</span>}
            &lt;div className=<span class="hljs-string">"border rounded-lg shadow-md p-4 sm:p-6"</span>&gt;
                &lt;p className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;Total Balance&lt;/p&gt;
                &lt;p className=<span class="hljs-string">"text-xl sm:text-2xl font-bold mt-2"</span>&gt;
                    {balance ? <span class="hljs-string">`<span class="hljs-subst">${balance.formatted}</span> <span class="hljs-subst">${balance.symbol}</span>`</span> : <span class="hljs-string">"Loading..."</span>}
                &lt;/p&gt;
            &lt;/div&gt;


        &lt;/div&gt;
    );
}
</code></pre>
<h3 id="heading-2-test-it">2. Test It</h3>
<ul>
<li><p>Connect your wallet.</p>
</li>
<li><p>Head to <code>/dashboard</code>.</p>
</li>
<li><p>You should now see your <strong>wallet address</strong>, a <strong>copy icon</strong>, and your <strong>ETH balance</strong>.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755809501585/14908e93-66d1-4dcf-b0dd-c3ccfbd8d190.png" alt class="image--center mx-auto" /></p>
<p>✅ With this, your dApp shows <strong>real-time account info</strong> after connection.<br />Next up, we’ll implement the <strong>Send ETH feature</strong> with a modal for transfers.</p>
<h2 id="heading-sending-eth-with-a-transaction-modal">🚀 Sending ETH with a Transaction Modal</h2>
<p>One of the key features of our dApp is the ability to <strong>send ETH</strong> directly from the dashboard. For this, we’ll build a <strong>modal form</strong> where users can:</p>
<ul>
<li><p>Enter a recipient’s address</p>
</li>
<li><p>Enter the amount to send</p>
</li>
<li><p>Confirm the transfer in MetaMask</p>
</li>
</ul>
<p>We’ll use <code>wagmi</code>’s <strong>useSendTransaction</strong> hook along with <code>viem</code>’s <code>parseEther</code> utility.</p>
<h3 id="heading-1-send-transaction-component">1. Send Transaction Component</h3>
<p>Inside <code>components/SendTransaction.tsx</code>, add the following:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { useSendTransaction, useWaitForTransactionReceipt } <span class="hljs-keyword">from</span> <span class="hljs-string">"wagmi"</span>;
<span class="hljs-keyword">import</span> { parseEther } <span class="hljs-keyword">from</span> <span class="hljs-string">"viem"</span>;
<span class="hljs-keyword">import</span> Link <span class="hljs-keyword">from</span> <span class="hljs-string">"next/link"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SendTransaction</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [to, setTo] = useState&lt;<span class="hljs-string">`0x<span class="hljs-subst">${<span class="hljs-built_in">string</span>}</span>`</span> | <span class="hljs-string">""</span>&gt;(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> [amount, setAmount] = useState(<span class="hljs-string">""</span>);

    <span class="hljs-keyword">const</span> { data: txHash, isPending, sendTransaction, error } = useSendTransaction();
    <span class="hljs-keyword">const</span> { isLoading: isConfirming, isSuccess: isConfirmed } =
        useWaitForTransactionReceipt({
            hash: txHash, <span class="hljs-comment">// hash of transaction</span>
        });

    <span class="hljs-keyword">const</span> handleSend = <span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">await</span> sendTransaction({
                to: to <span class="hljs-keyword">as</span> <span class="hljs-string">`0x<span class="hljs-subst">${<span class="hljs-built_in">string</span>}</span>`</span>,
                value: parseEther(amount), <span class="hljs-comment">// convert ETH string to wei</span>
            });
        } <span class="hljs-keyword">catch</span> (err) {
            <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Transaction failed:"</span>, err);
        }
    };

    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">"p-4 max-w-md mx-auto space-y-4 rounded"</span>&gt;
            &lt;h2 className=<span class="hljs-string">"text-lg font-semibold"</span>&gt;Send ETH&lt;/h2&gt;

            &lt;input
                <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span>
                placeholder=<span class="hljs-string">"Recipient address (0x...)"</span>
                value={to}
                onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setTo(e.target.value <span class="hljs-keyword">as</span> <span class="hljs-string">`0x<span class="hljs-subst">${<span class="hljs-built_in">string</span>}</span>`</span> | <span class="hljs-string">""</span>)}
                className=<span class="hljs-string">"w-full p-2 border rounded"</span>
            /&gt;

            &lt;input
                <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span>
                placeholder=<span class="hljs-string">"Amount (ETH)"</span>
                value={amount}
                onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setAmount(e.target.value)}
                className=<span class="hljs-string">"w-full p-2 border rounded"</span>
            /&gt;

            &lt;button
                onClick={handleSend}
                disabled={isPending}
                className=<span class="hljs-string">"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400"</span>
            &gt;
                {isPending ? <span class="hljs-string">"Sending..."</span> : <span class="hljs-string">"Send"</span>}
            &lt;/button&gt;

            {<span class="hljs-comment">/* Transaction States */</span>}
            {isPending &amp;&amp; &lt;p className=<span class="hljs-string">"text-yellow-600"</span>&gt;⏳ Transaction is being sent...&lt;/p&gt;}
            {isConfirming &amp;&amp; &lt;p className=<span class="hljs-string">"text-blue-600"</span>&gt;⏳ Waiting <span class="hljs-keyword">for</span> confirmation...&lt;/p&gt;}
            {isConfirmed &amp;&amp; (
                &lt;p className=<span class="hljs-string">"text-green-600"</span>&gt;
                    ✅ Transaction confirmed!
                &lt;/p&gt;
            )}

            {<span class="hljs-comment">/* Transaction Details */</span>}
            {txHash &amp;&amp; (
                &lt;div className=<span class="hljs-string">"mt-2 space-y-2"</span>&gt;
                    &lt;p className=<span class="hljs-string">"text-sm break-all"</span>&gt;
                        🔗 Tx Hash: {txHash}
                    &lt;/p&gt;
                    &lt;Link
                        href={<span class="hljs-string">`https://sepolia.etherscan.io/tx/<span class="hljs-subst">${txHash}</span>`</span>}
                        target=<span class="hljs-string">"_blank"</span>
                        className=<span class="hljs-string">"underline text-blue-600"</span>
                    &gt;
                        View on Etherscan
                    &lt;/Link&gt;
                    &lt;br /&gt;
                    &lt;Link
                        href={<span class="hljs-string">`/dashboard`</span>}
                        className=<span class="hljs-string">"underline text-purple-600"</span>
                    &gt;
                        Go to Dashboard
                    &lt;/Link&gt;
                &lt;/div&gt;
            )}

            {error &amp;&amp; (
                &lt;p className=<span class="hljs-string">"text-sm text-red-600"</span>&gt;
                    ❌ <span class="hljs-built_in">Error</span>: {error.message}
                &lt;/p&gt;
            )}
        &lt;/div&gt;
    );
}
</code></pre>
<h3 id="heading-2-add-modal-to-dashboard">2. Add Modal to Dashboard</h3>
<p>We’ll trigger the modal using a <strong>Send Money</strong> button.</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> {useAccount, useBalance} <span class="hljs-keyword">from</span> <span class="hljs-string">"wagmi"</span>;
<span class="hljs-keyword">import</span> {useState} <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> {Copy, Send, Wallet, ExternalLink, X} <span class="hljs-keyword">from</span> <span class="hljs-string">"lucide-react"</span>;
<span class="hljs-keyword">import</span> {redirect} <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;
<span class="hljs-keyword">import</span> SendTransaction <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/SendTransaction"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DashboardPage</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> {address, isConnected} = useAccount();
    <span class="hljs-keyword">const</span> {data: balance} = useBalance({address});

    <span class="hljs-keyword">const</span> [isModalOpen, setIsModalOpen] = useState(<span class="hljs-literal">false</span>);

    <span class="hljs-keyword">const</span> handleCopy = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (address) {
            navigator.clipboard.writeText(address);
            alert(<span class="hljs-string">"Wallet address copied!"</span>);
        }
    };

    <span class="hljs-keyword">if</span> (!isConnected) {
        <span class="hljs-keyword">return</span> (
            &lt;div className=<span class="hljs-string">"flex items-center justify-center min-h-screen p-4"</span>&gt;
                &lt;div className=<span class="hljs-string">"p-6 border rounded-lg shadow-md text-center w-full max-w-sm"</span>&gt;
                    &lt;p className=<span class="hljs-string">"text-lg font-semibold"</span>&gt;No wallet connected&lt;/p&gt;
                    &lt;p className=<span class="hljs-string">"text-sm text-gray-500 mt-2"</span>&gt;
                        Please connect your wallet to view your dashboard.
                    &lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        );
    }

    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">"p-4 sm:p-8 space-y-6 sm:space-y-8"</span>&gt;
            &lt;h1 className=<span class="hljs-string">"text-2xl sm:text-3xl font-bold"</span>&gt;Dashboard&lt;/h1&gt;

            {<span class="hljs-comment">/* Wallet Info */</span>}
            &lt;div className=<span class="hljs-string">"border rounded-lg shadow-md p-4 sm:p-6 flex items-center justify-between gap-4"</span>&gt;
                &lt;div className=<span class="hljs-string">"flex items-center gap-4 w-full"</span>&gt;
                    &lt;Wallet className=<span class="hljs-string">"w-8 h-8 sm:w-10 sm:h-10 text-blue-600"</span>/&gt;
                    &lt;div className=<span class="hljs-string">"min-w-0 flex-1"</span>&gt;
                        &lt;p className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;Connected Wallet&lt;/p&gt;
                        &lt;p className=<span class="hljs-string">"font-semibold text-sm sm:text-base break-all "</span>&gt;{address}&lt;/p&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
                &lt;button
                    onClick={handleCopy}
                    className=<span class="hljs-string">"p-2 border rounded-lg hover:bg-gray-100 flex-shrink-0"</span>
                &gt;
                    &lt;Copy className=<span class="hljs-string">"w-4 h-4"</span>/&gt;
                &lt;/button&gt;
            &lt;/div&gt;


            {<span class="hljs-comment">/* Balance Info */</span>}
            &lt;div className=<span class="hljs-string">"border rounded-lg shadow-md p-4 sm:p-6"</span>&gt;
                &lt;p className=<span class="hljs-string">"text-sm text-gray-500"</span>&gt;Total Balance&lt;/p&gt;
                &lt;p className=<span class="hljs-string">"text-xl sm:text-2xl font-bold mt-2"</span>&gt;
                    {balance ? <span class="hljs-string">`<span class="hljs-subst">${balance.formatted}</span> <span class="hljs-subst">${balance.symbol}</span>`</span> : <span class="hljs-string">"Loading..."</span>}
                &lt;/p&gt;
            &lt;/div&gt;

            {<span class="hljs-comment">/* Actions */</span>}
            &lt;div className=<span class="hljs-string">"flex flex-col gap-4 sm:flex-row"</span>&gt;
                &lt;button
                    onClick={<span class="hljs-function">() =&gt;</span> setIsModalOpen(<span class="hljs-literal">true</span>)}
                    className=<span class="hljs-string">"flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 w-full sm:w-auto"</span>
                &gt;
                    &lt;Send className=<span class="hljs-string">"w-4 h-4"</span>/&gt; Send Money
                &lt;/button&gt;

            &lt;/div&gt;

            {<span class="hljs-comment">/* Send Money Modal */</span>}
            {isModalOpen &amp;&amp; (
                &lt;div className=<span class="hljs-string">"fixed inset-0 flex items-center justify-center bg-black/40 p-4"</span>&gt;
                    &lt;div className=<span class="hljs-string">"bg-white rounded-lg shadow-lg w-full max-w-md p-6 relative"</span>&gt;
                        &lt;button
                            onClick={<span class="hljs-function">() =&gt;</span> setIsModalOpen(<span class="hljs-literal">false</span>)}
                            className=<span class="hljs-string">"absolute top-2 right-2 p-1 rounded hover:bg-gray-100"</span>
                        &gt;
                            &lt;X className=<span class="hljs-string">"w-5 h-5"</span>/&gt;
                        &lt;/button&gt;
                        &lt;SendTransaction/&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            )}
        &lt;/div&gt;
    );
}
</code></pre>
<h3 id="heading-3-test-the-flow">3. Test the Flow</h3>
<ol>
<li><p>Connect your wallet</p>
</li>
<li><p>Enter a <strong>recipient address</strong> and <strong>amount</strong></p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755809935391/f2f16ac9-0c39-41aa-af96-e54f757cc225.png" alt class="image--center mx-auto" /></p>
<p> 🎉 ETH is transferred on the <strong>testnet</strong></p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755809982212/263e6076-6a7d-4860-b322-5d4cd9ef58a4.png" alt class="image--center mx-auto" /></p>
<p> Confirm the transaction in MetaMask or in Etherscan</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755810181634/b59b04be-2b45-4c08-ae7a-fbc922238987.png" alt class="image--center mx-auto" /></p>
<p>✅ Now your dApp supports <strong>ETH transfers</strong> via a clean modal UI.</p>
<p>Next, we’ll build the <strong>Transactions Page</strong> to view past transfers with pagination.</p>
<h2 id="heading-viewing-past-transactions">📜 Viewing Past Transactions</h2>
<p>Once users can send ETH, the next logical step is to <strong>view their past transactions</strong>. This helps them keep track of transfers, amounts, and recipients directly from our dApp.</p>
<p>We’ll use <strong>Etherscan APIs</strong> (or any testnet block explorer API) to fetch transaction history for the connected wallet. For simplicity, we’ll show a <strong>paginated table</strong> of transactions.</p>
<h3 id="heading-1-transactions-page">1. Transactions Page</h3>
<p>Inside <code>app/transactions.tsx</code>, add the following:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> {useAccount} <span class="hljs-keyword">from</span> <span class="hljs-string">"wagmi"</span>;
<span class="hljs-keyword">import</span> {useEffect, useState} <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> {ExternalLink} <span class="hljs-keyword">from</span> <span class="hljs-string">"lucide-react"</span>;

<span class="hljs-keyword">interface</span> Transaction {
    hash: <span class="hljs-built_in">string</span>;
    <span class="hljs-keyword">from</span>: <span class="hljs-built_in">string</span>;
    to: <span class="hljs-built_in">string</span>;
    value: <span class="hljs-built_in">string</span>;
    timeStamp: <span class="hljs-built_in">number</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TransactionsPage</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> {address, isConnected} = useAccount();
    <span class="hljs-keyword">const</span> [transactions, setTransactions] = useState&lt;Transaction[]&gt;([]);
    <span class="hljs-keyword">const</span> [page, setPage] = useState(<span class="hljs-number">1</span>);
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">false</span>);

    <span class="hljs-comment">// replace this with your provider API</span>
    <span class="hljs-keyword">const</span> fetchTransactions = <span class="hljs-keyword">async</span> (pageNumber: <span class="hljs-built_in">number</span>) =&gt; {
        <span class="hljs-keyword">if</span> (!address) <span class="hljs-keyword">return</span>;
        setLoading(<span class="hljs-literal">true</span>);

        <span class="hljs-keyword">try</span> {
            <span class="hljs-comment">// Example using Etherscan API (you can use Alchemy/Moralis/Blockscout too)</span>
            <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(
                <span class="hljs-string">`https://api-sepolia.etherscan.io/api?module=account&amp;action=txlist&amp;address=<span class="hljs-subst">${address}</span>&amp;startblock=0&amp;endblock=99999999&amp;page=<span class="hljs-subst">${pageNumber}</span>&amp;offset=5&amp;sort=desc&amp;apikey=<span class="hljs-subst">${process.env.NEXT_PUBLIC_ETHERSCAN_API_KEY}</span>`</span>
            );
            <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json();
            setTransactions(data.result);

        } <span class="hljs-keyword">catch</span> (err) {
            <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error fetching transactions:"</span>, err);
        } <span class="hljs-keyword">finally</span> {
            setLoading(<span class="hljs-literal">false</span>);
        }
    };

    useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (isConnected) {
            fetchTransactions(page);
        }
    }, [page, isConnected]);

    <span class="hljs-keyword">if</span> (!isConnected) {
        <span class="hljs-keyword">return</span> (
            &lt;div className=<span class="hljs-string">"flex items-center justify-center min-h-screen"</span>&gt;
                &lt;div className=<span class="hljs-string">"p-6 border rounded-lg shadow-md text-center"</span>&gt;
                    &lt;p className=<span class="hljs-string">"text-lg font-semibold"</span>&gt;No wallet connected&lt;/p&gt;
                    &lt;p className=<span class="hljs-string">"text-sm text-gray-500 mt-2"</span>&gt;
                        Connect your wallet to view transactions.
                    &lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        );
    }

    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">"p-8 w-full"</span>&gt;
            &lt;h1 className=<span class="hljs-string">"text-2xl font-bold mb-6"</span>&gt;Transactions&lt;/h1&gt;

            {loading ? (
                &lt;p&gt;Loading transactions...&lt;/p&gt;
            ) : (
                &lt;div className=<span class="hljs-string">"overflow-x-auto border rounded-lg"</span>&gt;
                    &lt;table className=<span class="hljs-string">"min-w-full text-sm"</span>&gt;
                        &lt;thead className=<span class="hljs-string">"bg-gray-100"</span>&gt;
                        &lt;tr&gt;
                            &lt;th className=<span class="hljs-string">"px-4 py-2 text-left"</span>&gt;Sender&lt;/th&gt;
                            &lt;th className=<span class="hljs-string">"px-4 py-2 text-left"</span>&gt;Receiver&lt;/th&gt;
                            &lt;th className=<span class="hljs-string">"px-4 py-2"</span>&gt;Type&lt;/th&gt;
                            &lt;th className=<span class="hljs-string">"px-4 py-2"</span>&gt;<span class="hljs-built_in">Date</span>&lt;/th&gt;
                            &lt;th className=<span class="hljs-string">"px-4 py-2"</span>&gt;Amount (ETH)&lt;/th&gt;
                            &lt;th className=<span class="hljs-string">"px-4 py-2"</span>&gt;Status&lt;/th&gt;
                        &lt;/tr&gt;
                        &lt;/thead&gt;
                        &lt;tbody&gt;
                        {transactions.map(<span class="hljs-function">(<span class="hljs-params">tx</span>) =&gt;</span> {
                            <span class="hljs-keyword">const</span> <span class="hljs-keyword">type</span> =
                                tx.from.toLowerCase() === address?.toLowerCase()
                                    ? <span class="hljs-string">"Sent"</span>
                                    : <span class="hljs-string">"Received"</span>;

                            <span class="hljs-keyword">const</span> date = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(
                                <span class="hljs-built_in">parseInt</span>(tx.timeStamp.toString()) * <span class="hljs-number">1000</span>
                            ).toLocaleString();

                            <span class="hljs-keyword">const</span> amount = (<span class="hljs-built_in">parseFloat</span>(tx.value) / <span class="hljs-number">1e18</span>).toFixed(<span class="hljs-number">5</span>);

                            <span class="hljs-keyword">return</span> (
                                &lt;tr key={tx.hash} className=<span class="hljs-string">"border-t hover:bg-gray-50"</span>&gt;
                                    &lt;td className=<span class="hljs-string">"px-4 py-2"</span>&gt;{tx.from}&lt;/td&gt;
                                    &lt;td className=<span class="hljs-string">"px-4 py-2"</span>&gt;{tx.to}&lt;/td&gt;
                                    &lt;td className=<span class="hljs-string">"px-4 py-2 text-center"</span>&gt;{<span class="hljs-keyword">type</span>}&lt;/td&gt;
                                    &lt;td className=<span class="hljs-string">"px-4 py-2"</span>&gt;{date}&lt;/td&gt;
                                    &lt;td className=<span class="hljs-string">"px-4 py-2"</span>&gt;{amount}&lt;/td&gt;
                                    &lt;td className=<span class="hljs-string">"px-4 py-2 text-center"</span>&gt;
                                        &lt;a
                                            href={<span class="hljs-string">`https://sepolia.etherscan.io/tx/<span class="hljs-subst">${tx.hash}</span>`</span>}
                                            target=<span class="hljs-string">"_blank"</span>
                                            rel=<span class="hljs-string">"noopener noreferrer"</span>
                                            className=<span class="hljs-string">"inline-flex items-center gap-1 text-blue-600 hover:underline"</span>
                                        &gt;
                                            View &lt;ExternalLink className=<span class="hljs-string">"w-4 h-4"</span>/&gt;
                                        &lt;/a&gt;
                                    &lt;/td&gt;
                                &lt;/tr&gt;
                            );
                        })}
                        &lt;/tbody&gt;
                    &lt;/table&gt;
                &lt;/div&gt;
            )}

            {<span class="hljs-comment">/* Pagination Controls */</span>}
            &lt;div className=<span class="hljs-string">"flex justify-between mt-4"</span>&gt;
                &lt;button
                    disabled={page === <span class="hljs-number">1</span>}
                    onClick={<span class="hljs-function">() =&gt;</span> setPage(<span class="hljs-function">(<span class="hljs-params">p</span>) =&gt;</span> <span class="hljs-built_in">Math</span>.max(p - <span class="hljs-number">1</span>, <span class="hljs-number">1</span>))}
                    className=<span class="hljs-string">"px-4 py-2 border rounded disabled:opacity-50"</span>
                &gt;
                    Previous
                &lt;/button&gt;
                &lt;span className=<span class="hljs-string">"px-4 py-2"</span>&gt;Page {page}&lt;/span&gt;
                &lt;button
                    onClick={<span class="hljs-function">() =&gt;</span> setPage(<span class="hljs-function">(<span class="hljs-params">p</span>) =&gt;</span> p + <span class="hljs-number">1</span>)}
                    className=<span class="hljs-string">"px-4 py-2 border rounded"</span>
                &gt;
                    Next
                &lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    );
}
</code></pre>
<h3 id="heading-2-key-features">2. Key Features</h3>
<ul>
<li><p><strong>Etherscan API integration</strong> – fetches real transaction data</p>
</li>
<li><p><strong>Pagination support</strong> – only 5 transactions per page</p>
</li>
<li><p><strong>Clickable transaction hashes</strong> – link to Sepolia Etherscan explorer</p>
</li>
<li><p><strong>Formatted ETH values &amp; timestamps</strong></p>
</li>
</ul>
<h3 id="heading-3-user-flow">3. User Flow</h3>
<ol>
<li><p>User navigates to <strong>Transactions Page</strong></p>
</li>
<li><p>dApp fetches their <strong>latest transactions</strong> from Sepolia testnet</p>
</li>
<li><p>Users can browse through pages for full history</p>
</li>
<li><p>Each transaction is clickable → Opens in <strong>Etherscan</strong></p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755810778355/78a8d169-6048-4aa7-907b-e3fc9de9bfd2.png" alt class="image--center mx-auto" /></p>
<p>In this guide, we built a simple yet powerful <strong>Ethereum dApp</strong> using <strong>Next.js, Wagmi, and MetaMask</strong>.<br />We walked through:</p>
<ul>
<li><p>🔗 <strong>Connecting a crypto wallet</strong> with MetaMask</p>
</li>
<li><p>👛 <strong>Checking wallet balances</strong> on the Sepolia testnet</p>
</li>
<li><p>💸 <strong>Sending ETH transactions</strong> directly from the browser</p>
</li>
<li><p>📜 <strong>Viewing transaction history</strong> with pagination and Etherscan links</p>
</li>
</ul>
<p>This project gives you a <strong>solid foundation</strong> for building decentralized applications. From here, you can expand in many exciting directions:</p>
<h3 id="heading-possible-next-steps">🚀 Possible Next Steps</h3>
<ul>
<li><p><strong>Token Transfers (ERC-20):</strong> Add support for sending tokens like USDC or DAI.</p>
</li>
<li><p><strong>Smart Contracts:</strong> Deploy your own contracts and interact with them.</p>
</li>
<li><p><strong>NFT Support (ERC-721/1155):</strong> Mint, transfer, and display NFTs in your dApp.</p>
</li>
<li><p><strong>Improved UI/UX:</strong> Add notifications, confirmations, and a polished dashboard.</p>
</li>
<li><p><strong>Security Features:</strong> Integrate rate limiting, error handling, and input validation.</p>
</li>
</ul>
<p>👉 This dApp is just the starting point. With these building blocks, you can craft anything from a <strong>DeFi dashboard</strong> to a <strong>full-scale Web3 application</strong>.</p>
<hr />
<p>🌐 <strong>Try it out yourself</strong><br />You can explore the live version here: <a target="_blank" href="https://crypto-wallet-management.vercel.app/"><strong>Deployed dApp</strong></a></p>
<p>💻 <strong>View or fork the code</strong><br />Check out the full source code on GitHub: <a target="_blank" href="https://github.com/icon-gaurav/crypto-wallet-management"><strong>GitHub Repo</strong></a></p>
<hr />
<h3 id="heading-whats-next-in-our-blog-series">🔮 <strong>What’s Next in Our Blog Series?</strong></h3>
<p>In the next post, we’ll explore <strong>integrating smart contracts into our dApp</strong> so that users can interact with decentralized logic directly from the UI.</p>
]]></content:encoded></item><item><title><![CDATA[How I Created an MCP Server for PostgreSQL to Power AI Agents — Components, Architecture & Real Testing]]></title><description><![CDATA[Recently, I built a fully functional MCP (Model Control Plane) server powered by PostgreSQL—something I found incredibly useful when trying to make LLMs interact intelligently with structured data. If you've ever struggled to bolt on a REST API for e...]]></description><link>https://gauravbytes.dev/how-i-created-an-mcp-server-for-postgresql-to-power-ai-agents-components-architecture-and-real-testing</link><guid isPermaLink="true">https://gauravbytes.dev/how-i-created-an-mcp-server-for-postgresql-to-power-ai-agents-components-architecture-and-real-testing</guid><category><![CDATA[mcp]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[AI]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[claude.ai]]></category><category><![CDATA[Python]]></category><category><![CDATA[llm]]></category><category><![CDATA[Developer]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Tue, 13 May 2025 08:05:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746992148580/1aa31433-dc65-43cb-8eaf-1335c35a0f52.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I built a fully functional MCP (Model Control Plane) server powered by PostgreSQL—something I found incredibly useful when trying to make LLMs interact intelligently with structured data. If you've ever struggled to bolt on a REST API for every new resource or wished your AI agent could just “understand” your backend without glue code, this article is for you. Traditional APIs feel like a bottleneck when building modern, adaptive systems—especially when you’re trying to connect them to large language models or automation flows. That’s where MCP shines.</p>
<p>Instead of rigid endpoints and verbose documentation, MCP offers a dynamic, model-based architecture that’s inherently more compatible with how LLMs operate. In this guide, I’ll show you how to build an MCP server backed by PostgreSQL using FastMCP and psycopg2, explain the pain points it solves and walk you through the key steps to get it running—so you can focus on building smarter systems without drowning in boilerplate. Let’s dive in.</p>
<h1 id="heading-what-is-an-mcp-server"><strong>What is an MCP Server?</strong></h1>
<p>Model Context Protocol (MCP) is an open-source set of rules, developed by Anthropic, that establishes a standard way for applications to provide relevant context to Large Language Models (LLMs).</p>
<p>Envision MCP as a universal connector, much like a USB-C port for AI. Just as USB-C allows various devices to connect to a wide range of accessories in a standardized manner, MCP enables AI models to seamlessly interface with diverse data sources and tools. This standardization simplifies the process of building AI agents and complex workflows, offering pre-built integrations and the flexibility to work with different LLM providers while prioritizing data security within your own infrastructure.</p>
<p>Without interfaces like MCP, LLMs are limited to their built-in capabilities and training data. With MCP, they can be empowered to:</p>
<ul>
<li><p>Read files and databases</p>
</li>
<li><p>Execute commands</p>
</li>
<li><p>Access APIs</p>
</li>
<li><p>Interact with local tools</p>
</li>
<li><p>And more!</p>
</li>
</ul>
<p>All of this happens with user oversight and permission, making it both powerful and secure.</p>
<h1 id="heading-mcp-architecture">MCP Architecture</h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746294509997/436dab47-ec18-4b70-a603-ec8b30f14994.png" alt class="image--center mx-auto" /></p>
<p><strong>MCP</strong> has the following components</p>
<ul>
<li><p><strong>MCP Hosts</strong>: Programs like Claude Desktop, IDEs, or AI tools that want to access data through MCP</p>
</li>
<li><p><strong>MCP Clients</strong>: Protocol clients that maintain 1:1 connections with servers</p>
</li>
<li><p><strong>MCP Servers</strong>: Lightweight programs that each expose specific capabilities through the standardized Model Context Protocol</p>
</li>
<li><p><strong>Local Data Sources</strong>: Your computer’s files, databases, and services that MCP servers can securely access</p>
</li>
<li><p><strong>Remote Services</strong>: External systems available over the internet (e.g., through APIs) that MCP servers can connect to</p>
</li>
</ul>
<h1 id="heading-core-mcp-concepts"><strong>Core MCP Concepts</strong></h1>
<p><strong>MCP</strong> servers can provide three main types of capabilities:</p>
<ol>
<li><p><strong>Resources</strong>: File-like data that can be read by clients (like API responses or file contents)</p>
</li>
<li><p><strong>Tools</strong>: Functions that can be called by the LLM (with user approval)</p>
</li>
<li><p><strong>Prompts</strong>: Pre-written templates that help users accomplish specific tasks</p>
</li>
</ol>
<h2 id="heading-mcp-resource">MCP Resource</h2>
<p>Resources are MCP’s way of exposing read-only data to LLMs. A resource is anything that has content that can be read, such as:</p>
<ul>
<li><p>Files on your computer</p>
</li>
<li><p>Database records</p>
</li>
<li><p>API responses</p>
</li>
<li><p>Application data</p>
</li>
<li><p>System information</p>
</li>
</ul>
<p>Each resource has:</p>
<ul>
<li><p>A unique URI (like [<code>file:///example.txt</code>](file:///example.txt) or <code>database://users/123</code>)</p>
</li>
<li><p>A display name</p>
</li>
<li><p>Optional metadata (description, MIME type)</p>
</li>
<li><p>Content (text or binary data)</p>
</li>
</ul>
<h2 id="heading-mcp-tools"><strong>MCP Tools</strong></h2>
<p>Tools are executable functions that LLMs can call to perform actions or retrieve dynamic information. Unlike resources, which are read-only, and prompts, which structure LLM interactions, tools allow LLMs to actively do things like calculate values, make API calls, or modify data.</p>
<p>Tools enable LLMs to interact with systems and perform actions.</p>
<h2 id="heading-what-are-mcp-prompts"><strong>What are MCP Prompts?</strong></h2>
<p>Prompts in MCP are structured templates that servers provide to standardize interactions with language models. Unlike resources which provide data, or tools which execute actions, prompts define reusable message sequences and workflows that help guide LLM behavior in consistent, predictable ways.</p>
<p>In this article we are going to create mcp server for postgreSQL step by step</p>
<h1 id="heading-setting-up-your-development-environment">Setting Up Your Development Environment</h1>
<p>First, let’s install <code>uv</code> and set up our Python project and environment:</p>
<pre><code class="lang-powershell">powershell <span class="hljs-literal">-ExecutionPolicy</span> ByPass <span class="hljs-literal">-c</span> <span class="hljs-string">"irm https://astral.sh/uv/install.ps1 | iex"</span>
</code></pre>
<p>Make sure to restart your terminal afterwards to ensure that the <code>uv</code> command gets picked up.</p>
<h2 id="heading-create-project-directory">Create Project Directory</h2>
<p>Now, let’s create and set up our project:</p>
<pre><code class="lang-powershell"><span class="hljs-comment"># Create a new directory for our project</span>
uv init mcp<span class="hljs-literal">-server</span>
<span class="hljs-built_in">cd</span> mcp<span class="hljs-literal">-server</span>

<span class="hljs-comment"># Create virtual environment and activate it</span>
uv venv
.venv\Scripts\activate

<span class="hljs-comment"># Install dependencies</span>
uv add mcp[<span class="hljs-type">cli</span>] httpx

<span class="hljs-comment"># Create our server file</span>
<span class="hljs-built_in">new-item</span> server.py
</code></pre>
<p>Now let’s dive into building your server.</p>
<h2 id="heading-building-your-server">Building your server</h2>
<p>Update the <code>server.py</code> according to the following code:</p>
<ol>
<li><p>Import the packages</p>
</li>
<li><p>Database wrapper class for database connection and database queries</p>
</li>
<li><p>Define App Context</p>
</li>
<li><p>Define app lifespan and pass this lifespan to MCP instance along with MCP Server name</p>
</li>
</ol>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> contextlib <span class="hljs-keyword">import</span> asynccontextmanager
<span class="hljs-keyword">from</span> collections.abc <span class="hljs-keyword">import</span> AsyncIterator
<span class="hljs-keyword">from</span> dataclasses <span class="hljs-keyword">import</span> dataclass

<span class="hljs-keyword">import</span> asyncpg
<span class="hljs-keyword">from</span> mcp.server.fastmcp <span class="hljs-keyword">import</span> FastMCP, Context

<span class="hljs-comment"># Async PostgreSQL wrapper using asyncpg</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Database</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, pool: asyncpg.Pool</span>):</span>
        self.pool = pool

<span class="hljs-meta">    @classmethod</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">connect</span>(<span class="hljs-params">cls</span>) -&gt; "Database":</span>
        pool = <span class="hljs-keyword">await</span> asyncpg.create_pool(
            user=<span class="hljs-string">"#user"</span>,
            password=<span class="hljs-string">"#your_password"</span>,
            database=<span class="hljs-string">"Your_database_name"</span>,
            host=<span class="hljs-string">"#database_host"</span>,
            port=<span class="hljs-string">'#Port_number'</span>,
        )
        <span class="hljs-keyword">return</span> cls(pool)

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">disconnect</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">await</span> self.pool.close()

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">query</span>(<span class="hljs-params">self, query: str</span>) -&gt; list:</span>
        <span class="hljs-keyword">async</span> <span class="hljs-keyword">with</span> self.pool.acquire() <span class="hljs-keyword">as</span> conn:
            <span class="hljs-keyword">try</span>:
                <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> conn.fetch(query)
            <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
                print(<span class="hljs-string">f"Query error: <span class="hljs-subst">{e}</span>"</span>)
                <span class="hljs-keyword">return</span> []

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_schema</span>(<span class="hljs-params">self</span>) -&gt; list:</span>
        query = <span class="hljs-string">"""
        SELECT table_name FROM information_schema.tables
        WHERE table_schema = 'public'
        """</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> self.query(query)


<span class="hljs-meta">@dataclass</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppContext</span>:</span>
    db: Database


<span class="hljs-meta">@asynccontextmanager</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">app_lifespan</span>(<span class="hljs-params">server: FastMCP</span>) -&gt; AsyncIterator[AppContext]:</span>
    db = <span class="hljs-keyword">await</span> Database.connect()
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">yield</span> AppContext(db=db)
    <span class="hljs-keyword">finally</span>:
        <span class="hljs-keyword">await</span> db.disconnect()


mcp = FastMCP(<span class="hljs-string">"PostgresMCPServer"</span>, lifespan=app_lifespan)
</code></pre>
<p>The <strong>FastMCP</strong> class uses Python type hints and docstrings to automatically generate tool definitions, making it easy to create and maintain MCP tools.</p>
<h2 id="heading-implementing-mcp-tools"><strong>Implementing MCP tools</strong></h2>
<p>The tool execution handler is responsible for actually executing the logic of each tool. Let’s add it:</p>
<pre><code class="lang-python"><span class="hljs-meta">@mcp.tool("fetch_schema")</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_schema</span>(<span class="hljs-params">ctx: Context</span>) -&gt; str:</span>
    db = ctx.request_context.lifespan_context.db
    schema = <span class="hljs-keyword">await</span> db.fetch_schema()
    <span class="hljs-keyword">return</span> str([record[<span class="hljs-string">"table_name"</span>] <span class="hljs-keyword">for</span> record <span class="hljs-keyword">in</span> schema])


<span class="hljs-meta">@mcp.tool("fetch_all_tables")</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_all_tables</span>(<span class="hljs-params">ctx: Context</span>) -&gt; str:</span>
    db = ctx.request_context.lifespan_context.db
    query = <span class="hljs-string">"SELECT * FROM information_schema.tables WHERE table_schema='public'"</span>
    tables = <span class="hljs-keyword">await</span> db.query(query)
    <span class="hljs-keyword">return</span> str(tables)


<span class="hljs-meta">@mcp.tool("run_query")</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">run_query</span>(<span class="hljs-params">ctx: Context, query: str</span>) -&gt; str:</span>
    db = ctx.request_context.lifespan_context.db
    result = <span class="hljs-keyword">await</span> db.query(query)
    <span class="hljs-keyword">return</span> str(result)
</code></pre>
<h2 id="heading-httpsmodelcontextprotocolioquickstartserverrunning-the-serverrunning-the-server"><a target="_blank" href="https://modelcontextprotocol.io/quickstart/server#running-the-server"><strong>​</strong></a><strong>Running the server</strong></h2>
<p>Finally, let’s initialize and run the server:</p>
<pre><code class="lang-python"><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    <span class="hljs-comment"># Initialize and run the server</span>
    mcp.run()
</code></pre>
<p>Your server is complete! Run <code>uv run server.py</code> to confirm that everything’s working.</p>
<p>Let’s now test your server from an existing MCP host, Claude for Desktop.</p>
<h1 id="heading-testing-your-server-with-claude-for-desktop"><strong>Testing your server with Claude for Desktop</strong></h1>
<p>There are several ways to test your MCP server. One way is with Claude Desktop, and you can also use the MCP Inspector tool to test all capabilities during development.</p>
<h2 id="heading-setting-up-claude-desktop"><strong>Setting Up Claude Desktop</strong></h2>
<p>Here are the steps for setting up an MCP server in Claude Desktop:</p>
<ol>
<li><p>Install Claude for Desktop if you haven’t already</p>
</li>
<li><p>Open Claude and access Settings</p>
<p> You can access it in your [path of claude desktop installation]<strong>/claude_desktop_config.json</strong></p>
</li>
<li><p>Edit configuration</p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"mcpServers"</span>: {
     <span class="hljs-attr">"PostgresMCPServer"</span>: {
       <span class="hljs-attr">"command"</span>: <span class="hljs-string">"uv"</span>,
       <span class="hljs-attr">"args"</span>: [
         <span class="hljs-string">"run"</span>,
         <span class="hljs-string">"--with"</span>,
         <span class="hljs-string">"mcp[cli]"</span>,
         <span class="hljs-string">"mcp"</span>,
         <span class="hljs-string">"run"</span>,
         <span class="hljs-string">"C:\\ABSOLUTE\\PATH\\TO\\PARENT\\FOLDER\\server.py"</span>
       ]
     }
   }
 }
</code></pre>
</li>
</ol>
<p>This tells Claude for Desktop:</p>
<ol>
<li><p>There’s an MCP server named “weather”</p>
</li>
<li><p>To launch it by running <code>uv command</code> with the following <code>arguments</code></p>
</li>
</ol>
<p>Save the file, and restart <strong>Claude for Desktop</strong>.</p>
<h2 id="heading-test-with-claude-desktop"><strong>Test with Claude Desktop</strong></h2>
<p>Let’s make sure Claude for Desktop is picking up the 3 tools we’ve exposed in our <code>PostgresMCPServer</code> server. You can do this by looking for the setting icon</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746987377731/b1237596-594a-4224-8f9c-c86b6a60cae7.png" alt class="image--center mx-auto" /></p>
<p>After clicking on the setting icon, you should see the MCP server listed:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746987466775/2d4411d5-d79b-4a5a-8b41-fc3b8f2dd047.png" alt class="image--center mx-auto" /></p>
<p>After clicking the <code>PostgresMCPServer</code>, you should see the MCP tools listed:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746987583736/22a85c19-108a-40da-8b2d-6ccfb62ea513.png" alt class="image--center mx-auto" /></p>
<p>If you can see the tools here, you can now test your server</p>
<p>We will test the server using the following commands:</p>
<ol>
<li><p>Retrieve information on all tables within the database.</p>
</li>
<li><p>Use Claude Desktop to summarize the relationships between the tables.</p>
</li>
<li><p>Use Claude to generate a bar graph illustrating the comparison between events and registration counts.</p>
</li>
</ol>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://vimeo.com/1083801920/d9ec481fed?ts=0&amp;share=copy">https://vimeo.com/1083801920/d9ec481fed?ts=0&amp;share=copy</a></div>
<p> </p>
<p>Finally…</p>
<p>Building an MCP server powered by PostgreSQL offers a dynamic and efficient way to integrate large language models with structured data. By leveraging the Model Context Protocol, developers can create adaptive systems that bypass the limitations of traditional APIs, allowing AI agents to interact seamlessly with various data sources and tools. This approach not only simplifies the development process but also enhances the capabilities of AI models, enabling them to perform complex tasks with greater ease and security. By following the steps outlined in this guide, you can set up your own MCP server and unlock new possibilities for smarter, more responsive systems.</p>
]]></content:encoded></item><item><title><![CDATA[🚀 Understanding GraphQL Federation in Microservices Architecture]]></title><description><![CDATA[As applications grow in complexity, microservices become the go-to architectural pattern. But with them comes a new challenge: API sprawl. Each service manages its own schema, leading to a tangled mess of REST endpoints or isolated GraphQL APIs.
Ente...]]></description><link>https://gauravbytes.dev/understanding-graphql-federation-in-microservices-architecture</link><guid isPermaLink="true">https://gauravbytes.dev/understanding-graphql-federation-in-microservices-architecture</guid><category><![CDATA[GraphQL]]></category><category><![CDATA[Microservices]]></category><category><![CDATA[APIs]]></category><category><![CDATA[subgraph]]></category><category><![CDATA[Apollo GraphQL]]></category><category><![CDATA[backend]]></category><category><![CDATA[gateway]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[System Design]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Tue, 22 Apr 2025 05:23:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745222657126/9f8c4e67-957d-4211-a1f0-6a9dd42ef8e1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As applications grow in complexity, microservices become the go-to architectural pattern. But with them comes a new challenge: <strong>API sprawl</strong>. Each service manages its own schema, leading to a tangled mess of REST endpoints or isolated GraphQL APIs.</p>
<p><strong>Enter GraphQL Federation</strong>—a powerful solution that lets you compose a unified GraphQL API from multiple microservices, while keeping each service independently developed and deployed.</p>
<p>In this article, we’ll break down what GraphQL Federation is, how it works, and why it’s a game-changer for modern backend systems.</p>
<h1 id="heading-what-is-graphql-federation">🧩 What Is GraphQL Federation?</h1>
<p><strong>GraphQL Federation</strong> is a technique introduced by <strong>Apollo</strong> that allows multiple GraphQL services (called <em>subgraphs</em>) to be merged into a single, unified API gateway (called the <em>federated gateway</em>).</p>
<p>It solves a critical issue in microservice architectures: how to let teams work independently on their own GraphQL schemas, while still offering a seamless client experience through a <strong>single graph</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745221622982/008c9db7-593c-4826-abce-457f21b748de.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-key-components">Key Components:</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Component</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Subgraph</strong></td><td>An individual GraphQL service with part of the overall schema</td></tr>
<tr>
<td><strong>Federated Gateway</strong></td><td>The GraphQL server that stitches all subgraphs into one unified schema</td></tr>
<tr>
<td><strong>@key</strong> Directive</td><td>Used to define the primary key for an entity shared across subgraphs</td></tr>
<tr>
<td><strong>@requires / @provides</strong></td><td>Manage dependency fields between subgraphs</td></tr>
</tbody>
</table>
</div><h1 id="heading-core-concepts-explained">🔍 Core Concepts Explained</h1>
<h3 id="heading-1-entity-resolution-with-key">1. <strong>Entity Resolution with @key</strong></h3>
<p>If multiple services work on the same entity (e.g., <code>User</code>), the <code>@key</code> directive tells the gateway how to resolve it:</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> User <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">name:</span> String
}
</code></pre>
<h3 id="heading-2-service-extension-with-extends">2. <strong>Service Extension with @extends</strong></h3>
<p>A service can add fields to an entity defined in another service:</p>
<pre><code class="lang-graphql"><span class="hljs-comment"># In the reviews subgraph</span>
extend <span class="hljs-keyword">type</span> User <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
  <span class="hljs-symbol">id:</span> ID! 
  <span class="hljs-symbol">reviews:</span> [Review]
}
</code></pre>
<h3 id="heading-3-gateway-composition">3. <strong>Gateway Composition</strong></h3>
<p>The gateway uses service definitions from each subgraph (via introspection or static configs) and composes them into a single schema that the client can query.</p>
<h1 id="heading-real-world-example-e-commerce-platform">💡 Real-World Example: E-Commerce Platform</h1>
<p>Let’s say you're building an e-commerce platform with the following services:</p>
<ul>
<li><p><strong>User Service</strong>: Manages user data.</p>
</li>
<li><p><strong>Product Service</strong>: Manages products available in the store.</p>
</li>
<li><p><strong>Order Service</strong>: Manages customer orders and ties users to the products they purchase.</p>
</li>
</ul>
<p>With <strong>GraphQL Federation</strong>, each service defines its part of the schema and can <strong>extend entities</strong> from other services to build a unified graph.</p>
<h3 id="heading-schema-in-user-service">🔹 Schema in User Service:</h3>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> User <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">name:</span> String
  <span class="hljs-symbol">email:</span> String
}
</code></pre>
<p>This defines the <code>User</code> entity and its primary key (<code>id</code>). It can be referenced by other services.</p>
<h3 id="heading-schema-in-product-service">🔹 Schema in Product Service:</h3>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Product <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">name:</span> String
  <span class="hljs-symbol">price:</span> Float
}
</code></pre>
<p>Each product has its own ID, name, and price.</p>
<h3 id="heading-schema-in-order-service">🔹 Schema in Order Service:</h3>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Order <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
   <span class="hljs-symbol">id:</span> ID!
   <span class="hljs-symbol">quantity:</span> Int
   <span class="hljs-symbol">orderDate:</span> String
   <span class="hljs-symbol">user:</span>User <span class="hljs-meta">@provides</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>)
   <span class="hljs-symbol">product:</span>Product <span class="hljs-meta">@provides</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>)
   <span class="hljs-symbol">total:</span> Float
}

extend <span class="hljs-keyword">type</span> User <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
  <span class="hljs-symbol">id:</span> ID! 
}

extend <span class="hljs-keyword">type</span> Product <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
  <span class="hljs-symbol">id:</span> ID! 
}
</code></pre>
<p>This service defines the <code>Order</code> entity, and extends both <code>User</code> and <code>Product</code> entities to show which users placed orders and which products were purchased.</p>
<h3 id="heading-unified-query-at-the-gateway">🧪 Unified Query at the Gateway:</h3>
<p>Once the services are federated into a single gateway, clients can query them as one unified schema:</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">query</span> {
  orders {
    id
    quantity
    orderDate
    user {
      id
      name
      email
    }
    product {
      id
      name
      price
    }
  }
}
</code></pre>
<p>This single query fetches user details, their orders, and associated product details—<strong>even though the data is spread across three separate services</strong>.</p>
<h3 id="heading-how-federation-makes-this-work">🚦 How Federation Makes This Work</h3>
<ul>
<li><p>The <strong>User Service</strong> owns the <code>User</code> type.</p>
</li>
<li><p>The <strong>Product Service</strong> owns the <code>Product</code> type.</p>
</li>
<li><p>The <strong>Order Service</strong> stitches everything together by referencing <code>User</code> and <code>Product</code> entities using the <code>@extends</code> directive.</p>
</li>
</ul>
<p>This setup allows each team to focus on their domain, deploy independently, and still contribute to a <strong>shared graph</strong> that feels seamless to the client.</p>
<h1 id="heading-setting-up-apollo-federation-simplified-steps">⚙️ Setting Up Apollo Federation (Simplified Steps)</h1>
<p>To federate your services using Apollo Federation, follow these steps:</p>
<h3 id="heading-step-1-set-up-each-subgraph-user-product-order">🔧 Step 1: Set Up Each Subgraph (User, Product, Order)</h3>
<p>Each service is an <strong>independent GraphQL server</strong> using the <code>@apollo/subgraph</code> package.</p>
<h4 id="heading-11-install-dependencies">1.1 Install Dependencies</h4>
<pre><code class="lang-bash">npm install @apollo/subgraph graphql
</code></pre>
<h3 id="heading-user-service">👤 <strong>User Service</strong></h3>
<h4 id="heading-schema-user-schemagraphql">Schema (<code>user-schema.graphql</code>)</h4>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> User <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">name:</span> String
  <span class="hljs-symbol">email:</span> String
}
</code></pre>
<h4 id="heading-server-setup-indexjs">Server Setup (<code>index.js</code>)</h4>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { ApolloServer } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@apollo/server'</span>);
<span class="hljs-keyword">const</span> { buildSubgraphSchema } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@apollo/subgraph'</span>);
<span class="hljs-keyword">const</span> { startStandaloneServer } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@apollo/server/standalone'</span>);
<span class="hljs-keyword">const</span> gql = <span class="hljs-built_in">require</span>(<span class="hljs-string">'graphql-tag'</span>);

<span class="hljs-keyword">const</span> typeDefs = gql(<span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>).readFileSync(<span class="hljs-string">'./user-schema.graphql'</span>, <span class="hljs-string">'utf-8'</span>));
<span class="hljs-keyword">const</span> resolvers = {
  <span class="hljs-attr">User</span>: {
    __resolveReference(user) {
      <span class="hljs-keyword">return</span> users.find(<span class="hljs-function"><span class="hljs-params">u</span> =&gt;</span> u.id === user.id);
    },
  },
};

<span class="hljs-keyword">const</span> server = <span class="hljs-keyword">new</span> ApolloServer({
  <span class="hljs-attr">schema</span>: buildSubgraphSchema([{ typeDefs, resolvers }]),
});

startStandaloneServer(server, { <span class="hljs-attr">listen</span>: { <span class="hljs-attr">port</span>: <span class="hljs-number">4001</span> } });
</code></pre>
<hr />
<h3 id="heading-product-service">📦 <strong>Product Service</strong></h3>
<h4 id="heading-schema-product-schemagraphql">Schema (<code>product-schema.graphql</code>)</h4>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Product <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">name:</span> String
  <span class="hljs-symbol">price:</span> Float
}
</code></pre>
<h4 id="heading-server-setup-indexjs-1">Server Setup (<code>index.js</code>)</h4>
<p>Use the same <code>@apollo/subgraph</code> setup, listening on port <code>4002</code>.</p>
<h3 id="heading-order-service">📑 <strong>Order Service</strong></h3>
<p>This one <strong>extends</strong> both <code>User</code> and <code>Product</code> entities.</p>
<h4 id="heading-schema-order-schemagraphql">Schema (<code>order-schema.graphql</code>)</h4>
<pre><code class="lang-graphql"><span class="hljs-keyword">type</span> Order <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">user:</span> User
  <span class="hljs-symbol">products:</span> [Product]
  <span class="hljs-symbol">total:</span> Float
}

extend <span class="hljs-keyword">type</span> User <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">orders:</span> [Order]
}

extend <span class="hljs-keyword">type</span> Product <span class="hljs-meta">@key</span>(<span class="hljs-symbol">fields:</span> <span class="hljs-string">"id"</span>) {
  <span class="hljs-symbol">id:</span> ID!
  <span class="hljs-symbol">purchasedBy:</span> [User]
}
</code></pre>
<h4 id="heading-server-setup">Server Setup</h4>
<p>Use the same <code>@apollo/subgraph</code> setup, listening on port <code>4003</code>.</p>
<h3 id="heading-step-2-set-up-apollo-gateway">🛠 Step 2: Set Up Apollo Gateway</h3>
<p>This service <strong>composes the subgraphs</strong> and exposes one unified schema.</p>
<h4 id="heading-install-required-packages">Install Required Packages</h4>
<pre><code class="lang-bash">npm install @apollo/gateway graphql @apollo/server
</code></pre>
<h4 id="heading-gateway-setup-gatewayjs">Gateway Setup (<code>gateway.js</code>)</h4>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { ApolloServer } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@apollo/server'</span>);
<span class="hljs-keyword">const</span> { ApolloGateway } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@apollo/gateway'</span>);
<span class="hljs-keyword">const</span> { startStandaloneServer } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@apollo/server/standalone'</span>);

<span class="hljs-keyword">const</span> gateway = <span class="hljs-keyword">new</span> ApolloGateway({
  <span class="hljs-attr">serviceList</span>: [
    { <span class="hljs-attr">name</span>: <span class="hljs-string">'user'</span>, <span class="hljs-attr">url</span>: <span class="hljs-string">'http://localhost:4001'</span> },
    { <span class="hljs-attr">name</span>: <span class="hljs-string">'product'</span>, <span class="hljs-attr">url</span>: <span class="hljs-string">'http://localhost:4002'</span> },
    { <span class="hljs-attr">name</span>: <span class="hljs-string">'order'</span>, <span class="hljs-attr">url</span>: <span class="hljs-string">'http://localhost:4003'</span> },
  ],
});

<span class="hljs-keyword">const</span> server = <span class="hljs-keyword">new</span> ApolloServer({ gateway, <span class="hljs-attr">subscriptions</span>: <span class="hljs-literal">false</span> });

startStandaloneServer(server, { <span class="hljs-attr">listen</span>: { <span class="hljs-attr">port</span>: <span class="hljs-number">4000</span> } }).then(<span class="hljs-function">(<span class="hljs-params">{ url }</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`🚀 Gateway ready at <span class="hljs-subst">${url}</span>`</span>);
});
</code></pre>
<hr />
<h3 id="heading-step-3-start-all-services">🔁 Step 3: Start All Services</h3>
<p>In separate terminal tabs or scripts:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Terminal 1</span>
node user/index.js

<span class="hljs-comment"># Terminal 2</span>
node product/index.js

<span class="hljs-comment"># Terminal 3</span>
node order/index.js

<span class="hljs-comment"># Terminal 4</span>
node gateway.js
</code></pre>
<p>Now your GraphQL API is live at <a target="_blank" href="http://localhost:4000/graphql"><code>http://localhost:4000/graphql</code></a> and unified!</p>
<h3 id="heading-step-4-query-the-federated-schema">🧪 Step 4: Query the Federated Schema</h3>
<p>Test it with a powerful, nested query:</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">query</span> {
  orders {
    id
    quantity
    orderDate
    user {
      id
      name
      email
    }
    product {
      id
      name
      price
    }
  }
}
</code></pre>
<p>Even though <code>user</code>, <code>order</code>, and <code>product</code> are handled by <strong>separate microservices</strong>, this <strong>single query works flawlessly</strong> via federation!</p>
<h1 id="heading-pros-and-cons-of-using-graphql-federation">⚖️ Pros and Cons of Using GraphQL Federation</h1>
<p>Before adopting GraphQL Federation in your architecture, it’s important to weigh its advantages and potential challenges. Here’s a balanced look:</p>
<h3 id="heading-pros-of-graphql-federation">✅ Pros of GraphQL Federation</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Benefit</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Modular Architecture</strong></td><td>Each subgraph service is owned and maintained by individual teams. This promotes autonomy and scalability.</td></tr>
<tr>
<td><strong>Single Unified Graph</strong></td><td>The client interacts with one clean, unified API—regardless of how many services are involved behind the scenes.</td></tr>
<tr>
<td><strong>Independent Deployment</strong></td><td>Subgraphs can be deployed independently without needing to rebuild or restart the gateway or other services.</td></tr>
<tr>
<td><strong>Schema Collaboration</strong></td><td>Teams can contribute to shared entities (e.g., <code>User</code>, <code>Product</code>) using directives like <code>@extends</code> and <code>@key</code>, enabling tight yet controlled coupling.</td></tr>
<tr>
<td><strong>Optimized Developer Experience</strong></td><td>Great tools from Apollo like <code>Rover</code>, schema registry, and schema checks make collaboration and CI/CD smooth.</td></tr>
<tr>
<td><strong>Frontend Simplicity</strong></td><td>Frontend developers can query complex relationships in a single request, improving developer productivity and reducing over-fetching.</td></tr>
</tbody>
</table>
</div><h3 id="heading-cons-of-graphql-federation">⚠️ Cons of GraphQL Federation</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Limitation</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Increased Operational Complexity</strong></td><td>Managing multiple subgraph services and the gateway adds overhead to DevOps and deployment pipelines.</td></tr>
<tr>
<td><strong>Cross-Team Coordination</strong></td><td>Schema design requires coordination across teams when extending shared entities, especially in larger orgs.</td></tr>
<tr>
<td><strong>Performance Bottlenecks</strong></td><td>Poorly designed federated queries can result in N+1 problems or excessive network calls between services. Caching and batching become more critical.</td></tr>
<tr>
<td><strong>Learning Curve</strong></td><td>Developers must understand GraphQL directives (<code>@key</code>, <code>@extends</code>, etc.), entity resolution, and subgraph architecture.</td></tr>
<tr>
<td><strong>Debugging Can Be Tricky</strong></td><td>When things break, it can be harder to trace errors across subgraphs and the gateway.</td></tr>
<tr>
<td><strong>Gateway is a Single Point of Failure</strong></td><td>Unless properly scaled and load-balanced, the Apollo Gateway can become a bottleneck or SPOF (single point of failure).</td></tr>
</tbody>
</table>
</div><h3 id="heading-when-to-use-graphql-federation">🧠 When to Use GraphQL Federation</h3>
<p>Use Federation when:</p>
<ul>
<li><p>You have multiple teams working on different domains (e.g., user, orders, products).</p>
</li>
<li><p>You want to avoid monolithic GraphQL servers.</p>
</li>
<li><p>You’re already on a microservices architecture and need a clean API layer.</p>
</li>
<li><p>You need to enable schema composition and controlled entity extension.</p>
</li>
</ul>
<p>Avoid Federation when:</p>
<ul>
<li><p>You have a small team or a small monolithic app.</p>
</li>
<li><p>Your microservices don't share much schema or are loosely coupled.</p>
</li>
<li><p>You’re not familiar with GraphQL or don’t have time to invest in tooling/setup.</p>
</li>
</ul>
<h1 id="heading-conclusion-future-proof-your-backend">🎯 Conclusion: Future-Proof Your Backend</h1>
<p>GraphQL Federation brings structure, scalability, and clarity to the chaos of microservice APIs. It aligns perfectly with the modular development goals of backend teams, while keeping the developer experience smooth on both server and client ends.</p>
<p>If you're building a microservices architecture or already using GraphQL, <strong>federation is worth exploring</strong>—especially with tools like Apollo Federation leading the way.</p>
<p>👉 <strong>Ready to federate your GraphQL APIs?</strong><br />Start small—break one schema into subgraphs and try running a gateway locally. You’ll see the power of Federation in action.</p>
<p><strong>Don’t forget to share this post with your team and bookmark it for reference!</strong></p>
]]></content:encoded></item><item><title><![CDATA[Using DataLoader to Batch and Optimize Database Queries in GraphQL ⚡]]></title><description><![CDATA[What is DataLoader?
DataLoader is a generic utility developed by Facebook for batching and caching database queries efficiently in GraphQL applications. It helps in reducing redundant queries and solving the N+1 query problem by grouping multiple que...]]></description><link>https://gauravbytes.dev/using-dataloader-to-batch-and-optimize-database-queries-in-graphql</link><guid isPermaLink="true">https://gauravbytes.dev/using-dataloader-to-batch-and-optimize-database-queries-in-graphql</guid><category><![CDATA[Node.js]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[APIs]]></category><category><![CDATA[performance]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Express]]></category><category><![CDATA[backend]]></category><category><![CDATA[software development]]></category><category><![CDATA[SQL]]></category><category><![CDATA[MongoDB]]></category><category><![CDATA[prisma]]></category><category><![CDATA[Sequelize]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 17 Mar 2025 15:59:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742067484075/8c085cad-4915-4b16-a088-03ff760b6d95.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-what-is-dataloader">What is DataLoader?</h1>
<p>DataLoader is a generic utility developed by Facebook for batching and caching database queries efficiently in GraphQL applications. It helps in reducing redundant queries and solving the <strong>N+1 query problem</strong> by grouping multiple queries into a single batch request.</p>
<h3 id="heading-key-features-of-dataloader">Key Features of DataLoader:</h3>
<ol>
<li><p><strong>Batching</strong>: Combines multiple database requests into a single query to optimize performance.</p>
</li>
<li><p><strong>Caching</strong>: Stores results to prevent redundant database calls in the same request cycle.</p>
</li>
<li><p><strong>Asynchronous Execution</strong>: Uses promises to handle multiple database requests efficiently.</p>
</li>
</ol>
<p>By integrating DataLoader into GraphQL resolvers, we can significantly improve the efficiency and scalability of our APIs.</p>
<h1 id="heading-why-dataloader-is-necessary">Why DataLoader is Necessary 🧐</h1>
<p>GraphQL APIs provide flexibility in data fetching, allowing clients to request only the necessary data. However, this flexibility can lead to the <strong>N+1 query problem</strong>, a common performance issue where multiple queries to related data cause database inefficiencies.</p>
<h1 id="heading-the-n1-query-problem">The N+1 Query Problem 🏗️</h1>
<p>In a GraphQL resolver, if fetching related data requires separate queries for each parent record, it results in an exponential increase in database calls. Consider an example:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> resolvers = {
  <span class="hljs-attr">Query</span>: {
    <span class="hljs-attr">users</span>: <span class="hljs-keyword">async</span> () =&gt; <span class="hljs-keyword">await</span> db.User.findAll(),
  },
  <span class="hljs-attr">User</span>: {
    <span class="hljs-attr">posts</span>: <span class="hljs-keyword">async</span> (parent) =&gt; <span class="hljs-keyword">await</span> db.Post.findAll({ <span class="hljs-attr">where</span>: { <span class="hljs-attr">userId</span>: parent.id } })
  }
};
</code></pre>
<p>If we fetch 10 users along with their posts, the resolver first queries for users (<code>1 query</code>), then for each user, it fetches their posts (<code>10 additional queries</code>). This results in <strong>11 queries</strong> instead of an optimal <strong>2 queries</strong>.</p>
<h2 id="heading-how-dataloader-solves-this-problem">How DataLoader Solves This Problem</h2>
<p><a target="_blank" href="https://github.com/graphql/dataloader">DataLoader</a> batches multiple queries into a single request and caches results to avoid redundant calls. It allows GraphQL resolvers to efficiently fetch related data in bulk. 📦🔗⚡</p>
<h1 id="heading-step-by-step-implementation">Step-by-Step Implementation📝</h1>
<h3 id="heading-1-install-dataloader">1. Install DataLoader 🔧</h3>
<p>If not already installed, add DataLoader to your project:</p>
<pre><code class="lang-sh">npm install dataloader
</code></pre>
<h3 id="heading-2-create-a-dataloader-for-batching-queries">2. Create a DataLoader for Batching Queries 📊</h3>
<p>In your GraphQL setup, define a DataLoader instance to batch and cache database queries.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> DataLoader = <span class="hljs-built_in">require</span>(<span class="hljs-string">'dataloader'</span>);
<span class="hljs-keyword">const</span> db = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./models'</span>);

<span class="hljs-keyword">const</span> userPostLoader = <span class="hljs-keyword">new</span> DataLoader(<span class="hljs-keyword">async</span> (userIds) =&gt; {
  <span class="hljs-keyword">const</span> posts = <span class="hljs-keyword">await</span> db.Post.findAll({ <span class="hljs-attr">where</span>: { <span class="hljs-attr">userId</span>: userIds } });

  <span class="hljs-keyword">const</span> postsByUserId = userIds.map(<span class="hljs-function"><span class="hljs-params">id</span> =&gt;</span> posts.filter(<span class="hljs-function"><span class="hljs-params">post</span> =&gt;</span> post.userId === id));
  <span class="hljs-keyword">return</span> postsByUserId;
});
</code></pre>
<h3 id="heading-3-integrate-dataloader-in-resolvers">3. Integrate DataLoader in Resolvers 🔌</h3>
<p>Modify the resolver to use DataLoader instead of making separate queries.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> resolvers = {
  <span class="hljs-attr">Query</span>: {
    <span class="hljs-attr">users</span>: <span class="hljs-keyword">async</span> () =&gt; <span class="hljs-keyword">await</span> db.User.findAll(),
  },
  <span class="hljs-attr">User</span>: {
    <span class="hljs-attr">posts</span>: <span class="hljs-function">(<span class="hljs-params">parent, args, context</span>) =&gt;</span> context.userPostLoader.load(parent.id)
  }
};
</code></pre>
<h3 id="heading-4-attach-dataloader-to-context">4. Attach DataLoader to Context 🔗</h3>
<p>Ensure DataLoader is available in each request by adding it to the context in your GraphQL server setup.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { ApolloServer } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'apollo-server'</span>);

<span class="hljs-keyword">const</span> server = <span class="hljs-keyword">new</span> ApolloServer({
  typeDefs,
  resolvers,
  <span class="hljs-attr">context</span>: <span class="hljs-function">() =&gt;</span> ({
    <span class="hljs-attr">userPostLoader</span>: userPostLoader
  })
});
</code></pre>
<h1 id="heading-verifying-performance-improvements">Verifying Performance Improvements 📊</h1>
<p>Before implementing DataLoader, let's analyze the number of queries being made. Consider the following GraphQL query:</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">query</span> {
  users {
    id
    name
    posts {
      id
      title
    }
  }
}
</code></pre>
<h3 id="heading-without-dataloader">Without DataLoader : ❌</h3>
<ol>
<li><p><strong>Query to fetch users</strong> (1 query)</p>
<pre><code class="lang-sql"> <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">users</span>;
</code></pre>
</li>
<li><p><strong>Query for each user's posts</strong> (10 queries if there are 10 users)</p>
<pre><code class="lang-sql"> <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> posts <span class="hljs-keyword">WHERE</span> userId = <span class="hljs-number">1</span>;
 <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> posts <span class="hljs-keyword">WHERE</span> userId = <span class="hljs-number">2</span>;
 ...
 <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> posts <span class="hljs-keyword">WHERE</span> userId = <span class="hljs-number">10</span>;
</code></pre>
</li>
</ol>
<p>Total queries: <strong>11</strong></p>
<h3 id="heading-with-dataloader">With DataLoader : ✅</h3>
<ol>
<li><p><strong>Query to fetch users</strong> (1 query)</p>
<pre><code class="lang-sql"> <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">users</span>;
</code></pre>
</li>
<li><p><strong>Single batched query to fetch all posts at once</strong> (1 query)</p>
<pre><code class="lang-sql"> <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> posts <span class="hljs-keyword">WHERE</span> userId <span class="hljs-keyword">IN</span> (<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>,<span class="hljs-number">4</span>,<span class="hljs-number">5</span>,<span class="hljs-number">6</span>,<span class="hljs-number">7</span>,<span class="hljs-number">8</span>,<span class="hljs-number">9</span>,<span class="hljs-number">10</span>);
</code></pre>
</li>
</ol>
<p>Total queries: <strong>2</strong></p>
<h3 id="heading-performance-gains">Performance Gains : 🚀</h3>
<p>By reducing the number of queries from 11 to 2, DataLoader significantly reduces database load and improves response time. The reduction in queries is more noticeable as the dataset grows, ensuring better scalability and performance efficiency. Before DataLoader, fetching posts for 10 users resulted in <strong>11 queries</strong>. With DataLoader, it reduces to <strong>2 queries</strong>:</p>
<ol>
<li><p>Fetch all users</p>
</li>
<li><p>Fetch all posts in a single batch query</p>
</li>
</ol>
<h1 id="heading-integrating-dataloader-with-prismasequelizemongodb">Integrating DataLoader with Prisma/Sequelize/MongoDB 🛠️</h1>
<h3 id="heading-using-dataloader-with-prisma">Using DataLoader with Prisma</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> userPostLoader = <span class="hljs-keyword">new</span> DataLoader(<span class="hljs-keyword">async</span> (userIds) =&gt; {
  <span class="hljs-keyword">const</span> posts = <span class="hljs-keyword">await</span> prisma.post.findMany({
    <span class="hljs-attr">where</span>: { <span class="hljs-attr">userId</span>: { <span class="hljs-attr">in</span>: userIds } },
  });

  <span class="hljs-keyword">const</span> postsByUserId = userIds.map(<span class="hljs-function"><span class="hljs-params">id</span> =&gt;</span> posts.filter(<span class="hljs-function"><span class="hljs-params">post</span> =&gt;</span> post.userId === id));
  <span class="hljs-keyword">return</span> postsByUserId;
});
</code></pre>
<h3 id="heading-using-dataloader-with-sequelize">Using DataLoader with Sequelize</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> userPostLoader = <span class="hljs-keyword">new</span> DataLoader(<span class="hljs-keyword">async</span> (userIds) =&gt; {
  <span class="hljs-keyword">const</span> posts = <span class="hljs-keyword">await</span> Post.findAll({
    <span class="hljs-attr">where</span>: { <span class="hljs-attr">userId</span>: userIds },
  });

  <span class="hljs-keyword">return</span> userIds.map(<span class="hljs-function"><span class="hljs-params">id</span> =&gt;</span> posts.filter(<span class="hljs-function"><span class="hljs-params">post</span> =&gt;</span> post.userId === id));
});
</code></pre>
<h3 id="heading-using-dataloader-with-mongodb-mongoose">Using DataLoader with MongoDB (Mongoose)</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> userPostLoader = <span class="hljs-keyword">new</span> DataLoader(<span class="hljs-keyword">async</span> (userIds) =&gt; {
  <span class="hljs-keyword">const</span> posts = <span class="hljs-keyword">await</span> Post.find({ <span class="hljs-attr">userId</span>: { <span class="hljs-attr">$in</span>: userIds } });

  <span class="hljs-keyword">return</span> userIds.map(<span class="hljs-function"><span class="hljs-params">id</span> =&gt;</span> posts.filter(<span class="hljs-function"><span class="hljs-params">post</span> =&gt;</span> post.userId.toString() === id.toString()));
});
</code></pre>
<h1 id="heading-impact-of-using-dataloader">Impact of Using DataLoader</h1>
<ol>
<li><p><strong>Improved Performance</strong>: Reduces database queries, significantly optimizing response times.</p>
</li>
<li><p><strong>Better Scalability</strong>: Handles large GraphQL queries efficiently, making the API more scalable.</p>
</li>
<li><p><strong>Reduced Database Load</strong>: Batching reduces the number of queries, minimizing database stress.</p>
</li>
<li><p><strong>Caching Benefits</strong>: Avoids redundant database calls by caching previously fetched data.</p>
</li>
</ol>
<h1 id="heading-conclusion">Conclusion 💡</h1>
<p>Using DataLoader in GraphQL APIs is essential for optimizing database queries and solving the N+1 query problem. By batching requests and caching results, it significantly enhances performance and scalability. Whether you are using Prisma, Sequelize, or MongoDB, integrating DataLoader is a best practice for efficient GraphQL APIs.</p>
]]></content:encoded></item><item><title><![CDATA[Integrating Metrics and Analytics with Custom GraphQL Plugins : Enhance GraphQL APIs]]></title><description><![CDATA[Modern web applications demand flexible and high-performing APIs, and GraphQL has become the go-to solution for managing data flow between clients and servers. However, as applications grow in complexity, developers need a way to extend the functiona...]]></description><link>https://gauravbytes.dev/integrating-metrics-and-analytics-with-custom-graphql-plugins-enhance-graphql-apis</link><guid isPermaLink="true">https://gauravbytes.dev/integrating-metrics-and-analytics-with-custom-graphql-plugins-enhance-graphql-apis</guid><category><![CDATA[analytics]]></category><category><![CDATA[metrics]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[APIs]]></category><category><![CDATA[monitoring]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[Apollo GraphQL]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Wed, 01 Jan 2025 05:52:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735634262478/a56460cf-0236-4221-b42f-ddcd4ec5c002.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Modern web applications demand flexible and high-performing APIs, and GraphQL has become the go-to solution for managing data flow between clients and servers. However, as applications grow in complexity, developers need a way to extend the functionality of their GraphQL servers to meet unique requirements such as logging, monitoring, authentication, and more.</p>
<p>Enter <strong>GraphQL plugins</strong>—a powerful mechanism in Apollo Server that allows developers to customize and enhance their API functionality with ease.</p>
<p>In this blog, we’ll explore what GraphQL plugins are, set up a basic Apollo Server, and walk through creating a custom plugin to log query response times. We’ll also delve into advanced use cases and best practices for building scalable and efficient GraphQL APIs with Apollo Server plugins. Let’s dive in!</p>
<h2 id="heading-what-are-graphql-plugins">What Are GraphQL Plugins?</h2>
<p>GraphQL plugins in Apollo Server allow developers to inject custom logic into the server’s lifecycle. Whether it’s tracking performance, managing request authentication, or adding additional logging, plugins empower developers to fine-tune their GraphQL servers for specific needs.</p>
<h2 id="heading-lifecycle-hooks"><strong>Lifecycle Hooks</strong></h2>
<p>Plugins in Apollo Server operate on a series of lifecycle hooks. These hooks allow developers to tap into various stages of a GraphQL operation, such as:</p>
<ol>
<li><p><code>requestDidStart</code>: Triggered when a new GraphQL request is received.</p>
</li>
<li><p><code>didResolveOperation</code>: Triggered after the server successfully parses and validates the query.</p>
</li>
<li><p><code>executionDidStart</code>: Triggered before the query execution begins.</p>
</li>
<li><p><code>willSendResponse</code>: Triggered right before the server sends the response to the client.</p>
</li>
</ol>
<p>By leveraging these hooks, you can implement custom behaviors like query logging, error tracking, or request throttling.</p>
<p>The following diagram illustrates the sequence of events that fire for each request. Each of these events is documented in <a target="_blank" href="https://www.apollographql.com/docs/apollo-server/integrations/plugins-event-reference/">Apollo Server plugin events</a> <a target="_blank" href="https://www.apollographql.com/docs/apollo-server/integrations/plugins-event-reference/">and available on apollo web</a>site.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735631131223/f3d85c3d-eb97-4d8f-96c3-d75221b496f7.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-setting-up-apollo-server">Setting Up Apollo Server</h2>
<p>Before we jump into creating a custom plugin, let’s set up a basic Apollo Server with a simple schema and resolver. If you don’t already have Apollo Server installed, start by setting it up:</p>
<h3 id="heading-step-1-install-dependencies"><strong>Step 1: Install Dependencies</strong></h3>
<p>Run the following command to install the necessary packages:</p>
<pre><code class="lang-bash">npm install @apollo/server graphql
</code></pre>
<h3 id="heading-step-2-define-a-basic-schema"><strong>Step 2: Define a Basic Schema</strong></h3>
<p>Create a file named <code>schema.js</code> with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// we store posts list in a separate file</span>
<span class="hljs-keyword">import</span> {posts} <span class="hljs-keyword">from</span> <span class="hljs-string">"../../utils/data"</span>

<span class="hljs-keyword">const</span> typeDefs = <span class="hljs-string">`#graphql
    type Query {
        posts:[Post]
    }

    type Post {
        id: ID!
        title: String!
        content: String!
    }
`</span>;

<span class="hljs-keyword">const</span> resolvers = {
  <span class="hljs-attr">Query</span>: {
    <span class="hljs-attr">posts</span>: <span class="hljs-function">() =&gt;</span> {
            <span class="hljs-keyword">return</span> posts;
        },
  },
};

<span class="hljs-built_in">module</span>.exports = { typeDefs, resolvers };
</code></pre>
<h3 id="heading-step-3-set-up-the-apollo-server"><strong>Step 3: Set Up the Apollo Server</strong></h3>
<p>Create a file named <code>server.js</code> and set up Apollo Server:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { ApolloServer } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@apollo/server'</span>);
<span class="hljs-keyword">const</span> { typeDefs, resolvers } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./schema'</span>);

<span class="hljs-keyword">const</span> server = <span class="hljs-keyword">new</span> ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(<span class="hljs-function">(<span class="hljs-params">{ url }</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`🚀 Server ready at <span class="hljs-subst">${url}</span>`</span>);
});
</code></pre>
<p>Run the server using the following command:</p>
<pre><code class="lang-bash">node server.js
</code></pre>
<p>At this point, you have a basic Apollo Server running locally.</p>
<h2 id="heading-creating-a-custom-plugin-for-response-time-logging">Creating a Custom Plugin for Response Time Logging</h2>
<p>Now that we have a working Apollo Server, let’s create a custom plugin to log the response time of each GraphQL query.</p>
<h3 id="heading-step-1-understanding-the-plugin-lifecycle"><strong>Step 1: Understanding the Plugin Lifecycle</strong></h3>
<p>To measure response time, we’ll use the <code>requestDidStart</code> and <code>willSendResponse</code> hooks. Here’s the flow:</p>
<ol>
<li><p>Record the start time when the request begins.</p>
</li>
<li><p>Calculate the duration when the response is about to be sent.</p>
</li>
</ol>
<h3 id="heading-step-2-implementing-the-plugin"><strong>Step 2: Implementing the Plugin</strong></h3>
<p>Create a file named <code>loggingPlugin.js</code> with the following content:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> loggingPlugin = {
  <span class="hljs-keyword">async</span> requestDidStart(requestContext) {
    <span class="hljs-keyword">const</span> start = <span class="hljs-built_in">Date</span>.now();
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Query received: <span class="hljs-subst">${requestContext.request.query}</span>`</span>);

    <span class="hljs-keyword">return</span> {
      <span class="hljs-keyword">async</span> willSendResponse() {
        <span class="hljs-keyword">const</span> duration = <span class="hljs-built_in">Date</span>.now() - start;
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Response sent after <span class="hljs-subst">${duration}</span>ms`</span>);
      },
    };
  },
};

<span class="hljs-built_in">module</span>.exports = { loggingPlugin };
</code></pre>
<h3 id="heading-step-3-integrating-the-plugin"><strong>Step 3: Integrating the Plugin</strong></h3>
<p>Modify your <code>server.js</code> file to include the custom plugin:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { ApolloServer } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'@apollo/server'</span>);
<span class="hljs-keyword">const</span> { typeDefs, resolvers } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./schema'</span>);
<span class="hljs-keyword">const</span> { loggingPlugin } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./loggingPlugin'</span>);

<span class="hljs-keyword">const</span> server = <span class="hljs-keyword">new</span> ApolloServer({
  typeDefs,
  resolvers,
  <span class="hljs-attr">plugins</span>: [loggingPlugin],
});

server.listen().then(<span class="hljs-function">(<span class="hljs-params">{ url }</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`🚀 Server ready at <span class="hljs-subst">${url}</span>`</span>);
});
</code></pre>
<p>Restart the server, and you’ll see query logs along with response times in the console.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735631996618/4ef9dda1-89c7-4834-9863-8bd1af98351f.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-advanced-use-cases-for-apollo-server-plugins">Advanced Use Cases for Apollo Server Plugins</h2>
<p>Plugins offer immense flexibility, enabling you to build advanced features for your GraphQL APIs. Here are a few examples:</p>
<h3 id="heading-1-extending-the-logging-plugin"><strong>1. Extending the Logging Plugin</strong></h3>
<p>Enhance the plugin to include user-specific details or operation names in the logs. For instance:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Operation: <span class="hljs-subst">${requestContext.operationName}</span>`</span>);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`User ID: <span class="hljs-subst">${requestContext.context.userId}</span>`</span>);
</code></pre>
<h3 id="heading-2-enforcing-security-and-rate-limiting"><strong>2. Enforcing Security and Rate Limiting</strong></h3>
<p>Implement rate limiting or request validation to prevent abuse. For example:</p>
<ul>
<li><p>Track the number of queries.</p>
</li>
<li><p>Reject requests exceeding a predefined threshold.</p>
</li>
</ul>
<pre><code class="lang-javascript"><span class="hljs-keyword">if</span> (queryCount &gt; MAX_QUERIES) {
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Rate limit exceeded.'</span>);
}
</code></pre>
<h3 id="heading-3-monitoring-and-analytics"><strong>3. Monitoring and Analytics</strong></h3>
<p>Send query performance metrics to external tools like Prometheus, Grafana, or DataDog for detailed monitoring and analytics.</p>
<h3 id="heading-4-dynamic-schema-modifications"><strong>4. Dynamic Schema Modifications</strong></h3>
<p>Build a plugin that dynamically modifies the schema at runtime based on user roles or feature flags.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Apollo Server plugins are a powerful tool for enhancing the functionality and performance of your GraphQL APIs. From logging response times to enforcing security and enabling advanced monitoring, plugins offer endless possibilities to tailor your server to your application’s needs.</p>
<p>By following this guide, you now have the skills to create custom plugins and integrate them seamlessly into your Apollo Server. Experiment with different use cases and share your innovations with the community.</p>
<p>For further learning, check out the <a target="_blank" href="https://www.apollographql.com/docs/apollo-server/">Apollo Server documentation</a>. Happy coding! 🚀</p>
<p>If you found this guide helpful, share it with your network and let us know how you’re using Apollo Server plugins in your projects. Stay tuned for more tutorials on building scalable GraphQL APIs!</p>
]]></content:encoded></item><item><title><![CDATA[Ensuring API Reliability: Metrics, Tools, and Best Practices]]></title><description><![CDATA[APIs serve as the backbone of modern applications, acting as the crucial link that enables seamless communication between various services and systems. Ensuring the reliability of these APIs is essential for maintaining high levels of user satisfacti...]]></description><link>https://gauravbytes.dev/ensuring-api-reliability-metrics-tools-and-best-practices</link><guid isPermaLink="true">https://gauravbytes.dev/ensuring-api-reliability-metrics-tools-and-best-practices</guid><category><![CDATA[APIs]]></category><category><![CDATA[monitoring]]></category><category><![CDATA[Databases]]></category><category><![CDATA[scalability]]></category><category><![CDATA[development]]></category><category><![CDATA[backend]]></category><category><![CDATA[tools]]></category><category><![CDATA[performance]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Mon, 25 Nov 2024 07:13:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732012567530/f92eaa8f-f7c9-4fc4-82aa-49e89cbab47c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>APIs serve as the backbone of modern applications, acting as the crucial link that enables seamless communication between various services and systems. Ensuring the reliability of these APIs is essential for maintaining high levels of user satisfaction, achieving scalability, and ensuring operational efficiency. In this article, we will delve into the fundamental aspects required to ensure API reliability. We will explore the essential metrics that need to be tracked to assess the performance and stability of APIs. Additionally, we will discuss how to effectively monitor these metrics using powerful tools such as <strong>Prometheus</strong>. By understanding and implementing these practices, developers can enhance the robustness of their APIs, leading to improved application performance and a better user experience.</p>
<h3 id="heading-essential-elements-for-ensuring-api-reliability"><strong>Essential Elements for Ensuring API Reliability</strong></h3>
<ol>
<li><p><strong>Performance Monitoring</strong></p>
<ul>
<li><p>Ensure APIs respond quickly and handle concurrent requests efficiently.</p>
</li>
<li><p>Optimize for low latency and high throughput.</p>
</li>
</ul>
</li>
<li><p><strong>Scalability</strong></p>
<ul>
<li><p>Design APIs to handle increasing loads without degradation.</p>
</li>
<li><p>Use load balancers, auto-scaling groups, and caching mechanisms.</p>
</li>
</ul>
</li>
<li><p><strong>Error Handling</strong></p>
<ul>
<li><p>Implement comprehensive error logging and monitoring.</p>
</li>
<li><p>Provide clear error responses for better client-side debugging.</p>
</li>
</ul>
</li>
<li><p><strong>Security</strong></p>
<ul>
<li><p>Secure APIs with authentication, authorization, and encryption.</p>
</li>
<li><p>Monitor for unusual activity or unauthorized access attempts.</p>
</li>
</ul>
</li>
<li><p><strong>Availability</strong></p>
<ul>
<li><p>Aim for high uptime with robust failover mechanisms.</p>
</li>
<li><p>Use redundancy at the network, server, and data levels.</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-key-metrics-to-monitor-for-api-success"><strong>Key Metrics to Monitor for API Success</strong></h3>
<ol>
<li><p><strong>Performance Metrics</strong></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732012896194/e7df149f-88b7-42e3-b692-88a6218affcc.webp" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>Response Time (Latency):</strong> Average, 95th percentile (P95), and 99th percentile (P99) latencies.</p>
</li>
<li><p><strong>Request Rate (RPS):</strong> Total requests per second handled by the API.</p>
</li>
<li><p><strong>Error Rate:</strong> Percentage of requests resulting in errors (4xx or 5xx responses).</p>
</li>
<li><p><strong>Cache Hit Rate:</strong> Frequency of cache hits versus total cache requests.</p>
</li>
</ul>
</li>
<li><p><strong>Infrastructure Metrics</strong></p>
<ul>
<li><p><strong>CPU and Memory Usage:</strong> Resource consumption patterns under load.</p>
</li>
<li><p><strong>Disk I/O:</strong> Storage throughput for reading and writing data.</p>
</li>
<li><p><strong>Thread and Connection Pool Usage:</strong> Health of connection pools and threads.</p>
</li>
</ul>
</li>
<li><p><strong>Reliability Metrics</strong></p>
<ul>
<li><p><strong>Uptime:</strong> Measure of API availability, often reflected as an SLA.</p>
</li>
<li><p><strong>Dependency Latency:</strong> Response time for third-party APIs the service relies on.</p>
</li>
<li><p><strong>Timeouts and Retries:</strong> Frequency of timed-out requests and retries.</p>
</li>
</ul>
</li>
<li><p><strong>Usage Metrics</strong></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732013224567/577e0b11-279b-4e80-ba1c-b0baa7a8d1e9.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>Endpoint Popularity:</strong> Most accessed API endpoints.</p>
</li>
<li><p><strong>User Activity Patterns:</strong> Trends in API usage over time.</p>
</li>
<li><p><strong>Rate Limit Violations:</strong> Incidents where clients exceed allowed limits.</p>
</li>
</ul>
</li>
<li><p><strong>Security Metrics</strong></p>
<ul>
<li><p><strong>Authentication Failures:</strong> Invalid login attempts or token issues.</p>
</li>
<li><p><strong>Unusual IP Activity:</strong> Unexpected access patterns from specific IPs.</p>
</li>
<li><p><strong>Data Integrity Issues:</strong> Monitoring anomalies in data processing or storage.</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-how-to-monitor-these-metrics">How to Monitor These Metrics</h3>
<p>These tools provide complete solutions for monitoring performance, resource use, and reliability metrics.</p>
<ul>
<li><p><strong>Datadog:</strong> Comprehensive monitoring and alerting with integrated APM and logging.</p>
</li>
<li><p><strong>New Relic:</strong> APM with powerful diagnostics and distributed tracing.</p>
</li>
<li><p><strong>AWS CloudWatch:</strong> Built-in monitoring for AWS-based infrastructure and APIs.</p>
</li>
</ul>
<p>These tools stand out due to their wide adoption, robust features, and the ability to address various aspects of API monitoring, from performance metrics to log analysis.</p>
<h3 id="heading-best-practices-for-api-monitoring"><strong>Best Practices for API Monitoring</strong></h3>
<ol>
<li><p><strong>Use Distributed Tracing:</strong> Tools like Jaeger or Zipkin help trace requests across services, identifying bottlenecks.</p>
</li>
<li><p><strong>Implement Logging:</strong> Use structured logging to capture detailed request/response data for troubleshooting.</p>
</li>
<li><p><strong>Automate Alerts:</strong> Set up alerts for anomalies like high error rates, increased latency, or resource exhaustion.</p>
</li>
<li><p><strong>Conduct Regular Load Testing:</strong> Use tools like Apache JMeter or k6 to simulate traffic and identify scaling issues.</p>
</li>
<li><p><strong>Continuously Refine Metrics:</strong> Regularly review and update monitored metrics to align with evolving business needs.</p>
</li>
</ol>
<h3 id="heading-conclusion"><strong>Conclusion</strong></h3>
<p>In conclusion, ensuring API reliability is a multifaceted endeavor that requires careful attention to performance, scalability, error handling, security, and availability. By monitoring key metrics such as response time, error rate, and resource usage, developers can gain valuable insights into the health and performance of their APIs. Utilizing powerful monitoring tools like Prometheus, Datadog, and AWS CloudWatch can aid in effectively tracking these metrics and identifying potential issues before they impact users. Adopting best practices such as distributed tracing, structured logging, and regular load testing further enhances the robustness of APIs. By implementing these strategies, developers can significantly improve application performance, leading to a more reliable and satisfying user experience.</p>
]]></content:encoded></item><item><title><![CDATA[Automate Your API Testing: Integrate Postman with GitHub Actions for Seamless CI/CD]]></title><description><![CDATA[Introduction
Postman is a robust API development environment that enables you to create, send, test, and document APIs efficiently. GitHub Actions is a CI/CD platform designed to automate the testing and deployment of your code. By integrating Postma...]]></description><link>https://gauravbytes.dev/automate-your-api-testing-integrate-postman-with-github-actions-for-seamless-cicd</link><guid isPermaLink="true">https://gauravbytes.dev/automate-your-api-testing-integrate-postman-with-github-actions-for-seamless-cicd</guid><category><![CDATA[Postman]]></category><category><![CDATA[Testing]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[automation]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[Continuous Integration]]></category><category><![CDATA[Automated Testing]]></category><category><![CDATA[APIs]]></category><dc:creator><![CDATA[Gaurav Kumar]]></dc:creator><pubDate>Wed, 04 Sep 2024 08:07:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1725390224751/01788ebe-0adb-48c4-a0f4-7d89d28a4966.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction"><strong>Introduction</strong></h2>
<p>Postman is a robust API development environment that enables you to create, send, test, and document APIs efficiently. GitHub Actions is a CI/CD platform designed to automate the testing and deployment of your code. By integrating Postman with GitHub Actions, you can seamlessly automate your API testing as part of your development workflow.</p>
<h2 id="heading-prerequisites"><strong>Prerequisites:</strong></h2>
<ul>
<li><p>A GitHub account and repository.</p>
</li>
<li><p>A Postman collection containing your API tests.</p>
</li>
<li><p>A Postman environment containing the necessary variables for your tests (e.g., API keys, base URLs).</p>
</li>
</ul>
<h2 id="heading-steps"><strong>Steps:</strong></h2>
<ol>
<li><p><strong>Create a Postman Collection:</strong></p>
<ul>
<li><p>In Postman, create a new collection and add your API test cases.</p>
</li>
<li><p>Configure the necessary environment variables in your Postman environment.</p>
</li>
</ul>
</li>
<li><p><strong>Generate Postman API Key:</strong></p>
<ul>
<li><p>Go to the settings page and then API keys to generate a new API key</p>
</li>
<li><p>Save this API key to GitHub repository secrets</p>
</li>
</ul>
</li>
</ol>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1725388184733/f084a48c-1e47-468d-8107-c40dd7400693.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li><p><strong>Create a GitHub Actions Workflow:</strong></p>
<ul>
<li><p>In your GitHub repository, create a new file named <code>.github/workflows/postman-tests.yml</code>.</p>
</li>
<li><p>Paste the following code into the file, replacing the placeholders with your specific values:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">name:</span> <span class="hljs-string">Automated</span> <span class="hljs-string">Testing</span> <span class="hljs-string">using</span> <span class="hljs-string">Postman</span> <span class="hljs-string">CLI</span>

  <span class="hljs-attr">on:</span>
    <span class="hljs-attr">push:</span>
      <span class="hljs-attr">branches:</span> [<span class="hljs-string">main</span>]

  <span class="hljs-attr">jobs:</span>
    <span class="hljs-attr">automated-api-tests:</span>
      <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
      <span class="hljs-attr">steps:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">Postman</span> <span class="hljs-string">CLI</span>
          <span class="hljs-attr">run:</span> <span class="hljs-string">|
            curl -o- "https://dl-cli.pstmn.io/install/linux64.sh" | sh
</span>        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Login</span> <span class="hljs-string">to</span> <span class="hljs-string">Postman</span> <span class="hljs-string">CLI</span>
          <span class="hljs-attr">run:</span> <span class="hljs-string">postman</span> <span class="hljs-string">login</span> <span class="hljs-string">--with-api-key</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.POSTMAN_API_KEY</span> <span class="hljs-string">}}</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">API</span> <span class="hljs-string">tests</span>
          <span class="hljs-attr">run:</span> <span class="hljs-string">|</span>
            <span class="hljs-string">postman</span> <span class="hljs-string">collection</span> <span class="hljs-string">run</span> <span class="hljs-string">"[collection_id]"</span> <span class="hljs-string">-e</span> <span class="hljs-string">"[environment_id]"</span>
</code></pre>
</li>
</ul>
</li>
<li><p><strong>Commit and Push:</strong></p>
<ul>
<li>Commit and push the <code>.github/workflows/postman-tests.yml</code> file to your GitHub repository.</li>
</ul>
</li>
</ol>
<h2 id="heading-explanation"><strong>Explanation:</strong></h2>
<ul>
<li><p>The workflow is triggered by pushing events to the <code>main</code> branch.</p>
</li>
<li><p>It installs the Postman CLI for Linux</p>
</li>
<li><p>It uses the <code>postman login</code> command to authenticate with the Postman CLI</p>
</li>
<li><p>It runs the Postman collection specified by the <code>[collection_id]</code> placeholder (replace it with your actual collection ID). It also uses the environment defined by the <code>[environment_id]</code> placeholder (replace it with your actual environment ID) for variables.</p>
</li>
</ul>
<h2 id="heading-additional-considerations"><strong>Additional Considerations:</strong></h2>
<ul>
<li><p>You can customize the workflow to run tests on different branches, trigger on pull requests, or use different runners.</p>
</li>
<li><p>Consider using environment variables to store sensitive information (e.g., API keys) securely.</p>
</li>
<li><p>Explore other features of GitHub Actions, such as caching, artifacts, and secrets, to enhance your workflow.</p>
</li>
</ul>
<p>By following these steps, you can effectively automate the testing of your APIs using Postman and GitHub Actions, ensuring that your code changes are thoroughly tested before deployment.</p>
]]></content:encoded></item></channel></rss>