<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DivyanshuLohani</title>
    <description>The latest articles on DEV Community by DivyanshuLohani (@divyanshulohani).</description>
    <link>https://dev.to/divyanshulohani</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F763132%2F37c61118-cff2-42c3-ab2b-910cf50840a1.jpeg</url>
      <title>DEV Community: DivyanshuLohani</title>
      <link>https://dev.to/divyanshulohani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/divyanshulohani"/>
    <language>en</language>
    <item>
      <title>The Hidden Cost of AI Agents: Tracing Tokens, Tool Calls, and Retries in TypeScript</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Wed, 03 Jun 2026 05:21:47 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/the-hidden-cost-of-ai-agents-tracing-tokens-tool-calls-and-retries-in-typescript-42k5</link>
      <guid>https://dev.to/divyanshulohani/the-hidden-cost-of-ai-agents-tracing-tokens-tool-calls-and-retries-in-typescript-42k5</guid>
      <description>&lt;p&gt;AI agents do not become expensive all at once.&lt;/p&gt;

&lt;p&gt;They become expensive one small decision at a time.&lt;/p&gt;

&lt;p&gt;One extra routing call. One confidence check. One retry. One tool failure that triggers another LLM request. One formatting agent that exists because it felt cleaner during the first design.&lt;/p&gt;

&lt;p&gt;Individually, these calls look harmless. Together, they can turn a simple support request into a chain of model calls, tool executions, retries, and post-processing steps that nobody can easily explain from logs alone.&lt;/p&gt;

&lt;p&gt;That is the problem I wanted to explore with a small TypeScript project: a cost-aware customer support agent that routes incoming requests, validates actions, calls internal tools, and generates a final response.&lt;/p&gt;

&lt;p&gt;The goal was not just to build another agent demo. The goal was to answer a more practical engineering question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where is the LLM spend actually going?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.licdn.com%2Fdms%2Fimage%2Fv2%2FD5612AQGDPAtMxuUY9Q%2Farticle-inline_image-shrink_1000_1488%2FB56Z5fJDlgHkAI-%2F0%2F1779712679146%3Fe%3D1782345600%26v%3Dbeta%26t%3DXQRnd7b7OjRmQal0h4r311Zdx2HUyUrDKY8WmQtygLU" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.licdn.com%2Fdms%2Fimage%2Fv2%2FD5612AQGDPAtMxuUY9Q%2Farticle-inline_image-shrink_1000_1488%2FB56Z5fJDlgHkAI-%2F0%2F1779712679146%3Fe%3D1782345600%26v%3Dbeta%26t%3DXQRnd7b7OjRmQal0h4r311Zdx2HUyUrDKY8WmQtygLU" alt="Article content" width="760" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem With Agent Cost Visibility
&lt;/h3&gt;

&lt;p&gt;Most teams start with simple logs.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[10:14:02] RouterAgent: classified request as ORDER_CHANGE
[10:14:03] OrderAgent: fetched order details
[10:14:05] OrderAgent: generated response
[10:14:06] ResponseAgent: formatted final message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This is useful, but only at the surface level.&lt;/p&gt;

&lt;p&gt;It tells us that &lt;em&gt;something&lt;/em&gt; happened. It does not tell us how expensive the interaction was. It does not show how many LLM calls happened inside each agent. It does not clearly separate tool calls from model calls. It does not show whether retries happened. It does not reveal whether an agent made multiple calls for work that could have been handled by a cheaper model, a cache, or a simple rule.&lt;/p&gt;

&lt;p&gt;That becomes a real problem when the system grows.&lt;/p&gt;

&lt;p&gt;Consider a typical customer message:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"I need to change my shipping address for order #12345."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the surface, this looks like a single, narrow intent — change a shipping address. Any developer looking at a product backlog would estimate this as a fast, cheap operation. The reality at runtime is often quite different.&lt;/p&gt;

&lt;p&gt;A simple version of the agent flow might look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; RouterAgent classifies the request&lt;/li&gt;
&lt;li&gt; OrderAgent fetches order details&lt;/li&gt;
&lt;li&gt; OrderAgent validates whether the address can be changed&lt;/li&gt;
&lt;li&gt; OrderAgent generates a confirmation message&lt;/li&gt;
&lt;li&gt; ResponseAgent formats the final response&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That looks reasonable until you inspect the runtime behavior.&lt;/p&gt;

&lt;p&gt;The router might make one LLM call to classify the query, another call to verify confidence, and a third call if confidence is low. The order agent might use an LLM for validation even when the order state is clearly readable from the database. The response agent might use another LLM just to rewrite a message that could have been generated from a template.&lt;/p&gt;

&lt;p&gt;Suddenly, one support request is no longer one AI interaction. It is a small execution graph — and that graph has a cost attached to every edge.&lt;/p&gt;

&lt;p&gt;If you cannot see that graph, you cannot optimize it. And if you cannot optimize it during development, those costs compound silently in production.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.licdn.com%2Fdms%2Fimage%2Fv2%2FD5612AQFmkCjiyg0o4Q%2Farticle-inline_image-shrink_1000_1488%2FB56Z5fJEMfIQAI-%2F0%2F1779712681504%3Fe%3D1782345600%26v%3Dbeta%26t%3DtrhBgf6RP7OM1EhYhpbDQxj3vprSWPyhYUKqWpRe-Mo" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.licdn.com%2Fdms%2Fimage%2Fv2%2FD5612AQFmkCjiyg0o4Q%2Farticle-inline_image-shrink_1000_1488%2FB56Z5fJEMfIQAI-%2F0%2F1779712681504%3Fe%3D1782345600%26v%3Dbeta%26t%3DtrhBgf6RP7OM1EhYhpbDQxj3vprSWPyhYUKqWpRe-Mo" alt="Article content" width="760" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Building a Cost-Aware Agent Workflow
&lt;/h3&gt;

&lt;p&gt;For this project, I built the workflow around three basic ideas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First&lt;/strong&gt;, every meaningful agent phase should have a name. Anonymous execution is the enemy of cost visibility. When a step has no identity, there is no way to associate its cost with a design decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second&lt;/strong&gt;, every LLM call should capture token usage, model name, purpose, and estimated cost — not just a response string. The response content is what the application cares about. The metadata is what engineering should care about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third&lt;/strong&gt;, the full interaction should be inspectable as one execution tree, not scattered across flat logs. A tree preserves the parent-child relationships between agents, tool calls, and model calls. It makes retries visible as siblings, not mysteries.&lt;/p&gt;

&lt;p&gt;Here is a simplified wrapper around an LLM client that puts these ideas into practice:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { step } from "agent-inspect";

import { OpenAI } from "openai";

type ModelRate = {

  inputPerMillion: number;

  outputPerMillion: number;

};

type LLMCallMetadata = {

  model: string;

  purpose: string;

  promptTokens: number;

  completionTokens: number;

  totalTokens: number;

  estimatedCost: number;

  durationMs: number;

};

const MODEL_RATES: Record&amp;lt;string, ModelRate&amp;gt; = {

  "gpt-4.1-mini": {

    inputPerMillion: 0.4,

    outputPerMillion: 1.6,

  },

  "gpt-4.1": {

    inputPerMillion: 2,

    outputPerMillion: 8,

  },

};

export class CostAwareLLMClient {

  private openai: OpenAI;

  constructor(apiKey: string) {

    this.openai = new OpenAI({ apiKey });

  }

  async chat(params: {

    model: string;

    purpose: string;

    messages: Array&amp;lt;{ role: "system" | "user" | "assistant"; content: string }&amp;gt;;

  }): Promise&amp;lt;{ content: string; metadata: LLMCallMetadata }&amp;gt; {

    return step.llm(`llm:${params.purpose}`, async () =&amp;gt; {

      const startedAt = Date.now();

      const response = await this.openai.chat.completions.create({

        model: params.model,

        messages: params.messages,

      });

      const usage = response.usage;

      const promptTokens = usage?.prompt_tokens ?? 0;

      const completionTokens = usage?.completion_tokens ?? 0;

      const totalTokens = usage?.total_tokens ?? 0;

      const metadata: LLMCallMetadata = {

        model: params.model,

        purpose: params.purpose,

        promptTokens,

        completionTokens,

        totalTokens,

        estimatedCost: this.estimateCost(

          params.model,

          promptTokens,

          completionTokens

        ),

        durationMs: Date.now() - startedAt,

      };

      return {

        content: response.choices[0]?.message?.content ?? "",

        metadata,

      };

    });

  }

  private estimateCost(

    model: string,

    promptTokens: number,

    completionTokens: number

  ) {

    const rate = MODEL_RATES[model];

    if (!rate) {

      return 0;

    }

    const inputCost = (promptTokens / 1_000_000) * rate.inputPerMillion;

    const outputCost = (completionTokens / 1_000_000) * rate.outputPerMillion;

    return inputCost + outputCost;

  }

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Note on pricing:&lt;/strong&gt; The exact rates in MODEL_RATES should always come from the provider's latest pricing page. Model pricing changes frequently, sometimes without prominent announcements. The important part is the pattern: every LLM call returns both the content and the metadata needed to understand cost. The pricing table is just a configuration concern.&lt;/p&gt;

&lt;p&gt;The step.llm() boundary does something more than just wrap the call. It makes the model invocation a named, structured node inside the larger agent execution tree — which means you can later ask, "which step in which agent caused this spend?" and get a real answer.&lt;/p&gt;




&lt;h3&gt;
  
  
  Wrapping the Agent Run
&lt;/h3&gt;

&lt;p&gt;Next, I wrapped the entire support workflow with inspectRun(). This is the outermost boundary — the container that gives the whole interaction a single, inspectable identity.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { inspectRun, step } from "agent-inspect";

type SupportRequest = {

  customerId: string;

  message: string;

};

export async function handleSupportRequest(

  request: SupportRequest,

  agents: {

    router: RouterAgent;

    order: OrderAgent;

    refund: RefundAgent;

    response: ResponseAgent;

  }

) {

  return inspectRun(

    "support-agent-request",

    async () =&amp;gt; {

      const route = await step("route-request", async () =&amp;gt; {

        return agents.router.classify(request.message);

      });

      const result = await step(`handle-${route.intent.toLowerCase()}`, async () =&amp;gt; {

        if (route.intent === "ORDER_CHANGE") {

          return agents.order.handle(request);

        }

        if (route.intent === "REFUND") {

          return agents.refund.handle(request);

        }

        return {

          type: "GENERAL",

          message: "This request can be handled by the general support flow.",

        };

      });

      return step("prepare-final-response", async () =&amp;gt; {

        return agents.response.format(result);

      });

    },

    {

      traceDir: "./.agent-inspect",

    }

  );

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This structure enforces something that most agent frameworks skip: a &lt;strong&gt;single entry point with named phases&lt;/strong&gt;. Each call to step() is a named boundary. Each step.llm() inside an agent is a named model call. The result is an execution trace that mirrors your architecture — not just a raw sequence of events.&lt;/p&gt;

&lt;p&gt;This is where the article's main idea becomes concrete. The code is not just instrumented for debugging failures. It is instrumented to understand cost behavior &lt;em&gt;before&lt;/em&gt; failures occur. The trace answers questions like: where did time go? Where did tokens go? Did the expensive work happen in the right phase?&lt;/p&gt;




&lt;h3&gt;
  
  
  The RouterAgent Problem
&lt;/h3&gt;

&lt;p&gt;In the first version of the project, the router was doing too much.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class RouterAgent {

  constructor(private llm: CostAwareLLMClient) {}

  async classify(message: string) {

    return step("router-agent", async () =&amp;gt; {

      const classification = await step("classify-intent", async () =&amp;gt; {

        const { content, metadata } = await this.llm.chat({

          model: "gpt-4.1",

          purpose: "classify-intent",

          messages: [

            {

              role: "system",

              content:

                "Classify the support request as ORDER_CHANGE, REFUND, SHIPPING, PRODUCT, or GENERAL.",

            },

            {

              role: "user",

              content: message,

            },

          ],

        });

        return {

          intent: content.trim(),

          cost: metadata.estimatedCost,

        };

      });

      const confidence = await step("verify-classification-confidence", async () =&amp;gt; {

        const { content, metadata } = await this.llm.chat({

          model: "gpt-4.1",

          purpose: "verify-routing-confidence",

          messages: [

            {

              role: "user",

              content: How confident are you that this request is ${classification.intent}? Return a number between 0 and 1.\n\n${message},

            },

          ],

        });

        return {

          score: Number(content),

          cost: metadata.estimatedCost,

        };

      });

      if (confidence.score &amp;lt; 0.8) {

        return step("reclassify-with-context", async () =&amp;gt; {

          const { content } = await this.llm.chat({

            model: "gpt-4.1",

            purpose: "reclassify-intent",

            messages: [

              {

                role: "system",

                content:

                  "Reclassify the request with additional context. Return only the intent.",

              },

              {

                role: "user",

                content: Previous intent: ${classification.intent}\nConfidence: ${confidence.score}\nRequest: ${message},

              },

            ],

          });

          return {

            intent: content.trim(),

            source: "reclassification",

          };

        });

      }

      return {

        intent: classification.intent,

        source: "initial-classification",

      };

    });

  }

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This looked reasonable at first. The router was being careful. It classified, then verified, then reclassified when uncertain. This pattern is common in agent design — confidence thresholds feel responsible, like the system is checking its own work.&lt;/p&gt;

&lt;p&gt;But careful is not always cheap. And in this case, &lt;em&gt;careful&lt;/em&gt; was doing something more problematic: it was front-loading cost onto every request, including the easy ones.&lt;/p&gt;

&lt;p&gt;For every request, the router performed classification with a high-capability model. For many requests, it also performed a confidence check — again with the same model. For some requests, it performed reclassification as a third call. That meant the very first step in the system could consume the majority of the per-request budget before any actual business logic ran.&lt;/p&gt;

&lt;p&gt;This is the kind of issue that is easy to miss in flat logs. A log line that says RouterAgent: classified request does not show that the router made two or three model calls internally. It hides the retry. It hides the model version. It hides the cost. An execution tree does not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspecting the Trace
&lt;/h3&gt;

&lt;p&gt;After running a few sample support requests, I inspected the local trace using the CLI:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx agent-inspect list --dir ./.agent-inspect

npx agent-inspect view &amp;lt;run-id&amp;gt; --dir ./.agent-inspect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The output made the structural issue immediately visible:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;support-agent-request                                      [7.8s] ✓
├─ route-request                                           [3.9s] ✓
│  └─ router-agent                                         [3.8s] ✓
│     ├─ classify-intent                                   [1.4s] ✓
│     │  └─ llm:classify-intent                            [1.3s] ✓
│     ├─ verify-classification-confidence                  [1.1s] ✓
│     │  └─ llm:verify-routing-confidence                  [1.0s] ✓
│     └─ reclassify-with-context                           [1.2s] ✓
│        └─ llm:reclassify-intent                          [1.1s] ✓
├─ handle-order_change                                     [2.6s] ✓
│  └─ order-agent                                          [2.5s] ✓
│     ├─ fetch-order                                       [0.2s] ✓
│     ├─ validate-address-change                           [0.8s] ✓
│     │  └─ llm:validate-order-change                      [0.7s] ✓
│     └─ generate-confirmation                             [1.4s] ✓
│        └─ llm:generate-customer-response                 [1.3s] ✓
└─ prepare-final-response                                  [1.1s] ✓
   └─ response-agent                                       [1.0s] ✓
      └─ llm:format-final-response                         [0.9s] ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The numbers tell a clear story. Of the 7.8 seconds total, 3.9 seconds — exactly half — were spent before a single line of business logic ran. The router, whose job is to label an intent, was consuming as much wall-clock time and token budget as the actual order handling.&lt;/p&gt;

&lt;p&gt;The trace also immediately revealed two secondary issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The response-agent was making a model call purely for formatting — a job that did not require a model at all.&lt;/li&gt;
&lt;li&gt;  The validate-address-change step was calling an LLM, but the order fetch happened &lt;em&gt;before&lt;/em&gt; it. That is actually the right order — but it is only visible in the tree. In flat logs, you cannot tell whether deterministic data was used to constrain the model prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trace changed the conversation. Instead of saying "the support agent is expensive," I could now say: "the router is making three LLM calls on some requests, and the response agent is consuming model tokens for formatting." That is a different kind of engineering discussion. One is a budget complaint. The other is an actionable observation with a root cause attached.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimization 1: Use Rules Before Models
&lt;/h3&gt;

&lt;p&gt;The first fix was simple: do not call an LLM when the intent is obvious from the message itself.&lt;/p&gt;

&lt;p&gt;Routing is fundamentally a classification problem. For a large enough slice of real-world support requests, the classification is deterministic — a message that contains an order number and the phrase "shipping address" is almost certainly an ORDER_CHANGE. No model needs to make that call.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function classifyWithRules(message: string) {

  const normalized = message.toLowerCase();

  if (

    /order\s*#?\d+/i.test(message) &amp;amp;&amp;amp;

    (normalized.includes("shipping address") ||

      normalized.includes("delivery address") ||

      normalized.includes("change address"))

  ) {

    return {

      intent: "ORDER_CHANGE",

      confidence: 0.95,

      source: "rule",

    };

  }

  if (

    normalized.includes("refund") ||

    normalized.includes("money back") ||

    normalized.includes("cancel my order")

  ) {

    return {

      intent: "REFUND",

      confidence: 0.9,

      source: "rule",

    };

  }

  return null;

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then the router uses rules first and falls back to the LLM only when the message is genuinely ambiguous:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async classify(message: string) {

  return step("router-agent", async () =&amp;gt; {

    const ruleBasedRoute = await step("rule-based-routing", async () =&amp;gt; {

      return classifyWithRules(message);

    });

    if (ruleBasedRoute) {

      return ruleBasedRoute;

    }

    return step("llm-routing-fallback", async () =&amp;gt; {

      const { content } = await this.llm.chat({

        model: "gpt-4.1-mini",

        purpose: "classify-intent",

        messages: [

          {

            role: "system",

            content:

              "Classify this support request as ORDER_CHANGE, REFUND, SHIPPING, PRODUCT, or GENERAL. Return only the label.",

          },

          {

            role: "user",

            content: message,

          },

        ],

      });

      return {

        intent: content.trim(),

        confidence: 0.75,

        source: "llm",

      };

    });

  });

} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Two things changed here beyond just adding regex checks.&lt;/p&gt;

&lt;p&gt;First, the LLM fallback now uses gpt-4.1-mini instead of gpt-4.1. Routing is a short, low-complexity classification task. There is no reason to run a high-capability model on it. If the message is genuinely hard to classify, the mini model will still get it right most of the time — and if it does not, the agent's downstream error handling can manage the rare misroute far more cheaply than running the expensive model on every request.&lt;/p&gt;

&lt;p&gt;Second, the confidence-check loop is gone entirely. Instead of asking the model whether it was confident, the architecture now defines confidence at the source: rule-based matches carry explicit confidence scores, and the LLM fallback carries a fixed lower score. Confidence checking as a second LLM call is a design smell — it outsources a system-level decision to the model, at the model's cost.&lt;/p&gt;

&lt;p&gt;This is not about replacing AI with rules everywhere. It is about using the right tool at the right point in the workflow. If a deterministic rule can safely route obvious cases, the model should not be charged for that decision.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.licdn.com%2Fdms%2Fimage%2Fv2%2FD5612AQE3_qUPlyHixA%2Farticle-inline_image-shrink_1000_1488%2FB56Z5fJEDzGkAI-%2F0%2F1779712677728%3Fe%3D1782345600%26v%3Dbeta%26t%3DUSByR5czgIpjzWXKLdHmiLKjVp23UUgEI9DzM9hoH5s" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.licdn.com%2Fdms%2Fimage%2Fv2%2FD5612AQE3_qUPlyHixA%2Farticle-inline_image-shrink_1000_1488%2FB56Z5fJEDzGkAI-%2F0%2F1779712677728%3Fe%3D1782345600%26v%3Dbeta%26t%3DUSByR5czgIpjzWXKLdHmiLKjVp23UUgEI9DzM9hoH5s" alt="Article content" width="760" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimization 2: Make Model Choice a Per-Step Decision
&lt;/h3&gt;

&lt;p&gt;The second improvement was model selection — and the realization that treating model choice as a workflow-level constant is an architectural mistake.&lt;/p&gt;

&lt;p&gt;Different steps in an agent workflow have fundamentally different quality requirements. A customer-facing response that explains why an address change was rejected needs care, nuance, and natural language quality. A routing label that picks between five categories needs accuracy, not eloquence. A validation result that checks whether a timestamp is in the past needs neither.&lt;/p&gt;

&lt;p&gt;Applying the same model everywhere conflates these requirements. It uses the budget of a high-capability model for work that a cheaper model handles just as well.&lt;/p&gt;

&lt;p&gt;The fix is to make model selection explicit and purpose-driven:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const MODEL_BY_PURPOSE = {
  "classify-intent": "gpt-4.1-mini",
  "validate-order-change": "gpt-4.1-mini",
  "generate-customer-response": "gpt-4.1",
  "summarize-internal-tool-result": "gpt-4.1-mini",
} as const;

function modelForPurpose(purpose: keyof typeof MODEL_BY_PURPOSE) {
  return MODEL_BY_PURPOSE[purpose];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then each LLM call is tied explicitly to a purpose — and the model follows from the purpose:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { content, metadata } = await this.llm.chat({
  model: modelForPurpose("validate-order-change"),
  purpose: "validate-order-change",
  messages: [
    {
      role: "system",
      content:
        "Validate whether this address change is allowed based on the order state.",
    },
    {
      role: "user",
      content: JSON.stringify({
        order,
        requestedChange,
      }),
    },
  ],
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This table is also documentation. When a new developer reads MODEL_BY_PURPOSE, they immediately understand which steps are considered high-stakes (warranting a stronger model) and which are treated as routine. That is a design decision that is now visible in the code, not buried in a prompt file or tribal knowledge.&lt;/p&gt;

&lt;p&gt;In the trace, this also gives more actionable signal. When a run is expensive, you can distinguish between: the expensive model was used in the right place for the right reason, versus the expensive model was used for a step that did not require it. Without purpose-tagged model selection, those two cases look identical in a cost report.&lt;/p&gt;




&lt;h3&gt;
  
  
  Optimization 3: Remove the Formatting Agent
&lt;/h3&gt;

&lt;p&gt;The third issue was the ResponseAgent, and it is the most instructive problem because it came from a design philosophy, not a specific technical choice.&lt;/p&gt;

&lt;p&gt;The first version of the architecture treated every phase as an agent. That made the design look clean and consistent — every step was an agent, every agent had a name, every agent was wired into the orchestrator the same way. Architecturally, it felt principled.&lt;/p&gt;

&lt;p&gt;At runtime, it was wasteful.&lt;/p&gt;

&lt;p&gt;The ResponseAgent existed to produce a consistently formatted customer-facing message. But when the actual output of the OrderAgent is already structured — it contains a customer name, an order ID, a new address — formatting that data into a natural-sounding sentence is a template problem, not a model problem.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function formatAddressChangeConfirmation(input: {

  customerName: string;

  orderId: string;

  newAddress: string;

}) {

  return Hi ${input.customerName}, your shipping address for order ${input.orderId} has been updated to ${input.newAddress}.;

} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The template produces output that is deterministic, consistent, and free. The model call produced output that was variable, slightly inconsistent across runs, and expensive relative to the complexity of the task.&lt;/p&gt;

&lt;p&gt;The trap here is aesthetic. Agent-based architectures look elegant when every component is symmetrical. But symmetry at the design level does not translate to efficiency at the runtime level. Some steps in a workflow are genuinely model-shaped — they require language understanding, reasoning over ambiguous inputs, or generating novel text. Others are data-shaped — they take structured inputs and produce structured or templated outputs.&lt;/p&gt;

&lt;p&gt;Modeling a data-shaped step as an LLM call is not just wasteful. It is also &lt;em&gt;less reliable&lt;/em&gt; — template output is deterministic and testable in a way that model output is not.&lt;/p&gt;

&lt;p&gt;The updated trace reflected the change immediately:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;support-agent-request                                      [3.2s] ✓
├─ route-request                                           [0.1s] ✓
│  └─ router-agent                                         [0.1s] ✓
│     └─ rule-based-routing                                [0.1s] ✓
├─ handle-order_change                                     [2.8s] ✓
│  └─ order-agent                                          [2.7s] ✓
│     ├─ fetch-order                                       [0.2s] ✓
│     ├─ validate-address-change                           [0.8s] ✓
│     │  └─ llm:validate-order-change                      [0.7s] ✓
│     └─ generate-confirmation                             [1.6s] ✓
│        └─ llm:generate-customer-response                 [1.5s] ✓
└─ prepare-final-response                                  [0.1s] ✓
   └─ template-format-response                             [0.1s] ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Total request time dropped from 7.8 seconds to 3.2 seconds — a 59% reduction — without any change to the actual intelligence of the system. The model calls that remained were the ones doing genuinely model-shaped work: generating a natural-language confirmation message that requires understanding context, and validating an order change that requires reasoning about business rules.&lt;/p&gt;

&lt;p&gt;The important part is not just that the run became faster or cheaper. The important part is that the trace made the architectural waste &lt;em&gt;visible&lt;/em&gt; before it became a production cost problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Execution Trees Help With Cost Debugging
&lt;/h3&gt;

&lt;p&gt;Cost problems in AI systems are rarely isolated to one line of code.&lt;/p&gt;

&lt;p&gt;They usually come from the &lt;em&gt;shape&lt;/em&gt; of the workflow — and shape is exactly what flat logs cannot show.&lt;/p&gt;

&lt;p&gt;A retry policy may be too aggressive, triggering an LLM call after every tool failure instead of only after structured retries are exhausted. A router may be overthinking simple cases that deterministic logic could handle cheaply. A formatting step may be using a model when a template would produce better-tested output at zero cost. A validation step may be calling the model before fetching enough deterministic data to constrain the prompt — wasting tokens on an under-informed decision. A tool failure may trigger a second model call that masks the real root cause in the logs.&lt;/p&gt;

&lt;p&gt;Each of these is an architectural problem. None of them appear in a cost invoice. None of them are obvious in flat logs. All of them become visible in an execution tree.&lt;/p&gt;

&lt;p&gt;The reason is structural. Flat logs record events in sequence. An execution tree records &lt;em&gt;relationships&lt;/em&gt;. It shows which LLM call belongs to which agent. It shows whether a tool call happened before or after a model call — and whether that ordering was intentional. It shows which branches ran in parallel and which were sequential. It shows where retries happened and what triggered them. It shows whether the expensive call was part of routing, validation, generation, or cleanup.&lt;/p&gt;

&lt;p&gt;That structure matters because cost optimization is not only a token problem. Reducing prompt length by 15% is a micro-optimization. Eliminating an unnecessary routing loop is a structural optimization. The execution tree is how you find structural optimizations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local-First Tracing Fits the Development Loop
&lt;/h3&gt;

&lt;p&gt;There are strong hosted observability platforms for production AI systems — LangSmith, Arize, Helicone, and others — and they are valuable once a team needs dashboards, evaluations, collaboration, alerting, and long-term analysis across thousands of runs.&lt;/p&gt;

&lt;p&gt;But during development, the requirements are different. The developer is not trying to monitor a fleet of agents. They are trying to understand one run. They changed the routing logic. They want to know whether that change made the call cheaper or more expensive. They want to see the trace, compare it to the previous run, and keep iterating.&lt;/p&gt;

&lt;p&gt;A hosted observability dashboard introduces friction into that loop. There is authentication, a data pipeline, a UI to navigate. Those things are appropriate in production. During design iteration, they slow down the feedback cycle without adding proportional value.&lt;/p&gt;

&lt;p&gt;That is where a local-first tool like agent-inspect fits naturally. It writes traces to a local directory. It surfaces them through a terminal command. It supports named boundaries — inspectRun(), step(), step.llm(), step.tool() — that map directly onto the agent architecture. The result is a readable execution tree that makes agent behavior easier to reason about without leaving the development environment.&lt;/p&gt;

&lt;p&gt;The comparison to think about is code coverage tooling. A developer does not send code to a hosted service to see which lines were executed. They run the tests locally and inspect the output. The same principle applies to agent cost visibility during design iteration.&lt;/p&gt;

&lt;p&gt;For this project, local-first tracing was enough to answer the practical question: which parts of the agent are actually costing money? That question has to be answered before any optimization is worth doing. Getting it wrong means spending engineering time on the wrong problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.licdn.com%2Fdms%2Fimage%2Fv2%2FD5612AQFEwW-SKzObgQ%2Farticle-inline_image-shrink_1000_1488%2FB56Z5fJEG5GYAI-%2F0%2F1779712679301%3Fe%3D1782345600%26v%3Dbeta%26t%3Dlepqy62uHXio5XyyltgtMg4iC-ZX0IbsyLB_2WpcDds" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.licdn.com%2Fdms%2Fimage%2Fv2%2FD5612AQFEwW-SKzObgQ%2Farticle-inline_image-shrink_1000_1488%2FB56Z5fJEG5GYAI-%2F0%2F1779712679301%3Fe%3D1782345600%26v%3Dbeta%26t%3Dlepqy62uHXio5XyyltgtMg4iC-ZX0IbsyLB_2WpcDds" alt="Article content" width="760" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical Lessons
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Instrument before optimizing.&lt;/strong&gt; Without execution visibility, it is easy to guess wrong about where the cost is coming from. You may spend time reducing prompt length while the real issue is an unnecessary routing loop that adds two model calls before the prompt is even constructed. The trace tells you where to look. Guessing does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat model selection as an architectural decision.&lt;/strong&gt; A single agent workflow can and should use different models for classification, validation, summarization, and final response generation. The right model for a customer-facing message is not the right model for a routing label. Making this explicit in a purpose-to-model mapping turns a runtime cost driver into a visible, reviewable configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use deterministic logic where it is safe.&lt;/strong&gt; Rules, templates, caches, and structured tool outputs can remove unnecessary model calls without making the system less intelligent. The key question for each step is: does this step require language understanding and reasoning, or does it require deterministic computation over structured data? The answer should determine whether a model is involved at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traces are useful beyond failures.&lt;/strong&gt; Most observability tooling is set up to catch errors. But execution traces are equally useful for understanding cost, latency, retry behavior, and design complexity — even in runs that succeed. A successful run that costs three times what it should is still a problem. The trace shows it. The success status does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep local debugging simple during design iteration.&lt;/strong&gt; Not every development workflow needs a production observability stack on day one. The cost of integrating hosted tooling early is overhead at exactly the time when iteration speed matters most. A local trace and a terminal view are often enough to improve the design significantly before the system is ready to be monitored at scale.&lt;/p&gt;




&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;AI agents are not expensive only because models are expensive.&lt;/p&gt;

&lt;p&gt;They are expensive because agent workflows &lt;em&gt;multiply&lt;/em&gt; model calls. A single user request passes through a router, a validator, a tool handler, a formatter, and possibly a retry handler — and each of those phases may invoke an LLM, sometimes more than once. The cost is not on the label. It is inside the orchestration.&lt;/p&gt;

&lt;p&gt;This multiplication is not inherently bad. Some agent workflows genuinely require multiple model calls to do their job well. The problem is when the multiplication is accidental — when model calls accumulate through design decisions that nobody revisited after the first prototype, through retry policies that are too broad, through formatting steps that felt convenient, through confidence checks that nobody questioned.&lt;/p&gt;

&lt;p&gt;Execution-level visibility is what turns that accidental complexity into something that can be reasoned about. When you can see the run as a tree, you can stop treating cost as a monthly invoice problem and start treating it as an engineering feedback loop. You can point to a specific step, ask whether it should exist, ask whether it should use the model it is using, and make a decision based on evidence rather than assumption.&lt;/p&gt;

&lt;p&gt;For TypeScript teams building AI agents, &lt;a href="https://www.npmjs.com/package/agent-inspect" rel="noopener noreferrer"&gt;agent-inspect&lt;/a&gt; gives a lightweight way to inspect that loop locally. It does not need to replace production observability. It sits earlier in the workflow — during design — where developers are still shaping the architecture and the decisions are cheapest to change.&lt;/p&gt;

&lt;p&gt;And in many cases, that is exactly where the biggest cost savings begin: not in prompt tuning or model compression, but in the question of whether a given model call should exist at all.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NPM library:&lt;/em&gt; &lt;a href="https://www.npmjs.com/package/agent-inspect" rel="noopener noreferrer"&gt;&lt;em&gt;agent-inspect on npm&lt;/em&gt;&lt;/a&gt; &lt;em&gt;GitHub repository:&lt;/em&gt; &lt;a href="https://github.com/rajudandigam/agent-inspect" rel="noopener noreferrer"&gt;&lt;em&gt;rajudandigam/agent-inspect&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>agentaichallenge</category>
      <category>agentskills</category>
    </item>
    <item>
      <title>System Design Is Overrated (When You're a Beginner)</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Sat, 25 Apr 2026 05:57:24 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/system-design-is-overrated-when-youre-a-beginner-345p</link>
      <guid>https://dev.to/divyanshulohani/system-design-is-overrated-when-youre-a-beginner-345p</guid>
      <description>&lt;p&gt;&lt;em&gt;Why obsessing over architecture before you've built anything is quietly killing your growth as a developer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Picture this: You've just finished a beginner tutorial, you're buzzing with excitement, and you're ready to build your first real project. You open a new tab, start sketching out ideas and then you make the mistake of opening YouTube.&lt;/p&gt;

&lt;p&gt;Suddenly, you're drowning in videos about microservices, domain-driven design, event-sourcing, CQRS patterns, hexagonal architecture, things being obselete, things you should not be using all those things which you just learned are not used anymore etc etc. A senior developer on Twitter confidently declares: &lt;em&gt;"If you're not thinking about scalability from day one, you're doing it wrong."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And just like that, the excitement evaporates. Your simple todo app now needs a service mesh and a Kafka queue to be "done right."&lt;/p&gt;

&lt;p&gt;Sound familiar? You're not alone and this is one of the most common and damaging traps that beginners fall into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden trap of early architecture
&lt;/h2&gt;

&lt;p&gt;System design is a genuinely fascinating discipline. It's the kind of thing that makes experienced developers light up debating trade-offs, drawing boxes and arrows on whiteboards, thinking about how millions of users would interact with a system. And that energy is contagious.&lt;/p&gt;

&lt;p&gt;But for beginners, it often transforms into something far less productive:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A very sophisticated, very convincing form of procrastination.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You're not avoiding work, you feel like you're working. You're writing design docs, drawing architecture diagrams, thinking about abstractions. It feels like progress. It looks like progress. But nothing is getting built.&lt;/p&gt;

&lt;p&gt;Instead of writing the ten lines of code that would bring your feature to life, you're:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The over-engineering spiral&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Designing abstractions for code you haven't written yet&lt;/li&gt;
&lt;li&gt;  Creating interfaces for problems you don't actually have&lt;/li&gt;
&lt;li&gt;  Debating database schemas for an app with zero users&lt;/li&gt;
&lt;li&gt;  Watching "clean architecture" videos instead of writing a single function&lt;/li&gt;
&lt;li&gt;  Planning for 1,000,000 concurrent users when your app doesn't even start&lt;/li&gt;
&lt;li&gt;  Avoiding tech stacks, libraries, frameworks for problems you will never face in your app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're solving future problems for a future app that may never exist while the present app sits unbuilt on your hard drive.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost you can't see on a spreadsheet
&lt;/h3&gt;

&lt;p&gt;The damage from premature architecture isn't always obvious because it doesn't feel like damage. You're thinking! You're planning! But here's what's actually happening beneath the surface:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feedback loops slow to a crawl.&lt;/strong&gt; The fastest way to learn programming is to build something, see it work (or break), and iterate (that's how I got it at 2AM debug sessions without the GPT). Every hour spent in planning mode is an hour you're not getting real feedback from real running code. You're learning theory instead of developing intuition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complexity compounds before you have the tools to manage it.&lt;/strong&gt; When you introduce layers of abstraction early — repositories, factories, interfaces, dependency injection — before you understand why they exist, you're adding cognitive overhead without any of the benefits. The patterns feel arbitrary because you haven't yet experienced the pain they're designed to solve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Momentum dies quietly.&lt;/strong&gt; Every developer knows the energy of a new project. That excitement is a resource — and it's finite. Spending it on architecture debates rather than building features drains it fast. Many projects die not from technical failure, but from lost momentum during the planning phase. And if you're like me you can get overwheled by the things and just give up the project all together opening a way for it to the unfinished projects grave.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The biggest mistake I see beginners make is not writing bad code. It's not writing code at all."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You never learn what actually breaks.&lt;/strong&gt; This is the most subtle and most important cost. System design is essentially a collection of lessons learned from things going wrong at scale. But if you've never built something that broke under load, you have no frame of reference for those lessons. The concepts remain abstract, like reading a book about swimming without ever getting in the water. By choosing to plan and execute I don't say its bad that's how big organizations do work but as a beginner you don't have to think about millions of users when you're not even hosting your thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What beginners should actually do
&lt;/h2&gt;

&lt;p&gt;If you're in the early stages of your journey, here's the honest advice that most tutorials won't give you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop doing this&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Abstract before building&lt;/li&gt;
&lt;li&gt;  Plan for imaginary scale&lt;/li&gt;
&lt;li&gt;  Design perfect interfaces&lt;/li&gt;
&lt;li&gt;  Get stuck in tutorials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Start doing this&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Build, even if it's messy&lt;/li&gt;
&lt;li&gt;  Ship something that runs&lt;/li&gt;
&lt;li&gt;  Break it and learn why&lt;/li&gt;
&lt;li&gt;  Iterate fast and often&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Messy, working code is infinitely more valuable than elegant, unwritten code. A project that runs and solves a real problem, even if the codebase would make a senior engineer wince, teaches you more than any architecture course.&lt;/p&gt;

&lt;p&gt;When you actually build things, you naturally collide with real problems:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problems worth solving (when they appear naturally)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Duplicated logic that makes changes a nightmare&lt;/li&gt;
&lt;li&gt;  State scattered across your app with no clear owner&lt;/li&gt;
&lt;li&gt;  Functions doing ten things when they should do one&lt;/li&gt;
&lt;li&gt;  Code you can't change without breaking three other things&lt;/li&gt;
&lt;li&gt;  Features that should take an hour taking a day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't abstract concepts anymore. They're your code, causing you actual pain. &lt;em&gt;That&lt;/em&gt; is when system design stops being theory and starts being medicine and then you can understand why the systems that are recommeded are needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  A practical path through the early stages
&lt;/h3&gt;

&lt;p&gt;Instead of front-loading architecture, think about your growth in stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Make it work:&lt;/strong&gt; Write the simplest code that solves the problem. Don't worry about beauty. Just make it run.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Ship it:&lt;/strong&gt; Get it in front of real users even if that's just one friend or yourself. Real-world feedback is irreplaceable.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Feel the pain:&lt;/strong&gt; As your project grows, you'll notice things getting harder. Certain changes are tedious. Bugs keep reappearing. This friction is gold.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Refactor with purpose:&lt;/strong&gt; Now reach for patterns and principles — not to be "correct," but to fix the specific pain you've experienced. Design solving real problems sticks forever.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Repeat&lt;/strong&gt; Build the next thing. Each cycle, you'll have better instincts, and system design concepts will feel less like rules and more like tools.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When system design genuinely matters
&lt;/h2&gt;

&lt;p&gt;None of this means system design is useless far from it. It's one of the highest-leverage skills a senior developer can have. The key word is &lt;strong&gt;&lt;em&gt;timing&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;System design becomes valuable and meaningful when:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signs you're ready to go deeper&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You've shipped projects and felt specific, concrete pain points&lt;/li&gt;
&lt;li&gt;  You've refactored code and understand what made it better&lt;/li&gt;
&lt;li&gt;  You've worked on a codebase that others have also touched&lt;/li&gt;
&lt;li&gt;  You've had to change something and broken three other things&lt;/li&gt;
&lt;li&gt;  You're working on a team and need to communicate decisions clearly&lt;/li&gt;
&lt;li&gt;  Performance, reliability, or maintainability are causing real problems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this stage, system design isn't abstract theory — it's a toolkit. Every pattern you learn maps to a scar you have. You understand the trade-offs not from a blog post, but from experience. That understanding is durable in a way that tutorial knowledge simply isn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  The reality that nobody talks about
&lt;/h3&gt;

&lt;p&gt;Here's a secret that senior developers rarely volunteer: they didn't architect their best work perfectly from the start. The systems you admire were almost always built iteratively, with refactors along the way.&lt;/p&gt;

&lt;p&gt;The industry term for this is "evolutionary architecture" — and it's not a fallback for people who got it wrong. It's the &lt;em&gt;intended way&lt;/em&gt; to build software. Systems evolve as requirements become clearer, as usage patterns emerge, and as the team learns what actually matters.&lt;/p&gt;

&lt;p&gt;The best engineers don't write perfect code on the first pass. They write code fast, observe what happens, and improve based on reality — not imagination. System design, in practice, is often retrofitted onto working systems, not imposed on blank canvases.&lt;/p&gt;

&lt;p&gt;So when someone online tells you to "think about scalability from day one," what they probably mean is: don't do obviously terrible things. They don't mean you should design for Google's traffic before you have ten users.&lt;/p&gt;

&lt;h3&gt;
  
  
  A better mental model for building
&lt;/h3&gt;

&lt;p&gt;Try replacing "How do I design this correctly?" with a two-step question:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First:&lt;/em&gt; &lt;strong&gt;&lt;em&gt;How do I get this working as fast as possible?&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;Then, once it works:&lt;/em&gt; &lt;strong&gt;&lt;em&gt;Where is this causing me pain, and how do I fix that?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This reframe does something important — it puts experience before theory. You earn the right to refactor by first shipping something that runs. The improvements you make at that point are grounded in evidence, not speculation.&lt;/p&gt;

&lt;p&gt;It also keeps you moving, which matters more than most developers acknowledge. Consistent forward progress builds skills, confidence, and a portfolio. Endless planning builds neither.&lt;/p&gt;




&lt;h3&gt;
  
  
  The final word
&lt;/h3&gt;

&lt;p&gt;System design is a genuinely powerful discipline. There will come a point in your development journey where it clicks — where you look at a codebase and instinctively understand its structure, its trade-offs, and where it will crack under pressure. That moment is worth working toward.&lt;/p&gt;

&lt;p&gt;But you can't think your way there. You have to build your way there.&lt;/p&gt;

&lt;p&gt;If system design is currently stopping you from writing code, from shipping projects, from making mistakes and learning from them — then right now, for you, it's not a tool. It's a blocker.&lt;/p&gt;

&lt;p&gt;Don't let the pursuit of doing things &lt;em&gt;the right way&lt;/em&gt; prevent you from doing things &lt;em&gt;at all&lt;/em&gt;. The right way is a moving target that only becomes visible once you're in motion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just build. Then build again. Better will come.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  System design is important — but badly timed for beginners&lt;/li&gt;
&lt;li&gt;  Over-engineering before building kills momentum and feedback loops&lt;/li&gt;
&lt;li&gt;  Messy working code beats elegant unwritten code every time&lt;/li&gt;
&lt;li&gt;  Real understanding of design patterns comes from experiencing the pain they solve&lt;/li&gt;
&lt;li&gt;  Senior devs retrofit architecture — they don't pre-plan it perfectly&lt;/li&gt;
&lt;li&gt;  Build fast, break things, refactor with purpose, and repeat&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>My 2025</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Thu, 01 Jan 2026 18:04:58 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/my-2025-ljj</link>
      <guid>https://dev.to/divyanshulohani/my-2025-ljj</guid>
      <description>&lt;p&gt;2024 was about proving things to myself that I could build, learn fast, and keep shipping projects. 2025, on the other hand, was about &lt;strong&gt;understanding what it actually means to build software&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This year wasn’t flashy. It didn’t revolve around frameworks or buzzwords. It was messy, practical, and occasionally uncomfortable. I shipped code to production, dealt with real failures, made architectural decisions that had consequences, and learned lessons that no tutorial could’ve taught me.&lt;/p&gt;

&lt;p&gt;These are not lessons I learned by reading, they’re lessons I learned by &lt;em&gt;doing&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stepping Outside the Screen: Real-World Opportunities
&lt;/h2&gt;

&lt;p&gt;This year I finished high school and also got into actual real world things, for the longest time, my entire relationship with tech existed behind a screen but after my boards I had very much free time to go and explore the real tech world.&lt;/p&gt;

&lt;p&gt;This year, I attended my &lt;strong&gt;first ever tech fest&lt;/strong&gt;: &lt;strong&gt;DevFest by&lt;/strong&gt; &lt;a href="https://dev.to/in/gdg-ranchi-128833360/"&gt;GDG Ranchi&lt;/a&gt; . It might sound small on paper, but for me, it was a turning point. Seeing developers, speakers, and students in the same physical space made something click that online learning never quite did.&lt;/p&gt;

&lt;p&gt;Until then, tech had felt like a solo pursuit me, my pc and an internet connection. DevFest showed me the &lt;em&gt;human side&lt;/em&gt; of software: people sharing failures, debating decisions, and talking about systems they’ve actually broken and fixed in production.&lt;/p&gt;

&lt;p&gt;What surprised me most wasn’t the scale of the event, but the realization that I belonged there. I wasn’t “too early” or “underprepared.” I understood the problems being discussed, asked better questions, and left with a stronger sense of direction.&lt;/p&gt;

&lt;p&gt;This experience taught me that learning doesn’t only happen through code it also happens through exposure. Being in the same room as people building real things pushes you to raise your own standards. It made me want to ship better software, think more responsibly, and stop treating projects as disposable experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Is a Different Game Entirely
&lt;/h2&gt;

&lt;p&gt;Thanks to this habbit of writing blogs I got quite lucky I secured my first clients to work as a freelance web developer and take my skills to the real world. For a long time, I treated “shipping” as the finish line. If the app worked locally, deployed successfully, and didn’t crash immediately, I considered it done (I knew few edge cases but left it there because it was just a casual project). 2025 completely destroyed that illusion. This year I dealt with apps made for real users, real people using them, bugs that weren't just there all along but left unspotted due to manual testing and treating the app with care.&lt;/p&gt;

&lt;p&gt;Shipping code to production is not an extension of side projects, it’s a different discipline altogether. Suddenly, every decision carries weight you can't just break things and push to prod or like get a quick change. Logs matter a lot I mean a lot things can go wrong in the frontend as well as backend. Rollbacks matter. Downtime matters. Even small changes feel heavier when real users depend on them. One wrong mistake and your whole app is down for minutes cosing you a lot.&lt;/p&gt;

&lt;p&gt;In casual projects, mistakes are learning opportunities. In production, mistakes are responsibilities. A broken feature isn’t just “oops” it’s someone else’s workflow interrupted, someone's dilevery failed, data potentially affected, or trust slightly eroded.&lt;/p&gt;

&lt;p&gt;This year taught me that production software demands a mindset shift:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You think twice before deploying.&lt;/li&gt;
&lt;li&gt;  You test flows you previously ignored.&lt;/li&gt;
&lt;li&gt;  You plan for failure instead of assuming success.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest change wasn’t technical — it was psychological. I stopped asking &lt;em&gt;“Will this work?”&lt;/em&gt; and started asking &lt;em&gt;“What happens when this breaks?”&lt;/em&gt; "What more ways a user can perform this action to break things" these questions alone made me a better engineer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vulnerabilities Don’t Care About Your User Count
&lt;/h2&gt;

&lt;p&gt;One of the most uncomfortable lessons of 2025 was realizing how exposed even a small application can be. I used to believe that security becomes important &lt;em&gt;after&lt;/em&gt; you have users. That belief didn’t survive this year.&lt;/p&gt;

&lt;p&gt;Major vulnerabilities surfaced that directly affected production apps including one that took my own app down. It didn’t matter that my user base wasn’t massive. It didn’t matter that the app wasn’t popular. Once something is publicly accessible, it becomes a target.&lt;/p&gt;

&lt;p&gt;The most painful realization came at the end of the year when a critical CVE hit the ecosystem I was using. My production app was suddenly vulnerable, and the damage wasn’t theoretical it was immediate and real.&lt;/p&gt;

&lt;p&gt;That experience taught me hard rules I won’t forget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Never run applications as root in production — &lt;em&gt;ever&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;  Assume every service will be scanned, probed, and tested.&lt;/li&gt;
&lt;li&gt;  Security is not an “enterprise problem”; it’s a baseline responsibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security stopped being an abstract concept for me in 2025. It became personal. And once that happens, you never build the same way again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Projects That Shaped How I Think About Software
&lt;/h2&gt;

&lt;p&gt;2025 wasn’t about the number of projects I built, it was about how each project forced me to confront a different kind of complexity. Every serious project this year changed the way I reason about systems, trade-offs, and responsibility.&lt;/p&gt;

&lt;p&gt;One of the earliest was a &lt;strong&gt;grocery delivery app&lt;/strong&gt;, inspired by products like Blinkit. On the surface, it looked straightforward: users, products, carts, orders. In reality, it introduced me to time-sensitive workflows, state transitions, and operational pressure. Orders aren’t just data — they’re promises. Late updates, inconsistent states, or missing edge cases immediately translate into broken trust. You can read more about this project &lt;a href="https://linkedin.com/pulse/10-minute-grocery-delivery-app-challenges-tech-stack-key-lohani-mwizc?lipi=urn%3Ali%3Apage%3Ad_flagship3_profile_self_edit_featured_add_article%3B9PRJxT2mTvSzb3SAhHOQ1w%3D%3D" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Later, I went deep into &lt;strong&gt;live streaming, transcoding, and video processing&lt;/strong&gt;. This was a completely different mental model. I learned that video systems are not frontend problems —they’re infrastructure problems. Encoding formats, transcoding pipelines, latency, and resource usage all matter more than UI polish. This exploration reshaped how I think about performance and scalability. more about this &lt;a href="https://www.linkedin.com/pulse/going-live-localhost-nginx-rtmp-module-divyanshu-lohani-twrdc" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finflow&lt;/strong&gt;, my expense tracker, evolved significantly this year. What started as a web app grew into a system with major updates and a &lt;strong&gt;Flutter mobile app&lt;/strong&gt; that synced data with the web. Handling consistency across platforms taught me that shared state is one of the hardest problems in software. UI is forgiving — data corruption is not.&lt;/p&gt;

&lt;p&gt;When &lt;strong&gt;GitHub Actions were blocked&lt;/strong&gt; for me, I didn’t wait. I built a &lt;strong&gt;simple CI/CD pipeline&lt;/strong&gt; myself. It wasn’t fancy, but it worked — and more importantly, it forced me to understand deployments at a lower level. When the tooling disappeared, the learning multiplied.&lt;/p&gt;

&lt;p&gt;Payments were another major theme. I integrated &lt;strong&gt;multiple payment gateways&lt;/strong&gt;, explored different verification patterns, callback flows, retries, and failure handling. Payments taught me that correctness matters more than elegance. You don’t optimize for beauty when money is involved — you optimize for certainty.&lt;/p&gt;

&lt;p&gt;I briefly explored &lt;strong&gt;Flutter more deeply&lt;/strong&gt;, but eventually realized my long-term focus was better aligned with &lt;strong&gt;Expo and JavaScript-based ecosystems&lt;/strong&gt;. That detour wasn’t wasted — it clarified my direction.&lt;/p&gt;

&lt;p&gt;By the end of the year, I was building something bigger: a &lt;strong&gt;platform-style app inspired by Hago and WePlay&lt;/strong&gt;. Not just a game, but a system — users, sessions, state, and real-time interaction.&lt;/p&gt;

&lt;p&gt;Every project added a new layer to how I think. Not “how do I build this?” but “what breaks first, and why?”&lt;/p&gt;

&lt;h2&gt;
  
  
  ReadmeHub and Remembering Why I Build
&lt;/h2&gt;

&lt;p&gt;I vibe coded a simple app to try out vibe coding but also spread a positive message around the world a small, almost sarcastic project aimed at calling out fake participation during Hacktoberfest. It was for the developers who show up only for the T-shirt, not the contribution.&lt;/p&gt;

&lt;p&gt;The app wasn’t complex. It didn’t introduce new architectural challenges. But it served a purpose it let me build something opinionated, fast, and honest without worrying about polish or metrics.&lt;/p&gt;

&lt;p&gt;That mattered more than I expected.&lt;/p&gt;

&lt;p&gt;ReadmeHub reminded me that building can still be playful. That not every project needs to become a product. Some projects exist just to say something, explore an idea, or scratch an itch.&lt;/p&gt;

&lt;p&gt;In a year full of responsibility, production pressure, and real consequences, this small project helped me reconnect with why I started building in the first place.&lt;/p&gt;

&lt;p&gt;You can checkit out at: &lt;a href="https://readmehub.divyanshulohani.xyz/" rel="noopener noreferrer"&gt;https://readmehub.divyanshulohani.xyz/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Biggest Lesson Wasn’t Technical
&lt;/h2&gt;

&lt;p&gt;Looking back, the most important things 2025 taught me had very little to do with frameworks, languages, or tools. I didn't build too much projects I explored every aspect in my existing apps I could yeah some distractions were there that did contribute to the factor of not building new projects&lt;/p&gt;

&lt;p&gt;But I learned that building software is an act of responsibility. Your mistake can cost a lot in production a mistake is not just a mistake its a real thing you are accountable for.&lt;/p&gt;

&lt;p&gt;Once something is in production, it stops being “your code” and starts being something other people rely on. That shift changes how you think, how you test, how you deploy, and how you respond when things go wrong.&lt;/p&gt;

&lt;p&gt;I also learned that speed without intention leads to fragile systems, and that maturity in engineering often looks like choosing the simpler, safer option — even when you could build something more impressive.&lt;/p&gt;

&lt;p&gt;Most importantly, I learned that growth isn’t about how much you ship. It’s about how deeply you understand the consequences of what you ship.&lt;/p&gt;

&lt;p&gt;That mindset changed everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Ahead to 2026
&lt;/h2&gt;

&lt;p&gt;If 2025 was about learning through experience, 2026 will be about &lt;strong&gt;sharing those experiences openly and building more software tougher ones&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Next year, I want to create more content not tutorials chasing trends, but honest write-ups about what worked, what broke, and what I’d do differently. I want to document the decisions behind the code, not just the code itself.&lt;/p&gt;

&lt;p&gt;I’m entering 2026 with clearer priorities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Build fewer things, but build them better&lt;/li&gt;
&lt;li&gt;  Treat production as sacred&lt;/li&gt;
&lt;li&gt;  Keep learning, but stay grounded&lt;/li&gt;
&lt;li&gt;  Share the journey, not just the outcomes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;2025 taught me how fragile software can be. 2026 is about building it with more care and helping others do the same.&lt;/p&gt;

&lt;p&gt;I hope you spent your 2025 just as amazing and fantastic as I did and looking forward I Wish you all a Happy 2026&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>newyearchallenge</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Compromise of a Production VPS</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Wed, 10 Dec 2025 19:41:08 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/the-compromise-of-a-production-vps-4065</link>
      <guid>https://dev.to/divyanshulohani/the-compromise-of-a-production-vps-4065</guid>
      <description>&lt;p&gt;Security incidents often arrive quietly, without alarm bells or dramatic warnings. They sneak into production environments when we least expect them, on weekends, during moments of downtime, or when routine vigilance slips just a little.&lt;/p&gt;

&lt;p&gt;This is the story of how my production VPS was compromised by attackers who deployed crypto-mining malware and persistent agents. What followed was a long learning process filled with investigation, cleanup attempts, unexpected failures, and finally, a deep dive into what true production-grade security really means.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Sunday Evening Shock
&lt;/h2&gt;

&lt;p&gt;It began innocently. On a calm Sunday evening, I opened my production app to check something. Except… it didn’t load. No error page. No timeout. Just a silent blank screen nothing except the slpash screen.&lt;/p&gt;

&lt;p&gt;I assumed the server might be having a hiccup. Maybe a deployment issue or that core just again said I will not work. Maybe some error or the hosting service down (which is happening a lot recently) but I was so wrong.&lt;/p&gt;

&lt;p&gt;My first thought today is sunday lets leave it for tomorrow but curiousity began in my mind so I got to my workstation and tried to SSH into the VPS. &lt;strong&gt;Connection timed out.&lt;/strong&gt; Tried again. Timed out again I thought hmm... maybe connection failure or may the vps have been shutdown due to overdue bill I supposed. again I was so wrong.&lt;/p&gt;

&lt;p&gt;After multiple attempts, the SSH prompt finally appeared. the machine was slow and sluggish like running ls was also taking 3 - 4 seconds that sluggishness became my first real clue&lt;/p&gt;

&lt;h3&gt;
  
  
  The machine wasn’t down, it was overwhelmed.
&lt;/h3&gt;

&lt;p&gt;Once I logged in, the system felt painfully sluggish. Even typing commands felt like wading through molasses.&lt;/p&gt;

&lt;p&gt;I ran:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;top 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And saw something that changed the tone of the night and runined the few hours of my weekend&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bad Actor
&lt;/h3&gt;

&lt;p&gt;A process named &lt;strong&gt;&lt;code&gt;rysyslo\&lt;/code&gt;&lt;/strong&gt;, located in /usr/bin/rsyslo, was consuming roughly &lt;strong&gt;80% CPU&lt;/strong&gt; and a significant amount of RAM and almost all of the disk space&lt;/p&gt;

&lt;p&gt;I had never installed any binary by that name. Ubuntu the os, doesn’t ship such a file.&lt;/p&gt;

&lt;p&gt;A quick chat and search confirmed it. It was malicious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My server had been breached.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  First Cleanup Attempt; Sense of a False Victory
&lt;/h2&gt;

&lt;p&gt;I deleted the suspicious binary. Removed unfamiliar services. Killed the rogue processes. The load dropped. The system behaved normally. Everything felt fine. But deep down in my mind I knew that it may not have been gone with such simple attempts (spoiler alert: I was right) but still I left it as I thought I will backup the data tomorrow&lt;/p&gt;

&lt;h2&gt;
  
  
  The Next Day Things Get Worse, Not Better
&lt;/h2&gt;

&lt;p&gt;The following morning, I again opned the app, still unavailable. Over 16 hours of downtime at this point.&lt;/p&gt;

&lt;p&gt;SSH once again behaved erratically — refusing connections for minutes at a time. I even had to ask chat gpt to get me script to constantly try to get in using ssh because it just kept on timing out.&lt;/p&gt;

&lt;p&gt;When I finally got in, the situation had escalated dramatically.&lt;/p&gt;

&lt;p&gt;Running top revealed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;xmrig&lt;/strong&gt; — a notorious crypto-mining tool&lt;/li&gt;
&lt;li&gt;  Multiple disguised binaries with random names&lt;/li&gt;
&lt;li&gt;  Fake kernel threads impersonating system processes&lt;/li&gt;
&lt;li&gt;  A mysterious program called &lt;strong&gt;monarx-agent&lt;/strong&gt; consuming memory&lt;/li&gt;
&lt;li&gt;  Node processes I had not launched&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first obvious solution that came to my mind was to kill them but I knew this was not going to do anything because they must have set backdoors and other ways to restart the service. The system was no longer merely “infected.” It was &lt;strong&gt;owned&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Attackers’ Footprint
&lt;/h3&gt;

&lt;p&gt;I still am very beginner in devops and linux systems even though I use it as my daily driver. I asked chat gpt how can I clean the system follwed the steps closely and this bought me some time to backup any server data that was there.&lt;/p&gt;

&lt;p&gt;First I did basic scans to search for malicious services and found alive.service and lived.service these are too not created by the os and ofcourse not by me hence: malicious they both pointed to a script file located in the following locations:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/tmp/runnv/alive.sh
/tmp/runnv/lived.sh 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Both services were configured to:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Restart=always
RestartSec=5s 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;These scripts continuously fetched, restored, and relaunched the miner — even seconds after deletion. This explained everything: No matter how many times I killed a process or removed a binary, the malicious services resurrected them.&lt;/p&gt;

&lt;p&gt;This wasn’t a small compromise. It was a full-scale, multi-vector persistence setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Services Begin to Break
&lt;/h2&gt;

&lt;p&gt;Over time, the machine became unstable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  SSH stalled or dropped repeatedly&lt;/li&gt;
&lt;li&gt;  System operations slowed to a crawl&lt;/li&gt;
&lt;li&gt;  Memory usage spiked unpredictably&lt;/li&gt;
&lt;li&gt;  System logs showed irregularities&lt;/li&gt;
&lt;li&gt;  A fake agent (monarx-agent) reappeared no matter how many times it was removed&lt;/li&gt;
&lt;li&gt;  The machine would barely stay functional long enough for basic commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There was no longer any pretense of stability it became like the VPS was setup intentionally for mining bitcoin.&lt;/p&gt;

&lt;p&gt;The attackers had established &lt;strong&gt;root-level foothold&lt;/strong&gt; (this was due to my fault but more on this later), and the operating system had been modified in ways I could no longer trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Response to the things
&lt;/h2&gt;

&lt;p&gt;At this stage it was not about that how can I fix the server but the important server data that's on there should be extracted asap and the user data should be checked to see that it did not have a breech&lt;/p&gt;

&lt;p&gt;I began systematically backing up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  PostgreSQL database dumps using pgdump&lt;/li&gt;
&lt;li&gt;  Application server files&lt;/li&gt;
&lt;li&gt;  Media directories&lt;/li&gt;
&lt;li&gt;  Configuration files&lt;/li&gt;
&lt;li&gt;  User uploads&lt;/li&gt;
&lt;li&gt;  Essential environment variables&lt;/li&gt;
&lt;li&gt;  Reverse proxy configs&lt;/li&gt;
&lt;li&gt;  Logs for analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the system was barely responsive, everything required workarounds,&lt;/p&gt;

&lt;p&gt;For anything related to files I used rsync and copied the files to my local machine but this was not the usual way I had to write scripts to check the access of the ssh and then copy the files because the ssh connection kept on dropping.&lt;/p&gt;

&lt;p&gt;Eventually, I succeed in the task &lt;strong&gt;every important asset was safely backed up&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;After confirming data integrity and to my surprise the application data was untouched by the hackers they just wanted to use my machine to mine crypto, so I made the decision to shut down the compromised machine permanently and migrate to a fresh VPS instance because this was destroyed beyod repair.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching for the Entry Point
&lt;/h2&gt;

&lt;p&gt;After all the chaos I was geniunly curous that how did they get access to my server, I never had my keys leaked nor the IP so I began looking into this.&lt;/p&gt;

&lt;p&gt;I began by reviewing all forensic data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  /var/log/auth.log&lt;/li&gt;
&lt;li&gt;  SSH access attempts&lt;/li&gt;
&lt;li&gt;  Systemd unit file timestamps&lt;/li&gt;
&lt;li&gt;  Root-level process creation logs&lt;/li&gt;
&lt;li&gt;  Cron jobs&lt;/li&gt;
&lt;li&gt;  Unknown script origins&lt;/li&gt;
&lt;li&gt;  Unfamiliar executables&lt;/li&gt;
&lt;li&gt;  Running port lists&lt;/li&gt;
&lt;li&gt;  Binaries in /usr/bin, /tmp, /dev/shm, /usr/share/updater&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I discovered:&lt;/p&gt;

&lt;p&gt;I was right I did not had my keys leaked or something like that. The log only showed rejected SSH requests which I know happen because some people just leave everything to the default (I am one of those but not at this point)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything was good but annoyingly&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  ❗ There was no definitive entry point.
&lt;/h3&gt;

&lt;p&gt;So where did they get in?&lt;/p&gt;

&lt;p&gt;The prime suspect is the very recently disclosed &lt;a href="https://www.wiz.io/blog/critical-vulnerability-in-react-cve-2025-55182" rel="noopener noreferrer"&gt;&lt;strong&gt;React2Shell vulnerability (CVE-2025-55182)&lt;/strong&gt;&lt;/a&gt; revealed just a day before the attack. This vulnerability allows remote code execution under certain misconfigured environments.&lt;/p&gt;

&lt;p&gt;Web servers or exposed ports would have been particularly vulnerable. Another major factor:&lt;/p&gt;

&lt;p&gt;And yes I was right I checked the nginx logs for the server and there I see someone forcefully did a post request to every endpoint they could've found and there might been vulnerable endpoints which gave them access to the machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hostinger VPS firewall defaults to allow ALL inbound and outbound traffic.
&lt;/h3&gt;

&lt;p&gt;Unlike cloud platforms with strict security groups (AWS, GCP), this VPS was wide open at the network level. this was the default thing I never touched.&lt;/p&gt;

&lt;p&gt;This alone dramatically increases the attack surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Biggest Lesson — Never Run Applications as root
&lt;/h2&gt;

&lt;p&gt;One big mistake I had made knowingly that not to run apps with root privelleges that they can modify the system in anyway. My PostgreSQL server, which ran under a &lt;strong&gt;non-root system user&lt;/strong&gt;, remained untouched by the attacker, I know they were not looking for it but in case then it would've still remained safe No tampered files. No altered configurations. No corrupt data. No suspicious access. Even though the system was compromised, the database was safe.&lt;/p&gt;

&lt;p&gt;This incident reinforced a principle I will never ignore again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never run applications as root. Ever.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Give them the smallest permissions required to function and nothing more. not even modification outside the directory (unless your app has to access).&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rebuild
&lt;/h2&gt;

&lt;p&gt;After recovering and moving to a fresh VPS, I rebuilt the environment with proper security measures:&lt;/p&gt;

&lt;p&gt;Firstly I ensured that I do not run the process as the root user I create users for each module like for the server for the frontend or for the api. NO compromise should be made in setting users up.&lt;/p&gt;

&lt;p&gt;I enforced strict firewall rules only allow http and https ports to be expoed to the public because my app does run postgres but as an internal tool and the database should not be accisble publically all traffic is handled via nginx so other ports should not be open.&lt;/p&gt;

&lt;p&gt;This wasn’t just “rebuilding a server.” It was rebuilding an entire philosophy about production infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Security incidents can be unsettling, especially when a server is actively failing under the strain of malware and critical data must be preserved in a limited window of time. Yet, despite the pressure, this experience provided a deeper and more practical understanding of DevOps and cybersecurity than any structured lesson could have offered. Real incidents force you to confront the consequences of design decisions, configuration choices, and operational habits in a way theoretical learning never does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key lessons learned:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;A VPS exposed with open ports is an open invitation for attackers.&lt;/strong&gt; Any publicly accessible service increases the attack surface. Without strict access controls, monitoring, and proper configuration, automated scanners and bots will quickly identify and target vulnerabilities.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The presence of a crypto miner is a clear indicator of full system compromise.&lt;/strong&gt; Attackers who deploy miners rarely stop there; they often modify system files, escalate privileges, and establish hidden footholds. Once this occurs, trust in the system is lost. Rebuilding from a clean state is the only reliable response.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Attackers rely on persistence mechanisms that are deeper than a single malicious process.&lt;/strong&gt; Modern attacks may involve cron jobs, rootkits, altered binaries, SSH backdoors, and hidden user accounts. Removing one visible component does not eliminate the underlying compromise.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Least privilege is a critical layer of defense.&lt;/strong&gt; Systems designed so that applications do not run as root help ensure that, even if an intrusion occurs, the damage is limited. Proper isolation, permissions, and role separation can prevent attackers from accessing sensitive data or making irreversible changes.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Backups are invaluable during emergencies.&lt;/strong&gt; When a system is compromised, having recent, verified, and off-server backups can mean the difference between a smooth recovery and catastrophic data loss. Backup integrity and testing should be treated as core operational practices.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Default configurations are rarely secure.&lt;/strong&gt; Vendor defaults prioritize usability and compatibility, not security. Hardening a system requires deliberate configuration: limiting access, disabling unnecessary services, enforcing strong authentication, and enabling proper logging.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Incident response demands composure and methodical action.&lt;/strong&gt; In high-pressure situations, rushing often leads to mistakes. A structured approach—assess, contain, preserve evidence, recover, and improve—ensures that decisions are informed and effective rather than reactive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ultimately, this was far more than an unplanned lesson in server hardening. It served as a reminder that security is not a checkbox or a one-time setup. It is an ongoing discipline that requires continuous awareness, proactive measures, and a mindset of vigilance.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;My production VPS was fully compromised by attackers who deployed crypto-mining malware and multiple persistence mechanisms. Initial cleanup attempts failed because the system had already been taken over at the root level. After identifying disguised services, malicious binaries, and severe instability, I focused on backing up critical data before rebuilding everything on a fresh server.&lt;/p&gt;

&lt;p&gt;The investigation showed that open ports, default firewall settings, and a likely exploitation of a newly disclosed vulnerability created the entry point. The incident reinforced essential lessons: never expose unnecessary services, never run applications as root, always enforce least privilege, and treat security as an ongoing discipline—not a one-time setup.&lt;/p&gt;

</description>
      <category>react</category>
      <category>react2shell</category>
      <category>nextjs</category>
      <category>vulnerabilities</category>
    </item>
    <item>
      <title>When Hacktoberfest Became README-fest - I did this</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Wed, 08 Oct 2025 10:59:58 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/when-hacktoberfest-became-readme-fest-i-did-this-4kk2</link>
      <guid>https://dev.to/divyanshulohani/when-hacktoberfest-became-readme-fest-i-did-this-4kk2</guid>
      <description>&lt;h2&gt;
  
  
  The October of Open Source 🎃
&lt;/h2&gt;

&lt;p&gt;October is a magical month for developers. While most people think it as just a normal month transition from summer to winter, we developers have our own tradition, &lt;strong&gt;Hacktoberfest&lt;/strong&gt;. It’s that time of year when GitHub notifications blow up, Discord servers buzz with collaboration, and developers from every corner of the internet collectively shout,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Hey, let’s contribute to open source!”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hacktoberfest, started by &lt;strong&gt;DigitalOcean in 2014&lt;/strong&gt;, was a simple yet brilliant idea: encourage developers to contribute to open source, reward them with limited-edition swag, and spread the joy of collaboration. It lowered the barrier for beginners so suddenly, even your first small PR (Pull Request) felt like a ticket to the world of real-world software.&lt;/p&gt;

&lt;p&gt;It wasn’t just about code; it was about community. People made friends, joined projects, fixed typos, improved docs, and sometimes even built features that millions now use. For many developers, Hacktoberfest was their &lt;em&gt;gateway drug&lt;/em&gt; into open source.&lt;/p&gt;

&lt;p&gt;But like all things on the internet, what starts wholesome… eventually mutates into chaos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Source: The Internet’s Most Beautiful Mess
&lt;/h2&gt;

&lt;p&gt;To anyone outside the dev world, “open source” sounds like a technical term. But for us, it’s much more than that. it’s &lt;em&gt;a philosophy&lt;/em&gt;. It’s about sharing knowledge, collaboration without boundaries, and believing that software gets better when everyone can help shape it.&lt;/p&gt;

&lt;p&gt;I still remember my first real open source contribution. It was at &lt;a href="https://www.linkedin.com/company/formbricks/" rel="noopener noreferrer"&gt;Formbricks&lt;/a&gt; as a part of combined initiative that was &lt;a href="http://oss.gg" rel="noopener noreferrer"&gt;oss.gg&lt;/a&gt; I stepped into the world by just making a small change adding a &lt;em&gt;Last Used&lt;/em&gt; indicator to the login page.&lt;/p&gt;

&lt;p&gt;But as we know every coin has two faces open source is no exception to that, the noble idea of coming together and helping build good software for people collided with internet culture’s favorite game: &lt;strong&gt;the numbers game (&lt;/strong&gt;&lt;em&gt;it ruins everything&lt;/em&gt;&lt;strong&gt;)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: When “Open Source” Turned Into “Open Spam”
&lt;/h2&gt;

&lt;p&gt;Let’s be honest — the moment a reward system exists, people will find a shortcut to just abuse it and get the reward no matter the outcome.&lt;/p&gt;

&lt;p&gt;As Hacktoberfest grew, so did YouTube tutorials titled &lt;em&gt;“How to Win Hacktoberfest FAST!”&lt;/em&gt; New devs, excited to participate (and maybe get a free t-shirt), started following “guides” that basically said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Just find any repo, fix a typo, and submit four PRs.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And thus began the era of the &lt;strong&gt;README Pull Request Apocalypse&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Repos everywhere were getting flooded with contributions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  “Fixed a small grammar mistake.”&lt;/li&gt;
&lt;li&gt;  “Added full stop at the end.”&lt;/li&gt;
&lt;li&gt;  “Updated README title from H1 to H2.”&lt;/li&gt;
&lt;li&gt;  “Added emojis for ✨aesthetic✨.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some poeple &lt;em&gt;(yes people not devs)&lt;/em&gt; just be like I will add my name to the readme for what reason god knows but they will do.&lt;/p&gt;

&lt;p&gt;Open source maintainers were drowning in meaningless PRs. Some people were even creating repos &lt;em&gt;just to spam each other with fake PRs&lt;/em&gt;. Github like introduced a feature to like limit new users from contribution which can be turned off every major framework or library has that turned on, It became a game changed from who can get do meaningful help to who could get four green squares the fastest.&lt;/p&gt;

&lt;p&gt;Now, don’t get me wrong — I don’t blame anyone. For many, it wasn’t malice; it was &lt;em&gt;desperation to belong&lt;/em&gt;. Beginners wanted to see their name in the contributors list. They wanted proof they were “part of open source.”&lt;/p&gt;

&lt;p&gt;But in chasing that feeling, Hacktoberfest started losing what made it beautiful — genuine collaboration.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Pun-In-Code Response: Enter ReadmeHub
&lt;/h2&gt;

&lt;p&gt;I could’ve written a tweet thread complaining about it. Or made a meme. But instead, I thought — &lt;em&gt;why not turn the whole situation into a joke that actually works as an app?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That’s how &lt;strong&gt;ReadmeHub&lt;/strong&gt; was born — a parody platform for “&lt;em&gt;open source contributors&lt;/em&gt;” who just can’t resist updating README files.&lt;/p&gt;

&lt;p&gt;It’s like GitHub’s unserious cousin — built purely for fun and satire. You log in, type your username, and you’re now a “contributor.”&lt;/p&gt;

&lt;p&gt;The app has a variety of repositories to contribute to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;cool-project&lt;/strong&gt; – “The coolest project you’ll ever see (probably not)”&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;todo-app-9000&lt;/strong&gt; – “Yet another todo app that will definitely change your life”&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;useless-project&lt;/strong&gt; – “A project so useless, it’s actually useful”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can contribute to them in following ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Fixing Typos&lt;/li&gt;
&lt;li&gt;  Adding Emojis (AI thinks it makes a repo look professional)&lt;/li&gt;
&lt;li&gt;  Adding Quotes randomly&lt;/li&gt;
&lt;li&gt;  Vibe edit the readme using AI ofcourse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every “contribution” you make earns you &lt;em&gt;badges&lt;/em&gt; like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  🏆 &lt;strong&gt;Fixed 10 Typos&lt;/strong&gt; – Grammar police salute you!&lt;/li&gt;
&lt;li&gt;  🚀 &lt;strong&gt;Added 500 Emojis&lt;/strong&gt; – Houston, we have a contribution!&lt;/li&gt;
&lt;li&gt;  ☕ &lt;strong&gt;Coffee-Fueled Coder&lt;/strong&gt; – 2 a.m. commits, fueled by caffeine and chaos.&lt;/li&gt;
&lt;li&gt;  🐦 &lt;strong&gt;Early Bird&lt;/strong&gt; – You were here before it was cool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the best part? You can generate a &lt;strong&gt;shareable image&lt;/strong&gt; — a fake contribution card to flex on your developer friends.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9b2pnfran5ryl8hcfiu4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9b2pnfran5ryl8hcfiu4.png" alt="My Profile" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because sometimes, instead of fighting absurdity… it’s more fun to join in and make it &lt;em&gt;funnier.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 Try it here: &lt;a href="https://readmehub.divyanshulohani.xyz/" rel="noopener noreferrer"&gt;https://readmehub.divyanshulohani.xyz/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hidden Message Beneath the Joke 💭
&lt;/h3&gt;

&lt;p&gt;ReadmeHub isn’t just a meme project (okay, maybe 90% meme, 10% message). But it also says something about developer culture today.&lt;/p&gt;

&lt;p&gt;The world of programming is increasingly gamified — GitHub streaks, LeetCode badges, AI-assisted commits, contribution graphs. Sometimes, we forget that &lt;em&gt;code is supposed to solve problems&lt;/em&gt;, not just earn points.&lt;/p&gt;

&lt;p&gt;ReadmeHub pokes fun at that obsession with metrics. It’s satire for anyone who has ever stared at their GitHub profile thinking,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I need more green squares.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;or&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I need to have that perfect looking github graph"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s a reminder that &lt;em&gt;your value as a developer isn’t measured in PR counts or stars.&lt;/em&gt; It’s measured by curiosity, learning, and the problems you solve — even if they’re small.&lt;/p&gt;

&lt;p&gt;Ironically, by making a fake-contribution app, I ended up having deeper conversations with developers about &lt;em&gt;real&lt;/em&gt; open-source contribution. Sometimes humor opens doors that serious rants can’t.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Irony of It All
&lt;/h2&gt;

&lt;p&gt;The funniest (and maybe saddest) realization? A parody app about fake contributions probably got more engagement than many genuine open-source repos do in a month.&lt;/p&gt;

&lt;p&gt;But that’s also what makes developer culture fascinating — we love building weird things, just because we can.&lt;/p&gt;

&lt;p&gt;Every viral project starts with a mix of humor and curiosity. Whether it’s a useless API that returns random cat facts, or a website that tells you “how many coffees you’ve coded today,” — it’s all part of the same creative chaos that fuels open source.&lt;/p&gt;

&lt;p&gt;ReadmeHub fits right into that lineage — silly, smart, and just self-aware enough to make people smile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: What Hacktoberfest Still Means
&lt;/h2&gt;

&lt;p&gt;At its core, Hacktoberfest is still a beautiful idea. It gets thousands of people writing code, exploring projects, and learning skills they’ll carry for life maybe we will see some more open source projects.&lt;/p&gt;

&lt;p&gt;But maybe we all need a reminder: Contributions are more than commits — they’re collaboration, communication, and creativity combined. If you’re contributing to open source this October, do it for the right reasons: to learn, to share, to help.&lt;/p&gt;

&lt;p&gt;And if you’re just here for the badges, emojis, and “README updates”… don’t worry — we’ve got you covered. Visit &lt;a href="https://readmehub.divyanshulohani.xyz/" rel="noopener noreferrer"&gt;&lt;strong&gt;ReadmeHub&lt;/strong&gt;&lt;/a&gt; and go wild. After all, sometimes the best way to make a point is to &lt;strong&gt;turn it into a punchline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now go and fix some typos while I find a repo that can run on my pc.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>hacktoberfest</category>
      <category>opensource</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Implementing Real-Time Chat with SSE vs WebSockets (and Why I Chose One)</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Mon, 15 Sep 2025 04:12:03 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/implementing-real-time-chat-with-sse-vs-websockets-and-why-i-chose-one-2mn2</link>
      <guid>https://dev.to/divyanshulohani/implementing-real-time-chat-with-sse-vs-websockets-and-why-i-chose-one-2mn2</guid>
      <description>&lt;p&gt;Every developer building a chat app eventually stumbles into the same rabbit hole: &lt;strong&gt;Do I use WebSockets or Server-Sent Events (SSE)?&lt;/strong&gt; It sounds like a minor technical choice at first—just pick whichever tutorial looks nicer on YouTube and move on. But trust me, this one decision can quietly shape the scalability, complexity, and sanity of your project (and your future AWS bills if you pick wrong).&lt;/p&gt;

&lt;p&gt;When I started working on real-time communication for my Django app, I had to make this exact decision. The app needed chat, but chat wasn’t the &lt;em&gt;main&lt;/em&gt; feature just a nice to have. So naturally I didn't wnat to spend more time on selecting techonlogies and give it a whole new timeline.&lt;/p&gt;

&lt;p&gt;In this article, we’ll dive deep into &lt;strong&gt;what real-time communication really is&lt;/strong&gt;, explore &lt;strong&gt;how SSE and WebSockets work&lt;/strong&gt;, compare them side by side, and finally, I’ll share why I picked SSE for my project (and why it was some sort of inspired by linkedin).&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep Dive into SSE
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Sever Sent Events (SSE)&lt;/strong&gt; is like an open door which the client makes to the server the open door only allows one way communication from server to the client not the other way around it sends messages its just another TCP connection which just has its end packet forgotten so it keeps on going.&lt;/p&gt;

&lt;p&gt;It’s built on &lt;strong&gt;plain old HTTP&lt;/strong&gt;, so you don’t need to break your brain setting up new protocols.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Example: SSE in Django
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Server (Django view):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.http&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StreamingHttpResponse&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sse_chat_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;event_stream&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="c1"&gt;# In reality, pull from Redis/pub-sub
&lt;/span&gt;                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data: New message at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StreamingHttpResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;event_stream&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/event-stream&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Client (JavaScript):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/chat/stream/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;New message:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. You’ve got a real-time connection with about 20 lines of code. (Which is about 50 fewer than WebSockets usually require.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros of SSE
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  Simple and easy to impliment with very less of a overhaul.&lt;/li&gt;
&lt;li&gt;  One way communication mostly realtime but this can become a down side too&lt;/li&gt;
&lt;li&gt;  Your browser autoretrys the connection in an event of failure&lt;/li&gt;
&lt;li&gt;  Almost all browsers support it (Almost is due to IE)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons of SSE
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  So one way communication is also pro but can be a con depending on your project.&lt;/li&gt;
&lt;li&gt;  Not great if your app relies on the milisecond the message is sent you have to recieve it&lt;/li&gt;
&lt;li&gt;  No binary data (text only). If you need binary, look elsewhere. but can be useful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here’s the kicker: for 80% of “real-time” needs—like chats that aren’t the entire focus of the product—SSE is more than enough you don't need NASA like communication that every millisecond matters.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqff28swlic12em47zdi3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqff28swlic12em47zdi3.png" alt="SSE Connection Diagram" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep Dive into WebSockets
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;WebSockets are like having a direct phone line between client and server. Once connected, both can talk freely, as much as they want, whenever they want, just a private, always-on conversation.&lt;/p&gt;

&lt;p&gt;It’s a separate protocol that starts with an HTTP handshake and then upgrades to a &lt;strong&gt;persistent TCP connection&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Example: WebSockets
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Server (Node.js with&lt;/strong&gt; ws*&lt;em&gt;):&lt;/em&gt;*&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;wss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Received: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Echo: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Client:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws://localhost:8080&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onopen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello Server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you’ve got two-way communication—chat, games, collaborative docs, you name it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros of WebSockets
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  Both ways of communication can happen any time.&lt;/li&gt;
&lt;li&gt;  Latency is very low almost realtime&lt;/li&gt;
&lt;li&gt;  Perfect for chat apps if your main focus is reducing latency to the 10th of a second&lt;/li&gt;
&lt;li&gt;  Binary data handling is supported.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons of WebSockets
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  Complex to scale and has lots of network limitations&lt;/li&gt;
&lt;li&gt;  Requires extra setup in case of django it can be another level of headache&lt;/li&gt;
&lt;li&gt;  Its too powerful for simple apps and comes with the same cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffzwxob74d4hp0phaamqp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffzwxob74d4hp0phaamqp.png" alt="Comparison b/w two" width="625" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Real-World Implementation
&lt;/h2&gt;

&lt;p&gt;Here’s the context: I was building a Django app where chat was a &lt;strong&gt;nice-to-have feature&lt;/strong&gt;, not the core. Think of it as the beverages that come in a Burger King combo not needed but good to have. (Yes I don't like soft drinks that much)&lt;/p&gt;

&lt;p&gt;I first considered &lt;strong&gt;WebSockets&lt;/strong&gt;. They’re the gold standard for chat, after all. But as I dug deeper, I realized setting up Django Channels, handling scaling with Redis, configuring sticky sessions, and then debugging it all would be like bringing a rocket launcher to open a soda can.&lt;/p&gt;

&lt;p&gt;Then I started researching more on the topic and did some reverse engineering on apps I use such as linkedin what they use in their apps for the same I found that linkedin used SSE for the chats and it just works very well I know they have put a lot of thought in selecting this as their primary system but it gave me a way then I learned about SSE and implimented it in my app. It has allowed me to have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Real-time server → client updates.&lt;/li&gt;
&lt;li&gt;  Super easy scaling with existing HTTP infra.&lt;/li&gt;
&lt;li&gt;  Less complexity to manage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And to be honest that's all I need for my app I don't need like crazy good realtime updates its not Telegram or Discord. All I do is create a POST request for new message from clients and using simple redis queue and some pubsub magic it just works.&lt;/p&gt;

&lt;p&gt;Simple. Effective. And no therapy sessions required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Chat with SSE
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Server (Django + Redis pub/sub):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.http&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StreamingHttpResponse&lt;/span&gt;

    &lt;span class="n"&gt;redis_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StrictRedis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;event_stream&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;pubsub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pubsub&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;pubsub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;room_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pubsub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StreamingHttpResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;event_stream&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/event-stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Client:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/chat/stream/room123/`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;New message:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Sending messages&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#send&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/chat/send/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello world&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons Learned &amp;amp; Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Start simple.&lt;/strong&gt; Don’t dive into WebSockets unless you truly need bidirectional communication they just require lot more effort than they should. (If all you want is notifications, SSE is your friend.)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Scale smartly.&lt;/strong&gt; Use Redis pub/sub for message distribution. Throw in Nginx as a reverse proxy, and you’re good for a long time.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Fallbacks matter.&lt;/strong&gt; If SSE fails (say, old browsers), fall back to long-polling. Your users don’t care about protocols; they just want their message to show up.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Measure before you optimize.&lt;/strong&gt; Don’t assume you’ll have 10 million concurrent users tomorrow. Pick the tool that works now and is easy to evolve later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;To sum it up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;SSE&lt;/strong&gt;: Lightweight, simple, perfect for one-way updates.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;WebSockets&lt;/strong&gt;: Powerful, full-duplex, perfect for heavy chat/gaming apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For my project, I chose &lt;strong&gt;SSE&lt;/strong&gt;—because it was easy, scalable enough, and fit my use case without unnecessary complexity.&lt;/p&gt;

&lt;p&gt;Real-time communication isn’t about flexing the fanciest tech—it’s about delivering the right experience without making your life miserable as a developer.&lt;/p&gt;

&lt;p&gt;So, what about you? &lt;strong&gt;Did you pick SSE or WebSockets for your project?&lt;/strong&gt; Let me know—I’d love to hear your war stories.&lt;/p&gt;

</description>
      <category>sse</category>
      <category>websockets</category>
      <category>django</category>
      <category>python</category>
    </item>
    <item>
      <title>How Shipping Code in a Rush Can Cause Uncalculated Losses</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Tue, 09 Sep 2025 11:02:24 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/how-shipping-code-in-a-rush-can-cause-uncalculated-losses-3bg8</link>
      <guid>https://dev.to/divyanshulohani/how-shipping-code-in-a-rush-can-cause-uncalculated-losses-3bg8</guid>
      <description>&lt;p&gt;There’s a golden rule in software development—something every junior developer learns within their first month of writing code: &lt;strong&gt;“Never ship untested code to production.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It sounds simple enough, right? Like “look both sides before crosing a road” or “don't use metal in a microwave” Yet, every now and then, giant corporations (with teams of thousands of developers, mind you) somehow forget this rule. And when they do, the results are usually hilarious for users and catastrophic for the company’s balance sheet.&lt;/p&gt;

&lt;p&gt;This past week, Reliance JioMart (yes, the Reliance-backed e-commerce giant) may have had their mishappening. Officially, JioMart hasn’t said a word about it (and they probably never will—because who likes to admit they left the door wide open?). But users discovered something that looked suspiciously like a bug, and the telegram mafia did what it always does best: exploit it, and turn it into flowing cash&lt;/p&gt;

&lt;h2&gt;
  
  
  The Incident: When Offer Turned into Infinite Money Glitches
&lt;/h2&gt;

&lt;p&gt;On the surface, JioMart’s scheme looked innocent enough. They rolled out a promotion saying: spend ₹101 or more, and you’d get ₹100 off. Great deal, right? (No, not even if this was not the incident) The kind that makes you add that extra packet of biscuits to your cart just to qualify.&lt;/p&gt;

&lt;p&gt;Except… the way they set it up apparently had a fatal flaw. The promotional link they sent wasn’t just a one-time coupon link—it was reportedly a &lt;strong&gt;coupon factory&lt;/strong&gt;. Every time you clicked it (or tweaked it just a little), boom: a brand-new coupon worth ₹100 landed in your account. Unlimited. Endless. Like Willy Wonka’s chocolate river, but instead of chocolate, it was discount codes.&lt;/p&gt;

&lt;p&gt;And because the internet is faster at finding loopholes than companies are at closing them, people quickly caught on. Telegram groups saw a great opportunity and turned it to a cash thing. They started offering hundrends even thousands of cupons for free and once they ran out(which was pretty quick) they started selling them ₹10–20 each. Think about that: a ₹100 coupon going for just ₹10 or even free if you were lucky and quick. That’s not just a discount—that’s arbitrage, baby.&lt;/p&gt;

&lt;p&gt;In effect, JioMart’s marketing campaign accidentally created a &lt;strong&gt;black market economy&lt;/strong&gt; inside Telegram. Imagine explaining that in a board meeting:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Sir, our competitors are not the issue. Our biggest competitor is now a Telegram group run by some college kids selling our coupons at 90% off.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What (Allegedly) Went Wrong
&lt;/h2&gt;

&lt;p&gt;From digging into the reports (and yes, some curious minds actually tested the link), the bug seemed almost embarrassingly simple. The link they sent out had a &lt;strong&gt;query parameter&lt;/strong&gt; which likely was a tracking id for the specific customer but was not checked.&lt;/p&gt;

&lt;p&gt;But here’s the kicker: the parameter could accept &lt;em&gt;any random value&lt;/em&gt; and still generate a fresh coupon code like a coupon creating machine. Now, as developers, we can all imagine how this might have happened. Maybe someone forgot to add a validation check. Maybe QA was skipped because deadlines were looming. Or maybe the dev team thought, &lt;em&gt;“Who’s going to notice?”&lt;/em&gt; (Answer: everyone. Everyone noticed.)&lt;/p&gt;

&lt;p&gt;It raises the question: what kind of logic was powering this system? Was it something like:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (request.hasQueryParam) {
    generateCoupon();
} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Because that’s what it felt like. One missing if condition. One missing database check. One small oversight that spiraled into thousands of coupons.&lt;/p&gt;

&lt;p&gt;And the funny part? This wasn’t some deep, sophisticated hack that required genius-level skills. No, this was &lt;strong&gt;low-hanging fruit&lt;/strong&gt;. It’s the digital equivalent of leaving your shop unlocked at night with a sign saying, &lt;em&gt;“Please don’t steal.”&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Aftermath: Servers Down, Coupons Everywhere
&lt;/h2&gt;

&lt;p&gt;As word spread, chaos followed. JioMart’s cart service and related systems reportedly buckled under the load. Think about it: hundreds (maybe thousands) of people hammering the system, generating coupon after coupon, placing order after order.&lt;/p&gt;

&lt;p&gt;For customers, it was like Diwali came few weeks early. For JioMart, it must have looked like a DDoS attack—but powered not by bots, but by &lt;em&gt;real coupon hungry people.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Financially, the losses could have been massive. Even if only a fraction of the coupons were redeemed, each represented ₹100 in lost revenue. Multiply that by thousands, and you’re staring at some very uncomfortable spreadsheets.&lt;/p&gt;

&lt;p&gt;JioMart has not confirmed the incident. Which makes sense. You don’t exactly want your brand trending on Twitter under &lt;em&gt;#UnlimitedCoupons&lt;/em&gt;. Admitting it would be like saying, &lt;em&gt;“Yeah, we accidentally let people print money for a day. Oops.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But interestingly, the next day the link magically stopped working. Coincidence? Or a silent fix? I’ll let you decide. (Spoiler: it was a fix.). Still some sort of bug or intended feature like everyday you would click the same link you would still get a brand new coupon but seems like its intended working.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why This Matters (and Why It’s Funny)
&lt;/h3&gt;

&lt;p&gt;On one level, this is hilarious. Big company leaves giant hole, users exploit it, chaos ensues. It’s the kind of story that makes developers shake their heads and laugh nervously, because deep down, we all know: &lt;em&gt;this could happen to any of us if we’re not careful&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;But on another level, it’s dead serious. This wasn’t just a minor UI glitch or a typo in the app. This was a &lt;strong&gt;systemic design flaw&lt;/strong&gt;—the kind that directly hits revenue.&lt;/p&gt;

&lt;p&gt;The sarcasm writes itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  “Why bother with rate limiting? Let everyone generate coupons like new orders”&lt;/li&gt;
&lt;li&gt;  “Who needs backend validation when you can trust query parameters, the most trustworthy part of the internet?”&lt;/li&gt;
&lt;li&gt;  “Unit testing? Nah. Let production users do the testing for us. They’ll find bugs faster anyway.”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons for Developers (and Corporates Who Don’t Listen)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Validation is sacred.&lt;/strong&gt; Never trust input from the client side. Ever. It doesn’t matter if it’s a coupon code, a price, or a user ID—if your backend just accepts it blindly, you’re inviting chaos.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Abuse scenarios are real.&lt;/strong&gt; When designing a system, don’t just think, &lt;em&gt;“What’s the happy path?”&lt;/em&gt; Think, &lt;em&gt;“How would someone misuse this?”&lt;/em&gt; Because trust me, someone will.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Testing isn’t optional.&lt;/strong&gt; Yes, deadlines are tight. Yes, management wants the feature out “yesterday.” But the cost of rushing can dwarf the cost of a delay. Which would you rather explain in a meeting: &lt;em&gt;“We’re shipping a week late,”&lt;/em&gt; or &lt;em&gt;“We accidentally lost crores because our coupon system was a slot machine”?&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Transparency matters (sometimes).&lt;/strong&gt; JioMart chose silence, which is understandable. But imagine the goodwill they could have earned by acknowledging it lightly—“Hey, we goofed, it’s fixed, no free money anymore.” At least it would have made them human.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;Incidents like this aren’t rare. History is full of “fun” bugs that cost companies millions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  In 2014, a bug in Shoppers Stop’s online store let people order items worth thousands for ₹1.&lt;/li&gt;
&lt;li&gt;  In 2019, a Paytm glitch allowed people to generate unlimited movie tickets using promo codes.&lt;/li&gt;
&lt;li&gt;  And who can forget the infamous “Amazon accidentally listed expensive cameras for $94” bug? (Someone, somewhere, got Christmas and Diwali rolled into one.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common thread? &lt;strong&gt;Rushing to production without proper testing.&lt;/strong&gt; It’s the software equivalent of building a skyscraper but forgetting to check if the elevator works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Whether or not JioMart ever admits it, this was an &lt;em&gt;interesting choke point&lt;/em&gt; in their system design—a perfect example of how one unchecked assumption can snowball into a full-blown crisis.&lt;/p&gt;

&lt;p&gt;For developers, it’s a cautionary tale. For users, it was a once-in-a-lifetime sale. And for JioMart, it’s probably a very expensive lesson written into some internal post-mortem doc with the title: &lt;em&gt;“Why We Test Promo Systems Before Sending Them to 100 Million Users.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So the next time you’re tempted to push that untested code to production because “it’s just a small feature,” remember JioMart’s infinite coupon generator. In the world of software, shortcuts aren’t just risky—they’re sometimes very, very costly.&lt;/p&gt;

&lt;p&gt;Or, to put it simply: &lt;strong&gt;you can either test your code, or your users will test it for you—and they’ll do it with a lot more creativity.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>softwareengineering</category>
      <category>jokes</category>
      <category>product</category>
    </item>
    <item>
      <title>Why MVP Mindset Saves Projects (And How I Apply It)</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Tue, 12 Aug 2025 11:08:06 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/why-mvp-mindset-saves-projects-and-how-i-apply-it-5ei4</link>
      <guid>https://dev.to/divyanshulohani/why-mvp-mindset-saves-projects-and-how-i-apply-it-5ei4</guid>
      <description>&lt;h2&gt;
  
  
  The Project Graveyard in My Github
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F29vlzfejf960errt0kcz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F29vlzfejf960errt0kcz.png" alt="Screenshot of a GitHub profile showing multiple unfinished repositories" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have a lot of unfinished projects in my GitHub. They are digital tombstones of grand ambitions that died somewhere between "revolutionary idea" and "hmm, maybe I need user authentication AND real-time chat AND AI integration AND..." everything I can think of during the planing phase and only have kanban boards or todo lists for them never got to be real.&lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;p&gt;For years, I was that developer who'd start projects with a mental feature list longer than a CVS receipt. I'd spend weeks architecting the perfect system, planning for edge cases that might never happen, and building features "just in case." The result? A graveyard of half-built applications and a growing sense that I couldn't finish anything.&lt;/p&gt;

&lt;p&gt;Then I discovered the MVP mindset. Not just as a business concept, but as a personal productivity philosophy. And it changed everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MVP Actually Means
&lt;/h2&gt;

&lt;p&gt;Most people think MVP means "build something basic and hope for the best." That's wrong.&lt;/p&gt;

&lt;p&gt;MVP means building the smallest thing that solves a real problem and gives you real feedback. It's not about being lazy or cutting corners—it's about being ruthlessly focused on what actually matters.&lt;/p&gt;

&lt;p&gt;The skateboard-to-car analogy gets thrown around a lot, but here's what it really taught me: each iteration should be a complete, working solution. A skateboard gets you from point A to point B. So does a car. But you learn different things from each version. The way you maintain balance on a skateboard is not the same as gearing up in a car.&lt;/p&gt;

&lt;p&gt;My mistake for years was trying to build the car from day one. Spoiler alert: it doesn't work and after this realization I belive it will never work projects built from ground up should be treated like wheels without wheels skateboard or cars are just not possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Project That Actually Shipped
&lt;/h3&gt;

&lt;p&gt;Last year, I had an idea for a expesne management app specifically for me. My initial plan was insane:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  GitHub integration&lt;/li&gt;
&lt;li&gt;  Passbook integration&lt;/li&gt;
&lt;li&gt;  Accounts managemnet&lt;/li&gt;
&lt;li&gt;  Todo Apps&lt;/li&gt;
&lt;li&gt;  Custom admin dashboard&lt;/li&gt;
&lt;li&gt;  AI-powered suggestions&lt;/li&gt;
&lt;li&gt;  Native Mobile app&lt;/li&gt;
&lt;li&gt;  Dark mode (obviously)&lt;/li&gt;
&lt;li&gt;  Kanban boards&lt;/li&gt;
&lt;li&gt;  Budget system&lt;/li&gt;
&lt;li&gt;  Reports and analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I spent three weeks just setting up the database schema. Then I realized I was doing it again—building the app which is only needed by me or maybe few others when I launch&lt;/p&gt;

&lt;p&gt;So I stopped. Started over. Built the dumbest version possible: a single page where you could add expenses only no balance maintence nothing. That's it. No database, just localStorage. No user accounts, no fancy UI. Just a working app for tracking transactions and exporting them&lt;/p&gt;

&lt;p&gt;I shipped it in two days.&lt;/p&gt;

&lt;p&gt;And you know what? People used it. My friends started asking for the link. I got feedback I never expected. Some wanted categories. Others wanted due dates. A few asked about multiple accounts features&lt;/p&gt;

&lt;p&gt;But here's the key: I learned what people actually need, not what my delusional brain thought they needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Apply MVP Thinking Now
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The "One Problem" Rule
&lt;/h3&gt;

&lt;p&gt;Every project starts with one question: What's the ONE problem this solves? Not five problems. Not "it could also do this and that." One problem. My current project is expense tracker. The one problem it solves: I keep forgetting just stroing expense and nothing more than that.&lt;/p&gt;

&lt;p&gt;That's it. Not "social media for sharing things" or "Bank account passbook" Just "store and generate csvs of your transactions."&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The 80/20 Feature Cut
&lt;/h3&gt;

&lt;p&gt;I list every feature I can imagine, then cross out 80% of them.&lt;/p&gt;

&lt;p&gt;The remaining 20% becomes version 1. The crossed-out features? They go in a "maybe later" file. Most of the time, "later" never comes—and that's perfectly fine that feature may not be worth that time and effort maybe it will become a feature that is just there and only you use it and no one else which is obviously bad.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Two-Week Test
&lt;/h3&gt;

&lt;p&gt;If I can't build a working version in two weeks, the scope is too big. This isn't about rushing or cutting quality. It's about being realistic. Two weeks forces me to focus on what's actually essential versus what's just nice to have and not waste time in thinking peculiar edge cases which may be only hypothetical.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The "Embarrassing" Launch
&lt;/h3&gt;

&lt;p&gt;I launch when I'm slightly embarrassed by how basic it looks. This was hard to learn. I'm a perfectionist since I've been in my mind. But I realized that "slightly embarrassing" to me is often "perfectly adequate" to users what I seem imprefect everyone using my app is fine with like I still think the UI could be way better but it works and helps me focus on features first. And shipping something imperfect beats not shipping anything perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compound Effect of Small Wins
&lt;/h2&gt;

&lt;p&gt;Here's what I didn't expect: shipping small things consistently builds momentum faster than planning big things perfectly. When you plan things you get into a world where everything seems like ideal like the bodies in physics they do not apply in real world and maybe are too redundent for users like I told earlier some features may seem apealing to you but may not be useful for the users.&lt;/p&gt;

&lt;p&gt;Every completed project teaches you something. Every launch gives you confidence. Every piece of feedback makes the next version better and if you only plan and plan you will only plan and gain no expeirence in theory everything seems very easy like only I have to push it to vercel or netlify and the project is deployed but what if the time comes and you have to run it on a custom VPS how will you be able to do that.&lt;/p&gt;

&lt;p&gt;My unfinished projects taught me nothing except how to start things. My finished MVPs taught me how products actually work in the real world and what to expect when deploying them instead of thinking this feature would be a great thing or like make my app seem better to users as a solo developer you can not know if what are you thinking will be appealing to users hence you need to launch the bare minimum to get the things your users think what they actually need and what are just nice to have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The MVP mindset isn't just about building products faster (though it does that). It's about building products that people actually want what makes your core idea speak rather than a polished ui of a similar kind of app.&lt;/p&gt;

&lt;p&gt;Every feature you don't build is a feature that can't break, can't confuse users, and can't distract from your core value proposition. Constraints force creativity. Limitations inspire focus. It helps you to be more productive and a better person in hitting the deadlines better.&lt;/p&gt;

&lt;p&gt;Your first version doesn't need to be your final vision. It just needs to be the first step toward it. So what's your skateboard? What's the simplest thing you can build that solves a real problem for real people?&lt;/p&gt;

&lt;p&gt;Stop planning the car. Start building the skateboard it will then lead to better car than you expect your car to be it will not just run it will break all the records&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F77ptmisz4lqq3hxb59a3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F77ptmisz4lqq3hxb59a3.png" alt="A developer at their desk with a simple app on screen" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; MVP isn't about building cheap products—it's about learning fast and building what people actually want. Start small, ship quickly, iterate based on real feedback.&lt;/p&gt;

</description>
      <category>mvp</category>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>From Web to Mobile: Building PWAs with Next.js &amp; Bubblewrap</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Tue, 17 Jun 2025 14:19:01 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/from-web-to-mobile-building-pwas-with-nextjs-bubblewrap-337b</link>
      <guid>https://dev.to/divyanshulohani/from-web-to-mobile-building-pwas-with-nextjs-bubblewrap-337b</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;The Realization That Hit Me Like a Truck&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;So there I was, scrolling through my phone, and I noticed something weird. Almost every "app" I was using daily wasn't really an app—it was just a website pretending to be one. Twitter, Instagram, even some banking apps. They loaded fast, worked offline, and felt native.&lt;/p&gt;

&lt;p&gt;That's when it clicked: &lt;strong&gt;Progressive Web Apps (PWAs)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I'd been building React/Next.js apps for years, but I was missing this whole world where you could take your web app and make it feel like a proper mobile app. No app store approval hell. No learning Swift or Kotlin. Just good old web tech doing mobile things.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Wait, What Actually IS a PWA?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Okay, so before I dive into the how-to, let me break down what PWAs actually are—because I definitely overthought this at first.&lt;/p&gt;

&lt;p&gt;A PWA is basically a website that acts like a mobile app. It can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install on your phone like a real app&lt;/li&gt;
&lt;li&gt;Work offline (kind of)&lt;/li&gt;
&lt;li&gt;Send push notifications&lt;/li&gt;
&lt;li&gt;Access device features like camera, GPS, etc.&lt;/li&gt;
&lt;li&gt;Feel smooth and native-ish&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The magic happens through something called a &lt;strong&gt;Service Worker&lt;/strong&gt; (handles offline stuff) and a &lt;strong&gt;Web App Manifest&lt;/strong&gt; (tells the browser how to behave like an app).&lt;/p&gt;

&lt;p&gt;And the best part? If you're already building with Next.js, you're like 70% there already.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Why I Went Down This Rabbit Hole&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I had this side project that worked great on desktop, but mobile users kept asking "where's the app?" Every time someone visited on mobile, they'd try to find it in the app store.&lt;/p&gt;

&lt;p&gt;Building separate iOS and Android apps felt like overkill for what was essentially a web app. Enter PWAs and &lt;strong&gt;Bubblewrap&lt;/strong&gt;—Google's tool that takes your PWA and wraps it into a proper Android app you can actually publish to the Play Store.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmuafeyt4ea1x850r3kmy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmuafeyt4ea1x850r3kmy.png" alt="Screenshot of PWA vs native app comparison" width="750" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 1: Icons (Because First Impressions Matter)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You can't have a proper app without proper icons. I used to spend hours in Figma creating different icon sizes, but there's a much easier way.&lt;/p&gt;

&lt;p&gt;Head over to &lt;a href="https://pwa-icon-generator.vercel.app/" rel="noopener noreferrer"&gt;https://pwa-icon-generator.vercel.app/&lt;/a&gt; and upload your base icon. This tool generates all the sizes you need—from tiny 16x16 favicons to massive 512x512 app icons.&lt;/p&gt;

&lt;p&gt;Download the zip, extract it to your &lt;code&gt;public&lt;/code&gt; directory, and boom—icon problem solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 2: The Manifest (Next.js Makes This Easy)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's where Next.js really shines. Instead of manually creating a &lt;code&gt;manifest.json&lt;/code&gt; file, you can create a &lt;code&gt;manifest.ts&lt;/code&gt; file in your &lt;code&gt;app&lt;/code&gt; directory and Next.js will automatically generate the webmanifest for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MetadataRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;MetadataRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Manifest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Awesome App&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;short_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AwesomeApp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A really cool web application&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;start_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standalone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;background_color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;theme_color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#2563eb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;orientation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;portrait&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;icons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/icon512_maskable.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;512x512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/icon192.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;192x192&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;display: "standalone"&lt;/code&gt; is crucial—it tells the browser to hide the address bar and make your app feel like a native one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 3: Enter Bubblewrap (The Game Changer)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is where the magic happens. Bubblewrap is a CLI tool that takes your PWA and packages it into a proper Android app.&lt;/p&gt;

&lt;p&gt;First, install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @bubblewrap/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;IMPORTANT&lt;/strong&gt;: Do NOT run this in your Next.js project root. Seriously. I learned this the hard way when I accidentally pushed Android build files to production and my app crashed. Create a separate folder for this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-app-android
&lt;span class="nb"&gt;cd &lt;/span&gt;my-app-android
bubblewrap init &lt;span class="nt"&gt;--manifest&lt;/span&gt; https://yourapp.com/manifest.webmanifest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bubblewrap will ask you a bunch of questions—app name, package name, keystore password, etc. Just answer honestly, and it'll set up the entire Android project structure for you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[Image placeholder: Terminal showing bubblewrap init process]&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 4: Building Your App&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once the setup is done, building is surprisingly simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bubblewrap build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An APK file (for direct installation)&lt;/li&gt;
&lt;li&gt;An AAB file (for Play Store upload)&lt;/li&gt;
&lt;li&gt;All the necessary Android project files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The build process takes a few minutes, but when it's done, you have a real Android app that you can install on any device.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Step 5: The URL Bar Problem (And How to Fix It)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's something that tripped me up initially. Even after all this setup, your app might still show that ugly browser address bar. That's because Android needs to verify that your website actually "owns" this app.&lt;/p&gt;

&lt;p&gt;You fix this by creating a file at &lt;code&gt;public/.well-known/assetlinks.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"delegate_permission/common.handle_all_urls"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"android_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"package_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.myapp.example.twa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"sha256_cert_fingerprints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"A1:B2:C3:D4:E5:F6:G7:H8:I9:J0:K1:L2:M3:N4:O5:P6:Q7:R8:S9:T0:U1:V2:W3:X4:Y5:Z6"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"relation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"check_validation"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"android_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"package_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.myapp.example.twa"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get that SHA256 fingerprint, run this command in your Android project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;keytool &lt;span class="nt"&gt;-list&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-keystore&lt;/span&gt; android.keystore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter the password you set during bubblewrap init, and you'll see output like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Certificate fingerprints:
    SHA1: 12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78
    SHA256: A1:B2:C3:D4:E5:F6:G7:H8:I9:J0:K1:L2:M3:N4:O5:P6:Q7:R8:S9:T0:U1:V2:W3:X4:Y5:Z6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy that SHA256 value into your assetlinks.json file.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Play Store Gotcha&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If you're planning to publish to the Google Play Store, there's one more step. Google re-signs your app with their own key, so you'll need to add their SHA256 fingerprint to your assetlinks.json file as well.&lt;/p&gt;

&lt;p&gt;You can find this in the Google Play Console under Setup → App Signing. Just add it as a second entry in the fingerprints array. &lt;a href="https://dev.to/hyunsoong_i/twa-apps-how-to-hide-the-url-bar-browser-bar-and-display-app-as-full-screen-4cm3"&gt;A detailed guide&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What Actually Changed for Me&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After going through this whole process, here's what I gained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real app experience&lt;/strong&gt;: Users can install it from the Play Store, it appears in their app drawer, and it feels native.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better engagement&lt;/strong&gt;: People are way more likely to use something that's "installed" vs bookmarked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline capability&lt;/strong&gt;: With service workers, core functionality works even without internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One codebase&lt;/strong&gt;: I'm maintaining one Next.js app instead of separate web and mobile codebases.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Reality Check&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Let's be honest—PWAs aren't perfect. iOS support is still weird (thanks, Apple). Some device features are limited. And you'll still need to understand Android development concepts if you want to customize things.&lt;/p&gt;

&lt;p&gt;But for most web apps that want mobile presence, this approach is &lt;em&gt;so much faster&lt;/em&gt; than building separate native apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;So yeah—&lt;strong&gt;PWAs aren't just a buzzword&lt;/strong&gt;. If you're already building with Next.js and want to tap into the mobile app ecosystem without learning entirely new technologies, this is probably your best bet.&lt;/p&gt;

&lt;p&gt;The whole process took me maybe 3-4 hours from start to finish (including the learning curve and that one time I accidentally nuked my build folder).&lt;/p&gt;

&lt;p&gt;I'm still experimenting with advanced PWA features like background sync and push notifications, but so far, this has been one of the &lt;em&gt;smoothest paths from web to mobile&lt;/em&gt; I've ever taken.&lt;/p&gt;

&lt;p&gt;If you're sitting on a Next.js app wondering how to make it "feel more appy"—&lt;strong&gt;maybe it's time to go PWA&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>pwa</category>
      <category>pwabuilder</category>
    </item>
    <item>
      <title>Building Your Online Dev Identity (Before It's Too Late)</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Mon, 09 Jun 2025 06:57:05 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/building-your-online-dev-identity-before-its-too-late-3015</link>
      <guid>https://dev.to/divyanshulohani/building-your-online-dev-identity-before-its-too-late-3015</guid>
      <description>&lt;h2&gt;
  
  
  In 2025, Your Resume is Your Feed
&lt;/h2&gt;

&lt;p&gt;The best job offer you can get today isn't from a fancy MNC or a funded startup—it's from yourself, from the brand you've built online. Having your own identity online, shaped not by a company or a brand but by your own presence, is now a legitimate career path. It takes time, no doubt. But if you don't start today, it'll always stay "someday."&lt;/p&gt;

&lt;p&gt;The world has shifted. Traditional hiring processes are broken. HR departments are drowning in applications, and most resumes never even get seen by human eyes. Meanwhile, the most interesting opportunities are happening in DMs, through network connections, and from people who've been following your work for months. Your GitHub contributions, your blog posts, your hot takes on the latest JavaScript framework—that's your real resume now.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Regret (and How I Fixed It)
&lt;/h2&gt;

&lt;p&gt;I've been into development for over 6 years. I used to think, "Why post online? Who cares? If I'm good, people will find me." That's not how it works.&lt;/p&gt;

&lt;p&gt;For years, I was the typical "head down, code hard" developer. I believed in meritocracy—that good work speaks for itself. I'd build amazing projects, solve complex problems, and then... nothing. No one knew about it except my immediate team. I was essentially invisible in an industry where visibility increasingly matters.&lt;/p&gt;

&lt;p&gt;The wake-up call came when I saw junior developers with half my experience landing better opportunities simply because they had an online presence. They weren't necessarily better developers, but they were better at showing their work. That stung, but it was the reality check I needed.&lt;/p&gt;

&lt;p&gt;In 2023, I realized the harsh truth: No one is going to dream about you and magically send an opportunity your way. You have to tell the world, again and again, that you exist. And so, in 2024, I made a shift.&lt;/p&gt;

&lt;p&gt;I started posting updates about my dev projects, small wins, bugs I fixed, new tech I was trying—all on X (formerly Twitter). Then came blog posts: things I built, lessons learned, opinion pieces on tools, tech, or just stuff trending in the dev space. Hackathon updates, micro-tutorials—basically anything I was doing or learning.&lt;/p&gt;

&lt;p&gt;The hardest part? Getting over the imposter syndrome. Who was I to have opinions about React when Dan Abramov exists? Who cares about my struggles with Docker when there are experts everywhere? But here's the thing—beginners and intermediate developers learn better from people just a step ahead of them, not from the untouchable experts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Talking to a Wall (At First)
&lt;/h2&gt;

&lt;p&gt;nitially? It felt like shouting into a void. No followers. No engagement. Maybe a cousin would like it once in a while. But I kept writing.&lt;/p&gt;

&lt;p&gt;Those first few months were brutal for the ego. I'd spend hours crafting what I thought was a brilliant post about some obscure bug I'd fixed, only to get zero reactions. I started questioning everything—was my content bad? Was I posting at the wrong times? Was I even cut out for this?&lt;/p&gt;

&lt;p&gt;I experimented with reels and YouTube videos—but honestly, I was spending more time editing than coding. The pressure to make things “viral” took away from the joy of development itself. So I backed off from daily content and chose a sustainable pace instead. (Yes, you can still check out my &lt;a href="https://www.youtube.com/@divyanshuxwb" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt; and &lt;a href="https://www.instagram.com/divyanshuxwb" rel="noopener noreferrer"&gt;Instagram&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;This was a crucial lesson: sustainability beats perfection. I'd rather post one genuine article a week than burn out trying to create daily "content" that felt forced. The algorithm rewards consistency, but your sanity should come first.&lt;/p&gt;

&lt;p&gt;Then, out of nowhere, one article hit. It wasn't even my magnum opus—just a breakdown of a Skribbl clone I had built, paired with a few improvements I was thinking of. It was honest, scrappy, and unpolished—but real. And it resonated. Slowly, views started to roll in. A few comments, some shares. Nothing viral—just 500–600 views. But when you're starting from zero, that's momentum. It meant someone out there was listening. It felt validating.&lt;/p&gt;

&lt;p&gt;That article taught me something important: authenticity beats polish. People connect with real struggles and genuine insights more than they connect with perfectly curated content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Snowball Effect
&lt;/h2&gt;

&lt;p&gt;Now, even when I don’t have time for constant updates, I make sure I push 2–3 solid articles a month. These usually focus on a few key areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Problems I’m facing&lt;/strong&gt; – Whether it’s a tricky bug, a frustrating tool, or just a confusing decision I had to make, I document it. Writing about these challenges not only helps others but helps me make sense of my process too.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;How I solved something weird&lt;/strong&gt; – When something breaks and I fix it in a way that feels clever (or stupid), I write it down. These kinds of posts are always relatable to other devs and spark the best discussions.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Random insights&lt;/strong&gt; – Sometimes I notice a pattern, trend, or just have a thought that might help others. These posts are spontaneous but surprisingly well received.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn’t about going viral. I’m not trying to be the next dev influencer. It’s about being consistent—because consistency builds presence. The more I post, the more searchable and discoverable I become. And maybe someday, that consistency becomes the reason someone reaches out with a cool opportunity.&lt;/p&gt;

&lt;h2&gt;
  
  
  So How Do You Start?
&lt;/h2&gt;

&lt;p&gt;There is nothing I would say that is a secret sauce that I would tell you. You already know what you have to do and you're just lazy or don't feel like doing it but here are the steps anyways if you want there is nothing fancy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Pick a Platform:&lt;/strong&gt; X/Twitter is great for daily thoughts. Dev.to, Medium, Hashnode for longer articles. YouTube/Instagram if you like visual formats.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Share What You Do:&lt;/strong&gt; Doesn’t have to be mind-blowing. Ship something? Share it. Broke something? Share that too.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Engage:&lt;/strong&gt; Comment on others' posts. Be part of the community. You’ll learn &lt;em&gt;and&lt;/em&gt; get visibility.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Repeat:&lt;/strong&gt; Keep showing up, even if it feels like no one’s watching.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why It Matters
&lt;/h2&gt;

&lt;p&gt;When you have an online identity, it matters way more than you think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  People trust you more because they see the proof of work online and are confident on you and they can be sure you will be the one to finish their job&lt;/li&gt;
&lt;li&gt;  Recruiters find &lt;em&gt;you&lt;/em&gt;, not the other way around, its same as you know your teacher but the teacher you know teaches hundreds of kids so you have to be special to be in his eyes.&lt;/li&gt;
&lt;li&gt;  Once you gain traction you will also start getting inboud requests no more hassle of going on to freelancing platforms to find work your inbox will be filled of it.&lt;/li&gt;
&lt;li&gt;  You become what you wanted to become in less time because these things put up a high impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you’re learning dev or already good at it, your work deserves an audience. Start small. Stay consistent. Don’t care about metrics at first—just build.&lt;/p&gt;

&lt;p&gt;Your online presence is like compound interest: the earlier you start, the bigger it grows.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Start building your personal dev brand today. The internet won’t find you on its own.&lt;/p&gt;

&lt;p&gt;#PersonalBranding #DevJourney #ContentCreation #DeveloperLife&lt;/p&gt;

</description>
      <category>devops</category>
      <category>web</category>
      <category>programming</category>
      <category>fullstack</category>
    </item>
    <item>
      <title>Payment Gateway Chaos: How I Converted Multiple Providers into One</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Sun, 25 May 2025 20:25:35 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/payment-gateway-chaos-how-i-converted-multiple-providers-into-one-25bd</link>
      <guid>https://dev.to/divyanshulohani/payment-gateway-chaos-how-i-converted-multiple-providers-into-one-25bd</guid>
      <description>&lt;p&gt;Picture this: You're building an app, everything's going smooth, and then your client drops the bomb — &lt;em&gt;"Can we support PayU, Stripe, and maybe Razorpay too? You know, for flexibility."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Suddenly you're staring at three different APIs, each with their own quirks, data formats, and authentication methods. PayU wants a hash, Stripe wants sessions, and don't even get me started on their different ways of handling amounts (seriously, who decided some should use paise and others should use rupees?).&lt;/p&gt;

&lt;p&gt;I recently found myself in this exact situation while working on a project, and instead of copy-pasting code like a barbarian, I decided to build something that wouldn't make me want to throw my laptop out the window every time I needed to add a new gateway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Payment Gateway Spaghetti
&lt;/h2&gt;

&lt;p&gt;Most developers handle multiple payment gateways like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Create separate functions for each gateway&lt;/li&gt;
&lt;li&gt;  Copy-paste similar logic everywhere&lt;/li&gt;
&lt;li&gt;  End up with a maintenance nightmare&lt;/li&gt;
&lt;li&gt;  Cry a little when requirements change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's got to be a better way, right? &lt;strong&gt;&lt;em&gt;Right&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Abstract the Chaos Away
&lt;/h2&gt;

&lt;p&gt;Here's the pattern that saved my sanity (and probably my career):&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create a Universal Payment Interface
&lt;/h3&gt;

&lt;p&gt;First, I created a common type that could handle all payment gateways without losing my mind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PurchaseTransactionData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;itemType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PurchaseType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;productinfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PaymentData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Common fields for all payment gateways&lt;/span&gt;
      &lt;span class="nl"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;productinfo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;surl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;furl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// PayU specific fields&lt;/span&gt;
      &lt;span class="nl"&gt;txnid&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Stripe specific fields&lt;/span&gt;
      &lt;span class="nl"&gt;stripePayUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;publishableKey&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is it perfect? No. Does it work? Absolutely. The key insight here is that you need &lt;strong&gt;one interface to rule them all&lt;/strong&gt; — even if some fields are gateway-specific.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: The Abstract Base Class (The Real MVP)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PaymentData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PurchaseTransactionData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/types/payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PaymentStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@repo/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentGatewayBase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="nf"&gt;createPaymentData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PurchaseTransactionData&lt;/span&gt;
      &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PaymentData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PurchaseTransactionData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentTransaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;itemType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This abstract class does two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Forces&lt;/strong&gt; every gateway implementation to have a createPaymentData method&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Handles&lt;/strong&gt; the common transaction creation logic so you don't repeat yourself&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Gateway Implementations (Where the Magic Happens)
&lt;/h3&gt;

&lt;p&gt;Now here's where it gets fun. Each gateway just extends the base class and implements their specific logic:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PayU Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PayUGateway&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;PaymentGatewayBase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createPaymentData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PurchaseTransactionData&lt;/span&gt;
      &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PaymentData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;merchantKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;merchantSalt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPayUCredentials&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;merchantKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAYU&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;txnid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// PAYU wants floats&lt;/span&gt;
          &lt;span class="na"&gt;productinfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productinfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;firstname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;John Doe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1234567890&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;surl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/payu-response?status=success`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;furl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/payu-response?status=failure`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;merchantSalt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;merchantKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stripe Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StripeGateway&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;PaymentGatewayBase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createPaymentData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentTransaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PurchaseTransactionData&lt;/span&gt;
      &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PaymentData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripeCredentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getStripeCredentials&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stripeCredentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;customer_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;client_reference_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;price_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;product_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productinfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="na"&gt;unit_amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Stripe wants paise&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;billing_address_collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;success_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/shop/?transactionId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;cancel_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SITE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/stripe-response?transactionId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;stripePayUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;publishableKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: The Factory Pattern (Because We're Not Animals)
&lt;/h3&gt;

&lt;p&gt;But wait, there's more! Manually instantiating gateway classes everywhere is still messy. Enter the Factory pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PaymentGateway&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@repo/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PaymentGatewayBase&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./payment-base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PayUGateway&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./payu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StripeGateway&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./stripe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentGatewayFactory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PaymentGatewayBase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAYU&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PayUGateway&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StripeGateway&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unsupported payment gateway: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your server-side code becomes beautifully simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gateway&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PaymentGatewayFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectedGateway&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPaymentData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transactionData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Frontend Magic (The Cherry on Top)
&lt;/h3&gt;

&lt;p&gt;The frontend doesn't need to know about your fancy abstractions. It just needs to handle the response and redirect users accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PAYU&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nf"&gt;redirectToPaymentPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;STRIPE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
            &lt;span class="nx"&gt;paymentResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;stripePayUrl&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unsupported payment gateway&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each gateway gets its own redirect logic, but the main flow stays clean and predictable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F19oa639nwecevv0mwe1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F19oa639nwecevv0mwe1t.png" alt="Factory pattern image" width="720" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Adding New Gateways (The Real Test)
&lt;/h3&gt;

&lt;p&gt;Now adding a new payment gateway is genuinely simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Create a new class that extends PaymentGatewayBase&lt;/li&gt;
&lt;li&gt; Implement the createPaymentData method&lt;/li&gt;
&lt;li&gt; Add the case to your Factory class&lt;/li&gt;
&lt;li&gt; Add the frontend redirect logic&lt;/li&gt;
&lt;li&gt; That's it. You're done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Want to add Razorpay? Just create RazorpayGateway extends PaymentGatewayBase, add it to the factory, and handle the frontend redirect. No existing code needs to change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Beauty of This Approach&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This pattern gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Consistency&lt;/strong&gt;: Every gateway follows the same contract&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Maintainability&lt;/strong&gt;: Common logic lives in one place&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Scalability&lt;/strong&gt;: Adding new gateways doesn't break existing code&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Testability&lt;/strong&gt;: You can mock the abstract class for unit tests&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Sanity&lt;/strong&gt;: You won't hate your life when requirements change&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Payment gateway integration doesn't have to be a nightmare. With a little bit of abstraction and a lot of TypeScript magic, you can build something that's actually maintainable.&lt;/p&gt;

&lt;p&gt;The next time someone asks you to "just add one more payment gateway," you can smile knowingly instead of updating your resume.&lt;/p&gt;

&lt;p&gt;Now if you'll excuse me, I need to go figure out why Razorpay's webhook decided to stop working again. Some things never change.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>systemdesign</category>
      <category>stripe</category>
    </item>
    <item>
      <title>My Experience with TurboRepo &amp; Monorepos: From Chaos to Sanity</title>
      <dc:creator>DivyanshuLohani</dc:creator>
      <pubDate>Sun, 20 Apr 2025 04:47:17 +0000</pubDate>
      <link>https://dev.to/divyanshulohani/my-experience-with-turborepo-monorepos-from-chaos-to-sanity-1h0f</link>
      <guid>https://dev.to/divyanshulohani/my-experience-with-turborepo-monorepos-from-chaos-to-sanity-1h0f</guid>
      <description>&lt;h2&gt;
  
  
  A Dev Setup That Started Like Everyone Else’s
&lt;/h2&gt;

&lt;p&gt;If you’ve ever worked on a full-stack project, chances are your folder structure looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/client  
/server  
/README.md (you never updated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And at first—it’s fine. Until one day, I was debugging something and realize that I am just copy-pasting the same utility function between folders and breaking the DRY rule.&lt;/p&gt;

&lt;p&gt;Then the installs break. Then the dependency trees spiral. Then I am 10 commits deep into fixing a node_modules issue and forget what the feature even was.&lt;/p&gt;

&lt;p&gt;Welcome to my life… until I discovered &lt;strong&gt;Monorepos&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait, What Even Is a Monorepo?
&lt;/h2&gt;

&lt;p&gt;So, I had heard the term floating around in dev Twitter and Reddit threads, but I never really &lt;em&gt;got it&lt;/em&gt;. I thought it was just one of those "enterprise-only" things Google does.&lt;/p&gt;

&lt;p&gt;but it turend out—it’s just keeping all your packages, apps, and services in &lt;strong&gt;one codebase&lt;/strong&gt;, organized and connected like a family that actually talks to each other.&lt;/p&gt;

&lt;p&gt;You can share code. You can isolate builds. You can run tests on specific packages. And yeah, &lt;a href="https://www.linkedin.com/company/google/" rel="noopener noreferrer"&gt;Google&lt;/a&gt; does it. That was enough to get me curious.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enter: TurboRepo
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.licdn.com%2Fdms%2Fimage%2Fv2%2FD5612AQGrrYHfHPy4aw%2Farticle-inline_image-shrink_1000_1488%2FB56ZZPdhrDHQAQ-%2F0%2F1745089862776%3Fe%3D1750896000%26v%3Dbeta%26t%3DFGgYSfWvsJuvrI_g3BZ3IRQ1bPGteM-hAayTnHdkQ1c" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.licdn.com%2Fdms%2Fimage%2Fv2%2FD5612AQGrrYHfHPy4aw%2Farticle-inline_image-shrink_1000_1488%2FB56ZZPdhrDHQAQ-%2F0%2F1745089862776%3Fe%3D1750896000%26v%3Dbeta%26t%3DFGgYSfWvsJuvrI_g3BZ3IRQ1bPGteM-hAayTnHdkQ1c" alt="Turbo Repo" width="300" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I stumbled across TurboRepo, and the first thing I thought was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Wait, why didn’t anyone tell me about this sooner?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s a &lt;strong&gt;high-performance build system for monorepos&lt;/strong&gt;—designed to make your life less painful when managing multiple apps and packages in a single repo. And the cool part? It's designed for JS/TS devs like us.&lt;/p&gt;

&lt;p&gt;No 20-step YAML configs. Just set up your pipeline, link things with workspaces, and boom—stuff starts working.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Switch: Folder Chaos to Turbo Calm
&lt;/h2&gt;

&lt;p&gt;I had an ongoing project with the usual split: one folder for the frontend (React), another for the backend (Express).&lt;/p&gt;

&lt;p&gt;Converting that into a Turbo monorepo setup only took me &lt;strong&gt;an hour or two&lt;/strong&gt;. (And that too with Express, which has like &lt;em&gt;zero&lt;/em&gt; real monorepo resources out there—someone fix this please. (Article soon :)))&lt;/p&gt;

&lt;p&gt;Now it looks more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/apps  
  /web  
  /api  
/packages  
  /ui  
  /utils  
  /config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And suddenly, my build scripts aren’t yelling at me. My eslint config is shared. And I'm not doing npm install in two places like an absolute fool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Benefits I Felt
&lt;/h2&gt;

&lt;p&gt;Let’s be real, here’s what actually changed for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;One place for all code&lt;/strong&gt;: No more syncing packages manually or debugging two node_modules folders.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Code sharing became easy&lt;/strong&gt;: Utils, types, and configs now live in one packages/ folder and are shared across both frontend and backend.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Less context switching&lt;/strong&gt;: I don't need to cd into three places just to start the dev server.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;My disaster folder got fixed&lt;/strong&gt;: I once broke a project so badly because I didn’t understand workspaces properly. Turbo fixed that mess, cleaned up the structure, and gave me a fresh start.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Ease to Scale:&lt;/strong&gt; Now it would be easier to add more things like admin or something like that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Not All Rainbows
&lt;/h2&gt;

&lt;p&gt;Turbo is powerful, but it’s also a bit opinionated. Some things still need digging around, especially if you’re working outside the Next.js happy path.&lt;/p&gt;

&lt;p&gt;But once you get it running—it’s worth it.&lt;/p&gt;

&lt;p&gt;(Also, don’t forget to &lt;strong&gt;cache smartly&lt;/strong&gt;. Turbo's speed magic depends on it.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So yeah—&lt;strong&gt;monorepos are not just for big corps&lt;/strong&gt;. If you're building multi-package apps, it makes total sense even for solo devs or small teams. And TurboRepo? Chef's kiss. &lt;strong&gt;🤌&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’m still experimenting and learning, especially with backend-heavy setups (&lt;em&gt;looking at you, Express&lt;/em&gt;). But so far, I’d say this is one of the &lt;em&gt;cleanest architectural decisions&lt;/em&gt; I’ve made in months.&lt;/p&gt;

&lt;p&gt;If you’re stuck in the client-server folder spiral—&lt;strong&gt;maybe it’s time to go mono&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>monorepo</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
