<?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>Forem: Pau Dang</title>
    <description>The latest articles on Forem by Pau Dang (@paudang).</description>
    <link>https://forem.com/paudang</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3778555%2F0cf1e817-ad3e-4643-a257-b03aa03652f0.jpg</url>
      <title>Forem: Pau Dang</title>
      <link>https://forem.com/paudang</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/paudang"/>
    <language>en</language>
    <item>
      <title>Stop Wrestling with AWS: I Built a Tool to Generate Production-Ready Node.js &amp; Terraform</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Thu, 21 May 2026 03:31:19 +0000</pubDate>
      <link>https://forem.com/paudang/i-built-a-nodejs-generator-with-production-ready-aws-terraform-so-you-dont-have-to-1ne7</link>
      <guid>https://forem.com/paudang/i-built-a-nodejs-generator-with-production-ready-aws-terraform-so-you-dont-have-to-1ne7</guid>
      <description>&lt;h2&gt;
  
  
  A deep dive into how I automated setting up Node.js backends and AWS infrastructure using Terraform.
&lt;/h2&gt;

&lt;p&gt;If you’re anything like me, starting a new project is the most exciting part of development. You get to choose your architecture, set up your folders, and start writing code. &lt;/p&gt;

&lt;p&gt;But then comes the deployment phase...&lt;/p&gt;

&lt;p&gt;Suddenly, you're wrestling with the AWS Console. You need a VPC, public/private subnets, an Internet Gateway, Route Tables, NAT Gateways, Security Groups, an ALB, EC2 instances, and an RDS database. &lt;/p&gt;

&lt;p&gt;It takes hours, and if you miss a single route table entry, nothing works.&lt;/p&gt;

&lt;p&gt;I got tired of repeating this painful process. So, I decided to solve it. I built a CLI tool that not only generates a robust Node.js backend (Clean Architecture, TypeScript, etc.) but also includes &lt;strong&gt;production-ready AWS infrastructure as code (IaC) using Terraform&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is how it works, and how you can use it to spin up an AWS environment locally for free.&lt;/p&gt;




&lt;h2&gt;
  
  
  The "Everything is a Click" Problem
&lt;/h2&gt;

&lt;p&gt;ClickOps (configuring infrastructure manually via the AWS UI) is fine for learning, but terrible for repeatability. If you want to replicate a setup or tear it down, you have to click through 20 different screens.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;Node.js Quickstart Structure&lt;/strong&gt;, you can generate a complete backend with the infrastructure already wired up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nodejs-quickstart-structure@latest init &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"nodejs-service"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"TypeScript"&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"Clean Architecture"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"MySQL"&lt;/span&gt; &lt;span class="nt"&gt;--db-name&lt;/span&gt; &lt;span class="s2"&gt;"demo"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"REST APIs"&lt;/span&gt; &lt;span class="nt"&gt;--caching&lt;/span&gt; &lt;span class="s2"&gt;"None"&lt;/span&gt; &lt;span class="nt"&gt;--ci-provider&lt;/span&gt; &lt;span class="s2"&gt;"GitHub Actions"&lt;/span&gt; &lt;span class="nt"&gt;--auth&lt;/span&gt; None &lt;span class="nt"&gt;--terraform&lt;/span&gt; &lt;span class="s2"&gt;"Production"&lt;/span&gt; &lt;span class="nt"&gt;--no-include-security&lt;/span&gt; &lt;span class="nt"&gt;--advanced-options&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;( Pro tip: Insert a cool GIF or screenshot of your CLI running here!)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The CLI asks you a few questions (TypeScript? Clean Architecture? Postgres?) and then creates a complete repository. But the real magic happens in the &lt;code&gt;terraform/&lt;/code&gt; folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Flavors of Infrastructure (Because Cost Matters)
&lt;/h2&gt;

&lt;p&gt;Not every project is a massive enterprise app. Side projects need to be cheap, while production apps need to be highly available. I designed two Terraform templates to match these needs:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Standard Tier (The "Keep it Cheap" Setup)
&lt;/h3&gt;

&lt;p&gt;Perfect for side projects, MVPs, and dev environments. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single EC2 instance&lt;/li&gt;
&lt;li&gt;Single-AZ RDS database&lt;/li&gt;
&lt;li&gt;Shared NAT Gateway&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; Keep your AWS bill under $30/month (or even within the Free Tier).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Snippet of what gets generated:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"ec2"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/ec2"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
  &lt;span class="c1"&gt;# ... automatically wired to your VPC and DB&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Production Tier (High Availability)
&lt;/h3&gt;

&lt;p&gt;When you're ready for the big leagues. This tier provisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-AZ Architecture&lt;/strong&gt; for zero-downtime failover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application Load Balancer (ALB)&lt;/strong&gt; to distribute traffic smoothly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS WAF (Web Application Firewall)&lt;/strong&gt; to block SQLi and XSS automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Air-gapped RDS&lt;/strong&gt; in isolated subnets for maximum security.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Best Part: Testing Locally for FREE with LocalStack
&lt;/h2&gt;

&lt;p&gt;I know what you're thinking: &lt;em&gt;"Terraform is great, but running &lt;code&gt;terraform apply&lt;/code&gt; on AWS costs money. How do I test this without going broke?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This was a major pain point, so I integrated &lt;a href="https://localstack.cloud/" rel="noopener noreferrer"&gt;LocalStack&lt;/a&gt; support right out of the box. You can spin up the entire AWS architecture on your local machine using Docker—completely free. Setup &lt;a href="https://nodejs-quickstart-generator.netlify.app/infrastructure/aws-terraform.html#local-testing-localstack" rel="noopener noreferrer"&gt;Local Testing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just start LocalStack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;localstack start &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;localstack.tf&lt;/code&gt; file to point Terraform to your local machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
  &lt;span class="nx"&gt;access_key&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt;
  &lt;span class="nx"&gt;secret_key&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt;
  &lt;span class="nx"&gt;skip_credentials_validation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;skip_requesting_account_id&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;skip_metadata_api_check&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;s3_use_path_style&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;endpoints&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ec2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:4566"&lt;/span&gt;
    &lt;span class="nx"&gt;rds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:4566"&lt;/span&gt;
    &lt;span class="c1"&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;And run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply &lt;span class="nt"&gt;-auto-approve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boom. You now have a local VPC, EC2 instance, and RDS database running on your laptop. You can validate your entire infrastructure setup without giving Amazon your credit card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it out!
&lt;/h2&gt;

&lt;p&gt;I built this tool to save developers time and enforce best practices from day one. Whether you're building a quick weekend hackathon project or a serious enterprise application, the foundation is ready for you.&lt;/p&gt;

&lt;p&gt;Give it a spin, and let me know what you think!&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://nodejs-quickstart-generator.netlify.app/#configurator" rel="noopener noreferrer"&gt;Nodejs Quickstart Generator&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://youtu.be/Cxbb54T0uo8" rel="noopener noreferrer"&gt;How to use the tool and video demo&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/paudang/nodejs-service-terraform/tree/main/terraform" rel="noopener noreferrer"&gt;Sample Project: nodejs-service-terraform&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If this saves you a few hours of AWS headaches, dropping a ⭐ on the repo would mean the world to me. I'd love to hear your feedback in the comments!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>aws</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Stop Blindly Trusting Passport.js: How to Implement Secure OAuth CSRF Protection Manually</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Mon, 18 May 2026 08:00:00 +0000</pubDate>
      <link>https://forem.com/paudang/stop-blindly-trusting-passportjs-how-to-implement-secure-oauth-csrf-protection-manually-4fh2</link>
      <guid>https://forem.com/paudang/stop-blindly-trusting-passportjs-how-to-implement-secure-oauth-csrf-protection-manually-4fh2</guid>
      <description>&lt;p&gt;OAuth 2.0 is the backbone of modern authentication. But many developers treat it as a "set it and forget it" feature by using libraries like Passport.js. While these libraries are great, they often hide the critical security handshake happening under the hood—leaving your app vulnerable to &lt;strong&gt;OAuth CSRF attacks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this post, I'll show you how we implemented a Zero-Trust Social Login flow in &lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;nodejs-quickstart-structure v2.2.1&lt;/a&gt; using plain Node.js and Axios.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack: How a Hacker Steals Your Identity
&lt;/h2&gt;

&lt;p&gt;Most developers know about standard CSRF (Cross-Site Request Forgery). OAuth CSRF is a specific variant where an attacker tricks a victim into linking the &lt;em&gt;attacker's&lt;/em&gt; social account to the &lt;em&gt;victim's&lt;/em&gt; application account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here is exactly how the attack happens:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The Attacker's Setup&lt;/strong&gt;: A hacker visits your site and clicks "Link Google." They log into &lt;em&gt;their own&lt;/em&gt; Google account, but when Google redirects them back to your site, they &lt;strong&gt;stop&lt;/strong&gt; and copy the &lt;code&gt;code&lt;/code&gt; from the URL.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Phish&lt;/strong&gt;: The hacker sends a victim a link: &lt;code&gt;https://your-app.com/api/auth/google/callback?code=HACKER_CODE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Click&lt;/strong&gt;: The victim (already logged into your app) clicks the link.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Link&lt;/strong&gt;: Your server receives the &lt;code&gt;HACKER_CODE&lt;/code&gt;, validates it with Google, and sees it's a valid account. Since the victim is the one who sent the request, your server links the &lt;strong&gt;hacker's Google ID&lt;/strong&gt; to the &lt;strong&gt;victim's user profile&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Takeover&lt;/strong&gt;: The hacker can now simply click "Login with Google" on your site and they are instantly logged in as the victim.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The result?&lt;/strong&gt; A quiet, total account takeover.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Attack Flow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F35f3a6xttvetx0gn0u4n.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%2F35f3a6xttvetx0gn0u4n.png" alt=" " width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: The &lt;code&gt;state&lt;/code&gt; Parameter
&lt;/h2&gt;

&lt;p&gt;The OAuth2 spec provides a &lt;code&gt;state&lt;/code&gt; parameter specifically to prevent this. Here is how it looks:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Secure Flow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fskt5y84e330kgai95o6p.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%2Fskt5y84e330kgai95o6p.png" alt=" " width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Generate and Store a Secure State
&lt;/h3&gt;

&lt;p&gt;When the user clicks "Login with Google," don't just redirect them. Generate a random string, store it in a secure cookie, and pass it to the provider.&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="c1"&gt;// authController.js&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;googleLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Store state in an HttpOnly, SameSite=Lax cookie&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oauth_state&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;{&lt;/span&gt; 
    &lt;span class="na"&gt;httpOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Always use HTTPS in production&lt;/span&gt;
    &lt;span class="na"&gt;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="c1"&gt;// Valid for 10 minutes&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;authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://accounts.google.com/o/oauth2/v2/auth?`&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;client_id&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;GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;redirect_uri&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;GOOGLE_CALLBACK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;--- The magic happens here&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Verify the State on Callback
&lt;/h3&gt;

&lt;p&gt;When Google redirects the user back to your site, the first thing you must do is check if the &lt;code&gt;state&lt;/code&gt; in the URL matches the &lt;code&gt;state&lt;/code&gt; in your cookie.&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;googleCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;oauth_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Always clear the cookie immediately to prevent reuse&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oauth_state&lt;/span&gt;&lt;span class="dl"&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;state&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;savedState&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="nx"&gt;res&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;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Security Alert: State mismatch detected!&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="c1"&gt;// Now it's safe to exchange the code for a token&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://oauth2.googleapis.com/token&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;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client_id&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;GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client_secret&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;GOOGLE_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;redirect_uri&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;GOOGLE_CALLBACK_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorization_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Handle user data...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Manual Approach Wins
&lt;/h2&gt;

&lt;p&gt;By ditching "black-box" libraries and using a deterministic flow with Axios, you get:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Full Auditability&lt;/strong&gt;: You can log exactly what was sent and received.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Explicit Security&lt;/strong&gt;: You can't "forget" to validate the state because you are writing the logic yourself.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Leaner App&lt;/strong&gt;: No need for heavy middleware if you only need social login.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Want a Battle-Tested Template?
&lt;/h2&gt;

&lt;p&gt;Implementing this correctly across every project is tedious. That's why I built the &lt;strong&gt;&lt;a href="https://nodejs-quickstart-generator.netlify.app/#configurator" rel="noopener noreferrer"&gt;Node.js Quickstart Generator&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you want to see this implementation in action, check out these resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sample Project&lt;/strong&gt;: &lt;a href="https://github.com/paudang/nodejs-social-auth" rel="noopener noreferrer"&gt;paudang/nodejs-social-auth&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework&lt;/strong&gt;: &lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;paudang/nodejs-quickstart-structure&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://systemweakness.com/the-oauth-integration-debt-why-your-social-login-is-a-csrf-risk-c2008099c05e" rel="noopener noreferrer"&gt;The OAuth Integration Debt: Why Your Social Login Is a CSRF Risk&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you found this helpful, check out the repo and give it a ⭐!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;GitHub Repo: nodejs-quickstart-structure&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>backend</category>
      <category>security</category>
    </item>
    <item>
      <title>OAuth2 Account Takeovers: Building a Bulletproof Social Login Architecture</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Wed, 06 May 2026 15:45:54 +0000</pubDate>
      <link>https://forem.com/paudang/oauth2-account-takeovers-building-a-bulletproof-social-login-architecture-973</link>
      <guid>https://forem.com/paudang/oauth2-account-takeovers-building-a-bulletproof-social-login-architecture-973</guid>
      <description>&lt;p&gt;When implementing Social Login (Google, GitHub), many developers assume that the heavy lifting is handled by the provider. The truth is: &lt;strong&gt;the integration layer is where your system is most vulnerable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To tackle these vulnerabilities head-on, we must rethink the integration. Here is how to build a bulletproof, Zero-Trust social login architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pitfall 1: The Black Box Dependency
&lt;/h3&gt;

&lt;p&gt;Libraries like Passport.js are incredibly popular, but they wrap the OAuth flow into a "black box." In enterprise environments, you need total auditability. We opted for a custom &lt;code&gt;Axios&lt;/code&gt; implementation. This reduces the attack surface and allows precise domain-level error handling.&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="c1"&gt;// https://github.com/paudang/nodejs-social-auth/blob/main/src/infrastructure/auth/socialAuthService.ts&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;GoogleProvider&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;ISocialProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&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="nx"&gt;redirectUri&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ISocialProfile&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;params&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;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_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;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;GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirect_uri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redirectUri&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grant_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="s1"&gt;authorization_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Deterministic Token Exchange&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://oauth2.googleapis.com/token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&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="s1"&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="s1"&gt;application/x-www-form-urlencoded&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;access_token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profileResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.googleapis.com/oauth2/v2/userinfo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profileResponse&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profileResponse&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;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profileResponse&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Pitfall 2: Blind Account Linking
&lt;/h3&gt;

&lt;p&gt;What happens if an attacker registers on a secondary OAuth provider using your email address, and your system automatically links it? You just gave them an Account Takeover (ATO) vector.&lt;/p&gt;

&lt;p&gt;Our system prevents this by intelligently linking social profiles and nullifying passwords for OAuth-created accounts:&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="c1"&gt;// https://github.com/paudang/nodejs-social-auth/blob/main/src/usecases/auth/socialLoginUseCase.ts&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="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;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;user&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;User&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="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Disable traditional login&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;profile&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GitHub&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;profile&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="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="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Controlled linking&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;googleId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;googleId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;profile&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="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Update user...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bridging the Zero-Trust Gap
&lt;/h3&gt;

&lt;p&gt;Once authenticated via Google, we &lt;strong&gt;do not&lt;/strong&gt; trust their session indefinitely. We immediately bridge the user into our internal JWT system, fortified by a Redis "Nuclear Revoke" mechanism.&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%2F6aqzot0dnrtajqoeafyl.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%2F6aqzot0dnrtajqoeafyl.png" alt=" " width="800" height="661"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;  The Verify 'state' mechanism (CSRF protection) shown in the diagram represents the ideal architecture target. Cryptographic state validation is actively in development and will be codified in the next version of the generator.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can generate this exact, secure architecture for your next project using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nodejs-quickstart-structure@latest init &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"my-secure-app"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"TypeScript"&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"Clean Architecture"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"PostgreSQL"&lt;/span&gt; &lt;span class="nt"&gt;--db-name&lt;/span&gt; &lt;span class="s2"&gt;"demo"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"REST APIs"&lt;/span&gt; &lt;span class="nt"&gt;--caching&lt;/span&gt; &lt;span class="s2"&gt;"Redis"&lt;/span&gt; &lt;span class="nt"&gt;--ci-provider&lt;/span&gt; &lt;span class="s2"&gt;"GitHub Actions"&lt;/span&gt; &lt;span class="nt"&gt;--auth&lt;/span&gt; JWT &lt;span class="nt"&gt;--social-auth&lt;/span&gt; Google GitHub &lt;span class="nt"&gt;--no-include-security&lt;/span&gt; &lt;span class="nt"&gt;--advanced-options&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out the CLI tool at &lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;Nodejs Quickstart Generator&lt;/a&gt; and the reference implementation code at &lt;a href="https://github.com/paudang/nodejs-social-auth" rel="noopener noreferrer"&gt;nodejs-social-auth&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you missed the first part of this architectural series on JWT Revocation, you can read it here: &lt;a href="https://systemweakness.com/the-illusion-of-stateless-security-rethinking-jwt-revocation-at-scale-8426472c5022" rel="noopener noreferrer"&gt;The Illusion of Stateless Security: Rethinking JWT Revocation at Scale&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>node</category>
      <category>oauth</category>
      <category>security</category>
    </item>
    <item>
      <title>Slashed My Automation Suite from 9 Hours to 1 Hour with This Simple Caching Trick</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Thu, 30 Apr 2026 01:21:32 +0000</pubDate>
      <link>https://forem.com/paudang/slashed-my-automation-suite-from-9-hours-to-1-hour-with-this-simple-caching-trick-22n9</link>
      <guid>https://forem.com/paudang/slashed-my-automation-suite-from-9-hours-to-1-hour-with-this-simple-caching-trick-22n9</guid>
      <description>&lt;p&gt;We've all been there: you build an amazing automation suite, hit "Run", and realize it's going to take until next Tuesday to finish. &lt;/p&gt;

&lt;p&gt;Last week, I faced a massive bottleneck in my CI/CD pipeline. Here’s how I optimized a &lt;strong&gt;9-hour test suite down to just 1 hour&lt;/strong&gt; using a "Base Cache" strategy.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: The &lt;code&gt;npm install&lt;/code&gt; Nightmare
&lt;/h2&gt;

&lt;p&gt;I'm building a &lt;strong&gt;Node.js Quickstart Generator&lt;/strong&gt;. To ensure quality, I have to validate &lt;strong&gt;720 different combinations&lt;/strong&gt; of technologies (Clean Architecture, MVC, TypeScript, JavaScript, MySQL, MongoDB, Kafka, etc.).&lt;/p&gt;

&lt;p&gt;For every single test run, the script was doing a fresh &lt;code&gt;npm install&lt;/code&gt;. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Result:&lt;/strong&gt; 9 hours of total execution time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Culprit:&lt;/strong&gt; Redundant network fetches and disk I/O for the same packages over and over again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fudbnbmfms6d8ojrl3agl.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%2Fudbnbmfms6d8ojrl3agl.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Smart "Base Caching"
&lt;/h2&gt;

&lt;p&gt;The mindset shift was simple: &lt;strong&gt;Stop treating every project as a unique snowflake.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Even with 720 combinations, they share 95% of the same core dependencies. Here is the 3-step workflow I implemented:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Identify the "Base"
&lt;/h3&gt;

&lt;p&gt;I grouped projects by their heaviest dependencies: &lt;strong&gt;Language + Database&lt;/strong&gt;. This resulted in only &lt;strong&gt;8 unique Base Caches&lt;/strong&gt; (e.g., &lt;code&gt;TypeScript_PostgreSQL&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Bootstrap via Copy
&lt;/h3&gt;

&lt;p&gt;Instead of running &lt;code&gt;npm install&lt;/code&gt; from scratch, the script now uses this optimized logic, code example: &lt;a href="https://github.com/paudang/nodejs-quickstart-structure/blob/feature/oauht2-google-github/scripts/lib/validation-core.js#L500" rel="noopener noreferrer"&gt;https://github.com/paudang/nodejs-quickstart-structure/blob/feature/oauht2-google-github/scripts/lib/validation-core.js#L500&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Define the Base Cache key (Language + DB)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseHashKey&lt;/span&gt; &lt;span class="o"&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;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&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;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cachePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&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;testDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`node_modules_base_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseHashKey&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="c1"&gt;// 2. If a base cache exists, copy it in seconds&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;usedCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pathExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&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;projectPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;usedCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Run npm install (Use --prefer-offline for ultra-fast delta updates)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;npmCmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;usedCache&lt;/span&gt; 
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npm install --prefer-offline --no-audit&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="s1"&gt;npm install --no-audit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;npmCmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projectPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 4. Save the cache if it's the first run&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;usedCache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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;projectPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;cachePath&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;Because most of the packages are already in the &lt;code&gt;node_modules&lt;/code&gt; folder, npm just performs a quick delta update.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wesrr7smokm3374kwok.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%2F3wesrr7smokm3374kwok.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The optimization was a game-changer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Execution Time:&lt;/strong&gt; 9 Hours ➡️ &lt;strong&gt;1 Hour&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage Efficiency:&lt;/strong&gt; 80GB (if full-cached) ➡️ &lt;strong&gt;1.2GB&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev Experience:&lt;/strong&gt; I can now run the full validation suite multiple times a day instead of once a week.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaways for Developers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find the Real Bottleneck:&lt;/strong&gt; Don't micro-optimize your code if your Network/IO is the one killing your productivity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think in "Deltas":&lt;/strong&gt; If you can't cache everything, cache the 90% and compute the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate the Automation:&lt;/strong&gt; Always monitor your scripts. If it's slow, it’s a bug.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How do you handle heavy node_modules in your CI/CD?&lt;/strong&gt; I'd love to hear your tricks in the comments! 👇&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this helpful, feel free to give it a ❤️ and follow for more DevOps and Performance tips!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>node</category>
      <category>automation</category>
    </item>
    <item>
      <title>When Logout is not enough: Defending against Token Theft with Big Tech-grade Rotation.</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Mon, 20 Apr 2026 05:47:58 +0000</pubDate>
      <link>https://forem.com/paudang/when-logout-is-not-enough-defending-against-token-theft-with-big-tech-grade-rotation-3c28</link>
      <guid>https://forem.com/paudang/when-logout-is-not-enough-defending-against-token-theft-with-big-tech-grade-rotation-3c28</guid>
      <description>&lt;p&gt;Hi, I’m Pau Dang.&lt;/p&gt;

&lt;p&gt;Imagine a silent intruder. They steal a single &lt;strong&gt;Refresh Token&lt;/strong&gt; and maintain persistence in your system for months. Your user changes their password, logs out, and feels safe—but the intruder remains. This is the reality of systems that lack &lt;strong&gt;Stateless Invalidation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’m tired of boilerplates that ignore this &lt;strong&gt;Attack Vector.&lt;/strong&gt; In this post, I want to discuss the architectural blueprint for Enterprise-grade JWT security.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Revocation Gap: The Silent Crisis
&lt;/h2&gt;

&lt;p&gt;A signed JWT is a rogue agent if you cannot kill it. Pure statelessness is a high-risk gamble. Most developers treat JWT as a "set and forget" solution, but if you cannot revoke a session instantly, your security is an illusion. &lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;Revocation Gap&lt;/strong&gt;—the space between a user's intent to logout and the token's hardcoded expiration.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Solution: Redis Blacklisting &amp;amp; JTI
&lt;/h2&gt;

&lt;p&gt;To mitigate this without introducing a database bottleneck, we use &lt;strong&gt;JTI (JWT ID)&lt;/strong&gt; and &lt;strong&gt;Redis&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JTI&lt;/strong&gt;: Every token must carry a unique identifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation List&lt;/strong&gt;: On logout, we don't "delete" the token; we add its JTI to a high-speed Redis blacklist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL Precision&lt;/strong&gt;: By calculating the exact remaining life of the token, we ensure the Redis list remains lean and performant.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. The Climax: "Nuclear Revoke" (Reuse Detection)
&lt;/h2&gt;

&lt;p&gt;Standard token rotation is not enough. We need proactive defense. In my v2.1.0 implementation, we use &lt;strong&gt;Refresh Token Rotation&lt;/strong&gt; with an integrated tripwire.&lt;/p&gt;

&lt;p&gt;If the system receives an old Refresh Token that has already been rotated =&amp;gt; This is a definitive sign of an attack (&lt;strong&gt;Reuse&lt;/strong&gt;). Instead of just rejecting the request, the system triggers a &lt;strong&gt;"Nuclear Revoke"&lt;/strong&gt;: every active session for that user is instantly purged. It represents the ultimate trade-off: forcing a logout for the real user to ensure the absolute eviction of the intruder.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Implementation: Knowledge-as-Code
&lt;/h3&gt;

&lt;p&gt;View the full production-ready logic (automatically generated by &lt;code&gt;nodejs-quickstart-structure&lt;/code&gt;) on &lt;a href="https://github.com/paudang/nodejs-service/blob/main/src/controllers/authController.ts#L46" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AuthController.ts - Enterprise Refresh Logic&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;refreshToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JwtService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyRefreshToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Integrity Check (Redis Whitelist)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`refresh_tokens:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;activeTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cacheService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;cacheKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="c1"&gt;// --- THE TRIPWIRE: REUSE DETECTION ---&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;activeTokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Security Breach: Session revoking for user &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cacheService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "NUCLEAR REVOKE" - Kill all sessions&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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;401&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Critical: Session compromised&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="c1"&gt;// --- ROTATION ---&lt;/span&gt;
    &lt;span class="nx"&gt;activeTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;activeTokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jti&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;newTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JwtService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateTokens&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decoded&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="nx"&gt;activeTokens&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;newTokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshJti&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cacheService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;activeTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;res&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="nx"&gt;newTokens&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="nf"&gt;next&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Scaffolding as an Architectural Mitigation
&lt;/h2&gt;

&lt;p&gt;I’ve automated this entire "Engineering Soul" into my open-source project, &lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;nodejs-quickstart-structure&lt;/a&gt;. It supports &lt;strong&gt;5,280 combinations&lt;/strong&gt; of architecture and databases, but the core security blueprint remains rock-solid in all of them.&lt;/p&gt;

&lt;p&gt;Don't settle for "Hello World" security. Automate the excellence.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Configurator&lt;/strong&gt;: &lt;a href="https://nodejs-quickstart-generator.netlify.app/" rel="noopener noreferrer"&gt;Nodejs Quickstart Generator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stats&lt;/strong&gt;: 4,000+ downloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video Demo&lt;/strong&gt;: &lt;a href="https://youtu.be/NiWs-r2Ml78" rel="noopener noreferrer"&gt;Advanced JWT Security: Refresh Token Rotation &amp;amp; Nuclear Revoke Demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Author&lt;/strong&gt;: Pau Dang (Senior SE).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://systemweakness.com/the-illusion-of-stateless-security-rethinking-jwt-revocation-at-scale-8426472c5022" rel="noopener noreferrer"&gt;The Illusion of Stateless Security: Rethinking JWT Revocation at Scale.&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I Tested 5 CI/CD Providers for 2,640 Node.js Projects. Here’s What I Learned.</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Thu, 09 Apr 2026 08:48:29 +0000</pubDate>
      <link>https://forem.com/paudang/i-tested-5-cicd-providers-for-2640-nodejs-projects-heres-what-i-learned-7c6</link>
      <guid>https://forem.com/paudang/i-tested-5-cicd-providers-for-2640-nodejs-projects-heres-what-i-learned-7c6</guid>
      <description>&lt;p&gt;Hi DEV community,&lt;/p&gt;

&lt;p&gt;Stop manually configuring your &lt;code&gt;.yaml&lt;/code&gt; files. After benchmarking 5 major CI providers across &lt;strong&gt;2,640 unique project scaffolding permutations&lt;/strong&gt;, I’ve gathered the ultimate list of "Gotchas," fixes, and rankings to help you pick the right one for your enterprise Node.js app.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 The Ultimate CI/CD Ranking (2026 Edition)
&lt;/h2&gt;

&lt;p&gt;Based on stability, ease of networking, and resource management across 2,640 repositories.&lt;/p&gt;

&lt;h3&gt;
  
  
  🥇 #1. GitHub Actions (The "It Just Works" Choice)
&lt;/h3&gt;

&lt;p&gt;If your code is on GitHub, this is the winner. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Developer Experience&lt;/strong&gt;: 10/10. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret Management&lt;/strong&gt;: Seamless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro-Tip&lt;/strong&gt;: Use &lt;code&gt;actions/cache&lt;/code&gt; aggressively. It cuts E2E startup time for databases and Kafka by nearly 40%.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🥈 #2. GitLab CI (The "Total Control" Engine)
&lt;/h3&gt;

&lt;p&gt;Powerful, but requires more networking knowledge than GitHub.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The "Wait-On" Trick&lt;/strong&gt;: When running healthchecks in GitLab CI, the hostname is almost always &lt;code&gt;docker&lt;/code&gt;. Standardize your testing URLs to &lt;code&gt;http://docker:3001&lt;/code&gt; to save hours of debugging.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🥉 #3. Jenkins (The "Swiss Army Knife" of Enterprises)
&lt;/h3&gt;

&lt;p&gt;The hardest to set up, but the most rewarding for complex, private infra.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The "Network Wall"&lt;/strong&gt;: Jenkins running in a container often can't see the app it just "upped" via Docker Compose. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix&lt;/strong&gt;: Use &lt;code&gt;host.docker.internal&lt;/code&gt; as your &lt;code&gt;WAIT_ON_HOST&lt;/code&gt;. It allows the Jenkins container to bridge out to the host-mapped ports.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🥉 #4. CircleCI (The Speed Demon)
&lt;/h3&gt;

&lt;p&gt;Fast builds, but stay alert on memory limits.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OOM Prevention&lt;/strong&gt;: Don't let Jest eat your RAM. We found that adding &lt;code&gt;--maxWorkers=2&lt;/code&gt; to your test script prevents the dreaded &lt;code&gt;SIGKILL&lt;/code&gt; on free tier runners.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🥉 #5. Bitbucket Pipelines (The Team Connector)
&lt;/h3&gt;

&lt;p&gt;Great for Atlassian-heavy workflows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Legacy Stability&lt;/strong&gt;: If your builds fail with weird gRPC transport errors, add &lt;code&gt;DOCKER_BUILDKIT: "0"&lt;/code&gt; to your environment. It fixes compatibility issues with older pipeline runners.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📊 The Comparison Matrix (Developer Experience focus)
&lt;/h2&gt;

&lt;p&gt;I tracked these metrics across &lt;strong&gt;2,640 builds&lt;/strong&gt; to see which one makes life easiest for us.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;GitHub Actions&lt;/th&gt;
&lt;th&gt;GitLab CI&lt;/th&gt;
&lt;th&gt;Jenkins&lt;/th&gt;
&lt;th&gt;CircleCI&lt;/th&gt;
&lt;th&gt;Bitbucket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ease of Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚡ Instant&lt;/td&gt;
&lt;td&gt;✅ Fast&lt;/td&gt;
&lt;td&gt;🏗️ Complex&lt;/td&gt;
&lt;td&gt;✅ Fast&lt;/td&gt;
&lt;td&gt;✅ Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;"Cool" Factor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Market Actions&lt;/td&gt;
&lt;td&gt;DinD Sidecars&lt;/td&gt;
&lt;td&gt;Plugins&lt;/td&gt;
&lt;td&gt;Orbs&lt;/td&gt;
&lt;td&gt;Pipes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debug Mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Good Logs&lt;/td&gt;
&lt;td&gt;Great Logs&lt;/td&gt;
&lt;td&gt;Hard to Read&lt;/td&gt;
&lt;td&gt;SSH Debug&lt;/td&gt;
&lt;td&gt;Good Logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free Tier Limit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2,000 mins&lt;/td&gt;
&lt;td&gt;400 mins&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;2,500 mins&lt;/td&gt;
&lt;td&gt;50 mins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Verdict&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Best for OSS&lt;/td&gt;
&lt;td&gt;Best for DevOps&lt;/td&gt;
&lt;td&gt;Best for Pros&lt;/td&gt;
&lt;td&gt;Best for Speed&lt;/td&gt;
&lt;td&gt;Best for Jira&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🛠️ How we validated this at scale
&lt;/h2&gt;

&lt;p&gt;We built an automation tool that generates entire project structures (Clean Architecture, Typescript, Kafka, MySQL, etc.) and &lt;strong&gt;injects the perfect CI configuration&lt;/strong&gt; for any of these 5 providers. &lt;/p&gt;

&lt;p&gt;We didn't just write these configs—we refined them through 2,640 builds. We solved the hard stuff so you don't have to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🐳 &lt;strong&gt;Docker-Out-Of-Docker&lt;/strong&gt;: Fixed the Jenkins permission and networking walls.&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Wait-On Master&lt;/strong&gt;: Standardized healthchecks across host and container bridges.&lt;/li&gt;
&lt;li&gt;📉 &lt;strong&gt;OOM Prevention&lt;/strong&gt;: Added &lt;code&gt;--maxWorkers=2&lt;/code&gt; to keep CircleCI from killing your build.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💡 The Verdict
&lt;/h2&gt;

&lt;p&gt;Pick the provider that matches your code hosting platform first. But if you have complex, multi-service testing needs, &lt;strong&gt;GitHub Actions&lt;/strong&gt; and &lt;strong&gt;GitLab CI&lt;/strong&gt; are currently pulling ahead in the "Enterprise Free" space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which one are you using? Drop a comment below with your biggest CI/CD headache!&lt;/strong&gt; 👇&lt;/p&gt;




&lt;h3&gt;
  
  
  🛠️ Try it yourself:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  🚀 &lt;strong&gt;Live Web UI&lt;/strong&gt;: &lt;a href="https://paudang.github.io/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;Node.js Quickstart Generator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  🎬 &lt;strong&gt;YouTube Guide&lt;/strong&gt;: &lt;a href="https://youtu.be/Cxbb54T0uo8" rel="noopener noreferrer"&gt;Watch the walkthrough&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  📂 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;paudang/nodejs-quickstart-structure&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Love automation? Check out our Node.js Quickstart generator to get these CI configs out-of-the-box.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cicd</category>
      <category>microservices</category>
      <category>automation</category>
    </item>
    <item>
      <title>Stop Wasting Time on Boilerplate: Real-world Kafka &amp; PostgreSQL Demo in 8 minutes</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Tue, 07 Apr 2026 13:00:00 +0000</pubDate>
      <link>https://forem.com/paudang/stop-wasting-time-on-boilerplate-real-world-kafka-postgresql-demo-24bp</link>
      <guid>https://forem.com/paudang/stop-wasting-time-on-boilerplate-real-world-kafka-postgresql-demo-24bp</guid>
      <description>&lt;p&gt;After releasing the v2.0.0 Web UI for &lt;strong&gt;Node.js Quickstart Generator&lt;/strong&gt;, the most common question was: &lt;em&gt;"How does it handle real-world complexity?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So, I decided to record a full, 8-minute implementation demo building a &lt;strong&gt;Payment Service&lt;/strong&gt; from scratch. &lt;/p&gt;

&lt;h3&gt;
  
  
  📺 Watch: UI to Production Code in 8 Minutes
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://youtu.be/PmmxJLloZ1Q" rel="noopener noreferrer"&gt;https://youtu.be/PmmxJLloZ1Q&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ The Tech Stack (Zero-Prompt Setup)
&lt;/h2&gt;

&lt;p&gt;Instead of answering 20 CLI prompts, I used our new &lt;a href="https://paudang.github.io/nodejs-quickstart-structure/" rel="noopener noreferrer"&gt;Web Configurator&lt;/a&gt; to generate this exact stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture&lt;/strong&gt;: Clean Architecture (Domain, UseCase, Infra)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching&lt;/strong&gt;: Redis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messaging&lt;/strong&gt;: Kafka&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: Snyk Verified&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🏗️ Clean Architecture in Action
&lt;/h2&gt;

&lt;p&gt;The video shows exactly how the folder structure reflects a production-grade system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src/domain&lt;/code&gt;&lt;/strong&gt;: Pure entities with no dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src/usecases&lt;/code&gt;&lt;/strong&gt;: Where the transaction logic lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src/infrastructure&lt;/code&gt;&lt;/strong&gt;: Concrete implementations for Postgres connections and Kafka producers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📡 Live Kafka Flow
&lt;/h2&gt;

&lt;p&gt;The "Wow" moment is at &lt;strong&gt;05:00&lt;/strong&gt; in the video. We trigger a REST API call that producers a Kafka event, which is then picked up by a separate consumer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero configuration required&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-mapped events&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready for microservices&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🛡️ Enterprise-Grade Security
&lt;/h2&gt;

&lt;p&gt;We don't skip security. I ran &lt;code&gt;npm run security:check&lt;/code&gt; in the video to show how &lt;strong&gt;Snyk&lt;/strong&gt; is integrated from the start. Clean code is nothing if it isn't secure.&lt;/p&gt;




&lt;h3&gt;
  
  
  💻 Try it yourself
&lt;/h3&gt;

&lt;p&gt;Want to generate the exact same project as in the video? Run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nodejs-quickstart-structure@latest init &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"payment-service-nodejs"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"TypeScript"&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"Clean Architecture"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"PostgreSQL"&lt;/span&gt; &lt;span class="nt"&gt;--db-name&lt;/span&gt; &lt;span class="s2"&gt;"payment-db"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Kafka"&lt;/span&gt; &lt;span class="nt"&gt;--caching&lt;/span&gt; &lt;span class="s2"&gt;"Redis"&lt;/span&gt; &lt;span class="nt"&gt;--ci-provider&lt;/span&gt; &lt;span class="s2"&gt;"GitHub Actions"&lt;/span&gt; &lt;span class="nt"&gt;--include-security&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out our &lt;strong&gt;&lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/strong&gt; and let's end boilerplate fatigue together! 🌟&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Introducing Node.js Quickstart Generator v2.0.0: Automated Clean Architecture with a Sleek New UI</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Mon, 06 Apr 2026 12:24:35 +0000</pubDate>
      <link>https://forem.com/paudang/boilerplate-scaffold-pro-nodejs-apps-with-kafka-graphql-redis-in-30s-1ji9</link>
      <guid>https://forem.com/paudang/boilerplate-scaffold-pro-nodejs-apps-with-kafka-graphql-redis-in-30s-1ji9</guid>
      <description>&lt;p&gt;Hi everyone,&lt;/p&gt;

&lt;p&gt;How many times have you set up a new Node.js microservice by copying a folder from your last project? We all do it out of necessity. But after releasing v1.1.0, the scale of "boilerplate fatigue" became clear: &lt;strong&gt;over 4,000 developers&lt;/strong&gt; joined the journey in just one month.&lt;/p&gt;

&lt;p&gt;That growth sparked a realization: we needed more than just a template. Previously known as &lt;strong&gt;nodejs-quickstart-structure&lt;/strong&gt;, we have officially evolved into a full-scale Generator in v2.0.0.&lt;/p&gt;

&lt;p&gt;I built this tool to help ourselves: &lt;strong&gt;Why can't our starting points be enterprise-ready by default?&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🎥 The Evolution: From CLI (v1.1.0) to Web UI (v2.0.0)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F91vv5s809rqmznkvny5r.gif" 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%2F91vv5s809rqmznkvny5r.gif" alt=" " width="400" height="294"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;How it started: A powerful CLI that 4,000 of you loved (v1.1.0).&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7l88uefp8knkqs3iljw.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%2Ff7l88uefp8knkqs3iljw.png" alt=" " width="800" height="622"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Cxbb54T0uo8" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=Cxbb54T0uo8&lt;/a&gt;&lt;br&gt;
&lt;em&gt;How it’s going: A full-blown Web UI with 1:1 structure parity (v2.0.0).&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing: Node.js Quickstart Generator 2.0.0
&lt;/h2&gt;

&lt;p&gt;I built this tool to solve my own frustration: &lt;strong&gt;Why can't I just have a professional-grade starting point in one command?&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ What’s in the Box?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flexible Architecture&lt;/strong&gt;: Toggle between &lt;strong&gt;MVC&lt;/strong&gt; and &lt;strong&gt;Clean Architecture&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern Stack&lt;/strong&gt;: Pick &lt;strong&gt;JavaScript&lt;/strong&gt; or &lt;strong&gt;TypeScript&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communication Protocols&lt;/strong&gt;: Built-in support for &lt;strong&gt;REST&lt;/strong&gt;, &lt;strong&gt;GraphQL (Apollo)&lt;/strong&gt;, and &lt;strong&gt;Kafka&lt;/strong&gt; (Event-driven).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching Layer&lt;/strong&gt;: Pre-configured &lt;strong&gt;Redis&lt;/strong&gt; or In-memory cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise Standards&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Multi-stage Dockerfiles.&lt;/li&gt;
&lt;li&gt;Snyk &amp;amp; SonarCloud security hardening.&lt;/li&gt;
&lt;li&gt;Global error handling (ApiError, NotFoundError, etc.).&lt;/li&gt;
&lt;li&gt;Jest &amp;amp; Supertest with &lt;strong&gt;80% unit test coverage&lt;/strong&gt; out of the box.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌐 Next-Gen Web UI Configurator (v2.0.0)
&lt;/h3&gt;

&lt;p&gt;Type no more! We have completely evolved the configuration experience in our new &lt;a href="https://nodejs-quickstart-generator.netlify.app/" rel="noopener noreferrer"&gt;Web UI Configurator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It includes a &lt;strong&gt;Real-time Folder Simulation&lt;/strong&gt;. As you toggle between MVC and Clean Architecture, you see exactly what the project will look like. Once you're happy, just copy the &lt;strong&gt;Zero-Prompt CLI command&lt;/strong&gt; and run it in your terminal. No more prompts!&lt;/p&gt;




&lt;h3&gt;
  
  
  🦾 AI-Native Foundation
&lt;/h3&gt;

&lt;p&gt;We designed v2.0.0 for the future. If you use &lt;strong&gt;Cursor&lt;/strong&gt; or other AI coding agents, our built-in &lt;code&gt;.cursorrules&lt;/code&gt; will make your AI assistant 10x smarter about your specific architecture. No more hallucinations about where controllers or repos belong!&lt;/p&gt;

&lt;h3&gt;
  
  
  🎯 Get Started NOW
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nodejs-quickstart-structure@latest init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. No global install required.&lt;/p&gt;




&lt;h3&gt;
  
  
  🗺️ Road to 10k Downloads
&lt;/h3&gt;

&lt;p&gt;We just crossed &lt;strong&gt;4,000+ downloads&lt;/strong&gt; and want to reach 10k by the end of the year. If you love open-source tools that actually save you time, please:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give us a ⭐ on &lt;strong&gt;&lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Share this with your team.&lt;/li&gt;
&lt;li&gt;Drop a comment below—what feature should we add next? (gRPC? NestJS-style?)&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>node</category>
      <category>opensource</category>
    </item>
    <item>
      <title>24 Hours of Chaos: Saving My Open Source Project from a Supply Chain Attack (plain-crypto-js)</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Wed, 01 Apr 2026 01:01:37 +0000</pubDate>
      <link>https://forem.com/paudang/24-hours-of-chaos-saving-my-open-source-project-from-a-supply-chain-attack-dm8</link>
      <guid>https://forem.com/paudang/24-hours-of-chaos-saving-my-open-source-project-from-a-supply-chain-attack-dm8</guid>
      <description>&lt;p&gt;Hello world, &lt;/p&gt;

&lt;p&gt;I'm a Senior SE. Today, I want to share a "battle-tested" experience that just happened to my open-source project: &lt;strong&gt;nodejs-quickstart-structure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This isn't just about code; it’s a lesson in &lt;strong&gt;Incident Response&lt;/strong&gt; when facing professional malware designed to hijack npm, GitHub, and sensitive developer credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Threat: Axios &amp;amp; plain-crypto-js
&lt;/h2&gt;

&lt;p&gt;While developing version &lt;strong&gt;v2.0.0&lt;/strong&gt;, I fell victim to a &lt;strong&gt;Typosquatting&lt;/strong&gt; attack. A malicious package or a "shell" dependency injected malware into my local environment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Suspect:&lt;/strong&gt; Linked to the &lt;code&gt;plain-crypto-js&lt;/code&gt; incident (a malware variant targeting devs using Axios).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Behavior:&lt;/strong&gt; It didn't just break my system; it silently exfiltrated:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser Cookies:&lt;/strong&gt; Hijacking active sessions for Gmail, GitHub, and LinkedIn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH Keys:&lt;/strong&gt; Gaining unauthorized access to push code to repositories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm Tokens:&lt;/strong&gt; Attempting to publish malicious releases under my name.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. 0h00: Detection &amp;amp; Containment
&lt;/h2&gt;

&lt;p&gt;Immediately after noticing suspicious logs and file modifications, I followed the "Security Textbook" or you can check at &lt;a href="https://snyk.io/blog/axios-npm-package-compromised-supply-chain-attack-delivers-cross-platform" rel="noopener noreferrer"&gt;Axios npm Package Compromised: Supply Chain Attack Delivers Cross-Platform RAT&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Deleted Local Repos:&lt;/strong&gt; Wiped the execution environment of the malware.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Revoked All Sessions:&lt;/strong&gt; Used a clean device (mobile) to remotely sign out of Google, GitHub, Microsoft, and LinkedIn.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Untrusted Devices:&lt;/strong&gt; Removed my current machine from the "Trusted Devices" list of all critical accounts.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. The Battle for npm (The Support Battle)
&lt;/h2&gt;

&lt;p&gt;The worst-case scenario: The attacker hijacked the session and invalidated my 2FA (my stored Recovery Codes returned &lt;strong&gt;Invalid&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;I immediately contacted &lt;strong&gt;npm Support&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ticket ID: 4223695&lt;/strong&gt; was created.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8zbkud61jjc6erurqg8.jpeg" 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%2Fg8zbkud61jjc6erurqg8.jpeg" alt=" " width="750" height="1028"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Strategy:&lt;/strong&gt; Providing proof of ownership through my GitHub account (which I still control) and the project's long-standing commit history.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. The Decision: Eradication (Wipe &amp;amp; Rebuild)
&lt;/h2&gt;

&lt;p&gt;As an Architect, I know that if an OS is compromised by a Rootkit/Trojan, no antivirus can guarantee a 100% clean state. The only solution: &lt;strong&gt;Wipe &amp;amp; Rebuild&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Method:&lt;/strong&gt; Reset PC &amp;gt; Remove everything &amp;gt; &lt;strong&gt;Cloud download Windows&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why Cloud Download?&lt;/strong&gt; To ensure a fresh installation image directly from Microsoft, avoiding any malware lurking in the local Recovery partition.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Lessons Learned for Developers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Dependency Vigilance:&lt;/strong&gt; Always double-check new packages, especially those with names similar to popular libraries.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;2FA is Not Enough:&lt;/strong&gt; Attackers can bypass 2FA via Session Hijacking. Always be ready to &lt;strong&gt;Revoke Sessions&lt;/strong&gt; remotely.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Offline Recovery Codes:&lt;/strong&gt; Don't just store them on your computer. Print them or use a decoupled password manager.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Incident Response Mindset:&lt;/strong&gt; When hacked, stay calm and follow: &lt;strong&gt;Containment -&amp;gt; Asset Protection -&amp;gt; Eradication -&amp;gt; Recovery.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Currently, I am in the process of restoring a "sterile" environment to finalize &lt;strong&gt;v2.0.0&lt;/strong&gt; for &lt;code&gt;nodejs-quickstart-structure&lt;/code&gt;. You can check out the &lt;strong&gt;v2.0.0&lt;/strong&gt; beta details here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://paudang.github.io/nodejs-quickstart-structure/guide/browser-generator.html" rel="noopener noreferrer"&gt;Next gen Web UI - Browser Generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project will return with a higher security standard. I hope this story helps fellow developers protect their "digital children"!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>security</category>
    </item>
    <item>
      <title>How I Built a Node.js Generator with 1,680+ Combinations, Clean Architecture or MVC (Kafka)</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Thu, 26 Mar 2026 10:01:34 +0000</pubDate>
      <link>https://forem.com/paudang/how-i-built-a-nodejs-generator-with-1680-combinations-clean-architecture-or-mvc-kafka-38j9</link>
      <guid>https://forem.com/paudang/how-i-built-a-nodejs-generator-with-1680-combinations-clean-architecture-or-mvc-kafka-38j9</guid>
      <description>&lt;p&gt;Hi all,&lt;/p&gt;

&lt;p&gt;We’ve all been there: starting a new Node.js microservice from scratch. You spend 4 hours setting up Express, another 2 hours arguing over folder structure (MVC or Clean Architecture?), 3 hours configuring Prisma or Mongoose, and half a day trying to get a Kafka KRaft container to talk to your local app. &lt;/p&gt;

&lt;p&gt;By the time you're ready to write your first line of business logic, you're already exhausted.&lt;/p&gt;

&lt;p&gt;I decided to solve this once and for all. I built &lt;strong&gt;&lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;nodejs-quickstart-structure&lt;/a&gt;&lt;/strong&gt;—a CLI that doesn't just give you a "hello world," but scaffolds a production-ready engine tailored to your exact needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: The "Boilerplate exhaustion"
&lt;/h2&gt;

&lt;p&gt;Most generators give you a fixed stack. But in the real world, requirements vary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"This needs to be a simple MVC app."&lt;/li&gt;
&lt;li&gt;"This needs to be a Clean Architecture domain-driven service."&lt;/li&gt;
&lt;li&gt;"We need GraphQL, not REST."&lt;/li&gt;
&lt;li&gt;"We need Kafka for event-driven messaging."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: 1,680+ Scenarios in One CLI
&lt;/h2&gt;

&lt;p&gt;The core of this tool is its flexibility. By combining different technologies, it supports &lt;strong&gt;over 1,680 unique project combinations&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Languages&lt;/strong&gt;: TypeScript (Recommended) / JavaScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architectures&lt;/strong&gt;: MVC or Clean Architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databases&lt;/strong&gt;: MySQL, PostgreSQL, MongoDB, or None.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communications&lt;/strong&gt;: REST APIs, GraphQL, or Kafka (Event-Driven).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching&lt;/strong&gt;: Redis or Memory Cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: Hardened with Helmet, HPP, and automated Snyk/SonarCloud configs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt;: Ready-to-use workflows for GitHub Actions, GitLab CI, or Jenkins.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why "Clean Architecture"?
&lt;/h2&gt;

&lt;p&gt;For many projects, MVC starts fast but becomes a nightmare to test. I’ve implemented a robust Clean Architecture template where:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Domain&lt;/strong&gt;: Pure business logic (Entities/Use Cases).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure&lt;/strong&gt;: Database, Messaging (Kafka Client), Caching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interfaces&lt;/strong&gt;: Controllers, Express Routes, GraphQL Resolvers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This separation ensures your business logic doesn't care if you're using MongoDB or PostgreSQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-Native: Coding in 2026
&lt;/h2&gt;

&lt;p&gt;I realized that if the folder structure is standard, AI tools (like &lt;strong&gt;Cursor&lt;/strong&gt;, &lt;strong&gt;ChatGPT&lt;/strong&gt;, or &lt;strong&gt;Gemini&lt;/strong&gt;) can understand the codebase 10x faster. &lt;br&gt;
Every project generated includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.cursorrules&lt;/strong&gt;: Pre-configured rules so Cursor knows exactly how to add new features following your chosen architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;prompts/&lt;/strong&gt;: A library of "Agent Skills" to help you generate Use Cases or Repositories without breaking patterns.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Try it out
&lt;/h2&gt;

&lt;p&gt;You don't even need to install it globally. Just run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nodejs-quickstart-structure init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI will walk you through the setup. Within seconds, you'll have a project with &lt;strong&gt;80%+ unit test coverage&lt;/strong&gt;, &lt;strong&gt;ESLint/Prettier&lt;/strong&gt; configured, and a &lt;strong&gt;Docker Compose&lt;/strong&gt; file that actually works.&lt;/p&gt;

&lt;p&gt;Checking the official docs:&lt;br&gt;
👉 &lt;a href="https://paudang.github.io/nodejs-quickstart-structure/" rel="noopener noreferrer"&gt;Official Documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check out the repo and give it a ⭐ if it helps your workflow:&lt;br&gt;
👉 &lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;GitHub: paudang/nodejs-quickstart-structure&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear your feedback! What's the one thing you always struggle with when starting a new Node project? &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>microservices</category>
      <category>node</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Writing Flaky Tests: The Ultimate Node.js Testing Strategy (Unit + E2E)</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Mon, 23 Mar 2026 13:00:00 +0000</pubDate>
      <link>https://forem.com/paudang/stop-writing-flaky-tests-the-ultimate-nodejs-testing-strategy-unit-e2e-4d25</link>
      <guid>https://forem.com/paudang/stop-writing-flaky-tests-the-ultimate-nodejs-testing-strategy-unit-e2e-4d25</guid>
      <description>&lt;p&gt;Hi DEV community,&lt;/p&gt;

&lt;p&gt;If you are a Backend Developer working with Node.js, you have likely experienced the dreaded scenario: &lt;strong&gt;"It passes on my machine, but randomly fails on the CI/CD pipeline."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This phenomenon is known as &lt;strong&gt;Flaky Tests&lt;/strong&gt;. It usually stems from writing End-to-End (E2E) tests that share database states across test files, or due to network and infrastructure services (Redis, Kafka) not being fully initialized when the test begins.&lt;/p&gt;

&lt;p&gt;Today, I’m going to share the complete testing architecture and lessons I learned while building the automation framework &lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;nodejs-quickstart-structure&lt;/a&gt;. We will solve the core problem: &lt;strong&gt;How to build blazing fast Unit Tests and completely deterministic E2E Tests.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Crisis in 90% of Projects
&lt;/h2&gt;

&lt;p&gt;Many teams implement testing "half-heartedly":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit Tests:&lt;/strong&gt; Dependencies like the Database or Redis aren't mocked, causing the tests to drag on because they wait for network I/O.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E2E Tests:&lt;/strong&gt; Developers use their local dev database to run E2E suites. Test A creates a User, Test B checks the total number of Users. If they run in a different order, the test suite explodes! Some even use conditionals in tests: &lt;em&gt;&lt;code&gt;if (statusCode == 404) expect(404) else expect(201)&lt;/code&gt;&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This is a massive Anti-pattern!&lt;/strong&gt; A test must strictly return exactly one predictable result based on static inputs.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The "Big Tech" Strategy: Draw a Hard Line
&lt;/h2&gt;

&lt;p&gt;To fix this, you must strictly delineate your boundaries:&lt;/p&gt;

&lt;h3&gt;
  
  
  Unit Tests (Fast &amp;amp; Isolated)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goal:&lt;/strong&gt; Verify Business Logic (Use cases, Services, Domain).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule:&lt;/strong&gt; MOCK EVERYTHING. No real database connections, no external APIs, no touching Redis or Kafka.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed:&lt;/strong&gt; Thousands of test cases should execute in less than 2 seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  E2E Tests (Black-box &amp;amp; Automated Infra)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goal:&lt;/strong&gt; Verify the entire request flow (Route -&amp;gt; Controller -&amp;gt; Usecase -&amp;gt; Repo -&amp;gt; DB -&amp;gt; Response).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule:&lt;/strong&gt; Use the &lt;strong&gt;REAL Database, Redis, and Kafka&lt;/strong&gt; (spinned up via isolated Docker Containers or Testcontainers).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Characteristics:&lt;/strong&gt; Data must be TRUNCATED/Teared down before or after each test suite to guarantee a "clean room" environment. It runs slower, but absolute correctness is guaranteed.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. The Recipe for Perfect E2E Tests
&lt;/h2&gt;

&lt;p&gt;To prevent E2E tests from interfering with the developer's local development environment, follow these steps and source demo &lt;a href="https://github.com/paudang/nodejs-service-redis-kafka" rel="noopener noreferrer"&gt;nodejs-service-redis-kafka&lt;/a&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Fully isolate &lt;code&gt;jest.config.js&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Do not share your Unit test configurations with E2E. Create a dedicated &lt;code&gt;jest.e2e.config.js&lt;/code&gt; with a higher &lt;code&gt;testTimeout&lt;/code&gt; (e.g., 30 seconds to allow databases to boot).&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="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&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="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;./jest.config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;testMatch&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="s1"&gt;&amp;lt;rootDir&amp;gt;/tests/e2e/**/*.test.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;testPathIgnorePatterns&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="s1"&gt;/node_modules/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;testTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="na"&gt;clearMocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Use Node.js Scripts to Manage Docker Lifecycle
&lt;/h3&gt;

&lt;p&gt;Instead of forcing developers to manually type &lt;code&gt;docker-compose up&lt;/code&gt; before running tests, write an automated orchestration script:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Assign a dedicated port (&lt;code&gt;PORT=3001&lt;/code&gt; instead of &lt;code&gt;3000&lt;/code&gt;) to avoid Dev Server collisions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;execSync('docker-compose up -d db redis kafka')&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;wait-on&lt;/code&gt; npm package to &lt;strong&gt;poll the healthcheck&lt;/strong&gt; until dependencies are fully green.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npm run test:e2e:run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Clean up gracefully: &lt;code&gt;execSync('docker-compose down')&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wait for dependencies to prevent "Flaky connections"&lt;/span&gt;
&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`npx wait-on http-get://127.0.0.1:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;TEST_PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/health -t 120000`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jest --config ./jest.e2e.config.js&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;h3&gt;
  
  
  Step 3: Fix Kafka's "read ECONNRESET" Locally
&lt;/h3&gt;

&lt;p&gt;The most common issue when testing Kafka in a local E2E run is that the tests run on the host network while Kafka is stuck in the Docker bridged network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Explicitly map the &lt;code&gt;PLAINTEXT_HOST&lt;/code&gt; listener to a dedicated port (e.g., &lt;code&gt;9093&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;kafka&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9093:9093"&lt;/span&gt; &lt;span class="c1"&gt;# Host mapping&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:9093&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,PLAINTEXT_HOST://:9093,CONTROLLER://:9094&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your &lt;code&gt;.env.test&lt;/code&gt;, simply point &lt;code&gt;KAFKA_BROKER=localhost:9093&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Write "Iron-clad" Assertions
&lt;/h3&gt;

&lt;p&gt;Eliminate loose assertions. Because your database is wiped clean (or relies on random seeds like &lt;code&gt;Date.now()&lt;/code&gt;), the output status must be absolutely rigid!&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should create a user successfully via REST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uniqueEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`test_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;@example.com`&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;response&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;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SERVER_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&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="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uniqueEmail&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Strictly expect a 201 Created&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&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;
  
  
  4. Conclusion
&lt;/h2&gt;

&lt;p&gt;Shifting to an isolated, automated Docker test strategy will cost you 1-2 days of initial setup infrastructure work. But in return, it brings &lt;strong&gt;absolute peace of mind&lt;/strong&gt; to the team as the codebase scales.&lt;/p&gt;

&lt;p&gt;If you find this setup process too tedious, you can simply grab the exact folder structures, Docker automation scripts, and Jest configurations that I’ve already pre-configured out of the box in my open-source CLI generator:&lt;br&gt;
👉 &lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;nodejs-quickstart-structure on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop a star (⭐) if you find it helpful! Happy coding, and here's to never seeing &lt;code&gt;Test Failed randomly&lt;/code&gt; in GitHub Actions again!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>Master Caching Patterns: A Clean Architecture Guide with AI-Native Tooling</title>
      <dc:creator>Pau Dang</dc:creator>
      <pubDate>Thu, 19 Mar 2026 15:02:06 +0000</pubDate>
      <link>https://forem.com/paudang/master-caching-patterns-a-clean-architecture-guide-with-ai-native-tooling-p7j</link>
      <guid>https://forem.com/paudang/master-caching-patterns-a-clean-architecture-guide-with-ai-native-tooling-p7j</guid>
      <description>&lt;p&gt;In high-performance systems, caching is the ultimate "lifesaver" for reducing database load and optimizing response times. However, choosing the right pattern (Cache-Aside, Write-Through, etc.) depends heavily on your specific use case. In this article, I’ll walk you through a hands-on demo project that showcases 5 essential caching patterns, built with &lt;strong&gt;Clean Architecture&lt;/strong&gt; and the power of an AI-Native tool: &lt;strong&gt;nodejs-quickstart-structure&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&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%2Fe02fvwulm3sdwag3w0oy.png" alt=" " width="640" height="640"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  🚀 Why Caching?
&lt;/h2&gt;

&lt;p&gt;When scaling applications, the database often becomes the bottleneck. Caching strategically places frequently accessed data in memory (like Redis) to bypass slow disk I/O. But not all caching is equal. To understand the nuances, I built a showcase service from scratch.&lt;/p&gt;

&lt;p&gt;To skip the boilerplate and focus on the patterns, I used &lt;a href="https://github.com/paudang/nodejs-quickstart-structure" rel="noopener noreferrer"&gt;nodejs-quickstart-structure&lt;/a&gt;, a CLI tool designed for AI-ready, structured development.&lt;/p&gt;




&lt;h2&gt;
  
  
  📂 5 Caching Patterns You Must Know
&lt;/h2&gt;

&lt;p&gt;Here is a breakdown of the 5 patterns implemented in this demo:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Cache-Aside (Lazy Loading)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc2v6nge0uosjc2ljij9r.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%2Fc2v6nge0uosjc2ljij9r.png" alt=" " width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose&lt;/strong&gt;: The application manages the cache. Data is only loaded into the cache when specifically requested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use&lt;/strong&gt;: Best for systems with &lt;strong&gt;Read &amp;gt;&amp;gt; Write&lt;/strong&gt; ratios (e.g., User Profiles).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;: Memory efficiency; cache only contains data that is actually needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Read-Through
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fix6mhsj30nhr3wi96hf3.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%2Fix6mhsj30nhr3wi96hf3.png" alt=" " width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose&lt;/strong&gt;: Offloads data-fetching logic to the Cache Provider. The app simply calls "Get" from the cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use&lt;/strong&gt;: To keep the business logic (Use Case) clean and decouple from the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;: Extremely clean business code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Write-Through
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F62niw25r5y4p49hscguz.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%2F62niw25r5y4p49hscguz.png" alt=" " width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose&lt;/strong&gt;: Data is written to both the Cache and the Database simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use&lt;/strong&gt;: When &lt;strong&gt;Strong Consistency&lt;/strong&gt; is required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;: Minimal data inconsistency risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Write-Around
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furat61ubxcq86ujstqzi.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%2Furat61ubxcq86ujstqzi.png" alt=" " width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose&lt;/strong&gt;: Data is written directly to the DB, and the corresponding cache entry is invalidated (deleted).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use&lt;/strong&gt;: When the written data might not be read again immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;: Prevents "polluting" the cache with data that won't be reused soon.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Write-Back (Write-Behind)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5po6m7z4ljybcv4xxnol.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%2F5po6m7z4ljybcv4xxnol.png" alt=" " width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose&lt;/strong&gt;: Data is written to the Cache first; the DB update happens asynchronously in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use&lt;/strong&gt;: For &lt;strong&gt;exceptionally high write performance&lt;/strong&gt; (e.g., Logging, Real-time Metrics).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pros&lt;/strong&gt;: Near-instant write response.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠️ Technical Implementation (Step-by-Step)
&lt;/h2&gt;

&lt;p&gt;Follow this roadmap to see how I build a production-ready demo using Clean Architecture:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Project Initialization
&lt;/h3&gt;

&lt;p&gt;Bootstrap the project using the AI-Native CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nodejs-quickstart-structure@latest init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Selection Guide:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project name: &lt;code&gt;nodejs-service-caching-pattern&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Architecture: &lt;code&gt;Clean Architecture&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Database: &lt;code&gt;MySQL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Caching: &lt;code&gt;Redis&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Communication: &lt;code&gt;REST APIs&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Defining Domain Interfaces
&lt;/h3&gt;

&lt;p&gt;Abstract the caching and repository logic to ensure true decoupling.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/src/domain/services/ICacheService.ts" rel="noopener noreferrer"&gt;ICacheService.ts&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ICacheService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;set&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="kr"&gt;string&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="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;del&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&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;getOrSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetcher&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;ttl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;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;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/src/domain/repositories/IUserRepository.ts" rel="noopener noreferrer"&gt;IUserRepository.ts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Infrastructure Layer
&lt;/h3&gt;

&lt;p&gt;Implementation for Redis and Sequelize (MySQL).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/src/infrastructure/caching/redisClient.ts" rel="noopener noreferrer"&gt;redisClient.ts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/src/infrastructure/repositories/UserRepository.ts" rel="noopener noreferrer"&gt;UserRepository.ts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Database Seeding: Generate 1,000 dummy users for demo purposes.
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/flyway/sql/V20260319__seed_1000_users.sql" rel="noopener noreferrer"&gt;V20260319__seed_1000_users.sql&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Implementing the Use Cases
&lt;/h3&gt;

&lt;p&gt;This is where the pattern logic lives.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-Through Example&lt;/strong&gt;:
&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/src/usecases/getUserInfoReadThrough.ts" rel="noopener noreferrer"&gt;getUserInfoReadThrough.ts&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The cache provider handles DB fetching upon miss&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&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;cacheService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOrSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;cacheKey&lt;/span&gt;&lt;span class="p"&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&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;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&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;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write-Back Example&lt;/strong&gt;:
&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/src/usecases/updateUserWriteBack.ts" rel="noopener noreferrer"&gt;updateUserWriteBack.ts&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Update cache immediately, DB updates in background&lt;/span&gt;
&lt;span class="k"&gt;await&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;cacheService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updatedUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asyncDatabaseUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updatedUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can check sample code at here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/src/usecases/createUserWriteThrough.ts" rel="noopener noreferrer"&gt;createUserWriteThrough.ts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/src/usecases/createUserWriteAround.ts" rel="noopener noreferrer"&gt;createUserWriteAround.ts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/src/usecases/updateUserWriteBack.ts" rel="noopener noreferrer"&gt;updateUserWriteBack.ts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. API &amp;amp; Documentation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/src/interfaces/controllers/cacheDemoController.ts" rel="noopener noreferrer"&gt;cacheDemoController.ts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/paudang/nodejs-service-caching-pattern/blob/main/src/config/swagger.yml" rel="noopener noreferrer"&gt;swagger.yml&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔍 How to Test &amp;amp; Verify
&lt;/h2&gt;

&lt;p&gt;Once deployed, you can trigger these endpoints to verify the internal logic:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Test Cache-Aside / Read-Through (Read Path)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First Call (Cold Cache)&lt;/strong&gt;: &lt;code&gt;curl -X GET http://localhost:3000/api/demo/cache-aside/1&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Internal:&lt;/em&gt; App checks Redis (Miss) -&amp;gt; Queries MySQL -&amp;gt; Updates Redis -&amp;gt; Returns.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Subsequent Call (Warm Cache)&lt;/strong&gt;: Repeat the command.

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Internal:&lt;/em&gt; App checks Redis (Hit) -&amp;gt; Returns immediately with zero DB overhead.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Test Write-Through (Sync Write)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger&lt;/strong&gt;: &lt;code&gt;curl -X POST http://localhost:3000/api/demo/write-through -H "Content-Type: application/json" -d '{"name": "Performance", "email": "perf@test.com"}'&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Internal:&lt;/em&gt; Service writes to MySQL AND Redis simultaneously.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Test Write-Around (Clean Write)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger&lt;/strong&gt;: &lt;code&gt;curl -X POST http://localhost:3000/api/demo/write-around -H "Content-Type: application/json" -d '{"name": "Guest", "email": "guest@test.com"}'&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Internal:&lt;/em&gt; Writes to MySQL, then calls &lt;code&gt;DEL&lt;/code&gt; to invalidate the Redis key. The next read will be a Miss to load the fresh data.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Test Write-Back (Async Write)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger&lt;/strong&gt;: &lt;code&gt;curl -X PUT http://localhost:3000/api/demo/write-back/1 -H "Content-Type: application/json" -d '{"name": "Fast Update"}'&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Internal:&lt;/em&gt; Updates Redis immediately (super fast response). The DB update happens after a short delay asynchronously.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  💡 Final Thoughts on AI-Native Tooling
&lt;/h2&gt;

&lt;p&gt;What makes &lt;code&gt;nodejs-quickstart-structure&lt;/code&gt; special is not just the speed of initialization, but its &lt;strong&gt;AI-Native&lt;/strong&gt; nature. With &lt;code&gt;.cursorrules&lt;/code&gt; and pre-built prompts, it ensures that your project maintains Clean Architecture and consistent coding standards automatically from development to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full Source Code&lt;/strong&gt;: &lt;a href="https://github.com/paudang/nodejs-service-caching-pattern" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I hope this guide helps you choose the right caching pattern for your next project&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>redis</category>
      <category>microservices</category>
    </item>
  </channel>
</rss>
