<?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: 〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</title>
    <description>The latest articles on DEV Community by 〽️ 𝙍𝙤𝙨𝙝𝙖𝙣 (@roshan_ican).</description>
    <link>https://dev.to/roshan_ican</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F611585%2Fc599f97e-fd99-4fa3-b48f-cf9f52623553.jpeg</url>
      <title>DEV Community: 〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</title>
      <link>https://dev.to/roshan_ican</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/roshan_ican"/>
    <language>en</language>
    <item>
      <title>I Rewrote My Node.js/Express Backend in NestJS</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Sun, 09 Aug 2026 16:05:08 +0000</pubDate>
      <link>https://dev.to/roshan_ican/i-rewrote-my-nodejsexpress-backend-in-nestjs-231</link>
      <guid>https://dev.to/roshan_ican/i-rewrote-my-nodejsexpress-backend-in-nestjs-231</guid>
      <description>&lt;p&gt;I am currently working on a project where I have WebSockets, UDP and REST APIs, and although the V1 was almost ready and everything was working, the code had started to feel all over the place.&lt;/p&gt;

&lt;p&gt;I didn't always understand where everything was, especially because you don't usually see many projects where multiple protocols like REST, WebSockets and UDP are being used together.&lt;/p&gt;

&lt;p&gt;I wanted to reach a point where I could look at the code and immediately understand where things belong.&lt;/p&gt;

&lt;p&gt;At the same time, there was a real need to add a new &lt;code&gt;SUPER_ADMIN&lt;/code&gt; role and change some of the existing permission levels. So instead of continuing to patch the existing Express application, I decided to rewrite the project in &lt;a href="https://nestjs.com/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;NestJS&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The difference was visible in the structure
&lt;/h2&gt;

&lt;p&gt;In V1, the backend had separate folders for HTTP, services, database, WebSockets and other utilities:&lt;/p&gt;

&lt;p&gt;Nothing here is necessarily wrong, but as the project grew, following one feature could mean jumping between several different folders.&lt;/p&gt;

&lt;p&gt;With NestJS, I moved toward a more feature-oriented structure:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7bfnr0hetur5hn6cgq87.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7bfnr0hetur5hn6cgq87.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That small change made a big difference for me. I could look at the project and have a much better idea of where something belongs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with the database
&lt;/h2&gt;

&lt;p&gt;The first thing I worked on was the database.&lt;/p&gt;

&lt;p&gt;I redesigned parts of the schema and moved the main database access to Prisma. What I liked about Prisma was that the schema became much easier to understand and the database models were connected more naturally with TypeScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model User {
  id    String   @id @default(uuid())
  email String   @unique
  role  UserRole
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't completely remove raw SQL either. There are still places where SQL makes more sense, especially when I need database-specific functionality or more direct control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuilding the APIs
&lt;/h2&gt;

&lt;p&gt;After that, I started rewriting the APIs.&lt;/p&gt;

&lt;p&gt;In the old Express version, it was very easy for routes to slowly become responsible for validation, business logic, database calls and responses all in one place.&lt;/p&gt;

&lt;p&gt;With NestJS, I started separating these responsibilities:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&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;UsersController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&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="nd"&gt;Post&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="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateUserDto&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&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;dto&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 controller handles the request, the service handles the logic, and the database layer handles persistence.&lt;/p&gt;

&lt;p&gt;NestJS's dependency injection also meant I could declare what a class needs instead of manually creating and passing everything around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authorization
&lt;/h2&gt;

&lt;p&gt;Then I rebuilt the authorization because of the new &lt;code&gt;SUPER_ADMIN&lt;/code&gt; role and the permission changes.&lt;/p&gt;

&lt;p&gt;NestJS's guards and decorators made it possible to make permissions much more visible:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Roles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ADMIN&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="nd"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&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="kr"&gt;string&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I could look at a route and understand who is allowed to access it without searching through different parts of the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebSockets
&lt;/h2&gt;

&lt;p&gt;Next came the WebSocket side.&lt;/p&gt;

&lt;p&gt;NestJS provides WebSocket gateway support with Socket.IO, so I could keep the real-time communication in its own place.&lt;/p&gt;

&lt;p&gt;For example, rooms could be handled inside the gateway:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;WebSocketGateway&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;RealtimeGateway&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;WebSocketServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;SubscribeMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;join-room&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;joinRoom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;ConnectedSocket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;client&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;MessageBody&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;span class="nl"&gt;room&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;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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;room&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 the real-time communication has a clear place, while the actual business logic can stay in services.&lt;/p&gt;

&lt;p&gt;What NestJS gave me was a clear place to isolate it from the rest of the application and manage its lifecycle.&lt;/p&gt;

&lt;p&gt;So the mental model became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REST       → Controllers → Services → Database
WebSocket  → Gateway → Rooms → Services
UDP        → Transport → Services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also used &lt;a href="https://www.usebruno.com/?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Bruno&lt;/a&gt; while rebuilding the APIs. I found it to be a great alternative to Postman, especially for keeping API requests and collections close to the project while rebuilding and testing the endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The biggest benefit
&lt;/h2&gt;

&lt;p&gt;The biggest benefit I got from the rewrite wasn't performance.&lt;/p&gt;

&lt;p&gt;It was that I could finally understand the project in layers.&lt;/p&gt;

&lt;p&gt;I could look at the code and know where the API lives, where the business logic lives, where WebSockets are handled, where UDP is handled, and where permissions belong.&lt;/p&gt;

&lt;p&gt;And this is probably what I like most about opinionated frameworks.&lt;/p&gt;

&lt;p&gt;We don't have to decide between 10 or 20 different ways of structuring everything. NestJS gives us conventions, and we can follow them.&lt;/p&gt;

&lt;p&gt;That doesn't mean NestJS is universally better than Express. Express was perfectly capable of running my original project.&lt;/p&gt;

&lt;p&gt;For me, the rewrite was about making the architecture predictable and easier to understand.&lt;/p&gt;

&lt;p&gt;Sometimes the best reason to rewrite something isn't that the old version doesn't work.&lt;/p&gt;

&lt;p&gt;It's that you want the next version to be easier to understand.&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What I Learned from Reading JavaScript: The Good Parts</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Wed, 05 Aug 2026 18:13:07 +0000</pubDate>
      <link>https://dev.to/roshan_ican/what-i-learned-from-reading-javascript-the-good-parts-35ph</link>
      <guid>https://dev.to/roshan_ican/what-i-learned-from-reading-javascript-the-good-parts-35ph</guid>
      <description>&lt;h1&gt;
  
  
  What I Learned from Reading &lt;em&gt;JavaScript: The Good Parts&lt;/em&gt;
&lt;/h1&gt;

&lt;p&gt;I recently finished reading &lt;em&gt;JavaScript: The Good Parts&lt;/em&gt; by Douglas Crockford.&lt;/p&gt;

&lt;p&gt;Even though the book was written in 2008, and javascript is much more capable now i really wanted to understand how things evolved with time and as engineer whose most time has been writing web development i really needed to know what's good and bad about the language i use&lt;/p&gt;

&lt;p&gt;Here are the biggest lessons I took away.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Functions are first-class values
&lt;/h2&gt;

&lt;p&gt;This was probably the biggest mindset shift.&lt;/p&gt;

&lt;p&gt;Functions aren't special syntax—they're values.&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store them in variables&lt;/li&gt;
&lt;li&gt;pass them to other functions&lt;/li&gt;
&lt;li&gt;return them from functions&lt;/li&gt;
&lt;li&gt;create functions that create other functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This idea explains callbacks, higher-order functions, currying, modules, and closures.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Closures are one of JavaScript's superpowers
&lt;/h2&gt;

&lt;p&gt;An inner function remembers variables from its outer scope even after the outer function has finished executing.&lt;/p&gt;

&lt;p&gt;Once this clicked, concepts like private state, factories, memoization, and modules became much easier to understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Objects inherit through prototypes
&lt;/h2&gt;

&lt;p&gt;Objects don't inherit from classes.&lt;/p&gt;

&lt;p&gt;Every object has an internal prototype link, and property lookup walks up the prototype chain until it finds a match.&lt;/p&gt;

&lt;p&gt;Modern JavaScript gives us tools like:&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="nb"&gt;Object&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="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPrototypeOf&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to work with prototypes directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. &lt;code&gt;this&lt;/code&gt; isn't determined by where a function is written
&lt;/h2&gt;

&lt;p&gt;It's determined by &lt;strong&gt;how the function is called&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That single rule explains most of the confusing behavior around &lt;code&gt;this&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. JavaScript has some dangerous features
&lt;/h2&gt;

&lt;p&gt;Some language features are better avoided:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;eval()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;with&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;wrapper objects like &lt;code&gt;new Boolean()&lt;/code&gt; or &lt;code&gt;new String()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;implicit global variables&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;==&lt;/code&gt; when &lt;code&gt;===&lt;/code&gt; is what you actually want&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just because something exists doesn't mean it should be used.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. JavaScript was designed with some historical baggage
&lt;/h2&gt;

&lt;p&gt;Understanding these quirks makes many "weird" behaviors less mysterious.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;typeof null === "object"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;numbers are IEEE-754 floating point values&lt;/li&gt;
&lt;li&gt;floating-point precision issues (&lt;code&gt;0.1 + 0.2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;strings are UTF-16, so emojis can occupy two code units&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NaN&lt;/code&gt; is contagious&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Readability matters more than cleverness
&lt;/h2&gt;

&lt;p&gt;One quote that stuck with me was the idea that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you can't easily read the program, you can't confidently predict what it will do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Many of Crockford's recommendations ultimately come down to writing code that's easy for humans to understand.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Simplicity beats feature count
&lt;/h2&gt;

&lt;p&gt;One analogy from the book that I liked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;My microwave has dozens of buttons, but I only ever use &lt;strong&gt;Cook&lt;/strong&gt; and &lt;strong&gt;Clock&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The point wasn't about microwaves—it was about software design.&lt;/p&gt;

&lt;p&gt;A language or API doesn't become better by adding more features.&lt;/p&gt;

&lt;p&gt;It becomes better when the &lt;strong&gt;core features are well designed&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. JavaScript is much simpler than it first appears
&lt;/h2&gt;

&lt;p&gt;The language feels huge at first.&lt;/p&gt;

&lt;p&gt;But most of it is built on a few fundamental ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;objects&lt;/li&gt;
&lt;li&gt;functions&lt;/li&gt;
&lt;li&gt;prototypes&lt;/li&gt;
&lt;li&gt;closures&lt;/li&gt;
&lt;li&gt;scope&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once those concepts make sense, many "advanced" topics feel much more approachable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Things that are outdated today
&lt;/h2&gt;

&lt;p&gt;The book is almost two decades old, so I wouldn't follow everything literally.&lt;/p&gt;

&lt;p&gt;Today we'd use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;let&lt;/code&gt; / &lt;code&gt;const&lt;/code&gt; instead of &lt;code&gt;var&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ESLint instead of JSLint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JSON.parse()&lt;/code&gt; instead of custom parsers&lt;/li&gt;
&lt;li&gt;modules instead of IIFEs for encapsulation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Map&lt;/code&gt; and &lt;code&gt;Set&lt;/code&gt; where appropriate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the principles behind those recommendations are still valuable.&lt;/p&gt;




&lt;h3&gt;
  
  
  My biggest takeaway
&lt;/h3&gt;

&lt;p&gt;I started reading this book expecting to learn more JavaScript syntax.&lt;/p&gt;

&lt;p&gt;Instead, I finished it with a better understanding of &lt;strong&gt;why JavaScript behaves the way it does&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And that, more than any specific API or language feature, has already made me a better JavaScript developer.&lt;/p&gt;




&lt;p&gt;I think this makes a stronger LinkedIn post because it focuses on &lt;strong&gt;insights&lt;/strong&gt; rather than trying to cover every technical detail. People are more likely to engage with "here's how this book changed my understanding" than with a long list of language features.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Float64Array, TimSort and sort</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Wed, 05 Aug 2026 17:58:26 +0000</pubDate>
      <link>https://dev.to/roshan_ican/float64array-timsort-and-sort-4eef</link>
      <guid>https://dev.to/roshan_ican/float64array-timsort-and-sort-4eef</guid>
      <description>&lt;p&gt;So in JavaScript, there are multiple ways to sort data. The native JavaScript Array.sort() uses an algorithm from the TimSort family. Modern V8 engines use PowerSort, which is an optimized version of TimSort.&lt;/p&gt;

&lt;p&gt;I wanted to understand how different sorting approaches perform, so I compared them.&lt;/p&gt;

&lt;p&gt;The interesting part is that there is no single "fastest" sorting algorithm — it depends on the type of data and the use case.&lt;/p&gt;

&lt;p&gt;⚡ Float64Array.sort() — Fastest for pure numbers&lt;/p&gt;

&lt;p&gt;When the data is only numbers, Float64Array.sort() performs extremely well.&lt;/p&gt;

&lt;p&gt;new Float64Array(data).sort()&lt;/p&gt;

&lt;p&gt;The reason it is faster is because:&lt;/p&gt;

&lt;p&gt;It does not need a comparator function&lt;br&gt;
The engine already knows every value is a number&lt;br&gt;
Numbers are stored in a continuous block of memory&lt;br&gt;
No extra type checks or object handling are needed&lt;/p&gt;

&lt;p&gt;This allows the engine to directly work with the raw numeric representation in memory.&lt;/p&gt;

&lt;p&gt;However, it only works for numeric data and cannot handle objects or custom sorting rules.&lt;/p&gt;

&lt;p&gt;⚡ TimSort / PowerSort — Better for large and complex inputs&lt;/p&gt;

&lt;p&gt;TimSort combines:&lt;/p&gt;

&lt;p&gt;Insertion sort for small sections&lt;br&gt;
Merge sort for efficiently combining sorted sections&lt;/p&gt;

&lt;p&gt;It detects already sorted parts of the array (called runs) and uses that information to optimize sorting.&lt;/p&gt;

&lt;p&gt;This makes it very effective for:&lt;/p&gt;

&lt;p&gt;Large datasets&lt;br&gt;
Partially sorted data&lt;br&gt;
Real-world applications where data already has some order&lt;/p&gt;

&lt;p&gt;For larger and more complex inputs, TimSort can perform better than simpler sorting approaches because it adapts to the structure of the data.&lt;/p&gt;

&lt;p&gt;⚡ TimSort npm package&lt;/p&gt;

&lt;p&gt;Even though it uses the same general algorithm, the npm implementation is usually slower because it runs completely in JavaScript and does not get the same low-level optimizations as the built-in engine.&lt;/p&gt;

&lt;p&gt;The biggest lesson:&lt;/p&gt;

&lt;p&gt;The fastest sorting method depends on the situation:&lt;/p&gt;

&lt;p&gt;🔹 Pure numbers → Float64Array.sort() is usually the fastest&lt;br&gt;
🔹 Large real-world datasets → TimSort/PowerSort is highly optimized&lt;br&gt;
🔹 Custom objects and sorting rules → Array.sort() with a comparator&lt;/p&gt;

&lt;p&gt;Performance is not only about the algorithm. It also depends on:&lt;/p&gt;

&lt;p&gt;Memory layout&lt;br&gt;
Data types&lt;br&gt;
Native optimizations&lt;br&gt;
Function call overhead&lt;/p&gt;

&lt;p&gt;Understanding both the algorithm and the environment where it runs is what helps us write faster code. 🚀&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Debugging Production Alerts Without Chasing The Wrong Problem</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Sun, 17 May 2026 14:38:20 +0000</pubDate>
      <link>https://dev.to/roshan_ican/debugging-production-alerts-without-chasing-the-wrong-problem-1</link>
      <guid>https://dev.to/roshan_ican/debugging-production-alerts-without-chasing-the-wrong-problem-1</guid>
      <description>&lt;p&gt;oday I worked through a production alert that looked simple at first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Services are unhealthy. Memory issue?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But once we started digging, it turned out to be a mix of real issues, noisy errors, and one confusing timezone mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Assumption
&lt;/h2&gt;

&lt;p&gt;The alert mentioned memory problems, worker restarts, database warnings, and a third-party integration error.&lt;/p&gt;

&lt;p&gt;At first, it was tempting to group everything together and say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The system is running out of memory.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But that would have been too broad.&lt;/p&gt;

&lt;p&gt;So instead, we split the investigation by service.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Actually Happening
&lt;/h2&gt;

&lt;p&gt;One web service really was hitting memory pressure. Its process manager was killing workers after the total memory usage crossed the configured limit.&lt;/p&gt;

&lt;p&gt;That part was real.&lt;/p&gt;

&lt;p&gt;But another background worker was reported as “crash-looping.” When we checked the logs, it did not show the usual signs of a memory crash.&lt;/p&gt;

&lt;p&gt;There were no lines like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Process too large
Out of memory
Children dying rapidly
Exited with status 255
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, the logs showed graceful shutdowns and normal restarts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shutting down
Scheduler exiting
Bye
Booted application
Starting memory monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That changed the conclusion. The worker may have restarted, but the logs did not prove it was crashing because of memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Timezone Trap
&lt;/h2&gt;

&lt;p&gt;Another confusing part was time.&lt;/p&gt;

&lt;p&gt;One tool showed local time. Another exported logs in UTC.&lt;/p&gt;

&lt;p&gt;So a window that looked like this in the UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20:30 - 21:05
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;was actually this in the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;16:30 - 17:05 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That small difference can completely change an investigation.&lt;/p&gt;

&lt;p&gt;If you query the wrong time window, you can easily miss the real event or accidentally blame the wrong one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Noise
&lt;/h2&gt;

&lt;p&gt;There were also database warnings. They were real, but they were related to a small set of records and not directly tied to the memory alert.&lt;/p&gt;

&lt;p&gt;There was also a recurring third-party SMS error. It looked scary in the alert, but it was old background noise and not part of the main incident.&lt;/p&gt;

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

&lt;p&gt;After separating the signals, the situation looked more 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;Web memory pressure: real
Background worker memory crash-loop: not proven
Database warnings: real, but separate
Third-party SMS error: noisy, unrelated
Timezone mismatch: caused confusion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Production debugging is not just about finding errors.&lt;/p&gt;

&lt;p&gt;It is about separating related signals from unrelated ones.&lt;/p&gt;

&lt;p&gt;Before acting on an alert, it helps to ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which service actually produced this log?&lt;/li&gt;
&lt;li&gt;Is this the correct time window?&lt;/li&gt;
&lt;li&gt;Is the timestamp local time or UTC?&lt;/li&gt;
&lt;li&gt;Do we see the real failure signature?&lt;/li&gt;
&lt;li&gt;Is this new, or just recurring noise?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest lesson from today:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don’t fix the loudest symptom. First prove what is actually failing.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>devjournal</category>
      <category>monitoring</category>
      <category>performance</category>
      <category>sre</category>
    </item>
    <item>
      <title>Bulk Emails from a chat input — without Redis, queues, or worker services</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Sun, 17 May 2026 14:37:16 +0000</pubDate>
      <link>https://dev.to/roshan_ican/bulk-emails-from-a-chat-input-without-redis-queues-or-worker-services-4oak</link>
      <guid>https://dev.to/roshan_ican/bulk-emails-from-a-chat-input-without-redis-queues-or-worker-services-4oak</guid>
      <description>&lt;p&gt;I built a feature into my portfolio site that lets me paste a list of recruiter emails and a job description directly into a chat box.&lt;/p&gt;

&lt;p&gt;Once I type my unlock passphrase, it automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generates a tailored email for each recruiter,&lt;/li&gt;
&lt;li&gt;attaches my resume,&lt;/li&gt;
&lt;li&gt;sends the emails,&lt;/li&gt;
&lt;li&gt;and streams live progress back into the same chat bubble.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fun part is the architecture behind it.&lt;/p&gt;

&lt;p&gt;There’s no Redis.&lt;br&gt;
No BullMQ.&lt;br&gt;
No QStash.&lt;br&gt;
No separate worker service running somewhere.&lt;/p&gt;

&lt;p&gt;It’s just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js on Vercel&lt;/li&gt;
&lt;li&gt;Neon Postgres&lt;/li&gt;
&lt;li&gt;and &lt;code&gt;after()&lt;/code&gt; from Next.js 15.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;I originally started building it because I was tired of manually rewriting the same outreach emails over and over again while applying to jobs.&lt;/p&gt;

&lt;p&gt;Most “AI job application tools” feel overly automated and spammy, so I wanted something smaller and more controlled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I still choose the recruiters&lt;/li&gt;
&lt;li&gt;I still provide the job description&lt;/li&gt;
&lt;li&gt;I still manually trigger it&lt;/li&gt;
&lt;li&gt;but the repetitive work disappears&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The end result feels more like an assistant inside my portfolio chat than a mass-email bot.&lt;/p&gt;
&lt;h2&gt;
  
  
  Triggering the bulk flow
&lt;/h2&gt;

&lt;p&gt;The detection logic lives directly inside the chat route.&lt;/p&gt;

&lt;p&gt;If the message contains multiple email addresses and matches my existing outreach heuristic, the app switches into “bulk pipeline” mode.&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildRowsFromText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;hasCurrentOutreachContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;authed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;lockResponse&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;runId&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;startBulkPipeline&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;jobDescription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;drainRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;📤 Sending applications...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pipelineRunId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;runId&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 important bit here is &lt;code&gt;after()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Next.js 15+, &lt;code&gt;after()&lt;/code&gt; allows work to continue &lt;em&gt;after&lt;/em&gt; the response has already been sent back to the client.&lt;/p&gt;

&lt;p&gt;So the user instantly gets a response with a &lt;code&gt;pipelineRunId&lt;/code&gt;, while the actual email processing continues in the same serverless invocation.&lt;/p&gt;

&lt;p&gt;No worker queues.&lt;br&gt;
No background containers.&lt;br&gt;
No separate infrastructure.&lt;/p&gt;

&lt;p&gt;Just the existing runtime continuing execution after the response flushes.&lt;/p&gt;

&lt;p&gt;Honestly, this was the feature that made the whole architecture click for me.&lt;/p&gt;
&lt;h2&gt;
  
  
  Storage layer
&lt;/h2&gt;

&lt;p&gt;I used two tables in Neon:&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;pipeline_runs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Stores batch-level state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;total jobs&lt;/li&gt;
&lt;li&gt;sent count&lt;/li&gt;
&lt;li&gt;failed count&lt;/li&gt;
&lt;li&gt;original job description&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;pipeline_jobs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Stores one row per recipient:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email&lt;/li&gt;
&lt;li&gt;derived company name&lt;/li&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;attempts&lt;/li&gt;
&lt;li&gt;error messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The processing loop atomically claims one queued job at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;pipeline_jobs&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
     &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pipeline_jobs&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'queued'&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="o"&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 makes retries safe and keeps the pipeline idempotent even if Vercel retries the invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the emails
&lt;/h2&gt;

&lt;p&gt;For every claimed row, the flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Extract the company name from the email domain&lt;br&gt;
(&lt;code&gt;hiring@stripe.com&lt;/code&gt; → “Stripe”)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Call Groq using &lt;code&gt;llama-3.3-70b-versatile&lt;/code&gt; in structured JSON mode&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generate:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;ol&gt;
&lt;li&gt;Convert the plain text body into proper HTML:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;paragraphs&lt;/li&gt;
&lt;li&gt;bullet lists&lt;/li&gt;
&lt;li&gt;clickable links&lt;/li&gt;
&lt;li&gt;readable spacing&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Send through Gmail SMTP using Nodemailer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mark the row as sent&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also intentionally made the fallback behavior conservative.&lt;/p&gt;

&lt;p&gt;If someone uses a Gmail or generic email address, the system falls back to “Hiring Team” instead of hallucinating fake company names.&lt;/p&gt;

&lt;p&gt;Small detail, but it makes the emails feel way more natural.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live progress in the chat UI
&lt;/h2&gt;

&lt;p&gt;The response includes a &lt;code&gt;pipelineRunId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The frontend attaches that ID directly to the assistant message, and a &lt;code&gt;&amp;lt;PipelineProgress /&amp;gt;&lt;/code&gt; component renders underneath the same chat bubble.&lt;/p&gt;

&lt;p&gt;It polls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/lab/pipeline/[runId]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;every ~1.5 seconds until the run finishes.&lt;/p&gt;

&lt;p&gt;I considered SSE/websockets, but honestly polling was simpler and more reliable for Vercel Hobby deployments.&lt;/p&gt;

&lt;p&gt;Sometimes boring engineering decisions are the correct ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs
&lt;/h2&gt;

&lt;p&gt;This setup definitely has limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Function timeout
&lt;/h3&gt;

&lt;p&gt;Vercel Hobby gives ~60 seconds.&lt;/p&gt;

&lt;p&gt;Each email takes roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM generation&lt;/li&gt;
&lt;li&gt;SMTP send&lt;/li&gt;
&lt;li&gt;DB updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About 3–4 seconds total per row.&lt;/p&gt;

&lt;p&gt;So realistically one invocation comfortably handles ~10–15 emails.&lt;/p&gt;

&lt;p&gt;For my use case (“apply to a few recruiters at a time”), that’s completely fine.&lt;/p&gt;

&lt;p&gt;If I ever needed larger batches, I’d probably chunk the drain process and self-fanout recursively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gmail SMTP limits
&lt;/h3&gt;

&lt;p&gt;Gmail caps daily sends.&lt;/p&gt;

&lt;p&gt;Again, acceptable for personal usage.&lt;/p&gt;

&lt;p&gt;Switching to Resend or SendGrid would basically be changing one file.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;after()&lt;/code&gt; durability
&lt;/h3&gt;

&lt;p&gt;This is probably the biggest tradeoff.&lt;/p&gt;

&lt;p&gt;If the serverless invocation dies midway through processing, remaining rows simply stay in &lt;code&gt;queued&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Right now there’s no recovery daemon.&lt;br&gt;
No retry cron.&lt;br&gt;
No dead-letter queue.&lt;/p&gt;

&lt;p&gt;And honestly?&lt;/p&gt;

&lt;p&gt;I haven’t needed one yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I skipped BullMQ, QStash, and Inngest
&lt;/h2&gt;

&lt;p&gt;I actually started with BullMQ.&lt;/p&gt;

&lt;p&gt;Then I remembered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BullMQ wants Redis&lt;/li&gt;
&lt;li&gt;Redis wants a worker process&lt;/li&gt;
&lt;li&gt;worker processes don’t really fit Vercel well&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried QStash too.&lt;br&gt;
It worked.&lt;/p&gt;

&lt;p&gt;But it also felt like I was introducing another service for a scale problem I didn’t actually have.&lt;/p&gt;

&lt;p&gt;Same with Inngest.&lt;/p&gt;

&lt;p&gt;Eventually I realized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the runtime already exists&lt;/li&gt;
&lt;li&gt;the database already exists&lt;/li&gt;
&lt;li&gt;Next.js already provides &lt;code&gt;after()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I stopped overengineering it.&lt;/p&gt;

&lt;p&gt;The entire system is roughly ~250 lines.&lt;/p&gt;

&lt;p&gt;No orchestration layer.&lt;br&gt;
No infra maze.&lt;br&gt;
No queue dashboards.&lt;/p&gt;

&lt;p&gt;Just a simple pipeline that solves the actual problem.&lt;/p&gt;

&lt;p&gt;And honestly, that ended up being the right architecture.&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>Bulk Emails from a chat input — without Redis, queues, or worker services</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Sat, 09 May 2026 12:44:14 +0000</pubDate>
      <link>https://dev.to/roshan_ican/bulk-emails-from-a-chat-input-without-redis-queues-or-worker-services-41hb</link>
      <guid>https://dev.to/roshan_ican/bulk-emails-from-a-chat-input-without-redis-queues-or-worker-services-41hb</guid>
      <description>&lt;p&gt;I built a feature into my portfolio site that lets me paste a list of recruiter emails and a job description directly into a chat box.&lt;/p&gt;

&lt;p&gt;Once I type my unlock passphrase, it automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generates a tailored email for each recruiter,&lt;/li&gt;
&lt;li&gt;attaches my resume,&lt;/li&gt;
&lt;li&gt;sends the emails,&lt;/li&gt;
&lt;li&gt;and streams live progress back into the same chat bubble.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fun part is the architecture behind it.&lt;/p&gt;

&lt;p&gt;There’s no Redis.&lt;br&gt;
No BullMQ.&lt;br&gt;
No QStash.&lt;br&gt;
No separate worker service running somewhere.&lt;/p&gt;

&lt;p&gt;It’s just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js on Vercel&lt;/li&gt;
&lt;li&gt;Neon Postgres&lt;/li&gt;
&lt;li&gt;and &lt;code&gt;after()&lt;/code&gt; from Next.js 15.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;I originally started building it because I was tired of manually rewriting the same outreach emails over and over again while applying to jobs.&lt;/p&gt;

&lt;p&gt;Most “AI job application tools” feel overly automated and spammy, so I wanted something smaller and more controlled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I still choose the recruiters&lt;/li&gt;
&lt;li&gt;I still provide the job description&lt;/li&gt;
&lt;li&gt;I still manually trigger it&lt;/li&gt;
&lt;li&gt;but the repetitive work disappears&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The end result feels more like an assistant inside my portfolio chat than a mass-email bot.&lt;/p&gt;
&lt;h2&gt;
  
  
  Triggering the bulk flow
&lt;/h2&gt;

&lt;p&gt;The detection logic lives directly inside the chat route.&lt;/p&gt;

&lt;p&gt;If the message contains multiple email addresses and matches my existing outreach heuristic, the app switches into “bulk pipeline” mode.&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildRowsFromText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;hasCurrentOutreachContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;authed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;lockResponse&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;runId&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;startBulkPipeline&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;jobDescription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;drainRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;📤 Sending applications...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pipelineRunId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;runId&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 important bit here is &lt;code&gt;after()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Next.js 15+, &lt;code&gt;after()&lt;/code&gt; allows work to continue &lt;em&gt;after&lt;/em&gt; the response has already been sent back to the client.&lt;/p&gt;

&lt;p&gt;So the user instantly gets a response with a &lt;code&gt;pipelineRunId&lt;/code&gt;, while the actual email processing continues in the same serverless invocation.&lt;/p&gt;

&lt;p&gt;No worker queues.&lt;br&gt;
No background containers.&lt;br&gt;
No separate infrastructure.&lt;/p&gt;

&lt;p&gt;Just the existing runtime continuing execution after the response flushes.&lt;/p&gt;

&lt;p&gt;Honestly, this was the feature that made the whole architecture click for me.&lt;/p&gt;
&lt;h2&gt;
  
  
  Storage layer
&lt;/h2&gt;

&lt;p&gt;I used two tables in Neon:&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;pipeline_runs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Stores batch-level state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;total jobs&lt;/li&gt;
&lt;li&gt;sent count&lt;/li&gt;
&lt;li&gt;failed count&lt;/li&gt;
&lt;li&gt;original job description&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;pipeline_jobs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Stores one row per recipient:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email&lt;/li&gt;
&lt;li&gt;derived company name&lt;/li&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;attempts&lt;/li&gt;
&lt;li&gt;error messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The processing loop atomically claims one queued job at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;pipeline_jobs&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
     &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pipeline_jobs&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'queued'&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="o"&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 makes retries safe and keeps the pipeline idempotent even if Vercel retries the invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the emails
&lt;/h2&gt;

&lt;p&gt;For every claimed row, the flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Extract the company name from the email domain&lt;br&gt;
(&lt;code&gt;hiring@stripe.com&lt;/code&gt; → “Stripe”)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Call Groq using &lt;code&gt;llama-3.3-70b-versatile&lt;/code&gt; in structured JSON mode&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generate:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;ol&gt;
&lt;li&gt;Convert the plain text body into proper HTML:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;paragraphs&lt;/li&gt;
&lt;li&gt;bullet lists&lt;/li&gt;
&lt;li&gt;clickable links&lt;/li&gt;
&lt;li&gt;readable spacing&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Send through Gmail SMTP using Nodemailer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mark the row as sent&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also intentionally made the fallback behavior conservative.&lt;/p&gt;

&lt;p&gt;If someone uses a Gmail or generic email address, the system falls back to “Hiring Team” instead of hallucinating fake company names.&lt;/p&gt;

&lt;p&gt;Small detail, but it makes the emails feel way more natural.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live progress in the chat UI
&lt;/h2&gt;

&lt;p&gt;The response includes a &lt;code&gt;pipelineRunId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The frontend attaches that ID directly to the assistant message, and a &lt;code&gt;&amp;lt;PipelineProgress /&amp;gt;&lt;/code&gt; component renders underneath the same chat bubble.&lt;/p&gt;

&lt;p&gt;It polls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/lab/pipeline/[runId]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;every ~1.5 seconds until the run finishes.&lt;/p&gt;

&lt;p&gt;I considered SSE/websockets, but honestly polling was simpler and more reliable for Vercel Hobby deployments.&lt;/p&gt;

&lt;p&gt;Sometimes boring engineering decisions are the correct ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs
&lt;/h2&gt;

&lt;p&gt;This setup definitely has limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Function timeout
&lt;/h3&gt;

&lt;p&gt;Vercel Hobby gives ~60 seconds.&lt;/p&gt;

&lt;p&gt;Each email takes roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM generation&lt;/li&gt;
&lt;li&gt;SMTP send&lt;/li&gt;
&lt;li&gt;DB updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About 3–4 seconds total per row.&lt;/p&gt;

&lt;p&gt;So realistically one invocation comfortably handles ~10–15 emails.&lt;/p&gt;

&lt;p&gt;For my use case (“apply to a few recruiters at a time”), that’s completely fine.&lt;/p&gt;

&lt;p&gt;If I ever needed larger batches, I’d probably chunk the drain process and self-fanout recursively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gmail SMTP limits
&lt;/h3&gt;

&lt;p&gt;Gmail caps daily sends.&lt;/p&gt;

&lt;p&gt;Again, acceptable for personal usage.&lt;/p&gt;

&lt;p&gt;Switching to Resend or SendGrid would basically be changing one file.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;after()&lt;/code&gt; durability
&lt;/h3&gt;

&lt;p&gt;This is probably the biggest tradeoff.&lt;/p&gt;

&lt;p&gt;If the serverless invocation dies midway through processing, remaining rows simply stay in &lt;code&gt;queued&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Right now there’s no recovery daemon.&lt;br&gt;
No retry cron.&lt;br&gt;
No dead-letter queue.&lt;/p&gt;

&lt;p&gt;And honestly?&lt;/p&gt;

&lt;p&gt;I haven’t needed one yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I skipped BullMQ, QStash, and Inngest
&lt;/h2&gt;

&lt;p&gt;I actually started with BullMQ.&lt;/p&gt;

&lt;p&gt;Then I remembered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BullMQ wants Redis&lt;/li&gt;
&lt;li&gt;Redis wants a worker process&lt;/li&gt;
&lt;li&gt;worker processes don’t really fit Vercel well&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried QStash too.&lt;br&gt;
It worked.&lt;/p&gt;

&lt;p&gt;But it also felt like I was introducing another service for a scale problem I didn’t actually have.&lt;/p&gt;

&lt;p&gt;Same with Inngest.&lt;/p&gt;

&lt;p&gt;Eventually I realized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the runtime already exists&lt;/li&gt;
&lt;li&gt;the database already exists&lt;/li&gt;
&lt;li&gt;Next.js already provides &lt;code&gt;after()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I stopped overengineering it.&lt;/p&gt;

&lt;p&gt;The entire system is roughly ~250 lines.&lt;/p&gt;

&lt;p&gt;No orchestration layer.&lt;br&gt;
No infra maze.&lt;br&gt;
No queue dashboards.&lt;/p&gt;

&lt;p&gt;Just a simple pipeline that solves the actual problem.&lt;/p&gt;

&lt;p&gt;And honestly, that ended up being the right architecture.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Bulk Emails from a chat input — without Redis, queues, or worker services</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Sat, 09 May 2026 12:44:14 +0000</pubDate>
      <link>https://dev.to/roshan_ican/bulk-emails-from-a-chat-input-without-redis-queues-or-worker-services-3pae</link>
      <guid>https://dev.to/roshan_ican/bulk-emails-from-a-chat-input-without-redis-queues-or-worker-services-3pae</guid>
      <description>&lt;p&gt;I built a feature into my portfolio site that lets me paste a list of recruiter emails and a job description directly into a chat box.&lt;/p&gt;

&lt;p&gt;Once I type my unlock passphrase, it automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generates a tailored email for each recruiter,&lt;/li&gt;
&lt;li&gt;attaches my resume,&lt;/li&gt;
&lt;li&gt;sends the emails,&lt;/li&gt;
&lt;li&gt;and streams live progress back into the same chat bubble.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fun part is the architecture behind it.&lt;/p&gt;

&lt;p&gt;There’s no Redis.&lt;br&gt;
No BullMQ.&lt;br&gt;
No QStash.&lt;br&gt;
No separate worker service running somewhere.&lt;/p&gt;

&lt;p&gt;It’s just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js on Vercel&lt;/li&gt;
&lt;li&gt;Neon Postgres&lt;/li&gt;
&lt;li&gt;and &lt;code&gt;after()&lt;/code&gt; from Next.js 15.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;I originally started building it because I was tired of manually rewriting the same outreach emails over and over again while applying to jobs.&lt;/p&gt;

&lt;p&gt;Most “AI job application tools” feel overly automated and spammy, so I wanted something smaller and more controlled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I still choose the recruiters&lt;/li&gt;
&lt;li&gt;I still provide the job description&lt;/li&gt;
&lt;li&gt;I still manually trigger it&lt;/li&gt;
&lt;li&gt;but the repetitive work disappears&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The end result feels more like an assistant inside my portfolio chat than a mass-email bot.&lt;/p&gt;
&lt;h2&gt;
  
  
  Triggering the bulk flow
&lt;/h2&gt;

&lt;p&gt;The detection logic lives directly inside the chat route.&lt;/p&gt;

&lt;p&gt;If the message contains multiple email addresses and matches my existing outreach heuristic, the app switches into “bulk pipeline” mode.&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildRowsFromText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;hasCurrentOutreachContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;authed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;lockResponse&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;runId&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;startBulkPipeline&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;jobDescription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;drainRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;📤 Sending applications...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pipelineRunId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;runId&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 important bit here is &lt;code&gt;after()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Next.js 15+, &lt;code&gt;after()&lt;/code&gt; allows work to continue &lt;em&gt;after&lt;/em&gt; the response has already been sent back to the client.&lt;/p&gt;

&lt;p&gt;So the user instantly gets a response with a &lt;code&gt;pipelineRunId&lt;/code&gt;, while the actual email processing continues in the same serverless invocation.&lt;/p&gt;

&lt;p&gt;No worker queues.&lt;br&gt;
No background containers.&lt;br&gt;
No separate infrastructure.&lt;/p&gt;

&lt;p&gt;Just the existing runtime continuing execution after the response flushes.&lt;/p&gt;

&lt;p&gt;Honestly, this was the feature that made the whole architecture click for me.&lt;/p&gt;
&lt;h2&gt;
  
  
  Storage layer
&lt;/h2&gt;

&lt;p&gt;I used two tables in Neon:&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;pipeline_runs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Stores batch-level state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;total jobs&lt;/li&gt;
&lt;li&gt;sent count&lt;/li&gt;
&lt;li&gt;failed count&lt;/li&gt;
&lt;li&gt;original job description&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;pipeline_jobs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Stores one row per recipient:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email&lt;/li&gt;
&lt;li&gt;derived company name&lt;/li&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;attempts&lt;/li&gt;
&lt;li&gt;error messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The processing loop atomically claims one queued job at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;pipeline_jobs&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
     &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pipeline_jobs&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'queued'&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="o"&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 makes retries safe and keeps the pipeline idempotent even if Vercel retries the invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the emails
&lt;/h2&gt;

&lt;p&gt;For every claimed row, the flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Extract the company name from the email domain&lt;br&gt;
(&lt;code&gt;hiring@stripe.com&lt;/code&gt; → “Stripe”)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Call Groq using &lt;code&gt;llama-3.3-70b-versatile&lt;/code&gt; in structured JSON mode&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generate:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;ol&gt;
&lt;li&gt;Convert the plain text body into proper HTML:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;paragraphs&lt;/li&gt;
&lt;li&gt;bullet lists&lt;/li&gt;
&lt;li&gt;clickable links&lt;/li&gt;
&lt;li&gt;readable spacing&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Send through Gmail SMTP using Nodemailer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mark the row as sent&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also intentionally made the fallback behavior conservative.&lt;/p&gt;

&lt;p&gt;If someone uses a Gmail or generic email address, the system falls back to “Hiring Team” instead of hallucinating fake company names.&lt;/p&gt;

&lt;p&gt;Small detail, but it makes the emails feel way more natural.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live progress in the chat UI
&lt;/h2&gt;

&lt;p&gt;The response includes a &lt;code&gt;pipelineRunId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The frontend attaches that ID directly to the assistant message, and a &lt;code&gt;&amp;lt;PipelineProgress /&amp;gt;&lt;/code&gt; component renders underneath the same chat bubble.&lt;/p&gt;

&lt;p&gt;It polls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/lab/pipeline/[runId]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;every ~1.5 seconds until the run finishes.&lt;/p&gt;

&lt;p&gt;I considered SSE/websockets, but honestly polling was simpler and more reliable for Vercel Hobby deployments.&lt;/p&gt;

&lt;p&gt;Sometimes boring engineering decisions are the correct ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs
&lt;/h2&gt;

&lt;p&gt;This setup definitely has limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Function timeout
&lt;/h3&gt;

&lt;p&gt;Vercel Hobby gives ~60 seconds.&lt;/p&gt;

&lt;p&gt;Each email takes roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM generation&lt;/li&gt;
&lt;li&gt;SMTP send&lt;/li&gt;
&lt;li&gt;DB updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About 3–4 seconds total per row.&lt;/p&gt;

&lt;p&gt;So realistically one invocation comfortably handles ~10–15 emails.&lt;/p&gt;

&lt;p&gt;For my use case (“apply to a few recruiters at a time”), that’s completely fine.&lt;/p&gt;

&lt;p&gt;If I ever needed larger batches, I’d probably chunk the drain process and self-fanout recursively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gmail SMTP limits
&lt;/h3&gt;

&lt;p&gt;Gmail caps daily sends.&lt;/p&gt;

&lt;p&gt;Again, acceptable for personal usage.&lt;/p&gt;

&lt;p&gt;Switching to Resend or SendGrid would basically be changing one file.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;after()&lt;/code&gt; durability
&lt;/h3&gt;

&lt;p&gt;This is probably the biggest tradeoff.&lt;/p&gt;

&lt;p&gt;If the serverless invocation dies midway through processing, remaining rows simply stay in &lt;code&gt;queued&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Right now there’s no recovery daemon.&lt;br&gt;
No retry cron.&lt;br&gt;
No dead-letter queue.&lt;/p&gt;

&lt;p&gt;And honestly?&lt;/p&gt;

&lt;p&gt;I haven’t needed one yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I skipped BullMQ, QStash, and Inngest
&lt;/h2&gt;

&lt;p&gt;I actually started with BullMQ.&lt;/p&gt;

&lt;p&gt;Then I remembered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BullMQ wants Redis&lt;/li&gt;
&lt;li&gt;Redis wants a worker process&lt;/li&gt;
&lt;li&gt;worker processes don’t really fit Vercel well&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried QStash too.&lt;br&gt;
It worked.&lt;/p&gt;

&lt;p&gt;But it also felt like I was introducing another service for a scale problem I didn’t actually have.&lt;/p&gt;

&lt;p&gt;Same with Inngest.&lt;/p&gt;

&lt;p&gt;Eventually I realized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the runtime already exists&lt;/li&gt;
&lt;li&gt;the database already exists&lt;/li&gt;
&lt;li&gt;Next.js already provides &lt;code&gt;after()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I stopped overengineering it.&lt;/p&gt;

&lt;p&gt;The entire system is roughly ~250 lines.&lt;/p&gt;

&lt;p&gt;No orchestration layer.&lt;br&gt;
No infra maze.&lt;br&gt;
No queue dashboards.&lt;/p&gt;

&lt;p&gt;Just a simple pipeline that solves the actual problem.&lt;/p&gt;

&lt;p&gt;And honestly, that ended up being the right architecture.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How I Built a Production Observability Stack — And Broke It Twice Before It Worked</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Sun, 05 Apr 2026 13:36:40 +0000</pubDate>
      <link>https://dev.to/roshan_ican/how-i-built-a-production-observability-stack-and-broke-it-twice-before-it-worked-4oce</link>
      <guid>https://dev.to/roshan_ican/how-i-built-a-production-observability-stack-and-broke-it-twice-before-it-worked-4oce</guid>
      <description>&lt;p&gt;I used to dismiss monitoring as something you bolt on after the real engineering is done. Logs were noise. Metrics were "a later problem." Alerts were for teams with dedicated SREs, not a small startup running three service types on Render.&lt;/p&gt;

&lt;p&gt;I was wrong. Badly wrong. And it took a self-inflicted incident — where my own monitoring system became the thing that needed monitoring — to understand why observability is engineering, not afterthought.&lt;/p&gt;

&lt;p&gt;This is a detailed account of building our observability stack from scratch: what we built, what broke, why it broke, and what the architecture looks like today.&lt;/p&gt;




&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

&lt;p&gt;We run three service types on Render:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;web service&lt;/strong&gt; — the main API and frontend server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background workers&lt;/strong&gt; — async job processors (queuing, retries, scheduled tasks)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key-value stores&lt;/strong&gt; — Render's managed Redis-compatible service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before this project, each service logged to Render's default log drain. If something broke in production, you'd open the Render dashboard, pick a service, and scroll. No correlation across services, no metrics, no alerting, no history beyond what Render retained.&lt;/p&gt;

&lt;p&gt;The goal was simple to state: centralize everything into a self-hosted Grafana stack, set up alerting, and gain actual visibility into what the system was doing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Before getting into what went wrong, here's what we ended up with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Caddy&lt;/td&gt;
&lt;td&gt;Reverse proxy and automatic TLS. All external traffic enters here.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;syslog-proxy&lt;/td&gt;
&lt;td&gt;Custom Python container. Enforces token validation on inbound syslog, strips the token, forwards clean RFC 5424 to Alloy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector&lt;/td&gt;
&lt;td&gt;Disk-backed telemetry buffer between syslog-proxy and Alloy. Prevents message loss during restarts or slowdowns.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alloy&lt;/td&gt;
&lt;td&gt;Grafana's telemetry collector. Receives logs, applies filters and transformations, routes to the right backend.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loki&lt;/td&gt;
&lt;td&gt;Log storage. Stores log lines compressed and indexed by labels. Queried with LogQL.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mimir&lt;/td&gt;
&lt;td&gt;Metrics storage. Long-term Prometheus-compatible time-series backend.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tempo&lt;/td&gt;
&lt;td&gt;Trace storage. Stores distributed traces with minimal indexing — cheap to run.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grafana&lt;/td&gt;
&lt;td&gt;The UI. Dashboards, alert rules, and unified querying across Loki, Mimir, and Tempo.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full data flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Render services
    → syslog drain (TCP, RFC 5424)
    → syslog-proxy (token validation + strip)
    → Vector (disk-backed buffer)
    → Alloy (filter, transform, route)
    → Loki (logs) / Mimir (metrics) / Tempo (traces)
    → Grafana (dashboards + alerts)
    → Datadog + BetterStack (fan-out for external alerting)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of this runs on a single 4 GB DigitalOcean droplet, currently at ~30% total memory usage. That number matters — it means the stack is right-sized, not just "scaled until it stopped crashing."&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: No token enforcement on the log stream
&lt;/h2&gt;

&lt;p&gt;Render lets you configure a syslog log drain — a TCP endpoint that receives log lines in RFC 5424 format. You give Render a URL with a token embedded in it, and Render forwards logs there.&lt;/p&gt;

&lt;p&gt;The problem: that token is in the URL, but there's no native mechanism to validate it before your collector ingests the message. Render's outbound IP range is also shared across tenants. Any service within that range — from any organisation — can technically send data to your syslog endpoint if they know the address. And since we were pointing directly at Alloy, anything that reached the port got ingested.&lt;/p&gt;

&lt;p&gt;This is less of an active security threat and more of a correctness and isolation problem. You want to know that the logs you're querying are your logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; we wrote a lightweight Python container — &lt;code&gt;syslog-proxy&lt;/code&gt; — that sits between Render and Alloy. Every inbound TCP connection goes through it first. It reads the syslog message, extracts the token from the structured data field (RFC 5424 puts auth metadata in the &lt;code&gt;[SD-ID key="value"]&lt;/code&gt; block), validates it against a shared secret, strips it from the message, and forwards clean RFC 5424 to Alloy's syslog listener.&lt;/p&gt;

&lt;p&gt;If the token is missing or wrong, the connection is dropped. No entry.&lt;/p&gt;

&lt;p&gt;The key distinction: the proxy isn't &lt;em&gt;adding&lt;/em&gt; authentication to the stream. Render was already sending a token. The proxy is the &lt;em&gt;enforcement layer&lt;/em&gt; that was missing — the thing that actually checks it before data moves downstream.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: Connection drops under load
&lt;/h2&gt;

&lt;p&gt;After deploying the proxy, we started seeing intermittent message loss. Not every time, not obviously, but present — we'd notice gaps in log sequences that shouldn't have gaps.&lt;/p&gt;

&lt;p&gt;The root cause was straightforward: the proxy was synchronous and had no internal buffer. When Alloy was slow to accept a connection (startup, GC pause, momentary backpressure), the proxy would drop the TCP connection rather than queue the message. Lost message, no retry, no error surfaced to the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; we inserted Vector between syslog-proxy and Alloy.&lt;/p&gt;

&lt;p&gt;Vector is a high-performance, Rust-based telemetry agent built specifically for this kind of pipeline work. The relevant feature here is disk-backed buffering. Messages are written to disk immediately on receipt, then forwarded to the downstream destination. If Alloy is slow, Vector queues. If Alloy restarts entirely, Vector holds the messages and delivers once the connection is re-established.&lt;/p&gt;

&lt;p&gt;The pipeline now looks like: syslog-proxy validates and strips → hands off to Vector → Vector buffers to disk → Vector forwards to Alloy.&lt;/p&gt;

&lt;p&gt;Connection drops stopped immediately. We have not lost a log message to transport failure since.&lt;/p&gt;




&lt;h2&gt;
  
  
  The incident
&lt;/h2&gt;

&lt;p&gt;With the pipeline stable, we validated it end-to-end on our highest-volume service — the web API. It processed tens of thousands of log lines without issue. Alloy filtered and routed correctly. Loki ingested cleanly. Grafana showed the data. Everything looked good.&lt;/p&gt;

&lt;p&gt;So we did what seemed logical: we added all remaining services to the syslog drain simultaneously.&lt;/p&gt;

&lt;p&gt;Within two hours, the system was on fire.&lt;/p&gt;

&lt;p&gt;Both Datadog and BetterStack — external services we fan logs to in parallel for alerting and long-term retention — were overwhelmed. The Datadog exporter started returning &lt;code&gt;unexpected EOF&lt;/code&gt; from their intake API. This is Datadog &lt;em&gt;actively closing connections&lt;/em&gt;, not just timing out. They were rejecting us.&lt;/p&gt;

&lt;p&gt;Alert rules fired across the board. The alert channel, which was supposed to tell us when our &lt;em&gt;application&lt;/em&gt; was unhealthy, was now full of noise about the &lt;em&gt;monitoring pipeline&lt;/em&gt; being unhealthy. The observer had become the thing being observed.&lt;/p&gt;

&lt;p&gt;We rolled back all drain configurations to Render's defaults within minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Post-mortem
&lt;/h3&gt;

&lt;p&gt;The root cause was a combination of three things that compounded each other:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Unfiltered background worker logs.&lt;/strong&gt; Background job processors emit a lot of lifecycle noise by default — &lt;code&gt;job enqueued&lt;/code&gt;, &lt;code&gt;job started&lt;/code&gt;, &lt;code&gt;job completed&lt;/code&gt;, &lt;code&gt;job retried&lt;/code&gt;, and so on. These events fire on every job. At our job volume, a single background worker service can generate thousands of log lines per minute. None of this is signal for ongoing observability. It's useful when debugging a specific job failure, not for dashboards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Fan-out multiplication.&lt;/strong&gt; We were routing logs to three destinations simultaneously: Loki, Datadog, and BetterStack. A 3x spike in log volume becomes a 9x spike in outbound pipeline throughput. Every destination gets hit at the same time, with the same burst.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. All services at once.&lt;/strong&gt; Testing on one service — even the highest-volume one — told us the pipeline could handle a single source. It told us nothing about what happens when five sources open simultaneously. The aggregate volume was an order of magnitude higher than anything we'd validated.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we changed
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Incremental rollout.&lt;/strong&gt; After rolling back, we added services back one at a time with 24 hours of observation between each. If volume, error rates, and downstream health looked stable for 24 hours, we added the next one. The full rollout took five days instead of five minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log filtering in Alloy.&lt;/strong&gt; We defined explicit filter rules per service type. Background worker INFO-level lifecycle events (&lt;code&gt;enqueued&lt;/code&gt;, &lt;code&gt;started&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;) are now dropped by Alloy before they reach any destination. Only WARNING and above, plus specific job failure patterns, pass through. This cut background worker log volume by roughly 80%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume alerts on the ingestion pipeline.&lt;/strong&gt; We added alert rules on Alloy's own metrics — ingestion rate, error rate, downstream write failures. If the pipeline itself starts showing stress, we know before it cascades.&lt;/p&gt;




&lt;h2&gt;
  
  
  The detail that almost cost us — the 8 KB syslog limit
&lt;/h2&gt;

&lt;p&gt;After the system was stable, we noticed something odd: certain application errors were present in Render's own log view but missing from Loki. Not all of them. Just the large ones.&lt;/p&gt;

&lt;p&gt;The ones that were missing were stack traces. Full Java-style stack traces can easily exceed 100 KB. Syslog has a default maximum message size of &lt;strong&gt;8 KB&lt;/strong&gt; (defined in RFC 5424). Messages that exceed it are silently truncated or dropped, depending on the implementation.&lt;/p&gt;

&lt;p&gt;Silent. No error. No warning. No indication in any metric that a message was lost. The data simply didn't arrive.&lt;/p&gt;

&lt;p&gt;We increased the &lt;code&gt;max_message_len&lt;/code&gt; parameter in the syslog-proxy and Alloy's syslog listener to 256 KB. The missing stack traces appeared immediately.&lt;/p&gt;

&lt;p&gt;The lesson here is broader than syslog: &lt;strong&gt;check the defaults of every tool in your pipeline&lt;/strong&gt;. Buffer sizes, message limits, timeout values, retry caps — these are all set to something that was reasonable for a generic use case. They may not be reasonable for yours. And when they're wrong, most tools will not tell you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current state
&lt;/h2&gt;

&lt;p&gt;The stack has been running stably for several weeks. Current metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Droplet: 4 GB RAM, ~30% utilization under normal load&lt;/li&gt;
&lt;li&gt;Log ingestion: all three service types, filtered, continuous&lt;/li&gt;
&lt;li&gt;Storage: local filesystem (S3 planned for long-term retention)&lt;/li&gt;
&lt;li&gt;External fan-out: Datadog and BetterStack, with volume alerts before we approach their intake limits&lt;/li&gt;
&lt;li&gt;Alert coverage: application error rates, pipeline health, job failure patterns, infrastructure metrics via Mimir&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Grafana dashboards now show a real-time view of system health across all services. When something breaks, we know about it from an alert before a user reports it. That has happened twice since launch, and both times the alert fired before any user-facing degradation was detectable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Not all logs are worth storing.&lt;/strong&gt; Define what matters per service before you start ingesting. Background worker lifecycle events are not observability — they're debug information that belongs in a trace, not a log aggregator. Decide at collection time, not retention time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Buffer every I/O boundary.&lt;/strong&gt; Silent drops are worse than backpressure. Vector saved us from losing data during restarts and slowdowns. Put a buffer anywhere data moves between two systems that can fail independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fan-out multiplies everything.&lt;/strong&gt; A 3x spike in log volume doesn't hit three destinations at 3x each — it hits three destinations at 3x simultaneously, and each downstream system now has to handle the same burst. Design for the aggregate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Roll out incrementally.&lt;/strong&gt; One source, observe for 24 hours, then next. Validating on a single service tells you almost nothing about aggregate behavior. The incident would not have happened if we had added services one at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your defaults.&lt;/strong&gt; The 8 KB syslog limit is a good example of a default that works for most cases and silently breaks for edge cases. Every tool in your pipeline has limits like this. Read the config reference. Set explicit values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitor the monitor.&lt;/strong&gt; Your observability pipeline is a production system. It needs its own health metrics, its own alerts, and its own runbook. If the pipeline goes down during an incident, you're blind exactly when you need visibility most.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;Grafana · Loki · Mimir · Tempo · Alloy · Vector · Caddy · Render · DigitalOcean · S3 (planned)&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're building something similar or have questions about any part of the architecture, feel free to reach out.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>observibility</category>
      <category>infrastructure</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>From Backend Engineer to Building Production Infrastructure in AWS</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Sun, 05 Apr 2026 13:30:01 +0000</pubDate>
      <link>https://dev.to/roshan_ican/from-backend-engineer-to-building-production-infrastructure-in-aws-43fl</link>
      <guid>https://dev.to/roshan_ican/from-backend-engineer-to-building-production-infrastructure-in-aws-43fl</guid>
      <description>&lt;p&gt;Last year in September, I was laid off. By November, I'd landed a role at a startup with a responsibility I wasn't fully prepared for: build a reliable AWS infrastructure from scratch.&lt;br&gt;
I'd only ever worked as a fullstack engineer. I knew the basics — VPCs, load balancers, containers — but not at the level where I'd feel confident deploying anything to production, let alone architecting an entire infrastructure with a real budget on the line.&lt;br&gt;
But I took the job anyway. This is the story of what I did, what I got wrong, and what I learned along the way.&lt;/p&gt;

&lt;p&gt;This is the story of every mistake, pivot, and small win along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Starting Point: Rewriting Before Deploying
&lt;/h2&gt;

&lt;p&gt;Before I could deploy anything, there was a bigger problem. The existing backend was legacy JavaScript — no types, no structure, no clear separation of concerns. Deploying it as-is would just be putting broken code on expensive servers.&lt;/p&gt;

&lt;p&gt;So I rewrote it in TypeScript.&lt;/p&gt;

&lt;p&gt;I went with a 3-layer architecture: routes, controllers, and module functions. The goal was maintainability — whoever touches this codebase next should be able to find things without a treasure map. Most of the refactoring happened through Cursor, which made the migration significantly faster than doing it by hand.&lt;/p&gt;

&lt;p&gt;With the codebase stable, it was time to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Deployment: The ECS Experiment
&lt;/h2&gt;

&lt;p&gt;The first service that needed to go live was a CMS — a monorepo running a backend with SQLite. My initial plan was to standardise on ECS for everything. It felt like the "right" way to do containers on AWS.&lt;/p&gt;

&lt;p&gt;I containerised the backend, learned how ECR works, pushed the image, mounted an EBS volume for the SQLite database, and put an ELB in front of it. It took two full weekends. But it worked.&lt;/p&gt;

&lt;p&gt;Then I got the message from management: &lt;strong&gt;the cost of ECS is too high.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a simple CMS serving internal traffic, they were right. I was over-engineering it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pivot: Right-Sizing the Infrastructure
&lt;/h2&gt;

&lt;p&gt;I tore everything down and went back to research — blogs, documentation, conversations with LLMs to pressure-test ideas. The answer was simple once I stopped thinking in terms of "what's the most modern approach" and started thinking about "what does this workload actually need."&lt;/p&gt;

&lt;p&gt;A CMS doesn't need container orchestration. It needs a box that runs.&lt;/p&gt;

&lt;p&gt;Here's what I landed on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; EC2 &lt;code&gt;t3.small&lt;/code&gt; — cheap, sufficient, easy to manage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; AWS Amplify — fast deploys, built-in CDN, no server to maintain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; SQLite on EBS with weekly backups to S3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total cost: a fraction of the ECS setup. Same reliability for the workload. That was my first real win — not building the most impressive architecture, but building the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Docker Build Problem
&lt;/h2&gt;

&lt;p&gt;With the infrastructure sorted, a new bottleneck appeared: CI/CD.&lt;/p&gt;

&lt;p&gt;Every push triggered a Docker build, and each build was taking around &lt;strong&gt;5 minutes&lt;/strong&gt;. We were running a monorepo with pnpm, and the issue was layer caching — or rather, the lack of it. Every build was reinstalling every dependency from scratch.&lt;/p&gt;

&lt;p&gt;The fix was multi-stage builds. By separating the dependency installation layer from the application code layer, Docker could cache the expensive &lt;code&gt;pnpm install&lt;/code&gt; step and only rebuild what actually changed.&lt;/p&gt;

&lt;p&gt;Build times dropped from &lt;strong&gt;5 minutes to 20 seconds&lt;/strong&gt;. Across 4 repositories. That's the kind of improvement that compounds — every developer, every push, every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nightly API Testing Without a Dedicated Server
&lt;/h2&gt;

&lt;p&gt;The next requirement was daily test runs — a nightly job that would hit all our APIs, run the test suite with Jest and Supertest, and generate a report for the team.&lt;/p&gt;

&lt;p&gt;My first instinct was to spin up another EC2 instance. But that meant paying for a machine that sits idle 23 hours a day, plus managing its uptime, patching, and monitoring.&lt;/p&gt;

&lt;p&gt;Then I discovered that GitHub Actions provides temporary VMs for exactly this kind of job. No infrastructure to manage. No idle costs. The workflow spins up, runs 100+ API tests in about 5 minutes, generates an HTML report, uploads it to S3, and notifies the admins.&lt;/p&gt;

&lt;p&gt;A scheduled cron job in a YAML file replaced an entire server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Learned
&lt;/h2&gt;

&lt;p&gt;Looking back at these four months, the technical skills were the easy part. Docker, ECS, EC2, Amplify, GitHub Actions — you can learn any of these in a weekend. The harder lessons were about decision-making:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost awareness changes how you architect.&lt;/strong&gt; When you're spending someone else's money, every decision carries weight. "Best practice" doesn't mean anything if it's 10x the cost for a workload that doesn't need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first solution is rarely the right one.&lt;/strong&gt; ECS was a fine technology. It was the wrong choice for the problem. Being willing to tear something down after two weekends of work is a skill in itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boring infrastructure is good infrastructure.&lt;/strong&gt; An EC2 instance with a cron job isn't exciting. It's also not going to wake you up at 3am.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimise the inner loop.&lt;/strong&gt; The Docker build improvement saved 4+ minutes per push across 4 repos. Over a team and a quarter, that's days of developer time recovered.&lt;/p&gt;




&lt;p&gt;I'm not a DevOps engineer. I'm not a backend engineer either. Honestly, it's better to let go of that mindset entirely. In the coming times, just doing one thing won't be enough. You're an engineer — you solve problems. The label doesn't matter; the willingness to figure it out does.&lt;/p&gt;

&lt;p&gt;Months in and still going, I've learned that the gap between &lt;br&gt;
"knowing the theory" and "running it in production" is mostly filled with wrong first attempts and the willingness to start over.&lt;/p&gt;

&lt;p&gt;If you're in a similar position — thrust into infrastructure work without a roadmap — my advice is simple: deploy something, get it wrong, and iterate. The cloud makes it cheap to experiment, and every teardown teaches you more than any tutorial.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title># Schema Evolution &amp; Encoding: Building Future-Proof Data Systems 🚀</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Fri, 12 Sep 2025 11:35:41 +0000</pubDate>
      <link>https://dev.to/roshan_ican/-schema-evolution-encoding-building-future-proof-data-systems-4k8c</link>
      <guid>https://dev.to/roshan_ican/-schema-evolution-encoding-building-future-proof-data-systems-4k8c</guid>
      <description>&lt;p&gt;&lt;em&gt;How to design data-intensive applications that gracefully handle schema change&lt;/em&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%2Fimages.unsplash.com%2Fphoto-1558494949-ef010cbdcc31%3Fw%3D800%26h%3D400%26fit%3Dcrop" 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%2Fimages.unsplash.com%2Fphoto-1558494949-ef010cbdcc31%3Fw%3D800%26h%3D400%26fit%3Dcrop" alt="Schema Evolution Banner" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Challenge: Data That Evolves 📈
&lt;/h2&gt;

&lt;p&gt;In modern distributed systems, &lt;strong&gt;schema changes are inevitable&lt;/strong&gt;. Your application will grow, requirements will shift, and your data structures must adapt. But here's the catch: you need to maintain compatibility with existing data and older versions of your application.&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%2Fimages.unsplash.com%2Fphoto-1551288049-bebda4e38f71%3Fw%3D600%26h%3D300%26fit%3Dcrop" 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%2Fimages.unsplash.com%2Fphoto-1551288049-bebda4e38f71%3Fw%3D600%26h%3D300%26fit%3Dcrop" alt="Data Evolution Challenge" width="600" height="300"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding Compatibility Types 🔄
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backward Compatibility ⬅️
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;New code can read data written by old code&lt;/li&gt;
&lt;li&gt;Essential when upgrading applications gradually&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Forward Compatibility ➡️
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Old code can read data written by new code&lt;/li&gt;
&lt;li&gt;Critical for rolling back deployments safely&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%2Fimages.unsplash.com%2Fphoto-1504868584819-f8e8b4b6d7e3%3Fw%3D600%26h%3D300%26fit%3Dcrop" 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%2Fimages.unsplash.com%2Fphoto-1504868584819-f8e8b4b6d7e3%3Fw%3D600%26h%3D300%26fit%3Dcrop" alt="Compatibility Types" width="600" height="300"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Encoding = Serialization 📦
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Encoding&lt;/strong&gt; is the process of converting in-memory data structures into a byte sequence that can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stored in files&lt;/li&gt;
&lt;li&gt;Transmitted over networks&lt;/li&gt;
&lt;li&gt;Processed by different systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it as packaging your data for shipping! 📮&lt;/p&gt;




&lt;h2&gt;
  
  
  Traditional RPC Solutions 🌐
&lt;/h2&gt;

&lt;h3&gt;
  
  
  gRPC &amp;amp; Apache Thrift Approach
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Protocol Buffers example&lt;/span&gt;
&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="na"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// New field - backward compatible!&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;Key Strategies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Add new fields as &lt;strong&gt;optional&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ Assign &lt;strong&gt;default/null values&lt;/strong&gt; to new fields&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Generate classes&lt;/strong&gt; from schema definitions&lt;/li&gt;
&lt;li&gt;✅ Perfect for &lt;strong&gt;statically typed&lt;/strong&gt; languages (Java, C++, Go)&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%2Fimages.unsplash.com%2Fphoto-1451187580459-43490279c0fa%3Fw%3D600%26h%3D300%26fit%3Dcrop" 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%2Fimages.unsplash.com%2Fphoto-1451187580459-43490279c0fa%3Fw%3D600%26h%3D300%26fit%3Dcrop" alt="RPC Communication" width="600" height="300"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dynamic Language Problem 🐍
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Challenge:&lt;/strong&gt; Traditional schema-based systems don't play well with dynamic languages like JavaScript, Python, or Ruby.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No compile-time type checking&lt;/li&gt;
&lt;li&gt;Runtime schema validation needed&lt;/li&gt;
&lt;li&gt;Class generation feels unnatural&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%2Fimages.unsplash.com%2Fphoto-1627398242454-45a1465c2479%3Fw%3D600%26h%3D300%26fit%3Dcrop" 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%2Fimages.unsplash.com%2Fphoto-1627398242454-45a1465c2479%3Fw%3D600%26h%3D300%26fit%3Dcrop" alt="JavaScript Problem" width="600" height="300"&gt;&lt;/a&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3m5hgzgs51lw0h3qvsx.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%2Ft3m5hgzgs51lw0h3qvsx.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;Apache Avro 🌟&lt;/p&gt;

&lt;p&gt;Apache Avro solves the dynamic language compatibility problem with a genius approach: &lt;strong&gt;dual schemas&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%2Fimages.unsplash.com%2Fphoto-1518709268805-4e9042af2176%3Fw%3D600%26h%3D400%26fit%3Dcrop" 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%2Fimages.unsplash.com%2Fphoto-1518709268805-4e9042af2176%3Fw%3D600%26h%3D400%26fit%3Dcrop" alt="Apache Avro" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Avro's Dual Schema System 🎭
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Writer Schema 📝
&lt;/h3&gt;

&lt;p&gt;The schema used when &lt;strong&gt;encoding&lt;/strong&gt; the data&lt;/p&gt;

&lt;h3&gt;
  
  
  Reader Schema 📖
&lt;/h3&gt;

&lt;p&gt;The schema used when &lt;strong&gt;decoding&lt;/strong&gt; the data&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Writer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"record"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"User"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&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="nl"&gt;"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;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int"&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="nl"&gt;"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;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Reader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(v&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;field&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"record"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"User"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&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="nl"&gt;"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;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int"&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="nl"&gt;"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;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&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="nl"&gt;"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;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&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="s2"&gt;"null"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;h2&gt;
  
  
  How Avro Handles Compatibility ⚡
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Field Resolution Rules:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Field in writer but not reader&lt;/strong&gt; → &lt;strong&gt;Ignored&lt;/strong&gt; 🙈&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field in reader but not writer&lt;/strong&gt; → &lt;strong&gt;Default value used&lt;/strong&gt; 📋&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field in both&lt;/strong&gt; → &lt;strong&gt;Direct mapping&lt;/strong&gt; ✅&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This elegant system enables both forward and backward compatibility!&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%2Fimages.unsplash.com%2Fphoto-1557804506-669a67965ba0%3Fw%3D600%26h%3D300%26fit%3Dcrop" 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%2Fimages.unsplash.com%2Fphoto-1557804506-669a67965ba0%3Fw%3D600%26h%3D300%26fit%3Dcrop" alt="Compatibility Flow" width="600" height="300"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Schema Negotiation in Practice 🤝
&lt;/h2&gt;

&lt;p&gt;When two processes communicate over a &lt;strong&gt;bidirectional network connection&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant A as Service A
    participant B as Service B

    A-&amp;gt;&amp;gt;B: Connection Request + Schema Version
    B-&amp;gt;&amp;gt;A: Schema Negotiation Response
    A-&amp;gt;&amp;gt;B: Agreed Schema for Session
    Note over A,B: Use negotiated schema for&amp;lt;br/&amp;gt;entire connection lifetime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Both sides agree on schema version upfront&lt;/li&gt;
&lt;li&gt;✅ Optimal performance (no per-message overhead)&lt;/li&gt;
&lt;li&gt;✅ Clear compatibility guarantees&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Best Practices for Schema Evolution 📚
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do's ✅
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Always add new fields as &lt;strong&gt;optional&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Provide sensible &lt;strong&gt;default values&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;semantic versioning&lt;/strong&gt; for schemas&lt;/li&gt;
&lt;li&gt;Test compatibility with &lt;strong&gt;real data&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Document &lt;strong&gt;migration strategies&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Don'ts ❌
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Never remove required fields&lt;/li&gt;
&lt;li&gt;Avoid changing field types drastically&lt;/li&gt;
&lt;li&gt;Don't reuse field IDs/names for different purposes&lt;/li&gt;
&lt;li&gt;Never skip compatibility testing&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%2Fimages.unsplash.com%2Fphoto-1454165804606-c3d57bc86b40%3Fw%3D600%26h%3D300%26fit%3Dcrop" 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%2Fimages.unsplash.com%2Fphoto-1454165804606-c3d57bc86b40%3Fw%3D600%26h%3D300%26fit%3Dcrop" alt="Best Practices" width="600" height="300"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Impact 🌍
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Companies using these patterns:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Netflix&lt;/strong&gt;: Schema evolution for microservices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: Avro for data pipelines
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uber&lt;/strong&gt;: Protocol Buffers for service communication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airbnb&lt;/strong&gt;: Thrift for cross-service APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result? &lt;strong&gt;Zero-downtime deployments&lt;/strong&gt; and &lt;strong&gt;seamless data migrations&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%2Fimages.unsplash.com%2Fphoto-1552664730-d307ca884978%3Fw%3D600%26h%3D300%26fit%3Dcrop" 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%2Fimages.unsplash.com%2Fphoto-1552664730-d307ca884978%3Fw%3D600%26h%3D300%26fit%3Dcrop" alt="Success Stories" width="600" height="300"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: Future-Proof Your Data 🔮
&lt;/h2&gt;

&lt;p&gt;Schema evolution isn't just a technical detail—it's a &lt;strong&gt;business enabler&lt;/strong&gt;. By choosing the right encoding strategy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reduce deployment risks&lt;/strong&gt; ⚡&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable continuous delivery&lt;/strong&gt; 🚀
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support diverse technology stacks&lt;/strong&gt; 🌈&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintain system reliability&lt;/strong&gt; 🛡️&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;: Whether you choose Protocol Buffers, Thrift, or Avro, the principles remain the same—design for change from day one!&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading 📖
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dataintensive.net/" rel="noopener noreferrer"&gt;Designing Data-Intensive Applications&lt;/a&gt; by Martin Kleppmann&lt;/li&gt;
&lt;li&gt;&lt;a href="https://avro.apache.org/docs/" rel="noopener noreferrer"&gt;Apache Avro Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/protocol-buffers" rel="noopener noreferrer"&gt;Protocol Buffers Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thrift.apache.org/tutorial/" rel="noopener noreferrer"&gt;Apache Thrift Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;What's your experience with schema evolution? Share your war stories in the comments! 💬&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #DataEngineering #SystemDesign #Microservices #APIs #SoftwareArchitecture #Avro #Protobuf #Thrift&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Validating Environment Variables in Node.js with Zod</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Thu, 22 May 2025 20:00:01 +0000</pubDate>
      <link>https://dev.to/roshan_ican/validating-environment-variables-in-nodejs-with-zod-2epn</link>
      <guid>https://dev.to/roshan_ican/validating-environment-variables-in-nodejs-with-zod-2epn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Ever pushed a project to production only to realize an environment variable was missing or mistyped? I have—and it wasn’t fun.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Recently, I ran into a subtle but recurring issue in one of our Node.js projects: environment variables not loading correctly from the .env file. At first glance, this seemed trivial—just use dotenv, right? But as our configuration grew more complex, undefined values started creeping in silently, breaking features without clear errors.&lt;/p&gt;

&lt;p&gt;The Problem with Plain .env + process.env&lt;/p&gt;

&lt;p&gt;Like many teams, we began with this simple setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dotenv&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;dotenv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&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;CORE_DB_URI&lt;/span&gt; &lt;span class="o"&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;CORE_DB_URI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the issues quickly stacked up:&lt;br&gt;
    • We forgot to set some variables in staging or dev environments.&lt;br&gt;
    • Typos in variable names would go unnoticed until runtime.&lt;br&gt;
    • Some variables like PORT needed to be parsed (parseInt), while others had fallbacks—lots of boilerplate.&lt;/p&gt;

&lt;p&gt;Worst of all, we had no way to know what was missing or invalid until something failed. That’s not a great developer experience.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Breakthrough: Validating the Environment with Zod
&lt;/h2&gt;

&lt;p&gt;After some research and trial-and-error, we discovered a better way—define a schema for environment variables using zod, and parse process.env against it before anything else runs.&lt;/p&gt;

&lt;p&gt;Here’s what that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;z&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;zod&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;envSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;HOST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&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="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

  &lt;span class="na"&gt;MONGO_DB_URI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;nonempty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MONGO_DB_URI is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// ... add other required vars&lt;/span&gt;

  &lt;span class="na"&gt;AWS_ACCESS_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;nonempty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AWS_ACCESS_KEY is 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;SMTP_USERNAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;nonempty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SMTP_USERNAME is required&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we updated the config loader like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dotenv&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;dotenv&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;envSchema&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;./envSchema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;parsedEnv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;parsedEnv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;envSchema&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;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;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;✅ Environment validation passed&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;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;❌ Environment validation failed:&lt;/span&gt;&lt;span class="dl"&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;error&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;?.(),&lt;/span&gt; &lt;span class="kc"&gt;null&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Benefits We Gained
&lt;/h2&gt;

&lt;p&gt;This refactor gave us immediate wins:&lt;br&gt;
    • Validation at startup: The app won’t even run unless all required env vars are present.&lt;br&gt;
    • Helpful errors: Each missing variable now shows a clear error message.&lt;br&gt;
    • No more implicit casting or null-checks: Everything is validated and transformed up front.&lt;br&gt;
    • Cleaner config code: No more process.env.XYZ || "fallback" everywhere.&lt;/p&gt;

&lt;p&gt;Instead of this:&lt;br&gt;
&lt;code&gt;const CORE_DB_URI = process.env.CORE_DB_URI!;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We now use this:&lt;br&gt;
&lt;code&gt;const config = {&lt;br&gt;
  CORE_DB_URI: parsedEnv.CORE_DB_URI,&lt;br&gt;
};&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail Fast, Not in Production
&lt;/h2&gt;

&lt;p&gt;One thing that really sold me on this pattern &lt;br&gt;[&lt;a href="https://www.youtube.com/watch?v=eZBsBMf5zuQ" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=eZBsBMf5zuQ&lt;/a&gt;] &lt;br&gt; is how it catches config issues at runtime—before your app misbehaves in production.&lt;/p&gt;

&lt;p&gt;Without schema validation, a missing environment variable might not throw an error until it’s used—say, when connecting to a database or sending an email. That’s late. By then, users might already be affected.&lt;/p&gt;

&lt;p&gt;With zod, your app won’t even boot if a required environment variable is missing or misconfigured. That’s intentional. You get a clear error in development, staging, or CI pipelines—not after you’ve shipped broken code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It’s simple, safe, and clean.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Final Thoughts&lt;/p&gt;

&lt;p&gt;It’s easy to underestimate the importance of config hygiene—until it breaks something. Using zod for schema validation gives you type safety, meaningful errors, and cleaner code. It’s now a pattern I’ll carry into every serious Node.js project.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you’re still relying solely on .env + process.env, try wrapping it with validation. Your future self—and your team—will thank you.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>zod</category>
      <category>schema</category>
      <category>typescript</category>
    </item>
    <item>
      <title>From Storage to Stream: Delivering MongoDB Data Directly to Users</title>
      <dc:creator>〽️ 𝙍𝙤𝙨𝙝𝙖𝙣</dc:creator>
      <pubDate>Mon, 30 Dec 2024 20:16:48 +0000</pubDate>
      <link>https://dev.to/roshan_ican/from-storage-to-stream-delivering-mongodb-data-directly-to-users-4of0</link>
      <guid>https://dev.to/roshan_ican/from-storage-to-stream-delivering-mongodb-data-directly-to-users-4of0</guid>
      <description>&lt;p&gt;Step 1: MongoDB Cursor&lt;/p&gt;

&lt;p&gt;Here’s how we set up the cursor (reusing your snippet):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;userObject&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;serviceProviderName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ZYRO&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;zyroTransactionModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;finoTransactionModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;cursor&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;Cursor created successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2: Setting Up the ZIP File&lt;br&gt;
Use the yazl library to stream CSV data into a ZIP file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yazl&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;yazl&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;zipfile&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;yazl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ZipFile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/zip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Disposition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;attachment; filename=transactions.zip&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;zipfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&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;cleanup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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="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;Cleaning up resources...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;zipfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Finalize ZIP&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&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="s2"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&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="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 3: Creating Dynamic CSV Streams&lt;br&gt;
Generate CSV data dynamically and stream it into the ZIP file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createNewCSVStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;csvStream&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;Readable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nf"&gt;read&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="nx"&gt;csvStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Add headers&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;csvStream&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;filteredHeaders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transactionDownloadFields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userObject&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;role&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;currentCSVStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createNewCSVStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filteredHeaders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;zipfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addReadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentCSVStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transactions_part_1.csv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 4: Streaming MongoDB Data to CSV&lt;br&gt;
Stream the data from MongoDB directly into the CSV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;cursor&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;data&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;doc&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;csvRow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filteredHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;currentCSVStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;csvRow&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Write row&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;cursor&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;end&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;currentCSVStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// End the stream&lt;/span&gt;
    &lt;span class="nx"&gt;zipfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Finalize the ZIP&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Step 5: Processing Data from MongoDB Cursor&lt;br&gt;
Stream documents from the MongoDB cursor, transform them as needed, and dynamically write rows to the CSV stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;doc&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientDisconnected&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;Client disconnected. Stopping processing...&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;streamedCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;rowCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;row&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filteredHeaders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;transactionDownloadFields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;userObject&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;filteredHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filteredHeaders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

            &lt;span class="c1"&gt;// Fetch the corresponding field configuration from transactionDownloadFields&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;transactionDownloadFields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Get the value from the transaction document&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getValueFromTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Apply transformation if the field has a transform function&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalField&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="c1"&gt;// Enclose the value in double quotes&lt;/span&gt;
            &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;value&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"N/A"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;currentCSVStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Check if the row count has reached the threshold for the current CSV file&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_ROWS_PER_FILE&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="s2"&gt;`Threshold reached for file &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fileIndex&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Starting new file...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;currentCSVStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// End the current CSV stream&lt;/span&gt;
            &lt;span class="nx"&gt;currentCSVStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createNewCSVStream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Start a new stream&lt;/span&gt;
            &lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Reset the row count&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Finalize the current CSV stream if it has data&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentCSVStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;currentCSVStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Finalize the ZIP file&lt;/span&gt;
    &lt;span class="nx"&gt;zipfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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;`Successfully streamed &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;streamedCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; rows across &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fileIndex&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; files.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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;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;Error during processing:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;headersSent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&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="na"&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;Failed to generate ZIP file&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="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Cleanup: Close the MongoDB cursor&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;Error closing cursor:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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;Summary&lt;/p&gt;

&lt;p&gt;Document Iteration Using for await...of:&lt;/p&gt;

&lt;p&gt;Streams documents one by one from the MongoDB cursor efficiently.&lt;br&gt;
Enables real-time processing without loading all data into memory.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic CSV Row Generation:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Constructs each row dynamically by iterating over filteredHeaders.&lt;br&gt;
Applies transformations using a transform function, if defined in transactionDownloadFields.&lt;br&gt;
Row Threshold and File Splitting:&lt;/p&gt;

&lt;p&gt;Monitors the row count against the threshold (MAX_ROWS_PER_FILE).&lt;br&gt;
Ends the current CSV stream and starts a new one when the threshold is reached.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Error Handling:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Logs and sends an error response if an issue occurs during processing.&lt;br&gt;
Ensures proper cleanup by closing the MongoDB cursor in the finally block.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finalizing Streams:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pushes null to terminate the current CSV stream.&lt;br&gt;
Completes the ZIP file once all rows are processed.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>react</category>
      <category>node</category>
    </item>
  </channel>
</rss>
