<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Rabbit Byte Club]]></title><description><![CDATA[Discover Rabbit Byte Club, the go-to community for software enthusiasts. Engage in discussions, access exclusive resources, and elevate your software development skills alongside fellow tech lovers.]]></description><link>https://rabbitbyte.club/</link><image><url>https://rabbitbyte.club/favicon.png</url><title>Rabbit Byte Club</title><link>https://rabbitbyte.club/</link></image><generator>Ghost 5.80</generator><lastBuildDate>Sat, 11 Apr 2026 20:59:27 GMT</lastBuildDate><atom:link href="https://rabbitbyte.club/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Restaurantele din România au început să te fure pe față: îți bagă bacșiș pe bon și apoi te mai întreabă dacă vrei să lași și altul]]></title><description><![CDATA[<p>Industria restaurantelor din Rom&#xE2;nia are o obsesie: s&#x103; copieze America.</p><p>Meniuri &#xEE;n englez&#x103;. &#x201E;Fine dining.&quot; Plating fancy. Bac&#x219;i&#x219; procentual.</p><p>Doar c&#x103;, evident, au copiat fix partea care le aduce mai mul&#x21B;i bani.</p><p>Restul&#x2026; op&#x21B;ional.</p>]]></description><link>https://rabbitbyte.club/restaurantele-din-romania-au-inceput-sa-te-fure-pe-fata-iti-baga-bacsis-pe-bon-si-apoi-te-mai-intreaba-daca-vrei-sa-lasi-si-altul/</link><guid isPermaLink="false">69c9227ab71d68f7ff4311a4</guid><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Sun, 29 Mar 2026 21:22:38 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2026/03/ChatGPT-Image-Mar-29--2026-at-11_56_40-PM.png" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2026/03/ChatGPT-Image-Mar-29--2026-at-11_56_40-PM.png" alt="Restaurantele din Rom&#xE2;nia au &#xEE;nceput s&#x103; te fure pe fa&#x21B;&#x103;: &#xEE;&#x21B;i bag&#x103; bac&#x219;i&#x219; pe bon &#x219;i apoi te mai &#xEE;ntreab&#x103; dac&#x103; vrei s&#x103; la&#x219;i &#x219;i altul"><p>Industria restaurantelor din Rom&#xE2;nia are o obsesie: s&#x103; copieze America.</p><p>Meniuri &#xEE;n englez&#x103;. &#x201E;Fine dining.&quot; Plating fancy. Bac&#x219;i&#x219; procentual.</p><p>Doar c&#x103;, evident, au copiat fix partea care le aduce mai mul&#x21B;i bani.</p><p>Restul&#x2026; op&#x21B;ional.</p><hr><h2 id="cum-func%C8%9Bioneaz%C4%83-%C3%AEn-sua-%C8%99tii-modelul-pe-care-%C3%AEl-tot-imit%C4%83">Cum func&#x21B;ioneaz&#x103; &#xEE;n SUA (&#x219;tii, modelul pe care &#xEE;l tot imit&#x103;)</h2><p>Hai s&#x103; ne uit&#x103;m pe un bon real, de la&#xA0;<strong>Route 66 American Kitchen</strong>, 79 Pearl Street, New York City. 24 august 2025.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://rabbitbyte.club/content/images/2026/03/PHOTO-2025-08-24-22-10-32.jpg" class="kg-image" alt="Restaurantele din Rom&#xE2;nia au &#xEE;nceput s&#x103; te fure pe fa&#x21B;&#x103;: &#xEE;&#x21B;i bag&#x103; bac&#x219;i&#x219; pe bon &#x219;i apoi te mai &#xEE;ntreab&#x103; dac&#x103; vrei s&#x103; la&#x219;i &#x219;i altul" loading="lazy" width="1200" height="1600" srcset="https://rabbitbyte.club/content/images/size/w600/2026/03/PHOTO-2025-08-24-22-10-32.jpg 600w, https://rabbitbyte.club/content/images/size/w1000/2026/03/PHOTO-2025-08-24-22-10-32.jpg 1000w, https://rabbitbyte.club/content/images/2026/03/PHOTO-2025-08-24-22-10-32.jpg 1200w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">bon-nyc</span></figcaption></figure><p>M&#xE2;ncare de bar (nachos, quesadilla, mac &amp; cheese, chicken wings), c&#xE2;teva beri &#x219;i sucuri.</p>
<!--kg-card-begin: html-->
<table class="min-w-full border-collapse text-sm leading-[1.7] whitespace-normal"><thead class="text-left"><tr><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold"></th><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">Valoare</th></tr></thead><tbody><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Subtotal (m&#xE2;ncare + b&#x103;uturi)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">$148.00</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Tax (8.875%)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">$13.17</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Tip (18%)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">$26.64</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>Total</strong></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>$187.81</strong></td></tr></tbody></table>
<!--kg-card-end: html-->
<p>Observ&#x103; trei chestii:</p><ol><li><strong>Taxa (sales tax) e separat&#x103;.</strong>&#xA0;O vezi clar, 8.875%, aplicat&#x103; de stat.</li><li><strong>Bac&#x219;i&#x219;ul se calculeaz&#x103; la subtotal.</strong>&#xA0;18% din $148 = $26.64. Nu din $148 + $13.17. Din subtotal. F&#x103;r&#x103; taxe.</li><li><strong>Tu alegi.</strong>&#xA0;Pe bon scrie clar c&#xE2;te procente dai. E treaba ta.</li></ol><p>Simplu. Curat. Nu te p&#x103;c&#x103;le&#x219;te nimeni.</p><hr><h2 id="cum-func%C8%9Bioneaz%C4%83-%C3%AEn-rom%C3%A2nia-varianta-%E2%80%9Eoptimizat%C4%83">Cum func&#x21B;ioneaz&#x103; &#xEE;n Rom&#xE2;nia (varianta &#x201E;optimizat&#x103;&quot;)</h2><p>Acum hai s&#x103; vedem ce se &#xEE;nt&#xE2;mpl&#x103; pe un bon din Bucure&#x219;ti. Restaurant, seara.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://rabbitbyte.club/content/images/2026/03/d54fcf1a-ec90-4be1-ab4f-333912c1b9e8.jpeg" class="kg-image" alt="Restaurantele din Rom&#xE2;nia au &#xEE;nceput s&#x103; te fure pe fa&#x21B;&#x103;: &#xEE;&#x21B;i bag&#x103; bac&#x219;i&#x219; pe bon &#x219;i apoi te mai &#xEE;ntreab&#x103; dac&#x103; vrei s&#x103; la&#x219;i &#x219;i altul" loading="lazy" width="916" height="1864" srcset="https://rabbitbyte.club/content/images/size/w600/2026/03/d54fcf1a-ec90-4be1-ab4f-333912c1b9e8.jpeg 600w, https://rabbitbyte.club/content/images/2026/03/d54fcf1a-ec90-4be1-ab4f-333912c1b9e8.jpeg 916w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">bon-bucuresti</span></figcaption></figure>
<!--kg-card-begin: html-->
<table class="min-w-full border-collapse text-sm leading-[1.7] whitespace-normal"><thead class="text-left"><tr><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">Produs</th><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">Cant.</th><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">Pre&#x21B;</th><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">Sum&#x103;</th></tr></thead><tbody><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Ap&#x103; 750ml</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">5</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">24.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">120.00</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Cappuccino</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">1</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">18.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">18.00</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Recas Sole Chardonnay 150ml</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">1</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">55.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">55.00</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Ceai</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">2</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">18.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">36.00</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Pizza Suprema</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">1</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">69.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">69.00</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Pollo Cafe de Paris</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">1</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">62.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">62.00</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Cartofi Garibaldi</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">1</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">29.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">29.00</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>Tips</strong></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>39.00</strong></td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>Total</strong></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>428.00</strong></td></tr></tbody></table>
<!--kg-card-end: html-->
<p>&#x218;i apoi, jos pe bon:</p><blockquote><strong>&#x201E;Tipsul nu este inclus. / Tips not included.&quot;</strong></blockquote><p>&#x218;i sugestii: 10% &#x2192; 42.80 lei / 15% &#x2192; 64.20 lei / 20% &#x2192; 85.60 lei.</p><p>Aha.</p><hr><h3 id="problema-1-%C3%AE%C8%9Bi-iau-bac%C8%99i%C8%99-f%C4%83r%C4%83-s%C4%83-te-%C3%AEntrebe">Problema #1: &#xCE;&#x21B;i iau bac&#x219;i&#x219; f&#x103;r&#x103; s&#x103; te &#xEE;ntrebe</h3><p>Pe bon, la &#x201E;produse&quot;, apare o linie interesant&#x103;:&#xA0;<strong>Tips &#x2014; 39.00 lei.</strong></p><p>Adic&#x103; 10% din nota de m&#xE2;ncare. Pus acolo, &#xEE;ntre pizza &#x219;i total, ca &#x219;i cum ar fi un produs. Un fel de &#x201E;Por&#x21B;ie Bac&#x219;i&#x219;, 1 buc, 39 lei.&quot;</p><p>&#xCE;n SUA: tu alegi dac&#x103; dai.</p><p>&#xCE;n Rom&#xE2;nia: &#x21B;i se pune direct pe bon.</p><p>Dar stai, c&#x103; asta e doar &#xEE;nceputul.</p><hr><h3 id="problema-2-%C3%AE%C8%9Bi-sugereaz%C4%83-s%C4%83-mai-dai-%C3%AEnc%C4%83-o-dat%C4%83">Problema #2: &#xCE;&#x21B;i sugereaz&#x103; s&#x103; mai dai&#xA0;<em>&#xEE;nc&#x103; o dat&#x103;</em></h3><p>Ai deja 39 lei bac&#x219;i&#x219;. Pe care nu l-ai cerut.</p><p>Dar jos pe bon prime&#x219;ti, frumos, cu bilinguism:</p><p><em>&#x201E;V&#x103; oferim urm&#x103;toarele sugestii pentru calculul tipsului.&quot;</em></p><p>10%, 15%, 20%.</p><p>Adic&#x103;: &#x201E;&#x219;tim c&#x103; &#x21B;i-am luat deja, dar poate mai scoatem ni&#x219;te bani din tine.&quot;</p><hr><h3 id="problema-3-%C8%99i-cea-mai-jegoas%C4%83-calculeaz%C4%83-bac%C8%99i%C8%99ul-la-suma-cu-tva">Problema #3 (&#x219;i cea mai jegoas&#x103;): Calculeaz&#x103; bac&#x219;i&#x219;ul la suma cu TVA</h3><p>Aici e marea &#x21B;eap&#x103; pe care nu o vede aproape nimeni.</p><p>&#xCE;n Rom&#xE2;nia, pre&#x21B;urile de pe meniu&#xA0;<strong>includ TVA</strong>. &#x102;sta e un impozit pe care restaurantul &#xEE;l colecteaz&#x103; &#x219;i &#xEE;l d&#x103; statului. Nu e al lor. E al ANAF-ului.</p><p>Hai s&#x103; desfacem bonul:</p>
<!--kg-card-begin: html-->
<table class="min-w-full border-collapse text-sm leading-[1.7] whitespace-normal"><thead class="text-left"><tr><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">Produs</th><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">Pre&#x21B; cu TVA</th><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">Cot&#x103; TVA</th><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">Pre&#x21B; f&#x103;r&#x103; TVA</th><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">TVA</th></tr></thead><tbody><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Ap&#x103; (5&#xD7;24)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">120.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">11%</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">108.11</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">11.89</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Cappuccino</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">18.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">21%</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">14.88</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">3.12</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Vin (Chardonnay)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">55.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">21%</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">45.45</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">9.55</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Ceai (2&#xD7;18)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">36.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">11%</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">32.43</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">3.57</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Pizza Suprema</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">69.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">11%</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">62.16</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">6.84</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Pollo Cafe de Paris</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">62.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">11%</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">55.86</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">6.14</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Cartofi Garibaldi</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">29.00</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">11%</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">26.13</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">2.87</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>Total</strong></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>389.00</strong></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>345.02</strong></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>43.98</strong></td></tr></tbody></table>
<!--kg-card-end: html-->
<p>Apa, ceaiul, pizza, pui, cartofi =&#xA0;<strong>TVA 11%</strong>&#xA0;(m&#xE2;ncare &#x219;i b&#x103;uturi nealcoolice). Cappuccino, vin =&#xA0;<strong>TVA 21%</strong>&#xA0;(cafea + alcool).</p><p>Restaurantul prime&#x219;te efectiv:&#xA0;<strong>~345 lei.</strong></p><p>Statul prime&#x219;te:&#xA0;<strong>~44 lei.</strong></p><p>Dar bac&#x219;i&#x219;ul de pe bon? Se calculeaz&#x103; la&#xA0;<strong>389 lei</strong>. La suma cu TVA.</p><p>Adic&#x103; tu dai bac&#x219;i&#x219; &#x219;i pe taxele statului. Ceea ce e ca &#x219;i cum &#xEE;n New York ai da tip la $148 + $13.17 &#xEE;n loc de $148.</p><p>Doar c&#x103; &#xEE;n New York&#x2026;&#xA0;<strong>nu face nimeni asta.</strong></p><hr><h2 id="hai-s%C4%83-punem-cifrele-cap-la-cap">Hai s&#x103; punem cifrele cap la cap</h2><h3 id="bonul-din-new-york">Bonul din New York</h3>
<!--kg-card-begin: html-->
<table class="min-w-full border-collapse text-sm leading-[1.7] whitespace-normal"><thead class="text-left"><tr><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold"></th><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">Sum&#x103;</th></tr></thead><tbody><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">M&#xE2;ncare + b&#x103;uturi (subtotal)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">$148.00</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Sales tax (8.875%)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">$13.17</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Tip (18% din subtotal)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">$26.64</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>Total pl&#x103;tit</strong></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>$187.81</strong></td></tr></tbody></table>
<!--kg-card-end: html-->
<p>Bac&#x219;i&#x219; real:&#xA0;<strong>18% din ce prime&#x219;te restaurantul.</strong>&#xA0;Fix at&#xE2;t.</p><hr><h3 id="bonul-din-bucure%C8%99ti-scenariul-%E2%80%9Esugestie-acceptat%C4%83">Bonul din Bucure&#x219;ti (scenariul &#x201E;sugestie acceptat&#x103;&quot;)</h3><p>S&#x103; zicem c&#x103; faci ce scrie pe bon: ai 39 lei deja + mai la&#x219;i 10%.</p>
<!--kg-card-begin: html-->
<table class="min-w-full border-collapse text-sm leading-[1.7] whitespace-normal"><thead class="text-left"><tr><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold"></th><th scope="col" class="text-text-100 border-b-0.5 border-border-300/60 py-2 pr-4 align-top font-bold">Sum&#x103;</th></tr></thead><tbody><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Produse (cu TVA inclus)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">389.00 lei</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Tips pe bon (automat)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">39.00 lei</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">Tips suplimentar (10% &#x201E;sugerat&quot;)</td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top">42.80 lei</td></tr><tr><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>Total pl&#x103;tit</strong></td><td class="border-b-0.5 border-border-300/30 py-2 pr-4 align-top"><strong>470.80 lei</strong></td></tr></tbody></table>
<!--kg-card-end: html-->
<p>Bac&#x219;i&#x219; total:&#xA0;<strong>81.80 lei.</strong></p><p>Dar restaurantul prime&#x219;te, din m&#xE2;ncare, doar&#xA0;<strong>~345 lei</strong>&#xA0;(restul e TVA).</p><p>Deci bac&#x219;i&#x219;ul real e:&#xA0;<strong>81.80 / 345 = ~23.7%</strong></p><p><strong>Felicit&#x103;ri.</strong>&#xA0;Ai ajuns s&#x103; dai mai mult dec&#xE2;t &#xEE;n New York.</p><p>Dar nu pentru c&#x103; serviciul e mai bun. Nu pentru c&#x103; experien&#x21B;a e premium.</p><p>Ci pentru c&#x103;:</p><ul><li>&#x21B;i-au pus deja bac&#x219;i&#x219;</li><li>l-au calculat pe suma cu TVA</li><li>&#x219;i au sperat c&#x103; nu e&#x219;ti atent</li></ul><hr><h2 id="asta-nu-e-%E2%80%9Ecultur%C4%83-de-tipping">Asta nu e &#x201E;cultur&#x103; de tipping&quot;</h2><p>E o combina&#x21B;ie de:</p><p><strong>Lene.</strong>&#xA0;Pentru c&#x103; nu vor s&#x103; explice nimic.</p><p><strong>L&#x103;comie.</strong>&#xA0;Pentru c&#x103; nu le ajunge o dat&#x103;.</p><p><strong>&#x218;i pu&#x21B;in dispre&#x21B; fa&#x21B;&#x103; de client.</strong>&#xA0;Pentru c&#x103; mizeaz&#x103; pe faptul c&#x103; nu te ui&#x21B;i pe bon.</p><p>Dac&#x103; ar fi fost transparent, ar fi scris:</p><blockquote>&#x201E;10% bac&#x219;i&#x219; inclus. Nu mai l&#x103;sa&#x21B;i altul.&quot;</blockquote><hr><h2 id="industria-restaurantelor-din-rom%C3%A2nia-pe-scurt">Industria restaurantelor din Rom&#xE2;nia, pe scurt</h2><p><strong>Vrea:</strong></p><ul><li>bac&#x219;i&#x219; ca &#xEE;n America</li><li>pre&#x21B;uri ca &#xEE;n Vest</li></ul><p><strong>Dar ofer&#x103;:</strong></p><ul><li>transparen&#x21B;&#x103; zero</li><li>reguli inventate</li><li>&#x219;i calcule f&#x103;cute &#x201E;&#xEE;n favoarea casei&quot;</li></ul><hr><h2 id="ce-faci-data-viitoare">Ce faci data viitoare</h2><p>Regula e simpl&#x103;:</p><p><strong>1.</strong>&#xA0;Vezi dac&#x103; exist&#x103; &#x201E;tips&quot; pe bon.</p><p><strong>2.</strong>&#xA0;Dac&#x103; exist&#x103; &#x2192;&#xA0;<strong>nu mai la&#x219;i nimic.</strong>&#xA0;Ai dat deja.</p><p><strong>3.</strong>&#xA0;Dac&#x103; vrei s&#x103; fii corect p&#xE2;n&#x103; la cap&#x103;t &#x2192; calculezi bac&#x219;i&#x219;ul la&#xA0;<strong>subtotal f&#x103;r&#x103; TVA</strong>, nu la suma final&#x103;.</p><p>Exact ca &#xEE;n SUA. &#x218;tii, modelul pe care se tot pozeaz&#x103;.</p><hr><h2 id="concluzie">Concluzie</h2><p>Rom&#xE2;nia nu are problem&#x103; cu bac&#x219;i&#x219;ul.</p><p>Are problem&#x103; cu faptul c&#x103; vrea s&#x103;-l &#xEE;ncaseze de dou&#x103; ori &#x219;i s&#x103;-l calculeze gre&#x219;it prima dat&#x103;.</p><p>&#x218;i apropo, acel &#x201E;10% standard&quot; pe care toat&#x103; lumea &#xEE;l consider&#x103; rezonabil? E calculat la suma cu TVA. Adic&#x103; e de fapt&#xA0;<strong>11.3%</strong>&#xA0;din ce prime&#x219;te restaurantul. &#xCE;n America, 10% &#xEE;nseamn&#x103; 10%. &#xCE;n Rom&#xE2;nia, 10% &#xEE;nseamn&#x103; 11.3%. C&#x103; na, &#x219;i la procente facem &#x201E;optimizare&quot;.</p><p>&#x218;i c&#xE2;t timp lumea pl&#x103;te&#x219;te f&#x103;r&#x103; s&#x103; se uite, o s&#x103; continue.</p><p>Pentru c&#x103;, aparent, cel mai profitabil lucru &#xEE;n HoReCa nu e m&#xE2;ncarea.</p><p><strong>E matematica creativ&#x103; de pe bon.</strong></p>]]></content:encoded></item><item><title><![CDATA[Optimizări fiscale 2025: Cum îți deschizi o firmă în Moldova IT Park și plătești doar 7% taxe]]></title><description><![CDATA[<p>Dac&#x103; faci&#xA0;<strong>dezvoltare software</strong>,&#xA0;<strong>design computerizat</strong>,&#xA0;<strong>editare video</strong>,&#xA0;<strong>gestionezi platforme online</strong>&#xA0;sau conduci&#xA0;<strong>call centere/dispecerate</strong> &#x219;i te-ai s&#x103;turat de b&#x103;taia de joc din Rom&#xE2;nia, probabil te-ai &#xEE;ntrebat deja:&#xA0;<em>exist&#x103; o variant&#x103;</em></p>]]></description><link>https://rabbitbyte.club/optimizari-fiscale-2025-cum-iti-deschizi-o-firma-in-moldova-it-park-si-platesti-doar-7-taxe/</link><guid isPermaLink="false">68a0b8ddb71d68f7ff431125</guid><category><![CDATA[ro]]></category><category><![CDATA[optimizare-fiscala]]></category><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Sat, 16 Aug 2025 17:43:33 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2025/08/ChatGPT-Image-Aug-16--2025--08_43_08-PM.png" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2025/08/ChatGPT-Image-Aug-16--2025--08_43_08-PM.png" alt="Optimiz&#x103;ri fiscale 2025: Cum &#xEE;&#x21B;i deschizi o firm&#x103; &#xEE;n Moldova IT Park &#x219;i pl&#x103;te&#x219;ti doar 7% taxe"><p>Dac&#x103; faci&#xA0;<strong>dezvoltare software</strong>,&#xA0;<strong>design computerizat</strong>,&#xA0;<strong>editare video</strong>,&#xA0;<strong>gestionezi platforme online</strong>&#xA0;sau conduci&#xA0;<strong>call centere/dispecerate</strong> &#x219;i te-ai s&#x103;turat de b&#x103;taia de joc din Rom&#xE2;nia, probabil te-ai &#xEE;ntrebat deja:&#xA0;<em>exist&#x103; o variant&#x103; mai simpl&#x103; &#x219;i mai ieftin&#x103; s&#x103; rulezi o firm&#x103; &#xEE;n mod legal, dar f&#x103;r&#x103; s&#x103; te sufoce taxele?</em></p><p>R&#x103;spunsul scurt: da. Se nume&#x219;te&#xA0;<strong>Moldova IT Park</strong>&#xA0;&#x219;i vine cu un regim fiscal unic de&#xA0;<strong>7% pe venituri</strong>. F&#x103;r&#x103; scheme dubioase, f&#x103;r&#x103; complica&#x21B;ii. &#xCE;n articolul &#x103;sta &#xEE;&#x21B;i explic concret cum func&#x21B;ioneaz&#x103; &#x219;i cum te poate ajuta s&#x103; p&#x103;strezi mai mul&#x21B;i bani &#xEE;n buzunarul t&#x103;u.</p><hr><h2 id="ce-e-moldova-it-park">Ce e Moldova IT Park?</h2><p>Republica Moldova a creat un cadru special pentru firmele din IT &#x219;i activit&#x103;&#x21B;i conexe, unde:</p><ul><li>Pl&#x103;te&#x219;ti&#xA0;<strong>7% impozit pe cifra de afaceri</strong>.</li><li>Nu mai ai alte taxe ascunse (profit, salarii, contribu&#x21B;ii complicate).</li><li>Po&#x21B;i s&#x103;-&#x21B;i sco&#x21B;i banii ca salarii cu&#xA0;<strong>0% taxe</strong>.</li><li><strong>IT Park are contract garantat p&#xE2;n&#x103; cel pu&#x21B;in &#xEE;n 2038</strong>&#xA0;&#x2192; stabilitate pe termen lung.</li></ul><hr><h2 id="flexibilitate-pe-salarii">Flexibilitate pe salarii</h2><ul><li>Po&#x21B;i seta salariile orar sau lunar, dup&#x103; cum vrei.</li><li>Plata orara &#xEE;&#x21B;i da libertatea s&#x103;-&#x21B;i pl&#x103;te&#x219;ti luna asta&#xA0;<strong>5.000 lei</strong>, luna viitoare&#xA0;<strong>8.000 lei</strong>, &#xEE;n func&#x21B;ie de cashflow.</li></ul><hr><h2 id="conversia-valutar%C4%83">Conversia valutar&#x103;</h2><ul><li>Toate salariile se pl&#x103;tesc obligatoriu &#xEE;n&#xA0;<strong>lei moldovene&#x219;ti (MDL)</strong>.</li><li>Dac&#x103; vrei s&#x103; converte&#x219;ti &#xEE;n euro, dolari sau lei rom&#xE2;ne&#x219;ti &#x2192; exist&#x103; un&#xA0;<strong>comision de 1%</strong>.</li></ul><hr><h2 id="cum-func%C8%9Bioneaz%C4%83-taxele-exemplu-concret">Cum func&#x21B;ioneaz&#x103; taxele (exemplu concret)</h2><ul><li>Emi&#x21B;i o factur&#x103; de&#xA0;<strong>100.000 &#x20AC;</strong>&#xA0;c&#x103;tre un client din SUA sau Rom&#xE2;nia. (TVA = 0%. &#xCE;n Republica Moldova TVA-ul standard e 20%, dar se aplic&#x103; doar pentru tranzac&#x21B;ii interne, adic&#x103; atunci c&#xE2;nd vinzi c&#x103;tre firme din Moldova)</li><li>Pl&#x103;te&#x219;ti&#xA0;<strong>7.000 &#x20AC; impozit</strong>&#xA0;(7%).</li><li>R&#x103;m&#xE2;i cu&#xA0;<strong>93.000 &#x20AC;</strong>.</li><li>&#xCE;i sco&#x21B;i ca salarii</li><li>Cheltui direct cu cardul sau retragi la ATM banii</li></ul><hr><h2 id="nu-trebuie-s%C4%83-%C8%9Bi-schimbi-reziden%C8%9Ba-fiscal%C4%83">Nu trebuie s&#x103;-&#x21B;i schimbi reziden&#x21B;a fiscal&#x103;</h2><p>Un punct extrem de important &#x219;i pe care mul&#x21B;i &#xEE;l rateaz&#x103;:&#xA0;<strong>nu trebuie s&#x103;-&#x21B;i schimbi reziden&#x21B;a fiscal&#x103;</strong>&#xA0;ca s&#x103; beneficiezi de avantajele IT Park Moldova.</p><ul><li>Nu e nevoie s&#x103;-&#x21B;i mu&#x21B;i domiciliul sau reziden&#x21B;a &#xEE;n Republica Moldova.</li><li>Nu e nevoie s&#x103;-&#x21B;i sco&#x21B;i dividendele &#xEE;n Rom&#xE2;nia (care sunt supra-impozitate).</li><li>Totul se rezolv&#x103; prin&#xA0;<strong>salariu</strong>. </li></ul><hr><h2 id="salariul-brut-salariu-net">Salariul brut = salariu net</h2><p>Aici e magia sistemului:</p><ul><li><strong>Salariul brut este egal cu salariul net</strong>.</li><li>Nu exist&#x103; diferen&#x21B;a absurd&#x103; pe care o &#x219;tim din Rom&#xE2;nia, unde statul ia aproape jum&#x103;tate din salariul brut sub form&#x103; de taxe &#x219;i contribu&#x21B;ii.</li><li>&#xCE;n Moldova IT Park: ce &#xEE;&#x21B;i setezi ca salariu brut &#x2192; exact aia prime&#x219;ti pe card.</li></ul><p>&#x218;i asta nu e doar pentru tine, ca fondator:</p><ul><li>To&#x21B;i partenerii sau angaja&#x21B;ii t&#x103;i beneficiaz&#x103; de acela&#x219;i principiu.</li><li>Exemplu: dac&#x103; acum ai un angajat cu&#xA0;<strong>5.850 lei net &#xEE;n Rom&#xE2;nia</strong>, &#xEE;n IT Park &#xEE;i po&#x21B;i oferi&#xA0;<strong>10.225 lei</strong>&#xA0;f&#x103;r&#x103; s&#x103; te coste suplimentar.</li><li>Practic, &#xEE;i cre&#x219;ti salariul automat pentru c&#x103; statul nu mai mu&#x219;c&#x103; jum&#x103;tate din bani.</li></ul><hr><h2 id="doar-scenarii-win%E2%80%93win">Doar scenarii win&#x2013;win</h2><ul><li>Tu, ca antreprenor, r&#x103;m&#xE2;i cu mai mult profit real.</li><li>Angaja&#x21B;ii t&#x103;i au salarii mai mari, direct &#xEE;n buzunar, f&#x103;r&#x103; taxe absurde.</li><li>Totul e&#xA0;<strong>100% legal &#x219;i transparent</strong>.</li><li>Scapi de statul bananier din Rom&#xE2;nia, care doar &#xEE;&#x21B;i pune piedici &#x219;i &#xEE;&#x21B;i fur&#x103; din munca ta, &#x219;i &#xEE;&#x21B;i mu&#x21B;i businessul &#xEE;ntr-un cadru s&#x103;n&#x103;tos &#x219;i predictibil.</li></ul><hr><h2 id="procesul-de-deschidere">Procesul de deschidere</h2><ul><li>vii o singur&#x103; zi &#xEE;n Moldova &#x2192; semn&#x103;tur&#x103; digital&#x103; + cont bancar,</li><li>restul e 100% remote: banking online, contabilitate externalizat&#x103;, suport zilnic.</li></ul><hr><h2 id="costuri">Costuri</h2><ul><li><strong>1.500 &#x20AC; one-time</strong>&#xA0;&#x2013; deschiderea firmei (acte, IT Park, semn&#x103;tur&#x103; digital&#x103;).</li><li><strong>280 &#x20AC;/lun&#x103;</strong>&#xA0;&#x2013; mentenan&#x21B;&#x103; (contabilitate, audit, sediu legal, consultan&#x21B;&#x103; zilnic&#x103;). </li><li><strong>7% lunar</strong>&#xA0;pe veniturile facturate.</li><li><strong>1% comision de conversie valutar&#x103;</strong>&#xA0;(pentru toate pl&#x103;&#x21B;ile pe care le faci la comercian&#x21B;ii rom&#xE2;ni sau retrageri ATM).</li></ul><hr><h2 id="cine-te-poate-ajuta-bizonaire">Cine te poate ajuta: Bizonaire</h2><p>Dac&#x103; vrei s&#x103; faci pasul &#x103;sta simplu &#x219;i f&#x103;r&#x103; b&#x103;t&#x103;i de cap, &#xEE;&#x21B;i recomand pe&#xA0;<a href="https://md.linkedin.com/in/tudormardari?ref=rabbitbyte.club"><strong>Tudor Mardari</strong></a>, co-fondator&#xA0;<a href="https://bizonaire.com/?ref=rabbitbyte.club"><strong>Bizonaire</strong></a>.</p><ul><li><a href="https://bizonaire.com/?ref=rabbitbyte.club">Bizonaire</a> e o companie de consultan&#x21B;&#x103; juridic&#x103; &#x219;i fiscal&#x103; din Republica Moldova.</li><li>Sunt specializa&#x21B;i &#xEE;n &#xEE;nfiin&#x21B;area de firme &#xEE;n Moldova IT Park.</li><li>Se ocup&#x103; de: acte, &#xEE;nregistrare, semn&#x103;tura digital&#x103;, contabilitate, birou virtual, suport fiscal.</li><li>Practic, &#xEE;&#x21B;i dau o solu&#x21B;ie complet&#x103; &#x201E;la cheie&#x201D; &#x2192; tu doar vii o zi &#xEE;n Chi&#x219;in&#x103;u, iar restul se rezolv&#x103; de la distan&#x21B;&#x103;.</li></ul><p>Am v&#x103;zut ce fac pentru antreprenorii rom&#xE2;ni &#x219;i mi se pare cea mai bun&#x103; op&#x21B;iune dac&#x103; vrei s&#x103; profi&#x21B;i de IT Park.</p><hr><h2 id="concluzie-%C8%99i-rezumat-rapid">Concluzie &#x219;i rezumat rapid</h2><p>Dac&#x103; vrei s&#x103; optimizezi&#xA0;<strong>legal &#x219;i transparent</strong>&#xA0;taxele pentru o activitate &#xEE;n IT/digital:</p><ul><li><strong>7%</strong>&#xA0;&#x2013; tax&#x103; unic&#x103; pe venituri.</li><li><strong>1%</strong>&#xA0;&#x2013; comision conversie valutar&#x103; (MDL &#x2192; EUR/USD/RON).</li><li><strong>1.500 &#x20AC;</strong>&#xA0;&#x2013; cost ini&#x21B;ial (achitat o singur&#x103; dat&#x103;).</li><li><strong>Zbor Chi&#x219;in&#x103;u &#x2194; Rom&#xE2;nia</strong>&#xA0;&#x2013; dus-&#xEE;ntors &#xEE;ntr-o zi (sau un city break vineri&#x2013;duminic&#x103;, o singur&#x103; dat&#x103;).</li><li><strong>280 &#x20AC;/lun&#x103;</strong>&#xA0;&#x2013; contabilitate, consultan&#x21B;&#x103; fiscal&#x103; &#x219;i mentenan&#x21B;&#x103; firm&#x103;</li><li><strong>Stabilitate</strong>&#xA0;garantat&#x103; p&#xE2;n&#x103; &#xEE;n 2038.</li><li><strong>Flexibilitate la salarii</strong>&#xA0;&#x2013; po&#x21B;i seta liber sumele lunar/orar (ex: 5.000 lei &#xEE;n februarie, 8.000 lei &#xEE;n martie).</li></ul><figure class="kg-card kg-image-card"><img src="https://rabbitbyte.club/content/images/2025/08/output--1--1.png" class="kg-image" alt="Optimiz&#x103;ri fiscale 2025: Cum &#xEE;&#x21B;i deschizi o firm&#x103; &#xEE;n Moldova IT Park &#x219;i pl&#x103;te&#x219;ti doar 7% taxe" loading="lazy" width="1793" height="980" srcset="https://rabbitbyte.club/content/images/size/w600/2025/08/output--1--1.png 600w, https://rabbitbyte.club/content/images/size/w1000/2025/08/output--1--1.png 1000w, https://rabbitbyte.club/content/images/size/w1600/2025/08/output--1--1.png 1600w, https://rabbitbyte.club/content/images/2025/08/output--1--1.png 1793w" sizes="(min-width: 720px) 720px"></figure><p><strong>Disclaimer:</strong>&#xA0;Acest articol nu constituie consultan&#x21B;&#x103; fiscal&#x103; sau financiar&#x103;. Informa&#x21B;iile prezentate sunt bazate pe surse proprii &#x219;i experien&#x21B;&#x103; personal&#x103; &#x219;i nu reprezint&#x103; o garan&#x21B;ie c&#x103; acestea sunt sau vor fi aplicate efectiv. Textul are un scop exclusiv informativ &#x219;i educativ. Pentru luarea deciziilor financiare sau fiscale importante, v&#x103; recomand&#x103;m s&#x103; consulta&#x21B;i un contabil sau un consultant autorizat. Autorul nu &#xEE;&#x219;i asum&#x103; responsabilitatea pentru eventualele consecin&#x21B;e legale rezultate &#xEE;n urma aplic&#x103;rii informa&#x21B;iilor prezentate.</p>]]></content:encoded></item><item><title><![CDATA[Optimizare fiscală pentru băieții ultra-deștepți. Ce-i aia paradis fiscal?]]></title><description><![CDATA[<p><strong>&#x201E;Paradis fiscal&#x201D;? Ha! Mai bine <em>paradisul fraierilor</em>. Cine a tradus &#x201E;tax haven&#x201D; &#xEE;n &#x201E;paradis fiscal&#x201D; s&#x103;-&#x219;i bage limba-n blender &#x219;i s&#x103; nu mai scoat&#x103; prostii din gur&#x103;. Asta e ru&#x219;ine na&#x21B;ional&</strong></p>]]></description><link>https://rabbitbyte.club/optimizare-fiscala-pentru-baietii-ultra-destepti-ce-i-aia-paradis-fiscal/</link><guid isPermaLink="false">68625fbab71d68f7ff431098</guid><category><![CDATA[ro]]></category><category><![CDATA[optimizare-fiscala]]></category><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Mon, 30 Jun 2025 10:43:34 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2025/06/optimizare.webp" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2025/06/optimizare.webp" alt="Optimizare fiscal&#x103; pentru b&#x103;ie&#x21B;ii ultra-de&#x219;tep&#x21B;i. Ce-i aia paradis fiscal?"><p><strong>&#x201E;Paradis fiscal&#x201D;? Ha! Mai bine <em>paradisul fraierilor</em>. Cine a tradus &#x201E;tax haven&#x201D; &#xEE;n &#x201E;paradis fiscal&#x201D; s&#x103;-&#x219;i bage limba-n blender &#x219;i s&#x103; nu mai scoat&#x103; prostii din gur&#x103;. Asta e ru&#x219;ine na&#x21B;ional&#x103;.</strong></p><p><em>Haven</em> &#xEE;n englez&#x103; &#xEE;nseamn&#x103; <em>ad&#x103;post</em> &#x2014; nu <em>paradis</em>, fra&#x21B;ilor! Dar ce s&#x103; le ceri unor am&#x103;r&#xE2;&#x21B;i care habar n-au nici s&#x103; pronun&#x21B;e corect o fraz&#x103; &#xEE;n englez&#x103;? S-au uitat la cuvinte, au scos &#x201E;heaven&#x201D; din &#x219;apc&#x103;, &#x219;i-au zis &#x201E;mama, ce frumos, paradis fiscal!&#x201D;.</p><p>Nu e paradis, e un fel de bunc&#x103;r &#xEE;n care-&#x21B;i ascunzi banii. &#x218;i na, c&#xE2;nd tu crezi c&#x103; ai fost &#xEE;n paradis, vine ANAF-ul &#x219;i-&#x21B;i spune c&#x103; e doar un bunker plin de capcane.</p><p>Dac&#x103; vrei s&#x103; fii un adev&#x103;rat maestru al optimiz&#x103;rii fiscale offshore, &#x21B;ine minte un singur lucru:</p><p><strong>NU-&#x21A;I FACE FIRMA PE NUMELE T&#x102;U.</strong></p><p>&#x102;sta e pasul zero.</p><h3 id="pasul-1-alege-offshore-ul-potrivit-unul-%E2%80%9Eneprietenos%E2%80%9D-cu-anaf-ul">Pasul 1: Alege offshore-ul potrivit, unul &#x201E;neprietenos&#x201D; cu ANAF-ul</h3><p>S&#x103; nu fii fraier s&#x103; alegi o &#x21B;ar&#x103; de care ANAF &#x219;tie tot, cu acorduri de schimb de informa&#x21B;ii. Pentru asta, consult&#x103; lista oficial&#x103; a UE cu jurisdic&#x21B;ii <em>necooperante</em>. Panama? Da, &#xEE;nc&#x103; e pe <a href="https://www.consilium.europa.eu/ro/policies/eu-list-of-non-cooperative-jurisdictions/?ref=rabbitbyte.club" rel="noreferrer">lista aia</a>. </p><p>Adic&#x103; vrei s&#x103; fii &#xEE;ntr-un loc care-&#x21B;i ofer&#x103; o mic&#x103; <em>umbrel&#x103; de protec&#x21B;ie</em>, nu un spot publicitar pentru ANAF.</p><h3 id="pasul-2-nu-figura-tu-nic%C4%83ieri-pe-documente">Pasul 2: Nu figura tu nic&#x103;ieri pe documente</h3><p>Foarte important! Dac&#x103; ANAF-ul caut&#x103; informa&#x21B;ii despre firma din Panama &#x219;i te vede pe tine ca &#x201E;owner&#x201D; sau &#x201E;director&#x201D;, e gata. Ai intrat &#xEE;n vizor. Tu trebuie s&#x103; fii &#x201E;fantoma&#x201D; &#x2014; invizibil, f&#x103;r&#x103; urme directe. Firma offshore trebuie s&#x103; fie cea care apare &#xEE;n acte, pe site, &#xEE;n contracte. Ea este &#x201E;actorul&#x201D; de pe scen&#x103;, nu tu. Asta &#xEE;nseamn&#x103; c&#x103; nu trebuie s&#x103; apari nic&#x103;ieri &#xEE;n mod oficial.</p><h3 id="pasul-3-angajeaz%C4%83-%C8%9Bi-un-%E2%80%9Edomn-b%C4%83iat%E2%80%9D-local-s%C4%83-fie-fa%C8%9Ba-firmei">Pasul 3: Angajeaz&#x103;-&#x21B;i un &#x201E;domn b&#x103;iat&#x201D; local s&#x103; fie fa&#x21B;a firmei</h3><p>Nu te b&#x103;ga tu la semnat acte, c&#x103; e nasol. Mai bine g&#x103;se&#x219;ti un nominee director din Panama care s&#x103; figureze &#xEE;n acte, &#xEE;n timp ce tu faci mi&#x219;c&#x103;rile din spate.<br>Acest tip de serviciu il poti gasi de exemplu pe <a href="https://global.oneibc.com/cd/en/company-formation/jurisdiction/panama/nominee-service?ref=rabbitbyte.club" rel="noopener">Global One IBC</a>.</p><p>Practic, &#x201E;domnul b&#x103;iat&#x201D; e reprezentantul legal al firmei.</p><p><strong>Ce &#xEE;nseamn&#x103; Nominee Director?</strong></p><p>Simplu: e un tip (sau o firm&#x103;) care, pe h&#xE2;rtie, apare ca directorul firmei tale. Adic&#x103; el e trecut &#xEE;n documentele oficiale &#x2014; la stat, la registre, peste tot &#x2014; &#xEE;n locul t&#x103;u. Scopul? S&#x103;-&#x21B;i &#x21B;in&#x103; numele &#x219;i identitatea &#xEE;n secret, s&#x103; nu apari tu direct acolo, c&#x103; asta ar fi primul semnal ro&#x219;u pentru ANAF sau oricine altcineva.</p><hr><p><strong>Power of Attorney (&#xEE;mputernicire legal&#x103;)</strong></p><p>Tu &#xEE;i dai &#x103;stuia o &#xEE;mputernicire oficial&#x103; (un Power of Attorney, pe scurt POA) prin care &#xEE;l autorizezi s&#x103; semneze acte, s&#x103; vorbeasc&#x103; &#xEE;n numele firmei, s&#x103; bage &#x219;tampila &#x2014; dar doar c&#xE2;nd &#xEE;i spui tu. Adic&#x103; &#x103;la nu face ce vrea el, ci execut&#x103; ce &#xEE;i zici tu. Nu-i nimic liber la decizii sau gestionare zilnic&#x103;, e doar o marionet&#x103; legal&#x103;.</p><hr><p><strong>Tu controlezi totul, dar nu apari</strong></p><p>Chiar dac&#x103; pe h&#xE2;rtie nominee director-ul e &#x219;eful firmei, &#xEE;n realitate tu tragi sforile. E ca &#x219;i cum ai avea un scut &#xEE;n fa&#x21B;&#x103;, care te protejeaz&#x103;, dar tu e&#x219;ti acolo &#xEE;n spate, cu m&#xE2;inile pe toate butoanele.</p><p>Apropo, dac&#x103; vrei extra full privacy, &#xEE;&#x21B;i trebuie &#x219;i un nominee shareholder. Nominee director-ul ascunde cine gestioneaz&#x103; firma, dar dac&#x103; tu apari ca ac&#x21B;ionar, tot te pot lega de ea. A&#x219;a c&#x103; dac&#x103; vrei s&#x103; fii invizibil cu adev&#x103;rat, ia-&#x21B;i &#x219;i nominee shareholder.</p><h3 id="pasul-4-aflarea-t%C3%A2rzie-%E2%80%9Efirma-e-a-ta-ups-nu-%C8%99tiam%E2%80%9D">Pasul 4: Aflarea t&#xE2;rzie: &#x201E;Firma e a ta?! Ups, nu &#x219;tiam!&#x201D;</h3><p>Dup&#x103; c&#xE2;teva luni, &#x201E;&#xEE;&#x21B;i dai seama&#x201D; c&#x103; firma offshore era de fapt a ta. Serios, n-ai &#x219;tiut c&#x103; trebuie s&#x103; aduci banii &#xEE;napoi &#xEE;n Rom&#xE2;nia &#x219;i s&#x103;-i declari. E o ne&#xEE;n&#x21B;elegere de bun&#x103; credin&#x21B;&#x103;, nu-i a&#x219;a?</p><p>De aici &#xEE;ncepe <em>clasicul</em>:</p><ul><li>faci bani din consultan&#x21B;&#x103; c&#x103;tre firma offshore,</li><li>faci cum faci sa ajungi cu impozit pe profit 0 la SRL-ul tau local</li><li>&#xEE;n Panama impozitul pe venit e 0</li><li>deci banii de consultan&#x21B;&#x103; sunt ai t&#x103;i... legal!</li></ul><p>Dar statul rom&#xE2;n te vrea pe tine impozit&#xE2;nd acei bani, doar c&#x103; tu <em>nu i-ai adus &#xEE;n &#x21B;ar&#x103;</em>. &#x218;i asta-i &#x201E;problema&#x201D;.</p><hr><h3 id="pasul-5-sco%C8%9Bi-banii-%E2%80%9Edin-matrix%E2%80%9D%C8%99i-%C3%AEncepe-jocul-de%C8%99tep%C8%9Bilor">Pasul 5: Sco&#x21B;i banii &#x201E;din Matrix&#x201D; - &#x219;i &#xEE;ncepe jocul de&#x219;tep&#x21B;ilor</h3><p>Acum &#xEE;ncepe partea fain&#x103;, unde devii cu adev&#x103;rat &#x201E;b&#x103;iatul ultra-de&#x219;tept&#x201D;:</p><ul><li>&#xEE;&#x21B;i faci carduri de debit pe firma offshore &#x219;i cheltuie&#x219;ti f&#x103;r&#x103; s&#x103; dai socoteal&#x103;,</li><li>faci micro-cheltuieli insesizabile: restaurante, hoteluri, bilete de avion</li><li>c&#xE2;nd sumele devin mari, sco&#x21B;i cash din &#x21B;&#x103;ri non-UE (ca s&#x103; nu la&#x219;i urme &#xEE;n sistemul financiar european),</li><li>faci transferuri &#x219;i conversii de crypto din wallet &#xEE;n wallet p&#xE2;n&#x103; se pierde urma banilor.</li></ul><hr><h3 id="finalul-anaf-ul-n-are-ce-verifica">Finalul? ANAF-ul... n-are ce verifica!</h3><ul><li>Banii au fost cheltui&#x21B;i de o firm&#x103; str&#x103;in&#x103; (nu ai nicio leg&#x103;tur&#x103; oficial&#x103;).</li><li>Nu e&#x219;ti beneficiar oficial, pentru c&#x103; nu apari &#xEE;n acte.</li><li>Banii sunt f&#x103;cu&#x21B;i de firma din Panama, deci n-ai ce s&#x103; declari &#xEE;n Rom&#xE2;nia.</li><li>&#xCE;ntr-un eventual control, cel mult se poate discuta despre &#x201E;inten&#x21B;ia ta&#x201D;, dar f&#x103;r&#x103; acte concrete &#x219;i dovezi clare, e doar <em>paranoia fiscal&#x103;</em>.</li></ul><hr><h3 id="concluzie-nu-i-ilegal-s%C4%83-fii-mai-de%C8%99tept-dec%C3%A2t-statul">Concluzie: Nu-i ilegal s&#x103; fii mai de&#x219;tept dec&#xE2;t statul</h3><p>Ce fac b&#x103;ie&#x21B;ii ultra-de&#x219;tep&#x21B;i nu e neap&#x103;rat ilegal. E doar prea bine g&#xE2;ndit pentru nivelul actual de vigilen&#x21B;&#x103; al ANAF.<br>&#x218;i c&#xE2;t timp statul continu&#x103; s&#x103;-i pedepseasc&#x103; pe cei one&#x219;ti &#x219;i s&#x103; ignore pe cei &#x201E;optimi&#x219;ti&#x201D;, o s&#x103; tot apar&#x103; astfel de scheme.</p><p></p><p><strong>Disclaimer:</strong>&#xA0;Acest articol nu constituie consultan&#x21B;&#x103; fiscal&#x103; sau financiar&#x103;. Informa&#x21B;iile prezentate sunt bazate pe surse proprii &#x219;i experien&#x21B;&#x103; personal&#x103; &#x219;i nu reprezint&#x103; o garan&#x21B;ie c&#x103; acestea sunt sau vor fi aplicate efectiv. Textul are un scop exclusiv informativ &#x219;i educativ. Pentru luarea deciziilor financiare sau fiscale importante, v&#x103; recomand&#x103;m s&#x103; consulta&#x21B;i un contabil sau un consultant autorizat. Autorul nu &#xEE;&#x219;i asum&#x103; responsabilitatea pentru eventualele consecin&#x21B;e legale rezultate &#xEE;n urma aplic&#x103;rii informa&#x21B;iilor prezentate.</p>]]></content:encoded></item><item><title><![CDATA[Optimizare fiscală à la băieții deștepți – schema legală pe care statul n-o va închide prea curând]]></title><description><![CDATA[<p><strong>Rom&#xE2;nia func&#x21B;ioneaz&#x103; de multe ori ca un stat bananier:</strong> legi schimbate peste noapte, taxe inventate din pix, promisiuni de digitalizare doar pe h&#xE2;rtie. &#xCE;n tot acest haos fiscal, exist&#x103; &#xEE;ns&#x103; c&#xE2;teva g&#x103;uri &#xEE;n zidul</p>]]></description><link>https://rabbitbyte.club/schema-de-baieti-destepti-pe-care-o-poti-aplica-si-sigur-statul-nu-va-renunta-la-ea-ca-sa-nu-i-deranjeze-pe-baietii-destepti/</link><guid isPermaLink="false">685e7697b71d68f7ff431042</guid><category><![CDATA[ro]]></category><category><![CDATA[optimizare-fiscala]]></category><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Fri, 27 Jun 2025 11:39:24 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2025/06/Screenshot-2025-06-27-at-14.34.10.webp" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2025/06/Screenshot-2025-06-27-at-14.34.10.webp" alt="Optimizare fiscal&#x103; &#xE0; la b&#x103;ie&#x21B;ii de&#x219;tep&#x21B;i &#x2013; schema legal&#x103; pe care statul n-o va &#xEE;nchide prea cur&#xE2;nd"><p><strong>Rom&#xE2;nia func&#x21B;ioneaz&#x103; de multe ori ca un stat bananier:</strong> legi schimbate peste noapte, taxe inventate din pix, promisiuni de digitalizare doar pe h&#xE2;rtie. &#xCE;n tot acest haos fiscal, exist&#x103; &#xEE;ns&#x103; c&#xE2;teva g&#x103;uri &#xEE;n zidul sistemului. G&#x103;uri f&#x103;cute special <strong>ca s&#x103; nu deranjeze pe &#x201E;b&#x103;ie&#x21B;ii de&#x219;tep&#x21B;i&#x201D;</strong> &#x2014; corpora&#x21B;ii cu influen&#x21B;&#x103;, firme cu acces la consultan&#x21B;i scumpi &#x219;i clien&#x21B;i VIP care &#x219;tiu exact pe ce buton s&#x103; apese.</p><p>Dar iat&#x103; vestea bun&#x103;: una dintre aceste porti&#x21B;e <strong>este 100% legal&#x103; &#x219;i disponibil&#x103; &#x219;i pentru tine.</strong> Ai o firm&#x103; de IT, software, AI, SaaS, sau faci orice proiect tehnic unde inovezi ceva? Atunci ai &#x219;anse mari s&#x103; o folose&#x219;ti. &#x218;i crede-m&#x103;, nu o s&#x103; fie scoas&#x103; prea cur&#xE2;nd din Codul Fiscal &#x2014; prea mul&#x21B;i &#x201E;b&#x103;ie&#x21B;i de&#x219;tep&#x21B;i&#x201D; &#x219;i-ar pune pilele &#xEE;n cap.</p><p>Exist&#x103; o <strong>porti&#x21B;&#x103; legal&#x103;</strong> care &#xEE;&#x21B;i poate &#xEE;njum&#x103;t&#x103;&#x21B;i impozitul pe profit dac&#x103; o folose&#x219;ti cum trebuie. Nu, nu vorbim de offshore-uri, Panama sau interpu&#x219;i. Vorbim de ceva <strong>scris negru pe alb &#xEE;n Codul Fiscal</strong>, dar pe care doar consultan&#x21B;ii care cer 10.000 euro/an o explic&#x103; pe &#xEE;n&#x21B;elesul t&#x103;u.</p><p>Dac&#x103; ai firm&#x103; &#xEE;n IT, e foarte probabil ca o mare parte din ceea ce faci s&#x103; se califice pentru <strong>facilit&#x103;&#x21B;i fiscale R&amp;D</strong> (cercetare-dezvoltare). Doar c&#x103; trebuie s&#x103; &#x219;tii cum s-o ambalezi &#x219;i s&#x103; o justifici corect.</p><h2 id="%F0%9F%93%9C-legea-exact%C4%83">&#x1F4DC; Legea exact&#x103;</h2><p>Conform <strong>Codului Fiscal - Art. 20</strong>, ai dreptul la:</p><ul><li><strong>Deducere suplimentar&#x103; de 50%</strong> din cheltuielile eligibile pentru R&amp;D (peste deducerea normal&#x103;)</li><li><strong>Amortizare accelerat&#x103;</strong> pentru echipamentele dedicate activit&#x103;&#x21B;ilor de cercetare-dezvoltare</li></ul><blockquote>&#x1F517; <a href="https://static.anaf.ro/static/10/Anaf/legislatie/L_227_2015.htm?ref=rabbitbyte.club#A20">Text oficial &#x2013; Art. 20 din Codul Fiscal</a></blockquote><p>Plus Ordinul comun care stabile&#x219;te normele:</p><blockquote>&#x1F517; <a href="https://static.anaf.ro/static/10/Anaf/legislatie/OMFP_1056_2016.pdf?ref=rabbitbyte.club" rel="noreferrer">ORDIN Nr. 1056/4435/2016 din 5 iulie 2016</a> - stabile&#x219;te criterii &#x219;i modul de documentare</blockquote><p>&#x218;i ghidul de referin&#x21B;&#x103;:</p><blockquote>&#x1F517; <a href="https://www.oecd.org/content/dam/oecd/en/publications/reports/2015/10/frascati-manual-2015_g1g57dcb/9789264239012-en.pdf?ref=rabbitbyte.club" rel="noreferrer">Manualul Frascati (OECD</a>)</blockquote><h2 id="rezumat-final-%E2%80%9Eca-la-pro%C8%99ti%E2%80%9D-%E2%80%93-ce-trebuie-s%C4%83-%C8%99tii">Rezumat final &#x201E;ca la pro&#x219;ti&#x201D; &#x2013; Ce trebuie s&#x103; &#x219;tii</h2><h3 id="cum-s%C4%83-pl%C4%83te%C8%99ti-mai-pu%C8%9Bin-impozit-dac%C4%83-faci-cercetare-dezvoltare"><br>Cum s&#x103; pl&#x103;te&#x219;ti mai pu&#x21B;in impozit dac&#x103; faci cercetare-dezvoltare</h3><p>Dac&#x103; firma ta face chestii noi, experimente sau proiecte de cercetare, po&#x21B;i s&#x103; scazi din impozit o parte din cheltuielile astea.</p><p><strong>Ce po&#x21B;i sc&#x103;dea?</strong></p><ul><li>Banii da&#x21B;i pe salarii pentru oamenii care fac cercetare.</li><li>Cheltuielile cu echipamentele &#x219;i spa&#x21B;iile folosite.</li><li>Materiale &#x219;i servicii legate direct de proiect.</li><li>Partea din chirie &#x219;i utilit&#x103;&#x21B;i pentru cercetare.</li></ul><p><strong>Ce trebuie s&#x103; faci?</strong></p><ul><li>S&#x103; ai un proiect clar, cu ce vrei s&#x103; faci nou.</li><li>S&#x103; &#x21B;ii eviden&#x21B;&#x103; cheltuielilor pe care le faci.</li><li>S&#x103; &#xEE;nregistrezi corect totul &#xEE;n contabilitate.</li></ul><h2 id="%F0%9F%AA%9C-pas-cu-pas-cum-o-aplici-ca-s%C4%83-fii-blindat-legal">&#x1FA9C; Pas cu pas: Cum o aplici ca s&#x103; fii blindat legal</h2><p>Pentru ca schema s&#x103; &#x21B;in&#x103; la un eventual control, trebuie:</p><ol><li><strong>S&#x103; ai un experiment clar definit.</strong> Ce &#xEE;ncerci s&#x103; afli? Ce testezi?</li><li><strong>S&#x103; fie repetabil.</strong> Adic&#x103; oricine, dac&#x103; urmeaz&#x103; procesul t&#x103;u, s&#x103; poat&#x103; ob&#x21B;ine rezultate similare.</li><li><strong>Documenta&#x21B;ie complet&#x103;.</strong> Proces, date, rezultate. Salveaz&#x103; tot.</li><li><strong>Scop aplicativ.</strong> Adic&#x103; la final trebuie s&#x103; ai o solu&#x21B;ie practic&#x103;. Nu doar ai testat ca s&#x103; vezi ce iese.</li></ol><p>Practic, trebuie s&#x103; arate ca un <strong>mic proiect &#x219;tiin&#x21B;ific</strong>. Nu te speria, nu trebuie s&#x103; publici &#xEE;n Nature. Trebuie doar s&#x103; demonstrezi c&#x103; nu e o poveste inventat&#x103; retroactiv.</p><h2 id="%F0%9F%92%A1-exemplu-simplu-de-calcul">&#x1F4A1; Exemplu simplu de calcul</h2><p>S&#x103; zicem c&#x103; ai o firm&#x103; care face 150.000 euro venituri &#x219;i 100.000 euro cheltuieli (salarii, licen&#x21B;e, echipamente).</p><p>Dac&#x103; demonstrezi c&#x103; acele 100.000 euro au fost cheltuite pe un proiect R&amp;D, po&#x21B;i deduce &#xEE;nc&#x103; <strong>50% din ele</strong>, adic&#x103; &#xEE;nc&#x103; 50.000 euro.</p><p>Astfel:</p><ul><li>Venit = 150.000 euro</li><li>Cheltuieli normale = 100.000 euro</li><li>Deducere suplimentar&#x103; = 50.000 euro</li><li><strong>Rezultatul fiscal = 0</strong></li><li><strong>Impozit pe profit = 0</strong></li></ul><p>Statul a &#xEE;ncasat zero, tu ai reinvestit banii &#xEE;n firm&#x103;. Legal, frumos &#x219;i elegant.</p><h2 id="%F0%9F%A4%96-aplic%C4%83m-teoria-proiect-ai-pentru-recomand%C4%83ri-%C3%AEn-e-commerce">&#x1F916; Aplic&#x103;m teoria: Proiect AI pentru recomand&#x103;ri &#xEE;n e-commerce</h2><h3 id="%F0%9F%8E%AF-scopul">&#x1F3AF; Scopul</h3><p>Dezvoltarea unui modul AI care analizeaz&#x103; comportamentul userului &#x219;i recomand&#x103; produse relevante automat.</p><h3 id="%F0%9F%94%8D-experimentul">&#x1F50D; Experimentul</h3><ol><li>Alegem 3 modele AI (XGBoost, GPT-4, KNN)</li><li>Set&#x103;m criterii de comparare: timp de r&#x103;spuns, acurate&#x21B;e, conversii</li><li>Aplic&#x103;m pe 1.000 utilizatori dintr-un magazin online demo</li></ol><h3 id="%F0%9F%93%9D-documenta%C8%9Bie">&#x1F4DD; Documenta&#x21B;ie:</h3><ul><li>Fiecare model testat pe acelea&#x219;i date</li><li>Loguri de performan&#x21B;&#x103; salvate</li><li>Rapoarte comparative generate</li></ul><h3 id="%F0%9F%A7%AA-rezultatul">&#x1F9EA; Rezultatul</h3><p>Modelul GPT-4 are cele mai bune recomand&#x103;ri pentru produse peste 150 lei. Decidem s&#x103;-l integr&#x103;m &#xEE;n platforma noastr&#x103;.</p><h3 id="%E2%9C%85-exemplu-scop-aplicativ">&#x2705; Exemplu Scop aplicativ:</h3><ul><li>Cre&#x219;te valoarea medie a co&#x219;ului</li><li>Scade bounce rate</li><li>Conversie crescut&#x103; cu 14%</li></ul><p>Total cheltuieli &#xEE;n proiect: 70.000 lei. Deducere suplimentar&#x103;: 35.000 lei. Profit impozabil sc&#x103;zut cu 35.000 lei.</p><h2 id="%F0%9F%A7%A0-concluzie">&#x1F9E0; Concluzie</h2><p>Statul rom&#xE2;n a l&#x103;sat aceast&#x103; porti&#x21B;&#x103; legal&#x103; deschis&#x103; nu pentru c&#x103; e generos, ci pentru c&#x103; <strong>nu vrea s&#x103;-&#x219;i enerveze corpora&#x21B;iile</strong> care deja profit&#x103; de ea. E ca o schem&#x103; pentru b&#x103;ie&#x21B;i de&#x219;tep&#x21B;i, dar tu po&#x21B;i deveni unul din ei dac&#x103; joci legal &#x219;i documentat.</p><p>Dac&#x103; ai firm&#x103; &#xEE;n IT &#x219;i &#xEE;nc&#x103; n-ai aplicat schema asta, <strong>ori nu &#x219;tiai, ori n-ai fost atent</strong>. Acum &#x219;tii. Ia-&#x21B;i un folder &#xEE;n Google Drive &#x219;i &#xEE;ncepe s&#x103;-&#x21B;i faci primul experiment.</p><p><strong>Disclaimer:</strong>&#xA0;Acest articol nu constituie consultan&#x21B;&#x103; fiscal&#x103; sau financiar&#x103;. Informa&#x21B;iile prezentate sunt bazate pe surse proprii &#x219;i experien&#x21B;&#x103; personal&#x103; &#x219;i nu reprezint&#x103; o garan&#x21B;ie c&#x103; acestea sunt sau vor fi aplicate efectiv. Textul are un scop exclusiv informativ &#x219;i educativ. Pentru luarea deciziilor financiare sau fiscale importante, v&#x103; recomand&#x103;m s&#x103; consulta&#x21B;i un contabil sau un consultant autorizat. Autorul nu &#xEE;&#x219;i asum&#x103; responsabilitatea pentru eventualele consecin&#x21B;e legale rezultate &#xEE;n urma aplic&#x103;rii informa&#x21B;iilor prezentate.</p>]]></content:encoded></item><item><title><![CDATA[Optimizare fiscală 2025: 10 metode prin care nu te lași furat de statul bananier]]></title><description><![CDATA[<p><strong>Politicienii pare c&#x103; doresc s&#x103; devin&#x103; ac&#x21B;ionar majoritar &#xEE;n firma ta &#x2014; f&#x103;r&#x103; s&#x103; contribuie cu nimic.</strong> Dac&#x103; ai o micro&#xEE;ntreprindere sau un PFA, ai totu&#x219;i c&#xE2;teva unelte la &#xEE;ndem&#xE2;n&</p>]]></description><link>https://rabbitbyte.club/optimizare-fiscala-2025-10-metode-legale-sa-ti-tii-banii-in-firma-nu-la-stat/</link><guid isPermaLink="false">685b3aa2b71d68f7ff430fd4</guid><category><![CDATA[ro]]></category><category><![CDATA[optimizare-fiscala]]></category><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Wed, 25 Jun 2025 00:48:18 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2025/06/ChatGPT-Image-Jun-25--2025--03_45_23-AM.png" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2025/06/ChatGPT-Image-Jun-25--2025--03_45_23-AM.png" alt="Optimizare fiscal&#x103; 2025: 10 metode prin care nu te la&#x219;i furat de statul bananier"><p><strong>Politicienii pare c&#x103; doresc s&#x103; devin&#x103; ac&#x21B;ionar majoritar &#xEE;n firma ta &#x2014; f&#x103;r&#x103; s&#x103; contribuie cu nimic.</strong> Dac&#x103; ai o micro&#xEE;ntreprindere sau un PFA, ai totu&#x219;i c&#xE2;teva unelte la &#xEE;ndem&#xE2;n&#x103; ca s&#x103; nu devii doar un intermediar &#xEE;ntre clien&#x21B;i &#x219;i ANAF. Iat&#x103; cum s&#x103; optimizezi legal, eficient &#x219;i strategic.</p><h3 id="1-folose%C8%99te-%C8%9Bi-firma-ca-un-portofel-inteligent">1. <strong>Folose&#x219;te-&#x21B;i firma ca un portofel inteligent</strong></h3><p>Dac&#x103; ai micro&#xEE;ntreprindere, pl&#x103;te&#x219;ti <strong>impozit pe venit</strong>, nu pe profit. Asta &#xEE;nseamn&#x103; c&#x103; <strong>deductibilitatea cheltuielilor nu mai influen&#x21B;eaz&#x103; direct taxarea</strong>. Po&#x21B;i cump&#x103;ra pe firm&#x103; aproape orice &#x2013; chiar &#x219;i <strong>mici de la Kaufland</strong> &#x2013; f&#x103;r&#x103; s&#x103;-&#x21B;i ba&#x21B;i capul c&#x103; &#x201E;nu e deductibil&#x201D;.</p><p>Dar aici vine &#x219;mecheria: <strong>chiar dac&#x103; nu e deductibil&#x103;</strong>, cheltuiala tot <strong>iese din firm&#x103;</strong>. Adic&#x103; <strong>nu mai sco&#x21B;i banii ca dividende</strong>, deci <strong>nu mai pl&#x103;te&#x219;ti 8% impozit pe dividende</strong>. Ce &#xEE;nseamn&#x103; asta?</p><blockquote>Practic, ai un <strong>&#x201E;cashback fiscal&#x201D; de 8%</strong> pe orice cheltuial&#x103; f&#x103;cut&#x103; direct din firm&#x103;, &#xEE;n loc s&#x103; sco&#x21B;i banii &#x219;i s&#x103;-i cheltuie&#x219;ti personal dup&#x103; ce te-a taxat statul.</blockquote><blockquote>Aten&#x21B;ie totu&#x219;i: nu abuza &#x219;i nu for&#x21B;a nota.</blockquote><hr><h3 id="2-transform%C4%83-%C8%9Bi-apartamentul-%C3%AEn-sediu-de-firm%C4%83-%E2%80%94-%C8%99i-%C3%AEn-surs%C4%83-de-venit-legal%C4%83">2. <strong>Transform&#x103;-&#x21B;i apartamentul &#xEE;n sediu de firm&#x103; &#x2014; &#x219;i &#xEE;n surs&#x103; de venit legal&#x103;</strong></h3><p>Dac&#x103; ai o firm&#x103;, po&#x21B;i face un contract de &#xEE;nchiriere &#xEE;ntre tine (persoan&#x103; fizic&#x103;) &#x219;i firma ta (juridic). Firma &#xEE;&#x21B;i pl&#x103;te&#x219;te chirie, <strong>tu declari venitul ca persoan&#x103; fizic&#x103; &#x219;i pl&#x103;te&#x219;ti doar 8% impozit</strong>. Simplu &#x219;i legal.</p><p>&#xCE;n 2025, plafonul de CASS pentru venituri din chirii este de <strong>6 salarii minime brute pe an</strong>, adic&#x103; <strong>24.300 lei/an</strong> sau <strong>2.025 lei/lun&#x103;</strong>.</p><p>Dac&#x103; te &#xEE;ncadrezi sub acest plafon lunar, <strong>nu pl&#x103;te&#x219;ti CASS deloc</strong>. Doar 8% impozit pe venit. At&#xE2;t.</p><hr><blockquote><strong>Ce &#xEE;nseamn&#x103; asta, &#xEE;n realitate?</strong><br>&#xCE;n loc s&#x103; sco&#x21B;i bani din firm&#x103; ca dividende &#x219;i s&#x103; pl&#x103;te&#x219;ti <strong>16% impozit dividende</strong>, &#xEE;i sco&#x21B;i ca <strong>chirie</strong>, &#x219;i pl&#x103;te&#x219;ti doar <strong>8% impozit</strong>.<br><br>Asta &#xEE;nseamn&#x103; un <strong>cashback fiscal de 8%</strong> pe acei bani. Firma &#xEE;i cheltuie, tu &#xEE;i &#xEE;ncasezi, &#x219;i statul ia mai pu&#x21B;in.</blockquote><hr><h3 id="3-dividende-mai-rar-cass-mai-mic">3. <strong>Dividende mai rar = CASS mai mic</strong></h3><p><strong>Nu e obligatoriu s&#x103; sco&#x21B;i dividende trimestrial sau anual.</strong><br>Dac&#x103; le sco&#x21B;i o dat&#x103; la doi ani, pl&#x103;te&#x219;ti <strong>CASS o singur&#x103; dat&#x103;, pe suma total&#x103;</strong>.</p><p>&#x218;i dac&#x103; sco&#x21B;i mul&#x21B;i bani, practic vei pl&#x103;ti CASS 10% doar la echivalentul a 12 sau 24 de salarii minime, o dat&#x103; la 2 ani &#x2014; &#xEE;n loc s&#x103; pl&#x103;te&#x219;ti &#xEE;n fiecare an.</p><blockquote><strong>Exemplu simplu &#x219;i eficient</strong>:<br>La final de 2025, faci AGA pentru <strong>T3</strong>, sco&#x21B;i tot profitul deja acumulat &#x219;i &#xEE;l virezi ca dividende.<br>&#x27A1; &#xCE;n 2026, <strong>tr&#x103;ie&#x219;ti lini&#x219;tit tot anul din acei bani</strong>, pentru c&#x103; i-ai scos legal &#x219;i fiscal din timp.<br>&#x27A1; Pe persoan&#x103; fizic&#x103;, <strong>pl&#x103;te&#x219;ti CASS o singur&#x103; dat&#x103; pentru tot profitul</strong> &#x2014; nu de dou&#x103; ori, cum ai face dac&#x103; ai scoate dividende &#x219;i &#xEE;n 2025, &#x219;i &#xEE;n 2026.</blockquote><hr><h3 id="4-ai-%C8%99i-pfa-folose%C8%99te-l-ca-supap%C4%83-de-optimizare">4. Ai &#x219;i PFA? Folose&#x219;te-l ca supap&#x103; de optimizare</h3><p>Dac&#x103; ai PFA pe sistem real, e important s&#x103; &#x219;tii c&#x103; <strong>po&#x21B;i pl&#x103;ti 10% impozit &#x219;i 10% CASS, f&#x103;r&#x103; s&#x103; mai pl&#x103;te&#x219;ti CAS de 25% &#x2014; &#xEE;ns&#x103; asta este valabil doar dac&#x103; veniturile tale nu dep&#x103;&#x219;esc pragul de 48.600 lei pe an</strong>.</p><p><strong>Cum se calculeaz&#x103; impozitul?</strong><br>10% din suma ob&#x21B;inut&#x103; dup&#x103; ce din &#xEE;ncas&#x103;rile totale scazi cheltuielile deductibile, contribu&#x21B;ia la pensie &#x219;i contribu&#x21B;ia la s&#x103;n&#x103;tate.</p><p>De exemplu, dac&#x103; ai un PFA f&#x103;r&#x103; cheltuieli &#x219;i venituri de exact <strong>48.600 lei</strong>, vei pl&#x103;ti:</p><ul><li>10% CASS</li><li>plus 10% impozit pe venitul net (adic&#x103; venitul total minus CASS),<br>adic&#x103; &#xEE;n &#xEE;n jur de <strong>19% taxe totale</strong>.</li></ul><blockquote>Ca s&#x103; optimizezi, limiteaz&#x103;-&#x21B;i veniturile p&#xE2;n&#x103; la plafonul de CAS (echivalentul a 12 salarii minime brute pe an). E perfect legal &#x219;i &#xEE;&#x21B;i ajut&#x103; cashflow-ul.</blockquote><hr><h3 id="5-profit%C4%83-de-beneficiile-pentru-salaria%C8%9Bi-%E2%80%93-chiar-dac%C4%83-tu-e%C8%99ti-salariatul">5. <strong>Profit&#x103; de beneficiile pentru salaria&#x21B;i &#x2013; chiar dac&#x103; tu e&#x219;ti salariatul</strong></h3><p>Po&#x21B;i oferi bonusuri <strong>neimpozabile</strong> &#xEE;n valoare de 300 de lei de:</p><ul><li>Cr&#x103;ciun, Pa&#x219;te, 8 Martie (pentru femei),</li><li>1 Iunie (dac&#x103; ai copii &#x2013; 300 lei/copil),</li></ul><blockquote>Dac&#x103; e&#x219;ti angajat &#xEE;n propria firm&#x103;, po&#x21B;i beneficia de ele &#x2014; legal.</blockquote><hr><h3 id="6-turismul-intern-%C8%99i-delega%C8%9Biile-sunt-deductibile">6. <strong>Turismul intern &#x219;i delega&#x21B;iile sunt deductibile</strong></h3><p>Ai deplas&#x103;ri prin &#x21B;ar&#x103;? Le po&#x21B;i trata ca <strong>delega&#x21B;ii de serviciu</strong> &#x219;i deconta:</p><ul><li>cazare,</li><li>transport,</li><li>diurn&#x103;.</li></ul><blockquote>&#x218;i da, &#xEE;n unele cazuri, po&#x21B;i transforma un city break &#xEE;n conferin&#x21B;&#x103; de business. Legal, dar cu m&#x103;sur&#x103;.</blockquote><hr><h3 id="7-mas%C4%83-de-protocol-d%C4%83-i-cu-%C8%99ampania-armand-de-brignac-pe-not%C4%83">7. Mas&#x103; de protocol? D&#x103;-i cu &#x219;ampania Armand de Brignac pe not&#x103;!</h3><p>Mesele de afaceri pot fi deductibile par&#x21B;ial, p&#xE2;n&#x103; la 2% din profitul contabil.</p><p>Totu&#x219;i, dac&#x103; e&#x219;ti micro&#xEE;ntreprindere, nu trebuie s&#x103;-&#x21B;i faci prea multe griji legat de aceast&#x103; limit&#x103; de 2% din profit, <strong>pentru c&#x103; la micro impozitul se aplic&#x103; pe venit, nu pe profit</strong>.</p><p>A&#x219;adar, deductibilitatea cheltuielilor nu influen&#x21B;eaz&#x103; impozitul pe care &#xEE;l pl&#x103;te&#x219;ti, ceea ce &#xEE;nseamn&#x103; c&#x103; po&#x21B;i face astfel de cheltuieli f&#x103;r&#x103; s&#x103;-&#x21B;i complici prea mult calculele fiscale. </p><blockquote>&#xCE;n plus, pentru c&#x103; e&#x219;ti taxat pe venit &#x219;i nu pe profit, banii cheltui&#x21B;i direct din firm&#x103; scad suma pe care o vei scoate ca dividende, iar la dividendele &#xEE;n valoare de 16% practic ai un &#x201E;cashback&#x201D; de 16% pe ace&#x219;ti bani cheltui&#x21B;i &#x2014; un bonus important &#xEE;n optimizarea fiscal&#x103;.</blockquote><hr><h3 id="8-vrei-s%C4%83-treci-pe-impozit-pe-profit-vinde-%C8%9Bi-sufletul-firmei">8. Vrei s&#x103; treci pe impozit pe profit? Vinde-&#x21B;i sufletul firmei!</h3><p>Dac&#x103; te g&#xE2;nde&#x219;ti s&#x103; treci de la micro la impozit pe profit, nu uita s&#x103; vinzi tot ce ai pe persoan&#x103; fizic&#x103; c&#x103;tre firm&#x103;. Da, exact: casa, terenul, locul de parcare, laptopul, dulapul, biroul &#x2014; tot ce po&#x21B;i!</p><p>Scopul? S&#x103; te asiguri c&#x103; firma <strong>nu va avea profit niciodat&#x103;</strong> &#x2014; cheltuie&#x219;ti pe ce ai nevoie, iar astfel reduci impozitul pe profit.</p><blockquote>Asta e o manevr&#x103; legal&#x103; &#x219;i eficient&#x103; de optimizare fiscal&#x103;, dar bine&#xEE;n&#x21B;eles, trebuie f&#x103;cut&#x103; cu cap.</blockquote><hr><h3 id="9-card-de-credit-e%C8%99alonare-cash-flow-smart">9. Card de credit + e&#x219;alonare = cash flow smart</h3><p>F&#x103;-&#x21B;i un card de credit la CEC Bank, care &#xEE;&#x21B;i ofer&#x103; 24 de rate f&#x103;r&#x103; dob&#xE2;nd&#x103;. Orice taxe ai de pl&#x103;tit pe persoan&#x103; fizic&#x103; c&#x103;tre stat, pl&#x103;te&#x219;te-le prin ghiseul.ro cu cardul &#x103;sta. Astfel, o plat&#x103; mare de CASS, de exemplu, se poate e&#x219;alona pe 2 ani, f&#x103;r&#x103; dob&#xE2;nd&#x103;.</p><blockquote>Practic, &#xEE;n contextul infla&#x21B;iei uria&#x219;e din statul bananier Rom&#xE2;nia, c&#xE2;&#x219;tigi bani pentru c&#x103; nu pl&#x103;te&#x219;ti imediat suma imens&#x103;, iar tu p&#x103;strezi cash flow-ul &#xEE;n buzunar.</blockquote><hr><h3 id="10-sfaturi-avansate-le-discut%C4%83m-privat">10. <strong>Sfaturi avansate? Le discut&#x103;m privat.</strong></h3><p>Nu pot scrie tot public aici &#x2014; unele tactici cer context, profil &#x219;i consultan&#x21B;&#x103; individual&#x103;. Dac&#x103; vrei s&#x103; discut&#x103;m mai profund, &#xEE;mi po&#x21B;i scrie &#xEE;n privat.</p><hr><h2 id="%C3%AEn-loc-de-concluzie">&#xCE;n loc de concluzie:</h2><p>Statul nu va avea niciodat&#x103; grij&#x103; de banii t&#x103;i mai bine dec&#xE2;t tine. Fii atent, fii legal, dar nu fi naiv. <strong>Optimiza&#x21B;i, nu improviza&#x21B;i.</strong></p><p></p><p><strong>Disclaimer:</strong> Acest articol nu constituie consultan&#x21B;&#x103; fiscal&#x103; sau financiar&#x103;. Informa&#x21B;iile prezentate sunt bazate pe surse proprii &#x219;i experien&#x21B;&#x103; personal&#x103; &#x219;i nu reprezint&#x103; o garan&#x21B;ie c&#x103; acestea sunt sau vor fi aplicate efectiv. Textul are un scop exclusiv informativ &#x219;i educativ. Pentru luarea deciziilor financiare sau fiscale importante, v&#x103; recomand&#x103;m s&#x103; consulta&#x21B;i un contabil sau un consultant autorizat. Autorul nu &#xEE;&#x219;i asum&#x103; responsabilitatea pentru eventualele consecin&#x21B;e legale rezultate &#xEE;n urma aplic&#x103;rii informa&#x21B;iilor prezentate.</p>]]></content:encoded></item><item><title><![CDATA[🎬 M3U8 to MP4 — The Easiest Way to Convert Videos, Instantly & Privately]]></title><description><![CDATA[<p>You&#x2019;ve been there.</p><p>You find the perfect video, try to download it... and what do you get? A weird&#xA0;<code>.m3u8</code>&#xA0;file. No MP4. No playback. Just confusion.</p><p>We get it &#x2014; it&#x2019;s frustrating. That&#x2019;s why we built a better way.</p><h3 id="%F0%9F%9A%80-introducing-instant-m3u8-to-mp4-conversion">&#x1F680; Introducing:</h3>]]></description><link>https://rabbitbyte.club/introducing-the-newest-m3u8-to-mp4-tool/</link><guid isPermaLink="false">684edc4ab71d68f7ff430fbb</guid><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Sun, 15 Jun 2025 14:48:30 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2025/06/Screenshot-2025-06-15-at-17.47.59.png" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2025/06/Screenshot-2025-06-15-at-17.47.59.png" alt="&#x1F3AC; M3U8 to MP4 &#x2014; The Easiest Way to Convert Videos, Instantly &amp; Privately"><p>You&#x2019;ve been there.</p><p>You find the perfect video, try to download it... and what do you get? A weird&#xA0;<code>.m3u8</code>&#xA0;file. No MP4. No playback. Just confusion.</p><p>We get it &#x2014; it&#x2019;s frustrating. That&#x2019;s why we built a better way.</p><h3 id="%F0%9F%9A%80-introducing-instant-m3u8-to-mp4-conversion">&#x1F680; Introducing: Instant M3U8 to MP4 Conversion</h3><p>No installs. No extensions. No waiting. Just your browser.</p><p><strong>Paste a link. Get your MP4. That&#x2019;s it.</strong></p><p>Here&#x2019;s why it hits different:</p><ul><li>&#x2705;&#xA0;<strong>100% free</strong>&#xA0;&#x2014; and always will be</li><li>&#x2705;&#xA0;<strong>Unlimited conversions</strong>&#xA0;&#x2014; as long as your browser can handle it</li><li>&#x2705;&#xA0;<strong>No file size limits</strong>&#xA0;&#x2014; works even for big videos</li><li>&#x2705;&#xA0;<strong>Works in-browser</strong>&#xA0;&#x2014; no downloads, no setup</li><li>&#x2705;&#xA0;<strong>Lightning fast</strong>&#xA0;&#x2014; converts in seconds</li></ul><h3 id="%F0%9F%94%92-privacy-first-no-exceptions">&#x1F512; Privacy-First, No Exceptions</h3><p>We don&#x2019;t see your files. Everything happens&#xA0;<em>locally</em>, right inside your browser tab.</p><ul><li>No uploads</li><li>No snooping</li><li>No third-party servers</li><li>Zero data exposure</li></ul><p>You keep your files. We just help you convert them &#x2014; fast.</p><h3 id="%F0%9F%93%A6-got-a-zip-of-the-playlist-files-instead-of-a-link">&#x1F4E6; Got a&#xA0;<code>.zip</code>&#xA0;of the playlist files instead of a link?</h3><p>No problem. Drop it in. You&#x2019;ll still get an MP4.</p><hr><h3 id="try-it-now-%E2%86%92-httpswwwm3u8-to-mp4-convertercom">Try it now &#x2192;&#xA0;<a href="https://www.m3u8-to-mp4-converter.com/?ref=rabbitbyte.club">https://www.m3u8-to-mp4-converter.com/</a></h3><p>And yes &#x2014; it really is&#xA0;<em>that</em>&#xA0;simple.</p>]]></content:encoded></item><item><title><![CDATA[How to Avoid the Single-Threaded Trap in JavaScript]]></title><description><![CDATA[<p>JavaScript is often described as&#xA0;<strong>single-threaded</strong>, which means it executes one task at a time. But does this imply that every piece of code runs in complete isolation, with no ability to handle other tasks while waiting for asynchronous operations like HTTP responses or database requests? The answer is&</p>]]></description><link>https://rabbitbyte.club/js-is-single-threaded-trap/</link><guid isPermaLink="false">6712de45b71d68f7ff430dac</guid><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Fri, 01 Nov 2024 07:25:39 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2024/11/istockphoto-1312084086-612x612.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2024/11/istockphoto-1312084086-612x612.jpg" alt="How to Avoid the Single-Threaded Trap in JavaScript"><p>JavaScript is often described as&#xA0;<strong>single-threaded</strong>, which means it executes one task at a time. But does this imply that every piece of code runs in complete isolation, with no ability to handle other tasks while waiting for asynchronous operations like HTTP responses or database requests? The answer is&#xA0;<strong>no!</strong>&#xA0;In fact, JavaScript&#x2019;s event loop and promises allow it to handle asynchronous tasks efficiently while other code continues to run.</p><p>The truth is javascript <strong>is indeed single-threaded</strong>, however, misunderstanding how this works can lead to common pitfalls. One such trap is managing asynchronous operations like API requests, especially when trying to control access to shared resources without causing race conditions. Let&apos;s explore a real-world example and see how poor implementation can lead to serious bugs.<br><br><em>I encountered <strong>a bug</strong> in an application that required logging into a backend service to update data. Upon logging in, the app would receive an access token with a specified expiration date. Once this expiration date passed, we needed to re-authenticate before making any new requests to the update endpoint. The challenge arose because the login endpoint was throttled to a maximum of one request every five minutes, while the update endpoint needed to be called more frequently within that same five-minute window. It was critical for the logic to function correctly, yet the login endpoint was<strong> occasionally triggered multiple times</strong> within the five-minute interval, leading to the update endpoint  failing to work. While there were times when everything functioned as expected, this intermittent bug presented a <strong>more serious risk</strong>, as it could give a <strong>false sense of security</strong> at first, making it seem like the system was operating properly.</em><br><br>To illustrate this example, we&apos;re using a very basic NestJS app that includes the following services:</p><ul><li><strong>AppService</strong>: Acts as a controller to simulate two variants&#x2014; the bad version, which sometimes works and sometimes doesn&apos;t, and the good version, which is guaranteed to always function properly.</li><li><strong>BadAuthenticationService</strong>: Implementation for the bad version.</li><li><strong>GoodAuthenticationService</strong>: Implementation for the good version.</li><li><strong>AbstractAuthenticationService</strong>: Class responsible for maintaining the shared state between the GoodAuthenticationService and BadAuthenticationService.</li><li><strong>LoginThrottleService</strong>: Class that simulates the throttling mechanism of the login endpoint for the backend service.</li><li><strong>MockHttpService</strong>: Class that helps simulate HTTP requests.</li><li><strong>MockAwsCloudwatchApiService</strong>: Simulates an API call to the AWS CloudWatch logging system.</li></ul><p>I won&apos;t show the code for all these classes here; you can find it directly in the <a href="https://github.com/zenstok/nestjs-singlethread-trap?ref=rabbitbyte.club" rel="noreferrer">GitHub repository</a>. Instead, I will focus specifically on the logic and what needs to be changed for it to work correctly.</p><h3 id="the-bad-approach">The Bad Approach:</h3><pre><code class="language-typescript">@Injectable()
export class BadAuthenticationService extends AbstractAuthenticationService {
  async loginToBackendService() {
    this.loginInProgress = true; // this is BAD, we are inside a promise, it&apos;s asynchronous. it&apos;s not synchronous, javascript can execute it whenever it wants

    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com/login`, {
          password: &apos;password&apos;,
        }),
      );

      return response;
    } finally {
      this.loginInProgress = false;
    }
  }

  async sendProtectedRequest(route: string, data?: unknown) {
    if (!this.accessToken) {
      if (this.loginInProgress) {
        await new Promise((resolve) =&gt; setTimeout(resolve, 1000));
        return this.sendProtectedRequest(route, data);
      }

      try {
        await this.awsCloudwatchApiService.logLoginCallAttempt();
        const { data: loginData } = await this.loginToBackendService();
        this.accessToken = loginData.accessToken;
      } catch (e: any) {
        console.error(e?.response?.data);
        throw e;
      }
    }

    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com${route}`, data, {
          headers: {
            Authorization: `Bearer ${this.accessToken}`,
          },
        }),
      );

      return response;
    } catch (e: any) {
      if (e?.response?.data?.statusCode === 401) {
        this.accessToken = null;
        return this.sendProtectedRequest(route, data);
      }
      console.error(e?.response?.data);
      throw e;
    }
  }
}</code></pre><h3 id="why-this-is-a-bad-approach">Why This Is a Bad Approach:</h3><p>In the&#xA0;<code>BadAuthenticationService</code>, the method&#xA0;<code>loginToBackendService</code>&#xA0;sets&#xA0;<code>this.loginInProgress</code>&#xA0;to&#xA0;<code>true</code>&#xA0;when initiating a login request. However, since this method is asynchronous, it does not guarantee that the login status will be updated immediately. This could lead to multiple concurrent calls to the login endpoint within the throttling limit.</p><p>When&#xA0;<code>sendProtectedRequest</code>&#xA0;detects that the access token is absent, it checks if a login is in progress. If it is, the function waits for a second and then retries. However, if another request comes in during this time, it can trigger additional login attempts. This can lead to multiple calls to the login endpoint, which is throttled to allow only one call every minute. As a result, the update endpoint may fail intermittently, causing unpredictable behavior and a false sense of security when the system appears to be functioning properly at times.</p><p>In summary, the problem lies in the <strong>improper handling of asynchronous operations</strong>, which leads to potential race conditions that can break the logic of the application.</p><h3 id="the-good-approach">The Good Approach:</h3><pre><code class="language-typescript">@Injectable()
export class GoodAuthenticationService extends AbstractAuthenticationService {
  async loginToBackendService() {
    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com/login`, {
          password: &apos;password&apos;,
        }),
      );

      return response;
    } finally {
      this.loginInProgress = false;
    }
  }

  async sendProtectedRequest(route: string, data?: unknown) {
    if (!this.accessToken) {
      if (this.loginInProgress) {
        await new Promise((resolve) =&gt; setTimeout(resolve, 1000));
        return this.sendProtectedRequest(route, data);
      }

      // Critical: Set the flag before ANY promise call
      this.loginInProgress = true;

      try {
        await this.awsCloudwatchApiService.logLoginCallAttempt();
        const { data: loginData } = await this.loginToBackendService();
        this.accessToken = loginData.accessToken;
      } catch (e: any) {
        console.error(e?.response?.data);
        throw e;
      }
    }

    try {
      const response = await firstValueFrom(
        this.httpService.post(`https://backend-service.com${route}`, data, {
          headers: {
            Authorization: `Bearer ${this.accessToken}`,
          },
        }),
      );

      return response;
    } catch (e: any) {
      if (e?.response?.data?.statusCode === 401) {
        this.accessToken = null;
        return this.sendProtectedRequest(route, data);
      }
      console.error(e?.response?.data);
      throw e;
    }
  }
}</code></pre><h3 id="why-this-is-a-good-approach">Why This Is a Good Approach:</h3><p>In the&#xA0;<code>GoodAuthenticationService</code>, the&#xA0;<code>loginToBackendService</code>&#xA0;method is structured to handle the login logic efficiently. The key improvement is the management of the&#xA0;<code>loginInProgress</code>&#xA0;flag. It is set&#xA0;<strong>after</strong>&#xA0;confirming that an access token is absent and&#xA0;<strong>before</strong>&#xA0;any asynchronous operations begin. This ensures that once a login attempt is initiated, no other login calls can be made concurrently, effectively preventing multiple requests to the throttled login endpoint.</p><h3 id></h3><h3 id="demo-instructions">Demo Instructions</h3><p><strong>Clone the Repository:</strong></p><pre><code class="language-bash">git clone https://github.com/zenstok/nestjs-singlethread-trap.git
</code></pre><p><strong>Install the Necessary Dependencies:</strong></p><pre><code class="language-bash">cd nestjs-singlethread-trap
npm install
</code></pre><p><strong>Run the Application:</strong></p><pre><code class="language-bash">npm run start
</code></pre><p><strong>Simulate requests:</strong></p><ul><li>To simulate two requests with the&#xA0;<strong>bad version</strong>, call:</li></ul><pre><code class="language-bash">http://localhost:3000/simulate-2-requests-bad-version
</code></pre><ul><li>To simulate two requests with the&#xA0;<strong>good version</strong>, call:</li></ul><pre><code class="language-bash">http://localhost:3000/simulate-2-requests-good-version</code></pre><p></p><h3 id="conclusion-avoiding-javascripts-single-threaded-pitfalls">Conclusion: Avoiding JavaScript&apos;s Single-Threaded Pitfalls</h3><p>While JavaScript is single-threaded, it can handle asynchronous tasks like HTTP requests efficiently using promises and the event loop. However, improper handling of these promises, particularly in scenarios involving shared resources (like tokens), can lead to&#xA0;<strong>race conditions</strong>&#xA0;and&#xA0;<strong>duplicate actions</strong>.</p><p>The key takeaway is to&#xA0;<strong>synchronize</strong>&#xA0;asynchronous actions like logins to avoid such traps. Always ensure that your code is&#xA0;<strong>aware of ongoing processes</strong>&#xA0;and handles requests in a way that guarantees proper sequencing, even when JavaScript is multitasking behind the scenes.</p><hr><p><br>If you haven&apos;t already joined the <a href="https://rabbitbyte.club/community/" rel="noreferrer">Rabbit Byte Club</a>, now is your chance to hop into a thriving community of software enthusiasts, tech founders, and non-tech founders. Together, we share knowledge, learn from each other, and prepare to build the next big startup. Join us today and be part of an exciting journey towards innovation and growth!</p>]]></content:encoded></item><item><title><![CDATA[How to Create and Download Files of Unlimited Size in node.js/NestJS]]></title><description><![CDATA[<p>Hey, folks! Have you ever wondered if it&apos;s possible to send a file of unlimited size to your users, all while giving them real-time feedback during the download process? Well, today, we&apos;re diving into exactly that. Let&#x2019;s explore how we can achieve this in</p>]]></description><link>https://rabbitbyte.club/how-to-create-and-download-files-of-unlimited-size-in-node-js-nestjs/</link><guid isPermaLink="false">67018db8b71d68f7ff430d48</guid><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Sun, 06 Oct 2024 16:10:51 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2024/10/nest-logo.png" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2024/10/nest-logo.png" alt="How to Create and Download Files of Unlimited Size in node.js/NestJS"><p>Hey, folks! Have you ever wondered if it&apos;s possible to send a file of unlimited size to your users, all while giving them real-time feedback during the download process? Well, today, we&apos;re diving into exactly that. Let&#x2019;s explore how we can achieve this in <strong>NestJS</strong> using one of Node.js&apos;s most powerful features&#x2014;<strong>Streams</strong>.</p><p>In this guide, I&#x2019;m not just going to show you how to download files of <em>unlimited size</em>&#x2014;I&#x2019;ll also walk you through the magic of <strong>streams</strong> and how they really work behind the scenes.</p><h3 id="the-scenario-a-managers-report-download">The Scenario: A Manager&apos;s Report Download</h3><p>Imagine a manager needs to download a dynamically generated report containing invoice data, based on a user-defined date range. The twist? We have no idea how big the report will be because the user sets the parameters. We need to stream this data efficiently without overwhelming our server&#x2019;s memory. Here&#x2019;s where <strong>chunked transfer encoding</strong> comes into play. By setting the <code>Transfer-Encoding</code> header to <code>chunked</code> (<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding?ref=rabbitbyte.club">learn more here</a>), we can send the data in chunks, eliminating the need for a predetermined file size.</p><p>But wait&#x2014;how do we handle massive file sizes? The answer is simple: <strong>Node.js Streams</strong>.</p><h3 id="why-not-buffers">Why Not Buffers?</h3><p>At first, you might think we can handle this using file buffers. However, buffers are limited by the server&#x2019;s memory. If you&#x2019;re dealing with large files, buffers will quickly hit a ceiling. Instead, we turn to <strong>streams</strong>, which are the backbone of Node.js when it comes to data management.</p><p>Streams process data in small chunks, sending it out without holding everything in memory. This way, we can handle &quot;infinite&quot; files, or at least ones large enough to blow your mind.</p><h3 id="what-are-streams">What are streams?</h3><p>Streams in Node.js are powerful objects that let us process batches of data without loading everything into memory. Instead of holding the entire dataset, streams process chunks of data and pass them along as they go. So, because they don&#x2019;t hold all the data in memory, they can handle massive files. But where do these chunks go? They&#x2019;re passed on to other <strong>streams</strong>!</p><p>There are two key types of streams you&#x2019;ll be working with in Node.js: <strong>readable</strong> and <strong>writable</strong> streams.</p><h4 id="let%E2%80%99s-break-it-down-with-an-example">Let&#x2019;s break it down with an example:</h4><p>If you&#x2019;re reading a file&#x2019;s contents, you&#x2019;d use the <code>fs.createReadStream</code> method. This returns a <strong>readable stream</strong> object, which gives you chunks of the file&#x2019;s data bit by bit.</p><p>But once you&#x2019;ve read the data, how do you send it to the user as an HTTP response?</p><p>Well, here&#x2019;s the cool part: in Node.js, the response object itself is a <strong>writable stream</strong>. What does that mean? It means you can take the data you&#x2019;re reading from the readable stream and send it directly to the writable stream (the HTTP response) using a method called <code>.pipe</code>.</p><p>With just a few lines of code, the <code>.pipe</code> method lets you effortlessly stream file content directly to the client as an HTTP response&#x2014;no need to worry about memory limits or large file sizes. Simple, right?</p><h2 id="the-three-methods-to-stream-large-files-in-nestjs">The Three Methods to Stream Large Files in NestJS</h2><p>We&#x2019;ll walk through three different ways to stream large files in NestJS, ranging from basic to more advanced, giving you both performance and flexibility.</p><ol><li><strong>The Easy Version: Node.js Out of the Box</strong></li><li><strong>The Performant Version: String-Based Stream Chunks</strong></li><li><strong>The Buffer Version: For When Precision Matters</strong></li></ol><h3 id="1-the-easy-version">1. <strong>The Easy Version</strong></h3><p>In this approach, we use Node.js&apos;s built-in interfaces to handle the heavy lifting for us. Here&#x2019;s what the controller code looks like:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">@Get(&apos;/simple&apos;)
@Header(&apos;Content-Disposition&apos;, `attachment; filename=&quot;bigJson.json&quot;`)
@Header(&apos;Content-Type&apos;, &apos;application/json&apos;)
@Header(&apos;Transfer-Encoding&apos;, &apos;chunked&apos;)
getBigFileSimple(@Res() res: Response) {
  this.simpleService.getBigFile(res);
}
</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">src/app.controller.ts</span></p></figcaption></figure><p>The <code>@Header</code> decorators set the file to be an attachment, specify the content type, and enable chunked transfer encoding. This allows us to send the file in parts without knowing its total size upfront.</p><p>In our service, we have the following method:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">@Injectable()
export class SimpleService {
  constructor(private readonly jsonService: JsonService) {}

  getBigFile(res: Response) {
    return this.jsonService.createBigJsonFileStream().pipe(res);
  }
}
</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">src/simple.service.ts</span></p></figcaption></figure><p>With <strong>just one line of code</strong>, we pipe the file stream to the response! This elegant solution leverages Node.js&apos;s stream piping, making it both simple and powerful.</p><p>Now, you might wonder&#x2014;what exactly is this <code>createBigJsonFileStream()</code> method doing?</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">@Injectable()
export class JsonService {
  createBigJsonFileStream(rowsLength = 25000000): Readable {
    let currentRow = 1;
    rowsLength = this.getRandomLengthFromRange(rowsLength / 2, rowsLength);

    const stream = new Readable({
      read() {
        if (currentRow === 1) {
          this.push(&apos;[&apos;);
          this.push(`{&quot;id&quot;:${currentRow},&quot;name&quot;:&quot;element${currentRow}&quot;},`);
        } else if (currentRow === rowsLength) {
          this.push(`{&quot;id&quot;:${currentRow},&quot;name&quot;:&quot;element${currentRow}&quot;}`);
          this.push(&apos;]&apos;);
          this.push(null);
        } else {
          this.push(`{&quot;id&quot;:${currentRow},&quot;name&quot;:&quot;element${currentRow}&quot;},`);
        }
        currentRow++;
      },
    });

    return stream;
  }

  private getRandomLengthFromRange(min: number, max: number): number {
    return Math.floor(Math.random() * (max - min + 1) + min);
  }
}
</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">src/json.service.ts</span></p></figcaption></figure><p>In this case, we&#x2019;re not serving a file from disk. Remember, the scenario we&#x2019;re addressing is where a manager wants to download a report, choosing their own start and end dates. This means we need to dynamically generate the file based on the user&apos;s request. And how do we do that? You guessed it&#x2014;<strong>with streams!</strong></p><h3 id="what%E2%80%99s-happening-here">What&#x2019;s happening here?</h3><p>We&#x2019;re creating a <strong>readable stream</strong> using Node.js&#x2019;s <code>Readable</code> constructor (<a href="https://nodejs.org/api/stream.html?ref=rabbitbyte.club#new-streamreadableoptions">documentation here</a>). We&#x2019;re keeping things simple by focusing on the required <code>read()</code> method.</p><p>Here&#x2019;s how it works: every time the stream has data to offer, the <code>read()</code> method is called. As long as we keep pushing data into the stream (and don&#x2019;t push <code>null</code>), the stream will stay active and keep sending data.</p><p>In this example, we&#x2019;ve set a <code>rowsLength</code>&#x2014;the total number of rows our JSON file will have. <em>(Note: We&#x2019;re dynamically altering the <code>rowsLength</code> to simulate the manager selecting a start and end date, creating a random value to represent the report&#x2019;s size.)</em> Based on this, we push one row at a time into the stream. It&#x2019;s that simple! We&#x2019;re dynamically creating a huge JSON file, sending data chunk by chunk, and handling it all seamlessly without loading everything into memory.</p><h3 id="2-the-performant-version-string-based-chunks">2. <strong>The Performant Version: String-Based Chunks</strong></h3><p>In this approach, we fine-tune performance by manually controlling how the data is written to the response stream:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">@Injectable()
export class PerformantService {
  constructor(private readonly jsonService: JsonService) {}

  getBigFile(res: Response) {
    const jsonStream = this.jsonService.createBigJsonFileStream();
    jsonStream.setEncoding(&apos;utf-8&apos;);
    let chunkSize = 0;
    let bigChunk = &apos;&apos;;

    jsonStream.on(&apos;data&apos;, (chunk: string) =&gt; {
      bigChunk += chunk;
      chunkSize++;
      if (chunkSize === 1000) {
        const writable = res.write(bigChunk);
        if (!writable) {
          jsonStream.pause();
        }
        bigChunk = &apos;&apos;;
        chunkSize = 0;
      }
    });

    res.on(&apos;drain&apos;, () =&gt; {
      jsonStream.resume();
    });

    jsonStream.on(&apos;error&apos;, (err) =&gt; {
      console.error(err);
      res.end();
    });

    jsonStream.on(&apos;end&apos;, () =&gt; {
      if (bigChunk.length) {
        res.write(bigChunk);
      }
      res.end();
    });
  }
}
</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">src/performant.service.ts</span></p></figcaption></figure><h3 id="so-what-do-we-have-here">So, what do we have here?</h3><p>What do we have here? Well, the logic might look a bit complex, but that&apos;s because we&#x2019;re handling the process of writing to the res writable stream manually. Why? We believe that our approach is faster than simply piping the stream.</p><p>So, how do we do it? First, we set the encoding of our JSON stream to UTF-8 using <code>jsonStream.setEncoding(&apos;utf-8&apos;);</code>. This ensures that instead of receiving default Buffer objects from the stream, we get data as UTF-8 encoded strings.</p><p>Next, we define two variables: chunkSize, initialized to 0, and bigChunk, an empty string. The cool thing about streams is that we can listen to events on them. For example, the data event gives us a data chunk every time the stream is read. But when is read called? It&apos;s triggered as soon as the readable stream is instantiated and continues until all the data is read&#x2014;pretty cool, right? We know the stream has finished reading when we hit the end event.</p><p>Now, when data arrives from the stream, we want to be efficient. Instead of sending small chunks to the res stream, we batch 1,000 chunks together and send them as one large chunk to our HTTP response, which is itself a writable stream. But wait&#x2014;why does the write method return a boolean? This introduces us to a crucial concept in Node.js streams: <a href="https://nodejs.org/en/learn/modules/backpressuring-in-streams?ref=rabbitbyte.club">backpressure</a>. As Node.js describes it, &#x201C;backpressure occurs when data builds up behind a buffer during data transfer.&#x201D; In simpler terms, it means the res stream is telling us, &quot;Slow down! I can&apos;t keep up!&quot;&quot; The solution? We stop sending data! This is done by calling <code>jsonStream.pause()</code>, which halts the stream from reading further.</p><p>But what happens next? Now, we need to listen for another stream event&#x2014;this time on the res writable stream. We need the stream to signal when it has recovered from the backpressure. This is where the &quot;drain&quot; event comes in. The &quot;drain&quot; event is emitted when it&#x2019;s appropriate to resume writing data to the stream. So, once we catch this event, we simply resume reading from our big JSON stream!</p><p>Finally, when the JSON stream reaches the end event, we check if there&#x2019;s any leftover data in bigChunk and write it to the res stream. After that, we call res.end() to signal that the response is complete.</p><p>And just like that, we&#x2019;ve manually controlled how the data flows through the streams, handled backpressure, and delivered a dynamic file as a response!</p><h3 id="3-the-buffer-version-chunking-with-buffers">3. <strong>The Buffer Version: Chunking with Buffers</strong></h3><p>For a more complex scenario, say we want to send large buffer chunks. Here&apos;s how we can handle it:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">@Injectable()
export class BufferService {
  constructor(private readonly jsonService: JsonService) {}

  getBigFile(res: Response) {
    const jsonStream = this.jsonService.createBigJsonFileStream();

    const sendingChunkSize = 50 * 1024 * 1024; // 50 MiBs, Make sure to set the sending chunk size larger than the chunk buffer size to avoid unnecessary iterations in the while loop.
    let currentChunkSize = 0;
    let sendingChunk = Buffer.alloc(sendingChunkSize);

    jsonStream.on(&apos;data&apos;, (chunk: Buffer) =&gt; {
      while (chunk.byteLength &gt; 0) {
        const availableSpace = sendingChunkSize - currentChunkSize;

        if (chunk.byteLength &lt;= availableSpace) {
          // If the chunk fits within the remaining space
          chunk.copy(sendingChunk, currentChunkSize);
          currentChunkSize += chunk.byteLength;
          break;
        }

        // Fill the remaining space in the sendingChunk
        chunk.subarray(0, availableSpace).copy(sendingChunk, currentChunkSize);
        currentChunkSize += availableSpace;

        // Send the filled buffer
        const writable = res.write(sendingChunk);

        if (!writable) {
          jsonStream.pause();
        }

        // Reset for the next chunk
        sendingChunk = Buffer.alloc(sendingChunkSize);
        currentChunkSize = 0;
        chunk = chunk.subarray(availableSpace); // Process the rest of the chunk
      }
    });

    res.on(&apos;drain&apos;, () =&gt; {
      jsonStream.resume(); // Resume when writable
    });

    jsonStream.on(&apos;end&apos;, () =&gt; {
      // Send any remaining data that hasn&apos;t been flushed
      if (currentChunkSize &gt; 0) {
        res.write(sendingChunk.subarray(0, currentChunkSize));
      }
      res.end();
    });

    jsonStream.on(&apos;error&apos;, (err) =&gt; {
      console.error(err); // Log error for debugging
      res.end();
    });
  }
}
</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">src/buffer.service.ts</span></p></figcaption></figure><p>Oh no, what&#x2019;s going on here? It looks so complicated!</p><p>Well, that&#x2019;s because it <em>is</em> a bit tricky! But here&#x2019;s the backstory: we received a request from our boss to send file downloads in chunks of <strong>50 MiB</strong>, because he has blazing-fast internet, he wants to take full advantage of it.</p><p>So, what&#x2019;s the plan?</p><p>We start by setting our sending chunk size to <strong>50 MiB</strong>. Next, we initialize a <code>currentChunkSize</code> variable to <code>0</code> and create a new buffer object, allocating exactly <strong>50 MiB</strong> of memory.</p><p>Now, here&#x2019;s where it gets interesting. Since we&#x2019;re using fixed-size buffers, we need to ensure that all the JSON data fits perfectly within the allocated space. To do this, we calculate the available space in the current sending chunk. As long as the JSON chunk fits within the remaining space, we copy it to the sending chunk and update the <code>currentChunkSize</code>:</p><pre><code class="language-typescript">chunk.copy(sendingChunk, currentChunkSize);
currentChunkSize += chunk.byteLength;
</code></pre><p>The <code>copy()</code> method copies the bytes from the <code>chunk</code> buffer into <code>sendingChunk</code>, starting at the <code>currentChunkSize</code> (which marks the first free byte in the buffer).</p><h3 id="what-if-the-available-space-is-larger-than-the-json-chunk">What if the available space is larger than the JSON chunk?</h3><p>This is where things get a bit messy! In this case, we need to extract exactly the right number of bytes from the JSON chunk to fill the available space in the sending chunk. Once it&#x2019;s perfectly full, we send the chunk to the HTTP response.</p><p>After that, we reset the <code>sendingChunk</code> to a fresh buffer and set <code>currentChunkSize</code> back to <code>0</code>.</p><h3 id="but-what-about-the-rest-of-the-json-chunk">But what about the rest of the JSON chunk?</h3><p>Good question! Any remaining data in the JSON chunk is processed in the next iteration of our loop. The loop ensures that this remaining data is added to the new <code>sendingChunk</code>. This loop continues as long as the JSON chunk is larger than the available space in the buffer.</p><p>Technically, we could set the buffer size to send chunks as small as <strong>1 byte</strong> to the response stream, but that would be incredibly inefficient. So, to avoid unnecessary iterations in the while loop, it&#x2019;s important to set a reasonable chunk size.</p><h3 id="demo">Demo</h3><p>For the demo, let&apos;s <a href="https://github.com/zenstok/nestjs-big-file-download?ref=rabbitbyte.club">git clone the project here</a>. After cloning, install the dependencies using <code>npm install</code>, and then run the project with <code>npm run start</code>.</p><p>On the index page, you&#x2019;ll find a tutorial that explains how to call each version in action, as shown in the screenshot below:</p><figure class="kg-card kg-image-card"><img src="https://rabbitbyte.club/content/images/2024/10/image.png" class="kg-image" alt="How to Create and Download Files of Unlimited Size in node.js/NestJS" loading="lazy" width="2000" height="456" srcset="https://rabbitbyte.club/content/images/size/w600/2024/10/image.png 600w, https://rabbitbyte.club/content/images/size/w1000/2024/10/image.png 1000w, https://rabbitbyte.club/content/images/size/w1600/2024/10/image.png 1600w, https://rabbitbyte.club/content/images/2024/10/image.png 2000w" sizes="(min-width: 720px) 720px"></figure><hr><h3 id="conclusion">Conclusion</h3><p>By leveraging the power of Node.js streams, we can deliver files of virtually any size without consuming large amounts of memory. Whether you&apos;re aiming for simplicity, performance, or handling large buffer sizes, these approaches have you covered.</p><p>I hope this guide helped you understand how to work with large file downloads in NestJS. Have any ideas or topics you&apos;d like me to cover next? Drop a comment! And don&#x2019;t forget to subscribe to my newsletter on <a href="https://rabbitbyte.club/">rabbitbyte.club</a> for more tips!</p>]]></content:encoded></item><item><title><![CDATA[Part 3/3: How to Implement Refresh Tokens through Http-Only Cookie in NestJS and React]]></title><description><![CDATA[<p>Hello everyone,</p><p>Welcome to the final episode of our three-part series on token management in a <strong>NestJS + React application</strong>. In the <a href="https://rabbitbyte.club/how-to-implement-refresh-tokens-with-token-rotation-in-nestjs/" rel="noreferrer">first two posts</a>, we walked through the process of implementing refresh token logic by storing tokens in local storage. Today, we&#x2019;ll advance to a more secure method</p>]]></description><link>https://rabbitbyte.club/how-to-implement-refresh-tokens-through-http-only-cookie-in-nestjs-and-react/</link><guid isPermaLink="false">66d55f5fb71d68f7ff430c78</guid><category><![CDATA[NestJs]]></category><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Mon, 02 Sep 2024 06:50:57 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2024/09/nest-logo.png" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2024/09/nest-logo.png" alt="Part 3/3: How to Implement Refresh Tokens through Http-Only Cookie in NestJS and React"><p>Hello everyone,</p><p>Welcome to the final episode of our three-part series on token management in a <strong>NestJS + React application</strong>. In the <a href="https://rabbitbyte.club/how-to-implement-refresh-tokens-with-token-rotation-in-nestjs/" rel="noreferrer">first two posts</a>, we walked through the process of implementing refresh token logic by storing tokens in local storage. Today, we&#x2019;ll advance to a more secure method by implementing HTTP-only cookies for refresh tokens.</p><h3 id="why-use-http-only-cookies">Why Use HTTP-Only Cookies?</h3><p>HTTP-only cookies allow us to store sensitive data, such as refresh tokens, in a way that cannot be accessed by JavaScript. This means that even if there are vulnerabilities in your code or third-party libraries, a hacker won&apos;t be able to retrieve the refresh token.</p><p>However, even with HTTP-only cookies, there&apos;s still a risk of requests being made on behalf of the user through XSS attacks. The key advantage is that hackers will not be able to access the value of the refresh token directly; they would need to execute targeted XSS attacks on the application&apos;s endpoints, which requires prior knowledge of the system.</p><h4 id="why-not-store-both-access-and-refresh-tokens-in-http-only-cookies">Why Not Store Both Access and Refresh Tokens in HTTP-Only Cookies?</h4><p>By keeping the access token out of cookies, we protect against CSRF (Cross-Site Request Forgery) attacks. To mitigate XSS attacks, we set a short expiry time on the access token, limiting the damage even if it&#x2019;s compromised.</p><h4 id="should-we-worry-about-csrf-on-the-refresh-tokens-endpoint">Should We Worry About CSRF on the /refresh-tokens Endpoint?</h4><p>Not really. Since this endpoint doesn&#x2019;t compromise the system or user data, CSRF attacks here are less of a concern.</p><h4 id="enhancing-security-hashing-refresh-tokens">Enhancing Security: Hashing Refresh Tokens</h4><p>To further improve security, we also hash the refresh tokens in the database. We use a hashing algorithm without salt, allowing us to reproduce the same hash for previously used tokens to check if they have been blacklisted. With this improvement, in case of database leaks, the hacker will not be able to read any refresh tokens!</p><h3 id="getting-started">Getting Started</h3><p>To begin, ensure you&apos;ve completed the guides in <a href="https://rabbitbyte.club/how-to-implement-refresh-tokens-with-token-rotation-in-nestjs/" rel="noreferrer">Part 1</a> and <a href="https://rabbitbyte.club/how-to-integrate-refresh-tokens-in-react-app/" rel="noreferrer">Part 2</a> for app installation. Afterward, follow these steps to implement the necessary changes.</p><p>If you want to jump straight into the code, you can <a href="https://github.com/zenstok/nestjs-auth-refresh-token-example/tree/part-3?ref=rabbitbyte.club">check out the repository here on the &quot;part-3&quot; branch</a>.</p><h4 id="step-1-define-cookie-configuration-and-helper-function">Step 1: Define Cookie Configuration and Helper Function</h4><p>First, set up a cookie configuration and create a helper function to extract the refresh token from cookies:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">import { Request } from &apos;express&apos;;

export const cookieConfig = {
  refreshToken: {
    name: &apos;refreshToken&apos;,
    options: {
      path: &apos;/&apos;, // For production, use &apos;/auth/api/refresh-tokens&apos;. We use &apos;/&apos; for localhost in order to work on Chrome.
      httpOnly: true,
      sameSite: &apos;strict&apos; as &apos;strict&apos;,
      secure: true,
      maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days; must match Refresh JWT expiration.
    },
  },
};

export const extractRefreshTokenFromCookies = (req: Request) =&gt; {
  const cookies = req.headers.cookie?.split(&apos;; &apos;);
  if (!cookies?.length) {
    return null;
  }

  const refreshTokenCookie = cookies.find((cookie) =&gt;
    cookie.startsWith(`${cookieConfig.refreshToken.name}=`)
  );

  if (!refreshTokenCookie) {
    return null;
  }

  return refreshTokenCookie.split(&apos;=&apos;)[1] as string;
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/api/constants/cookies.ts</span></p></figcaption></figure><h4 id="step-2-enable-cors-to-accept-cookies">Step 2: Enable CORS to Accept Cookies</h4><p>Ensure the CORS policy allows credentials so that our backend can receive cookies from the frontend:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">app.enableCors({ origin: true, credentials: true }); // Set the correct origin for production.
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/main.ts</span></p></figcaption></figure><h4 id="step-3-update-the-token-generation-method">Step 3: Update the Token Generation Method</h4><p>Modify the <code>generateTokenPair</code> method to set the refresh token as an HTTP-only cookie whenever it&apos;s called:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">async generateTokenPair(
  user: Express.User,
  res: Response,
  currentRefreshToken?: string,
  currentRefreshTokenExpiresAt?: Date,
) {
  const payload = { sub: user.id, role: user.role };

  res.cookie(
    cookieConfig.refreshToken.name,
    await this.generateRefreshToken(
      user,
      currentRefreshToken,
      currentRefreshTokenExpiresAt,
    ),
    {
      ...cookieConfig.refreshToken.options,
    },
  );

  return {
    access_token: this.jwtService.sign(payload), // JWT module is configured in auth.module.ts for access token.
  };
}
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/modules/authentication/auth-refresh-token.service.ts</span></p></figcaption></figure><h4 id="step-4-update-jwt-strategy-to-use-cookies">Step 4: Update JWT Strategy to Use Cookies</h4><p>Modify the refresh JWT strategy to retrieve the token from cookies instead of using bearer token authentication:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">jwtFromRequest: ExtractJwt.fromExtractors([
  (req: Request) =&gt; extractRefreshTokenFromCookies(req),
]),
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/modules/authentication/strategies/jwt-refresh.strategy.ts</span></p></figcaption></figure><h4 id="step-5-adjust-token-handling-and-add-cookie-clearing-endpoint">Step 5: Adjust Token Handling and Add Cookie Clearing Endpoint</h4><p>When calling the <code>/refresh-tokens</code> endpoint, the token should now be extracted from the HTTP-only cookie, replacing the previous method of using Bearer authorization.</p><p>Additionally, we must implement a <code>/clear-auth-cookie</code> endpoint to remove the cookie. This endpoint should be invoked during the logout action on the frontend. The reason for this is that HTTP-only cookies cannot be cleared programatically from the frontend, so this ensures the user is properly logged out.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">@Public()
@UseGuards(JwtRefreshAuthGuard)
@Post(&apos;refresh-tokens&apos;)
refreshTokens(
  @Req() req: Request,
  @Res({ passthrough: true }) res: Response,
) {
  if (!req.user) {
    throw new InternalServerErrorException();
  }

  return this.authRefreshTokenService.generateTokenPair(
    (req.user as any).attributes,
    res,
    extractRefreshTokenFromCookies(req) as string,
    (req.user as any).refreshTokenExpiresAt,
  );
}

@Public()
@Post(&apos;clear-auth-cookie&apos;)
clearAuthCookie(@Res({ passthrough: true }) res: Response) {
  res.clearCookie(cookieConfig.refreshToken.name);
}
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/modules/authentication/authentication.controller.ts</span></p></figcaption></figure><h4 id="step-6-update-frontend-logic">Step 6: Update Frontend Logic</h4><p>Now regarding the frontend project, we will actually simplify the logic a little bit.</p><ol><li><strong>Remove Refresh Token from Local Storage:</strong></li></ol><p>Clear any references to the refresh token in the <code>AuthClientStore</code> class:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const ACCESS_TOKEN_KEY = &quot;rabbit.byte.club.access.token&quot;;

class AuthClientStore {
  static getAccessToken() {
    return localStorage.getItem(ACCESS_TOKEN_KEY);
  }

  static setAccessToken(token: string) {
    localStorage.setItem(ACCESS_TOKEN_KEY, token);
  }

  static removeAccessToken(): void {
    localStorage.removeItem(ACCESS_TOKEN_KEY);
  }
}

export default AuthClientStore;
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/client-store/auth-client-store.ts</span></p></figcaption></figure><ol start="2"><li><strong>Update Request Methods:</strong></li></ol><p>Remove the <code>refreshToken</code> variable from the <code>sendProtectedRequest</code> method, as it&apos;s no longer needed.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const sendProtectedRequest = (
  method: ApiMethod,
  path: string,
  // eslint-disable-next-line
  body?: any,
  init?: RequestInit,
) =&gt; {
  const authToken = AuthClientStore.getAccessToken();
  if (!authToken) {
    throw new Error(&quot;No auth token found&quot;);
  }

  return sendRequest(method, path, body, authToken, init);
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/api/hooks/use-api.ts</span></p></figcaption></figure><ol start="3"><li><strong>Include Credentials in Requests:</strong></li></ol><p>Very important: Update the <code>login</code> and <code>refresh token</code> API integration methods to include credentials, ensuring that cookies can be set and sent correctly.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const login = async (email: string, password: string) =&gt; {
 const response = await sendRequest(
   ApiMethod.POST,
   routes.auth.login,
   {
     email,
     password,
   },
   undefined,
   { credentials: &quot;include&quot; }, // Required update
 );

 AuthClientStore.setAccessToken(response.access_token);

 return response;
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const refreshTokens = async () =&gt; {
 clearTimeout(timeout);
 if (!debouncedPromise) {
   debouncedPromise = new Promise((resolve, reject) =&gt; {
     debouncedResolve = resolve;
     debouncedReject = reject;
   });
 }

 timeout = setTimeout(() =&gt; {
   const executeLogic = async () =&gt; {
     const response = await sendRequest(
       ApiMethod.POST,
       routes.auth.refreshTokens,
       undefined,
       undefined,
       { credentials: &quot;include&quot; }, // Required update
     );

     AuthClientStore.setAccessToken(response.access_token);
   };

   executeLogic().then(debouncedResolve).catch(debouncedReject);

   debouncedPromise = null;
 }, 200);

 return debouncedPromise;
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><ol start="4"><li><strong>Define Clear Auth Cookie API Call:</strong></li></ol><p>Implement the <code>clearAuthCookie</code> API call:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const clearAuthCookie = () =&gt; {
  return sendRequest(
    ApiMethod.POST,
    routes.auth.clearAuthCookie,
    undefined,
    undefined,
    { credentials: &quot;include&quot; },
  );
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><ol start="5"><li><strong>Call <code>clearAuthCookie</code> on Logout:</strong></li></ol><p>Ensure the auth cookie is cleared on logout:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const logout = () =&gt; {
  AuthClientStore.removeAccessToken();
  return clearAuthCookie();
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><p>And that&apos;s it! With these steps, the refresh token logic is now integrated into an HTTP-only cookie. You can proceed to <a href="https://rabbitbyte.club/how-to-integrate-refresh-tokens-in-react-app/" rel="noreferrer">Part 2</a> of the tutorial to test the solution.</p><p>If you&apos;d like me to cover more interesting topics about the node.js ecosystem, feel free to leave your suggestions in the comments section. Don&apos;t forget to subscribe to my newsletter on <a>rabbitbyte.club</a> for updates!</p>]]></content:encoded></item><item><title><![CDATA[Part 2/3: How to Integrate Refresh Tokens in React]]></title><description><![CDATA[<p>Hello, guys!</p><p>On the premise that our App is immune to XSS attacks, we will store both access &amp; refresh tokens in the local storage. For this, we will use React which escapes any values embedded in JSX before rendering them, greatly helping us in countering XSS attacks.</p><p>This is</p>]]></description><link>https://rabbitbyte.club/how-to-integrate-refresh-tokens-in-react-app/</link><guid isPermaLink="false">665dedafb71d68f7ff430ace</guid><category><![CDATA[React]]></category><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Tue, 30 Jul 2024 06:57:37 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2024/07/react-logo.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2024/07/react-logo.jpeg" alt="Part 2/3: How to Integrate Refresh Tokens in React"><p>Hello, guys!</p><p>On the premise that our App is immune to XSS attacks, we will store both access &amp; refresh tokens in the local storage. For this, we will use React which escapes any values embedded in JSX before rendering them, greatly helping us in countering XSS attacks.</p><p>This is the second episode in our three-part series on implementing refresh tokens. In our <a href="https://rabbitbyte.club/how-to-implement-refresh-tokens-with-token-rotation-in-nestjs/" rel="noreferrer">previous article</a>, we explored how to implement refresh tokens in NestJS. Be sure to check it out if you&apos;re looking to easily run the demo from this tutorial.</p><p>While storing both access and refresh tokens in local storage is convenient, it does come with security risks. Even with robust XSS attack prevention, there&apos;s still a vulnerability to attacks via third-party libraries. Fortunately, in the final episode of this series, we&apos;ll demonstrate how to securely store refresh tokens using HTTP-only cookies, which enhances security.</p><h3 id="implementation-overview">Implementation overview</h3><p>To keep the application straightforward, we&apos;ve implemented only the core features necessary to demonstrate the functionality. Here&#x2019;s what we aim to achieve and some potential caveats to be aware of:</p><h4 id="objectives">Objectives:</h4><ol><li><strong>Persistent Authentication</strong>: We want the user to remain authenticated even after refreshing the page.</li><li><strong>Automatic Logout</strong>: The user should be logged out automatically when an API endpoint returns a 401 Unauthorized response.</li><li><strong>Seamless Data Access</strong>: When calling a protected endpoint, the app should retrieve data if a valid refresh token exists. If the first request returns a 401, the app should attempt to refresh the tokens. Only if the subsequent request after refreshing also fails with a 401 should the user be logged out.</li></ol><h4 id="potential-caveat">Potential Caveat:</h4><p>A notable issue is that the refresh token endpoint call invalidates the previous refresh token. This can create a problem if <code>/auth/refresh-tokens</code> is called more than once concurrently, as it may lead to inconsistencies or failures in token handling. To mitigate this, we must ensure that the refresh token is not requested multiple times concurrently, even if multiple protected requests are made across the app.</p><p>If you want to jump directly to the GitHub repo to see how we did it, you can <a href="https://github.com/zenstok/react-auth-refresh-token-example?ref=rabbitbyte.club">check it out here</a>.</p><h3 id="getting-started">Getting started</h3><p>Since the authentication state is a global concept in the app, we need to ensure that every component can access this state. The best way to manage this is by using React Context to define a global context provider. We will look into this later in the tutorial.<br><br>First, we define a class responsible for managing the manipulation of local storage keys for access and refresh tokens:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const ACCESS_TOKEN_KEY = &quot;rabbit.byte.club.access.token&quot;;
const REFRESH_TOKEN_KEY = &quot;rabbit.byte.club.refresh.token&quot;;

class AuthClientStore {
  static getAccessToken() {
    return localStorage.getItem(ACCESS_TOKEN_KEY);
  }

  static setAccessToken(token: string) {
    localStorage.setItem(ACCESS_TOKEN_KEY, token);
  }

  static removeAccessToken(): void {
    localStorage.removeItem(ACCESS_TOKEN_KEY);
  }

  static getRefreshToken() {
    return localStorage.getItem(REFRESH_TOKEN_KEY);
  }

  static setRefreshToken(token: string) {
    localStorage.setItem(REFRESH_TOKEN_KEY, token);
  }

  static removeRefreshToken(): void {
    localStorage.removeItem(REFRESH_TOKEN_KEY);
  }
}

export default AuthClientStore;
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/client-store/auth-client-store.ts</span></p></figcaption></figure><p>Next, we define a <code>useApi</code> hook, which abstracts how requests are sent to our app server, both protected and unprotected:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">import AuthClientStore from &quot;../../auth/client-store/auth-client-store.ts&quot;;
import { ApiMethod } from &quot;../types.ts&quot;;

const apiUrl = import.meta.env.VITE_API_BASE_URL as string;

const sendRequest = (
  method: ApiMethod,
  path: string,
  // eslint-disable-next-line
  body?: any,
  authToken?: string | null,
  init?: RequestInit,
) =&gt; {
  return fetch(apiUrl + path, {
    method,
    ...(body &amp;&amp; { body: JSON.stringify(body) }),
    ...init,
    headers: {
      &quot;Content-Type&quot;: &quot;application/json&quot;,
      ...(authToken &amp;&amp; { Authorization: `Bearer ${authToken}` }),
      ...init?.headers,
    },
  }).then((response) =&gt; {
    if (response.status &gt;= 400) {
      throw response;
    }
    return response.json();
  });
};

const sendProtectedRequest = (
  method: ApiMethod,
  path: string,
  // eslint-disable-next-line
  body?: any,
  useRefreshToken = false,
  init?: RequestInit,
) =&gt; {
  const authToken = useRefreshToken
    ? AuthClientStore.getRefreshToken()
    : AuthClientStore.getAccessToken();
  if (!authToken) {
    throw new Error(&quot;No auth token found&quot;);
  }

  return sendRequest(method, path, body, authToken, init);
};

export const useApi = () =&gt; {
  return { sendRequest, sendProtectedRequest };
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/api/hooks/use-api.ts</span></p></figcaption></figure><p>Please note that the <code>sendProtectedRequest</code> method accepts an optional <code>useRefreshToken</code> parameter. This is used exclusively by routes that refresh the token, as they require the refresh token for bearer authentication. In all other cases, the default behavior is to use the access token.</p><p>Now we need to define a useAuthApi hook where the magic happens.</p><p>First, we will use useApi hook from which we need to call sendRequest and sendProtectedRequest methods:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">export const useAuthApi = () =&gt; {
  const { sendRequest, sendProtectedRequest } = useApi();
}
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><h5 id="implementing-authentication-login-logout-and-token-refresh">Implementing Authentication: Login, Logout, and Token Refresh</h5><p>Now, let&apos;s define the <code>login</code> function, which, upon successful authentication, will set the access and refresh tokens in the local storage.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">export const useAuthApi = () =&gt; {
  const { sendRequest, sendProtectedRequest } = useApi();

  const login = async (email: string, password: string) =&gt; {
    const response = await sendRequest(ApiMethod.POST, routes.auth.login, {
      email,
      password,
    });

    AuthClientStore.setAccessToken(response.access_token);
    AuthClientStore.setRefreshToken(response.refresh_token);

    return response;
  };
}
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><p>Next, the <code>logout</code> function is straightforward: it simply removes the access and refresh tokens from local storage.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">export const useAuthApi = () =&gt; {
  const { sendRequest, sendProtectedRequest } = useApi();

  const login = async (email: string, password: string) =&gt; {
    const response = await sendRequest(ApiMethod.POST, routes.auth.login, {
      email,
      password,
    });

    AuthClientStore.setAccessToken(response.access_token);
    AuthClientStore.setRefreshToken(response.refresh_token);

    return response;
  };

  const logout = () =&gt; {
    AuthClientStore.removeAccessToken();
    AuthClientStore.removeRefreshToken();
  };
}
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><p>An essential method to define is <code>refreshTokens</code>, which has a simple logic similar to the <code>login</code> function:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const refreshTokens = async () =&gt; {
  const response = await sendProtectedRequest(
    ApiMethod.POST,
    routes.auth.refreshTokens,
    undefined,
    AuthClientStore.getRefreshToken(),
  );

  AuthClientStore.setAccessToken(response.access_token);
  AuthClientStore.setRefreshToken(response.refresh_token);
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><p>Note that because the refresh tokens endpoint requires the refresh token as an authentication method, we pass the refresh token as a parameter to <code>sendProtectedRequest</code> so it uses the refresh token as the bearer instead of the default access token used for other requests.</p><p>Is this all we need for the <code>refreshTokens</code> method? Well, keep in mind that each call to refresh the access token will invalidate the previous refresh tokens. So, if two parts of the app concurrently need to refresh the access token, one request might fail because the first request will invalidate the refresh token being used. Therefore, it&apos;s crucial to ensure this method is called only once. To manage this logic efficiently, we need to decorate this method a little bit.</p><h5 id="debouncing-refresh-tokens-requests-and-managing-authentication-state">Debouncing Refresh Tokens Requests and Managing Authentication State</h5><p>To handle cases where multiple parts of the app might concurrently call the refresh tokens method, we need to debounce these calls so that only one request is made. We also need to ensure that each caller receives the same access and refresh token pair. Here&apos;s how we achieve this:</p><p>First, we define some variables outside of the hook to manage the debounce logic:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">/*
 * These variables are used to debounce the refreshTokens function
 */
let debouncedPromise: Promise&lt;unknown&gt; | null = null;
let debouncedResolve: (...args: unknown[]) =&gt; void;
let debouncedReject: (...args: unknown[]) =&gt; void;
let timeout: number;
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><p>Now, we update the <code>refreshTokens</code> method to include debouncing:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const refreshTokens = async () =&gt; {
  clearTimeout(timeout);
  if (!debouncedPromise) {
    debouncedPromise = new Promise((resolve, reject) =&gt; {
      debouncedResolve = resolve;
      debouncedReject = reject;
    });
  }

  timeout = setTimeout(() =&gt; {
    const executeLogic = async () =&gt; {
      const response = await sendProtectedRequest(
        ApiMethod.POST,
        routes.auth.refreshTokens,
        undefined,
        AuthClientStore.getRefreshToken(),
      );

      AuthClientStore.setAccessToken(response.access_token);
      AuthClientStore.setRefreshToken(response.refresh_token);
    };

    executeLogic().then(debouncedResolve).catch(debouncedReject);

    debouncedPromise = null;
  }, 200);

  return debouncedPromise;
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><p>Here&#x2019;s how it works:</p><ul><li>We clear the timeout whenever a new caller invokes this method within a 200ms window, effectively debouncing the calls.</li><li>The <code>debouncedPromise</code> ensures that all callers receive the same promise, which resolves or rejects when the token refresh logic completes.</li><li>After processing, <code>debouncedPromise</code> is reset to handle new calls later.</li></ul><p>Next, we define a method that acts as a gatekeeper for protected API routes. It attempts a request and, if it fails with a 401 (Unauthorized) error, refreshes the access token and retries the request:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const sendAuthGuardedRequest = async (
  userIsNotAuthenticatedCallback: () =&gt; void,
  method: ApiMethod,
  path: string,
  body?: any,
  init?: RequestInit,
) =&gt; {
  try {
    return await sendProtectedRequest(method, path, body, undefined, init);
  } catch (e) {
    if (e?.status === 401) {
      try {
        await refreshTokens();
      } catch (e) {
        userIsNotAuthenticatedCallback();
        throw e;
      }
      return await sendProtectedRequest(method, path, body, undefined, init);
    }

    throw e;
  }
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><p>The <code>userIsNotAuthenticatedCallback</code> parameter allows the authentication context provider to update the global auth state, which any component in the app can listen to.</p><p>Finally, we define a method for checking if the user is authenticated by calling the <code>/auth/me</code> endpoint. This should be executed on app startup:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">const me = (userIsNotAuthenticatedCallback: () =&gt; void) =&gt; {
  return sendAuthGuardedRequest(
    userIsNotAuthenticatedCallback,
    ApiMethod.GET,
    routes.auth.me,
  ) as Promise&lt;User&gt;;
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><p>Our hook is now complete, this is the full version of it:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">/*
 * These variables are used to debounce the refreshTokens function
 */
let debouncedPromise: Promise&lt;unknown&gt; | null;
let debouncedResolve: (...args: unknown[]) =&gt; void;
let debouncedReject: (...args: unknown[]) =&gt; void;
let timeout: number;

export const useAuthApi = () =&gt; {
  const { sendRequest, sendProtectedRequest } = useApi();

  const login = async (email: string, password: string) =&gt; {
    const response = await sendRequest(ApiMethod.POST, routes.auth.login, {
      email,
      password,
    });

    AuthClientStore.setAccessToken(response.access_token);
    AuthClientStore.setRefreshToken(response.refresh_token);

    return response;
  };

  const logout = () =&gt; {
    AuthClientStore.removeAccessToken();
    AuthClientStore.removeRefreshToken();
  };
  const refreshTokens = async () =&gt; {
    clearTimeout(timeout);
    if (!debouncedPromise) {
      debouncedPromise = new Promise((resolve, reject) =&gt; {
        debouncedResolve = resolve;
        debouncedReject = reject;
      });
    }

    timeout = setTimeout(() =&gt; {
      const executeLogic = async () =&gt; {
        const response = await sendProtectedRequest(
          ApiMethod.POST,
          routes.auth.refreshTokens,
          undefined,
          AuthClientStore.getRefreshToken(),
        );

        AuthClientStore.setAccessToken(response.access_token);
        AuthClientStore.setRefreshToken(response.refresh_token);
      };

      executeLogic().then(debouncedResolve).catch(debouncedReject);

      debouncedPromise = null;
    }, 200);

    return debouncedPromise;
  };

  const sendAuthGuardedRequest = async (
    userIsNotAuthenticatedCallback: () =&gt; void,
    method: ApiMethod,
    path: string,
    // eslint-disable-next-line
    body?: any,
    init?: RequestInit,
  ) =&gt; {
    try {
      return await sendProtectedRequest(method, path, body, undefined, init);
    } catch (e) {
      if (e?.status === 401) {
        try {
          await refreshTokens();
        } catch (e) {
          userIsNotAuthenticatedCallback();
          throw e;
        }
        return await sendProtectedRequest(method, path, body, undefined, init);
      }

      throw e;
    }
  };

  const me = (userIsNotAuthenticatedCallback: () =&gt; void) =&gt; {
    return sendAuthGuardedRequest(
      userIsNotAuthenticatedCallback,
      ApiMethod.GET,
      routes.auth.me,
    ) as Promise&lt;User&gt;;
  };

  return { login, logout, me, sendAuthGuardedRequest };
};

</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-api.ts</span></p></figcaption></figure><h5 id="implementing-the-auth-provider-component">Implementing the Auth Provider component</h5><p>Now let&apos;s have a look at our auth provider component, responsible with our global auth state.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">type ContextType = {
  isAuthenticated: boolean;
  login(email: string, password: string): Promise&lt;void&gt;;
  logout(): void;
  me(): Promise&lt;User&gt;;
  sendAuthGuardedRequest(
    method: ApiMethod,
    path: string,
    // eslint-disable-next-line
    body?: any,
    init?: RequestInit,
  ): Promise&lt;unknown&gt;;
};

const AuthContext = createContext&lt;ContextType | undefined&gt;(undefined);

function AuthProvider({ children }: { children: ReactNode }) {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const {
    login: authLogin,
    logout: authLogout,
    me: authMe,
    sendAuthGuardedRequest: authSendAuthGuardedRequest,
  } = useAuthApi();

  const login = async (email: string, password: string) =&gt; {
    try {
      await authLogin(email, password);
      setIsAuthenticated(true);
    } catch (e) {
      setIsAuthenticated(false);
      throw e;
    }
  };

  const logout = () =&gt; {
    authLogout();
    setIsAuthenticated(false);
  };

  const me = async () =&gt; {
    const user = await authMe(() =&gt; {
      setIsAuthenticated(false);
    });
    setIsAuthenticated(true);

    return user;
  };

  const sendAuthGuardedRequest = async (
    method: ApiMethod,
    path: string,
    // eslint-disable-next-line
    body?: any,
    init?: RequestInit,
  ) =&gt; {
    return authSendAuthGuardedRequest(
      () =&gt; {
        setIsAuthenticated(false);
      },
      method,
      path,
      body,
      init,
    );
  };

  return (
    &lt;AuthContext.Provider
      value={{
        isAuthenticated,
        login,
        logout,
        me,
        sendAuthGuardedRequest,
      }}
    &gt;
      {children}
    &lt;/AuthContext.Provider&gt;
  );
}

export { AuthProvider, AuthContext };
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/providers/auth-provider.tsx</span></p></figcaption></figure><p>In this implementation, we&#x2019;ve essentially wrapped the authentication API hook methods and manage the <code>isAuthenticated</code> state based on API responses. The final method in this component is very important: it is used by all other hooks or components that need to perform protected requests to our API. By incorporating the <code>userIsNotAuthenticated</code> callback, we ensure that when an endpoint call fails due to token expiration, the authentication state is updated. This approach allows the <code>isAuthenticated</code> state to be set to <code>false</code>, prompting all components across the app to adjust their behavior accordingly.</p><p>Next, we&#x2019;ll define a <code>useAuthContext</code> hook to simplify access to the authentication state:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">export const useAuthContext = () =&gt; {
  const ctx = useContext(AuthContext);

  if (!ctx) {
    throw new Error(&quot;useAuthContext must be within AuthProvider&quot;);
  }

  return ctx;
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/auth/hooks/use-auth-context.ts</span></p></figcaption></figure><h5 id="wrapping-your-app-with-authprovider">Wrapping Your App with AuthProvider</h5><p>Ensure your application is wrapped with the <code>AuthProvider</code> to provide authentication state to all components.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">ReactDOM.createRoot(document.getElementById(&quot;root&quot;)!).render(
  &lt;React.StrictMode&gt;
    &lt;AuthProvider&gt;
      &lt;App /&gt;
    &lt;/AuthProvider&gt;
  &lt;/React.StrictMode&gt;,
);

</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/main.tsx</span></p></figcaption></figure><p>Next, we&#x2019;ll define a <code>useUserApi</code> hook to handle API methods related to user operations:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">export const useUserApi = () =&gt; {
  const { sendAuthGuardedRequest } = useAuthContext();

  const findAllUsers = async (
    limit: number,
    offset: number,
  ): Promise&lt;FindAllUsersResponse&gt; =&gt; {
    const queryString = buildQueryParams([
      { key: &quot;limit&quot;, value: limit.toString() },
      { key: &quot;offset&quot;, value: offset.toString() },
    ]);

    return sendAuthGuardedRequest(
      ApiMethod.GET,
      routes.user.findAll + queryString,
    );
  };

  const findOneUser = async (id: number): Promise&lt;User&gt; =&gt; {
    return sendAuthGuardedRequest(ApiMethod.GET, routes.user.findOne(id));
  };

  return { findAllUsers, findOneUser };
};
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/features/user/hooks/use-user-api.ts</span></p></figcaption></figure><p>Note how we are using the <code>sendAuthGuardedRequest</code> method from the auth context.</p><p>Now, let&apos;s take a look at our <code>App</code> component.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">function App() {
  const { isAuthenticated, login, logout, me } = useAuthContext();
  const [appIsLoading, setAppIsLoading] = useState(true);

  const { findAllUsers } = useUserApi();

  useEffect(() =&gt; {
    me()
      .catch(() =&gt; {})
      .finally(() =&gt; setAppIsLoading(false));
  }, []);

  if (appIsLoading) {
    return &lt;div&gt;Loading...&lt;/div&gt;;
  }

  if (!isAuthenticated) {
    return (
      &lt;form
        style={ display: &quot;flex&quot;, flexDirection: &quot;column&quot;, gap: 16 }
        onSubmit={(e) =&gt; {
          e.preventDefault();
          login(e.target[0].value, e.target[1].value);
        }}
      &gt;
        &lt;div&gt;Authentication&lt;/div&gt;
        &lt;input placeholder=&quot;Email&quot; /&gt;
        &lt;input placeholder=&quot;Password&quot; type=&quot;password&quot; /&gt;
        &lt;button type=&quot;submit&quot;&gt;Login&lt;/button&gt;
      &lt;/form&gt;
    );
  }

  return (
    &lt;&gt;
      &lt;div&gt;
        &lt;a href=&quot;https://rabbitbyte.club&quot; target=&quot;_blank&quot;&gt;
          &lt;img
            src={rabitByteClubLogo}
            className=&quot;logo&quot;
            alt=&quot;logo-rabbit-byte&quot;
          /&gt;
        &lt;/a&gt;
      &lt;/div&gt;
      &lt;h1&gt;Rabbit Byte Club&lt;/h1&gt;
      &lt;div style={{ display: &quot;flex&quot;, gap: 16 }}&gt;
        &lt;button
          onClick={() =&gt; {
            for (let i = 0; i &lt; 5; i++) {
              findAllUsers(10, 0);
            }
          }}
        &gt;
          Simulate 5 concurrent requests
        &lt;/button&gt;
        &lt;button onClick={() =&gt; logout()}&gt;Logout&lt;/button&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
}

export default App;
</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/App.tsx</span></p></figcaption></figure><p>We define an <code>appLoading</code> state, which is set to <code>false</code> once the <code>/auth/me</code> endpoint completes, regardless of success or failure. While the user is not authenticated, we display the login form.</p><p>If the user is authenticated, a button is provided to simulate 5 concurrent requests, allowing you to test the logic easily in the demo.</p><h3 id="demo">DEMO</h3><p><strong>Prerequisites:</strong></p><p>To test the feature quickly and easily, set the JWT access token expiration to 10 seconds in your backend application:</p><pre><code class="language-typescript">JwtModule.registerAsync({
  inject: [ConfigService],
  useFactory: (configService: ConfigService&lt;EnvironmentVariables&gt;) =&gt; ({
    secret: configService.get(&apos;jwtSecret&apos;),
    signOptions: { expiresIn: &apos;10s&apos; },
  }),
}),
</code></pre><p>Set the JWT refresh token expiration to 30 seconds:</p><pre><code class="language-typescript">    const newRefreshToken = this.jwtService.sign(
      { sub: authUserId },
      {
        secret: this.configService.get(&apos;jwtRefreshSecret&apos;),
        expiresIn: &apos;30s&apos;,
      },
    );
</code></pre><p>Additionally, you may want to temporarily disable throttling on the refresh tokens endpoint:</p><pre><code class="language-typescript">// @Throttle({
//   short: { limit: 1, ttl: 1000 },
//   long: { limit: 2, ttl: 60000 },
// })
@ApiBearerAuth()
@Public()
@UseGuards(JwtRefreshAuthGuard)
@Post(&apos;refresh-tokens&apos;)
refreshTokens(@Request() req: ExpressRequest) {
  if (!req.user) {
    throw new InternalServerErrorException();
  }
  return this.authRefreshTokenService.generateTokenPair(
    (req.user as any).attributes,
    req.headers.authorization?.split(&apos; &apos;)[1],
    (req.user as any).refreshTokenExpiresAt,
  );
}
</code></pre><p><strong>Steps to Run the Demo:</strong></p><ol><li><strong>Clone the </strong><a href="https://github.com/zenstok/react-auth-refresh-token-example?ref=rabbitbyte.club"><strong>Github Project</strong></a><strong>:</strong></li></ol><pre><code class="language-bash">git clone https://github.com/zenstok/react-auth-refresh-token-example
</code></pre><ol start="2"><li><strong>Install dependencies:</strong></li></ol><pre><code class="language-bash">npm install
</code></pre><ol start="3"><li><strong>Run the App:</strong></li></ol><pre><code class="language-bash">npm run dev
</code></pre><ol start="4"><li><strong>Ensure Backend is Running:</strong><br>Navigate to the backend directory and run:</li></ol><pre><code class="language-bash">yarn dc up
</code></pre><ol start="5"><li><strong>Fill in users in the database if needed:</strong><br>In a new terminal in backend directory run:</li></ol><pre><code class="language-bash"> yarn dc-db-init
</code></pre><ol start="6"><li><strong>Log in:</strong><br>Enter the following credentials on the login screen:<br><br><strong>Email:</strong> <a>admin@admin.com</a><br><strong>Password:</strong> 1234</li></ol><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9f9frpccao20e12zfrl3.png" class="kg-image" alt="Part 2/3: How to Integrate Refresh Tokens in React" loading="lazy" width="1118" height="682"></figure><p></p><ol start="7"><li><strong>Test Functionality:</strong><br>After logging in, you will see two buttons: <strong>Simulate 5 Concurrent Requests</strong> and <strong>Log Out</strong>.</li></ol><p><br>Open your browser&apos;s Network tab.</p><p>Click <strong>Simulate 5 Concurrent Requests</strong>.</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n8fe8isx1ohvjib9t0m8.png" class="kg-image" alt="Part 2/3: How to Integrate Refresh Tokens in React" loading="lazy" width="1406" height="892"></figure><p></p><p>You will see the effect of the refresh token logic in action.</p><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7c9u2j2qpj6fjjl734br.png" width="1526" height="646" loading="lazy" alt="Part 2/3: How to Integrate Refresh Tokens in React"></div></div></div></figure><p>The app will wait for a single call to the refresh tokens endpoint and then rerun the requests. Success!</p><p><strong>Verification of Objectives:</strong></p><ol><li><strong>Persistent Authentication:</strong> Refresh the page and ensure you remain authenticated. &#x2705;</li><li><strong>Automatic Logout:</strong> Log in and wait for more than 30 seconds. After pressing <strong>Simulate 5 Concurrent Requests</strong>, confirm that you are logged out. &#x2705;<br><br><br><br></li></ol><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ak3u1eww5eqplhg0wna.png" class="kg-image" alt="Part 2/3: How to Integrate Refresh Tokens in React" loading="lazy" width="3108" height="1450"></figure><ol start="3"><li><strong>Seamless Data Access:</strong> When calling a protected endpoint, the app should return data if a valid refresh token exists. &#x2705;</li></ol><h3 id="conclusion">Conclusion<br></h3><p>I hope this tutorial has been helpful in your journey to implement refresh tokens in React. Stay tuned for the final episode of this series, where we&apos;ll swap the backend and frontend logic to use HTTP-only cookies for the refresh token.</p><p>If you&apos;d like me to cover more interesting topics about the node.js ecosystem, feel free to leave your suggestions in the comments section. Don&apos;t forget to subscribe to my newsletter on <a href="rabbitbyte.club" rel="noreferrer">rabbitbyte.club</a> for updates!</p><p><strong>Post Creation:</strong><br><a href="https://rabbitbyte.club/how-to-implement-refresh-tokens-through-http-only-cookie-in-nestjs-and-react/" rel="noreferrer">Check out Part 3 of this series</a>, where we update the app to use HTTP-only cookies for refresh tokens.<br></p>]]></content:encoded></item><item><title><![CDATA[Part 1/3: How to Implement Refresh Tokens with Token Rotation in NestJS]]></title><description><![CDATA[<p>In this episode, we will learn how to implement refresh tokens using local storage as a strategy for storing both access and refresh tokens. If you want to jump directly to the GitHub repo, you can <a href="https://github.com/zenstok/nestjs-auth-refresh-token-example?ref=rabbitbyte.club">access it here</a>.</p><h3 id="prerequisites">Prerequisites</h3><p>Before diving into this guide, it&apos;s important to</p>]]></description><link>https://rabbitbyte.club/how-to-implement-refresh-tokens-with-token-rotation-in-nestjs/</link><guid isPermaLink="false">664e0b88b71d68f7ff430a8b</guid><category><![CDATA[NestJs]]></category><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Thu, 06 Jun 2024 06:30:00 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2024/06/nest-logo.png" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2024/06/nest-logo.png" alt="Part 1/3: How to Implement Refresh Tokens with Token Rotation in NestJS"><p>In this episode, we will learn how to implement refresh tokens using local storage as a strategy for storing both access and refresh tokens. If you want to jump directly to the GitHub repo, you can <a href="https://github.com/zenstok/nestjs-auth-refresh-token-example?ref=rabbitbyte.club">access it here</a>.</p><h3 id="prerequisites">Prerequisites</h3><p>Before diving into this guide, it&apos;s important to have some experience with NestJS and the implementation of Passport strategies in NestJS. If you&apos;re not familiar with these topics, please visit the following articles from the NestJS documentation:</p><ul><li><a href="https://docs.nestjs.com/recipes/passport?ref=rabbitbyte.club">Passport Integration</a></li></ul><h3 id="why-do-we-use-refresh-tokens">Why Do We Use Refresh Tokens?</h3><p>Even though we use HTTPS to encrypt network traffic, there are additional steps we can take to prevent malicious users from stealing access tokens through methods like social engineering or library hacks. Refresh tokens allow a user to stay logged in for a long time without needing to log in again, provided they are active users. We can log them out after a set period, such as six months, if they have been inactive.</p><p>Why use refresh tokens instead of a single access token with a long or no expiration date? Because we want to counter token theft with short-lived access tokens, so attackers are likely to obtain expired tokens. Additionally, refresh tokens can provide a way to revoke user access without resetting the JWT signing key and logging out all users.</p><h3 id="implementation-overview">Implementation Overview</h3><p>When we log in for the first time, we receive a token pair that includes an access token and a refresh token. As the access token approaches expiration, we can obtain a new token pair by requesting a new set of tokens from our authentication server. The server will provide the new token pair only if the refresh token is provided.</p><p>Why do you think I mentioned receiving a new token pair consisting of both access token and refresh token? If I have the refresh token with a long expiration date, wouldn&apos;t it have been sufficient to just get a new access token? Well, storing the refresh token with long expiration date invalidates the logic that hackers obtain short-lived tokens that are likely to be expired. Furthermore, this approach eliminates the possibility of implementing the &apos;permanently logged in&apos; users feature, even if they are active. So, what we do is when we request a new token pair, we immediately invalidate the previous refresh token through a mechanism called refresh token rotation.</p><h3 id="refresh-token-rotation">Refresh Token Rotation</h3><p>Refresh token rotation operates by generating a blacklist which will &quot;force invalidate&quot; previously used refresh tokens. When a new token pair is requested, we utilize a refresh token and then include this used refresh token in our blacklist. This means that if a hacker gains control of a refresh token, it will already be invalid if the user has refreshed their token pair.</p><p>But what if the hacker gets a fresh, valid refresh token? You&apos;ve got two options: if you spot the hack right away, though that&apos;s unlikely, you can quickly get a new token pair to make the hacker&apos;s refresh token worthless. If the hacker uses your refresh token and it&apos;s marked invalid, there&apos;s not much you can do. They might have access to your app indefinitely until you change the JWT signing key. To stop this, we can store the refresh token in an HTTP-only cookie and guard against CSRF attacks. That way, even if your app has XSS vulnerabilities, the hacker can&apos;t read the refresh token. If you&apos;re interested in an article about storing refresh tokens in HTTP-only cookies, leave a comment, and I&apos;ll get right on it.</p><h3 id="getting-started">Getting Started</h3><p>For this article, we&apos;ll focus on the core logic and keep the app simple.</p><p>Clone the project from GitHub and start the Docker containers:</p><pre><code class="language-sh">yarn dc up</code></pre><p>Pre-fill your database with two users:</p><pre><code class="language-sh">yarn dc-db-init</code></pre><p>Access Swagger at <code>localhost:3000/docs</code> to log in. For the admin user, use:<br>Email: <a>admin@admin.com</a><br>Password: 1234</p><p>Our app allows refreshing the token pair by calling the <code>/refresh-tokens</code> endpoint. When called with a refresh token as bearer auth, it invalidates the previous token. Try calling the endpoint twice with the same token to see the <code>401 Unauthorized error</code> on the second call.</p><p>The core logic is in the authentication module. We have three guards:</p><ul><li><strong>Local Auth Guard</strong>: For initial authentication with email and password.</li><li><strong>JWT Auth Guard</strong>: Protects all app routes globally, defined as an <code>APP_GUARD</code> in <code>app.module.ts</code>, uses access token for validation.</li><li><strong>JWT Refresh Auth Guard</strong>: Guards the <code>/refresh-tokens endpoint</code>, uses refresh token for validation.</li></ul><p>The critical aspect here is the interaction between access tokens and refresh tokens, so I&apos;ll skip discussing the local auth guard. For the JWT auth guard, we utilize the JWT strategy from the <code>&apos;passport-jwt&apos;</code> package.</p><p>In the following section, we define how to extract the JWT from the request and the JWT signature key, which we set in the environment. In the validate method, we receive the payload of the JWT, which we use to retrieve the user ID and verify if the user exists in the database before granting access if true.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    private userService: UserService,
    configService: ConfigService&lt;EnvironmentVariables&gt;,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get(&apos;jwtSecret&apos;),
    });
  }

  async validate(payload: any): Promise&lt;User | null&gt; {
    const authUser = await this.userService.findOne(payload.sub);
    if (!authUser) {
      throw new UnauthorizedException();
    }
    return authUser;
  }
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/modules/authentication/strategies/jwt.strategy.ts</span></p></figcaption></figure><p>Similarly, for the JWT refresh auth guard, we employ the same JWT strategy from the&#xA0;<code>&apos;passport-jwt&apos;</code>&#xA0;package. The distinction here from the JWT strategy file is that we utilize a different secret key for JWT token generation, and we return both the user attributes and the refresh token expiration date. This expiration date becomes necessary later in the process.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, &apos;jwt-refresh&apos;) {
  constructor(
    private userService: UserService,
    configService: ConfigService&lt;EnvironmentVariables&gt;,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get(&apos;jwtRefreshSecret&apos;),
    });
  }

  async validate(payload: any) {
    const authUser = await this.userService.findOne(payload.sub);
    if (!authUser) {
      throw new UnauthorizedException();
    }
    return {
      attributes: authUser,
      refreshTokenExpiresAt: new Date(payload.exp * 1000),
    };
  }
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/modules/authentication/strategies/jwt-refresh.strategy.ts</span></p></figcaption></figure><p>In the authentication.controller&apos;s login method, we observe that we call the login method, which, in turn, invokes the generateTokenPair from our&#xA0;<code>AuthRefreshTokenService</code>. It&apos;s important to note that we also implement a throttle mechanism to limit the number of requests on the login route, thereby preventing brute force attacks, with a maximum of 2 requests per second and a maximum of 5 login attempts per 60 seconds.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">@Throttle({ short: { limit: 2, ttl: 1000 }, long: { limit: 5, ttl: 60000 } })
@ApiBody({ type: UserLoginDto })
@Public()
@UseGuards(LocalAuthGuard)
@Post(&apos;login&apos;)
login(@Request() req: any) {
  return this.authenticationService.login(req.user);
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/modules/authentication/authentication.controller.ts</span></p></figcaption></figure><p>From within authentication service:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">login(user: User) {
  return this.authRefreshTokenService.generateTokenPair(user);
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/modules/authentication/authentication.service.ts</span></p></figcaption></figure><p>The auth.refresh.token.service.ts looks like this:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">export class AuthRefreshTokenService {
  constructor(
    private jwtService: JwtService,
    private configService: ConfigService&lt;EnvironmentVariables&gt;,
    @InjectRepository(AuthRefreshToken)
    private authRefreshTokenRepository: Repository&lt;AuthRefreshToken&gt;,
  ) {}

  async generateRefreshToken(authUserId: number, currentRefreshToken?: string, currentRefreshTokenExpiresAt?: Date) {
    const newRefreshToken = this.jwtService.sign(
      { sub: authUserId },
      { secret: this.configService.get(&apos;jwtRefreshSecret&apos;), expiresIn: &apos;30d&apos; },
    );

    if (currentRefreshToken &amp;&amp; currentRefreshTokenExpiresAt) {
      if (await this.isRefreshTokenBlackListed(currentRefreshToken, authUserId)) {
        throw new UnauthorizedException(&apos;Invalid refresh token.&apos;);
      }

      await this.authRefreshTokenRepository.insert({
        refreshToken: currentRefreshToken,
        expiresAt: currentRefreshTokenExpiresAt,
        userId: authUserId,
      });
    }

    return newRefreshToken;
  }

  private isRefreshTokenBlackListed(refreshToken: string, userId: number) {
    return this.authRefreshTokenRepository.existsBy({ refreshToken, userId });
  }

  async generateTokenPair(user: User, currentRefreshToken?: string, currentRefreshTokenExpiresAt?: Date) {
    const payload = { email: user.email, sub: user.id };
    return {
      access_token: this.jwtService.sign(payload),
      refresh_token: await this.generateRefreshToken(user.id, currentRefreshToken, currentRefreshTokenExpiresAt),
    };
  }

  @Cron(CronExpression.EVERY_DAY_AT_6AM)
  async clearExpiredRefreshTokens() {
    await this.authRefreshTokenRepository.delete({ expiresAt: LessThanOrEqual(new Date()) });
  }
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/modules/authentication/auth-refresh-token.service.ts</span></p></figcaption></figure><p>Looking at&#xA0;<code>generateRefreshToken</code>&#xA0;method, we generate a new refresh token with a 30-days expiration. If we don&apos;t receive the optional currentRefreshToken and currentRefreshTokenExpiresAt parameters, we simply return the newly created refresh token, as expected after a successful login.</p><p>Examining the&#xA0;<code>refreshTokens</code>&#xA0;method below in the authentication controller, we notice the implementation of a throttle mechanism: a maximum of 1 request per second or 2 requests per 60 seconds. We invoke generateTokenPair with user attributes, the used refresh token, and its expiration date:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">@Throttle({
  short: { limit: 1, ttl: 1000 },
  long: { limit: 2, ttl: 60000 },
})
@ApiBearerAuth()
@Public()
@UseGuards(JwtRefreshAuthGuard)
@Post(&apos;refresh-tokens&apos;)
refreshTokens(@Request() req: ExpressRequest) {
  if (!req.user) {
    throw new InternalServerErrorException();
  }
  return this.authRefreshTokenService.generateTokenPair(
    (req.user as any).attributes,
    req.headers.authorization?.split(&apos; &apos;)[1],
    (req.user as any).refreshTokenExpiresAt,
  );
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/modules/authentication/authentication.controller.ts</span></p></figcaption></figure><p>The&#xA0;<code>generateTokenPair</code>&#xA0;method of&#xA0;<code>AuthRefreshTokenService</code>, when invoked with&#xA0;<code>currentRefreshToken</code>&#xA0;and&#xA0;<code>currentRefreshTokenExpiresAt</code>, checks if the current token is blacklisted and throws an error if it&apos;s reused. For the first-time usage, it inserts this token into our auth refresh tokens database table, effectively acting as our blacklist.</p><p>In the final method of our service, we have a cron job responsible for deleting all refresh tokens whose expiration date has passed, as we no longer need to retain them in the database.</p><p>This is part 1 of a 3-episode series. In the next episode, I will show you how to manage access and refresh tokens easily in a React app. In episode 3, we&apos;ll delve deeper into storing the refresh token in an HTTP-only cookie instead of local storage. This approach prevents attackers from reading the refresh token, even if your app is vulnerable to XSS attacks.</p><p>If you&apos;d like me to cover more interesting topics about the node.js ecosystem, feel free to leave your suggestions in the comments section. Don&apos;t forget to subscribe to my newsletter on&#xA0;<a href="https://rabbitbyte.club/">rabbitbyte.club</a>&#xA0;for updates!<br><br><strong>Post Creation:</strong><br><a href="https://rabbitbyte.club/how-to-handle-refresh-tokens-in-react-app/" rel="noreferrer">Check out Part 2 of this series</a>, where we integrate this backend with a React App.</p>]]></content:encoded></item><item><title><![CDATA[NestJS Dependency Injection in Worker Threads]]></title><description><![CDATA[<p>There may be cases when you need to do CPU-intensive tasks on the backend like big JSON parsing, video encoding, compression, etc. If you have at least 2 cores in your processor&apos;s configuration you can run javascript in parallel for these tasks and not block the main thread</p>]]></description><link>https://rabbitbyte.club/nestjs-dependency-injection-in-worker-threads/</link><guid isPermaLink="false">65f1d77a31a08a0461e253b7</guid><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Sun, 17 Mar 2024 09:40:04 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2024/03/nest-logo-2.png" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2024/03/nest-logo-2.png" alt="NestJS Dependency Injection in Worker Threads"><p>There may be cases when you need to do CPU-intensive tasks on the backend like big JSON parsing, video encoding, compression, etc. If you have at least 2 cores in your processor&apos;s configuration you can run javascript in parallel for these tasks and not block the main thread of the NestJS app that handles client requests.</p><p>An excellent way of doing this is by using node.js&#xA0;<a href="https://nodejs.org/api/worker_threads.html?ref=rabbitbyte.club" rel="noreferrer">worker threads</a>.</p><p>You shouldn&apos;t use worker threads for I/O operations as node.js already gracefully handles this for you.<br><br>If you&apos;re eager to dive into the complete example right away, you can <a href="https://github.com/zenstok/nestjs-worker-thread-example?ref=rabbitbyte.club">check out the GitHub repo for the full example.&#xA0;</a><br><br>In order to illustrate this, we will use a very simple service that calculates the fibonacci sum, which is a CPU-intensive task, a great candidate for node.js worker threads!</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">import { Injectable } from &apos;@nestjs/common&apos;;

@Injectable()
export class FibonacciService {
  fibonacci(n) {
    if (n &lt;= 1) {
      return 1;
    }
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/fibonacci/fibonacci.service.ts</span></p></figcaption></figure><p><br>Now, let&apos;s dive into the code snippet that launches the worker thread:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">import { Injectable, Logger } from &apos;@nestjs/common&apos;;
import { Worker, isMainThread } from &apos;worker_threads&apos;;
import workerThreadFilePath from &apos;./worker-threads/config&apos;;

@Injectable()
export class AppService {
  private readonly logger = new Logger(AppService.name);

  checkMainThread() {
    this.logger.debug(
      &apos;Are we on the main thread here?&apos;,
      isMainThread ? &apos;Yes.&apos; : &apos;No.&apos;,
    );
  }

  // do not run this from the worker thread or you will spawn an infinite number of threads in cascade
  runWorker(fibonacci: number): string {
    this.checkMainThread();
    
    const thisService = this;
    const worker = new Worker(workerThreadFilePath, {
      workerData: fibonacci,
    });
    worker.on(&apos;message&apos;, (fibonacciSum) =&gt; {
      thisService.logger.verbose(&apos;Calculated sum&apos;, fibonacciSum);
    });
    worker.on(&apos;error&apos;, (e) =&gt; console.log(&apos;on error&apos;, e));
    worker.on(&apos;exit&apos;, (code) =&gt; console.log(&apos;on exit&apos;, code));

    return &apos;Processing the fibonacci sum... Check NestJS app console for the result.&apos;;
  }
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/app.service.ts</span></p></figcaption></figure><p>As we can see from the snippet above, we defined a new worker, gave it a path to a file to be executed by the worker, and gave it as param the workerData. Worker data is data sent from the main thread to the other thread. <br><br>We are using the constant <code>workerThreadFilePath</code> from associated config file on the <em>same level in the directory tree</em> so we can safely use the path for the worker thread regardless of where the app is deployed.</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">// it will import the compiled js file from dist directory
const workerThreadFilePath = __dirname + &apos;/findFibonacciSum.js&apos;;

export default workerThreadFilePath;</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/worker-threads/config.ts</span></p></figcaption></figure><p>Take a note that we are using the js extension here because the transpiled output of typescript, as we know, it&apos;s javascript.<br></p><p><br>Let&apos;s explore the file which will be executed in the worker thread:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">import { NestFactory } from &apos;@nestjs/core&apos;;
import { workerData, parentPort } from &apos;worker_threads&apos;;
import { AppModule } from &apos;../app.module&apos;;
import { FibonacciService } from &apos;../fibonacci/fibonacci.service&apos;;
import { AppService } from &apos;../app.service&apos;;

async function run() {
  const app = await NestFactory.createApplicationContext(AppModule);
  const appService = app.get(AppService);
  const fibonacciService = app.get(FibonacciService);

  const fibonacciNumber: number = workerData; // this is data received from main thread


  // here we apply business logic inside the worker thread
  appService.checkMainThread();
  const fibonacciSum = fibonacciService.fibonacci(fibonacciNumber);
  parentPort.postMessage(fibonacciSum);
}

run();</code></pre><figcaption><p><span style="white-space: pre-wrap;">src/worker-threads/findFibonacciSum.ts</span></p></figcaption></figure><p>In order to have access to dependency injection in our NestJs app in the thread, we leverage <a href="https://docs.nestjs.com/standalone-applications?ref=rabbitbyte.club" rel="noreferrer">Nest standalone application</a> which is a wrapper around the Nest&#xA0;<strong>IoC container</strong>, which holds all instantiated classes.</p><p><br>Now, we can execute any method from any service.</p><p></p><h3 id="caveats">Caveats:&#xA0;</h3><ul><li>Be cautious when using dependency injection from NestJS in a worker thread, as it comes with a cost. The startup process of the NestJS app must be awaited before accessing the dependency injection. Therefore, utilize this method only if absolutely necessary, or if the startup bottleneck is negligible in terms of overall performance.</li><li>If your app setup is starting processes that you don&apos;t want to run on the separate thread you will need to configure your app module into a&#xA0;<a href="https://docs.nestjs.com/fundamentals/dynamic-modules?ref=rabbitbyte.club">dynamic module</a>&#xA0;to accept a parameter to not initiate the setup.<br>Example:<br>const app = await <em><strong>NestFactory</strong></em>.createApplicationContext(<br>  AppModule.<em>register</em>({ attachRabbitMqConsumers: false }),<br>);<br></li></ul><h3 id="github-repository">Github repository:</h3><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/zenstok/nestjs-worker-thread-example/blob/master/src/app.service.ts?ref=rabbitbyte.club"><div class="kg-bookmark-content"><div class="kg-bookmark-title">nestjs-worker-thread-example/src/app.service.ts at master &#xB7; zenstok/nestjs-worker-thread-example</div><div class="kg-bookmark-description">This project illustrates how to use node.js worker thread in a NestJS app and have access to dependency injection. - zenstok/nestjs-worker-thread-example</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg" alt="NestJS Dependency Injection in Worker Threads"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">zenstok</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/dc7f24477a992e4ee94889dcfb2aa39e9e7ab48ecac74e62a6632a447ede7854/zenstok/nestjs-worker-thread-example" alt="NestJS Dependency Injection in Worker Threads"></div></a></figure><p><br>If you&apos;d like me to cover more interesting topics about the node.js ecosystem, feel free to leave your suggestions in the comments section. Don&apos;t forget to subscribe to my newsletter on <a href="https://rabbitbyte.club/" rel="noreferrer">rabbitbyte.club</a> for updates!</p>]]></content:encoded></item><item><title><![CDATA[Part 3/3: How to deploy a production app to Kubernetes (GKE)]]></title><description><![CDATA[<p><strong>Introduction:</strong><br>This blog post is the final installment of a three-part series dedicated to scaling a NestJS chat app to handle millions of users. In the <a href="https://dev.to/zenstok/how-to-scale-a-chat-app-to-millions-of-users-in-nestjs-2k0k?ref=rabbitbyte.club">first two episodes</a>, we discussed the scaling process and demonstrated how to deploy it locally using Docker and Minikube. However, in this final episode,</p>]]></description><link>https://rabbitbyte.club/3-3-how-to-deploy-a-production-app-to-kubernetes-gke/</link><guid isPermaLink="false">65018db1220ba3034a10a67c</guid><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Wed, 03 Jan 2024 08:45:00 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2024/01/gke.webp" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2024/01/gke.webp" alt="Part 3/3: How to deploy a production app to Kubernetes (GKE)"><p><strong>Introduction:</strong><br>This blog post is the final installment of a three-part series dedicated to scaling a NestJS chat app to handle millions of users. In the <a href="https://dev.to/zenstok/how-to-scale-a-chat-app-to-millions-of-users-in-nestjs-2k0k?ref=rabbitbyte.club">first two episodes</a>, we discussed the scaling process and demonstrated how to deploy it locally using Docker and Minikube. However, in this final episode, our focus shifts to deploying the app in a production environment on Google Kubernetes Engine (GKE). If you want to jump directly to a working solution, you can check out the <code>deploy-on-gke</code> branch of the GitHub repository <a href="https://github.com/zenstok/nestjs-scalable-chat-app-example/tree/deploy-on-gke?ref=rabbitbyte.club">here</a>.</p><p>While it is recommended to read the previous episodes for a comprehensive understanding of the scaling process, this post can also serve as a standalone guide specifically for deploying the app to GKE in a production-ready manner. By the end of this episode, you will possess all the necessary tools and knowledge to successfully deploy your NestJS chat app on GKE, ready to handle high user loads.</p><p><strong>Section 1: Prerequisites</strong><br>Before diving into the deployment process, ensure you have the following prerequisites in place:</p><ul><li>A Google Cloud account.</li><li>The Google Cloud SDK installed on your local machine (you can find installation instructions <a href="https://cloud.google.com/sdk/docs/install?ref=rabbitbyte.club">here</a>).</li><li>Basic familiarity with Docker and Kubernetes concepts.</li></ul><p><strong>Section 2: Creating a GKE Cluster</strong><br>The first step is to set up a GKE cluster, which will serve as the runtime environment for your application. Here are the steps:</p><ul><li>Create a new Google Cloud project through the GUI and ensure billing is activated.</li></ul><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t1o47do9gi2rhg5gklia.png" class="kg-image" alt="Part 3/3: How to deploy a production app to Kubernetes (GKE)" loading="lazy" width="2818" height="1152"></figure><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9e51bigjsvp8xua30ys8.png" class="kg-image" alt="Part 3/3: How to deploy a production app to Kubernetes (GKE)" loading="lazy" width="924" height="700"></figure><ul><li>Enable the Kubernetes Engine API for the newly created project (billing should be activated).</li></ul><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lc6b0nairop8omdpkrs3.png" class="kg-image" alt="Part 3/3: How to deploy a production app to Kubernetes (GKE)" loading="lazy" width="1234" height="509"></figure><ul><li>Switch to the Google Cloud CLI and authenticate using the command <code>gcloud auth login</code>.</li><li>Make sure your active project with billing activated is set to the correct one by running <code>gcloud config set project &lt;YOUR-PROJECT-ID&gt;</code>.</li><li>Create a Kubernetes cluster using the command <code>gcloud container clusters create scale-chat-app-cluster</code>.</li></ul><p>This process may take a little time. Afterward, create a static external IP address, which is needed to make your app accessible over the internet:</p><pre><code class="language-bash">gcloud compute addresses create tutorial-ip --global
</code></pre><p>Check the details of the newly created IP address:</p><pre><code class="language-bash">gcloud compute addresses describe --global tutorial-ip
</code></pre><p>Now, update your DNS records for your chosen domain to point to this IP address.</p><p><strong>Section 3: Containerizing Your Application</strong><br>To deploy your application to Kubernetes, it must be packaged into a container image. For this tutorial, we&apos;ve already published the Docker image on Docker Hub as a public image <a href="https://hub.docker.com/r/zenstok/scalable-chat-app-example-backend?ref=rabbitbyte.club">here</a>. Docker Hub serves as a container registry from which your Kubernetes app will retrieve the container image. If you want to use a private image, you can do so. If you&apos;re interested in learning how to publish your own image, you can follow a Docker tutorial <a href="https://docs.docker.com/get-started/04_sharing_app/?ref=rabbitbyte.club">here</a>.</p><p><strong>Section 4: Defining Kubernetes Deployment and Service YAML</strong><br>In this section, we&apos;ll delve into the Kubernetes Deployment and Service YAML files. These files define how your application should be deployed and exposed. We&apos;ll discuss key parameters and provide examples to help you create your own YAML files.</p><ul><li>To start, use the Kubernetes YAML files from the <a href="https://github.com/zenstok/nestjs-scalable-chat-app-example/tree/improved-version%2Bdeploy-on-kubernetes/k8s?ref=rabbitbyte.club">previous tutorial</a>. What you&apos;ll need to add:</li><li>A GKE <strong>Ingress Controller</strong>. An ingress controller receives and load-balances traffic from outside Kubernetes to pods running in a Kubernetes cluster.</li><li>A default backend for this Ingress. Any requests that don&apos;t match the paths in the rules field are sent to the Service and port specified in the defaultBackend field. We will have an Ingress with no rules so that all requests are sent to this default backend.</li><li>Annotations for the Ingress to capture the IP address set above.</li><li>A managed certificate service where we will define the domains we own so that the app can point to that domain and have SSL.</li><li>A frontend config that will automatically redirect all requests to HTTPS.</li><li>A readiness probe configuration in our deployment config to inform GKE about pod health (for this, you will also need to implement a health route, which is a route that always returns a 200 status on GET).</li></ul><p>The ingress config looks like this:</p><pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: backend-ingress
  annotations:
    kubernetes.io/ingress.global-static-ip-name: tutorial-ip
    networking.gke.io/managed-certificates: managed-cert
    kubernetes.io/ingress.class: &quot;gce&quot;
    networking.gke.io/v1beta1.FrontendConfig: ssl-redirect
  labels:
    app: backend
spec:
  defaultBackend:
    service:
      name: backend-service
      port:
        number: 3000
</code></pre><p>The backend config looks like this:</p><pre><code class="language-yaml">apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  name: my-backendconfig
spec:
  timeoutSec: 120
</code></pre><p>The backend service should be annotated with your backend config:</p><pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: backend-service
  labels:
    app: backend
  annotations:
    cloud.google.com/backend-config: &apos;{&quot;ports&quot;: {&quot;3000&quot;:&quot;my-backendconfig&quot;}}&apos;
</code></pre><p>The frontend config looks like this:</p><pre><code class="language-yaml">apiVersion: networking.gke.io/v1beta1
kind: FrontendConfig
metadata:
  name: ssl-redirect
spec:
  redirectToHttps:
    enabled: true
</code></pre><p>The managed certificate looks like this:</p><pre><code class="language-yaml">apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
  name: managed-cert
spec:
  domains:
    - subdomain.put-your-domain-here.com
</code></pre><p>The readiness probe looks like this: (It has to be added in backend-deployment.yaml at <strong>spec.template.spec.containers</strong>)</p><pre><code class="language-yaml">          readinessProbe:
            httpGet:
              path: &quot;/health&quot;
              port: 3000
            initialDelaySeconds: 10
            timeoutSeconds: 5
</code></pre><p>If your Docker image is private, you will need to create a Kubernetes secret to store your Docker private image credentials. However, for the purpose of this tutorial, we will continue using the public image we&apos;ve used in our previous episodes.</p><p>Here&apos;s how to create a Kubernetes secret for private Docker images:</p><pre><code class="language-bash">kubectl create secret docker-registry regcred \
    --docker-server=https://index.docker.io/v1 \
    --docker-username=&lt;your-name&gt; \
    --docker-password=&lt;your-password&gt; \
    --docker-email=&lt;your-email&gt;
</code></pre><p>Please note that this example assumes you are using Docker Hub as your registry. If you are using a different Docker registry, make sure to set the <code>--docker-server</code> option accordingly to match your registry&apos;s URL.</p><p>In your backend-deployment yaml spec (<strong>spec.template.spec.imagePullSecrets</strong>), you will have to add this secret in order to be able to pull the image:</p><pre><code class="language-yaml">      imagePullSecrets:
        - name: regcred
</code></pre><p><strong>Section 5: Deploying Your Application to GKE</strong><br>Now, it&apos;s time to deploy your application to the GKE cluster. Follow these steps:</p><ul><li>Ensure you are in the correct Kubernetes context:</li></ul><pre><code class="language-bash">kubectl config get-contexts
</code></pre><p>If necessary, switch to the appropriate context:</p><pre><code class="language-bash">kubectl config use-context &lt;YOUR-CONTEXT-NAME&gt;
</code></pre><ul><li>Run the following command in your project directory:</li></ul><pre><code class="language-bash">kubectl apply -f k8s
</code></pre><p>You will need to wait for the Google-managed certificate to finish provisioning, which may take up to 60 minutes. You can check the status of the certificate using this command:</p><pre><code class="language-bash">kubectl describe managedcertificate managed-cert
</code></pre><p><strong>Section 6: Scaling and Managing Your Application</strong><br>Kubernetes</p><p>GKE offers powerful scaling and management capabilities. For example, you can update the number of replicas or resources used per pod.</p><ul><li>Add a resources variable to the containers in backend-deployment.yaml: (make sure the cluster has sufficient resources)</li></ul><pre><code class="language-yaml">resources:
  limits:
    cpu: &quot;2000m&quot;    # Maximum CPU limit
    memory: &quot;4Gi&quot;   # Maximum memory limit
  requests:
    cpu: &quot;1000m&quot;    # Minimum CPU request
    memory: &quot;1Gi&quot;   # Minimum memory request
</code></pre><p><strong>Section 7: Conclusion</strong><br>In conclusion, Kubernetes GKE offers a scalable and reliable environment for deploying applications. Throughout this blog post, we&apos;ve explored the essential configuration options necessary to successfully deploy your application in a production-ready environment.</p>]]></content:encoded></item><item><title><![CDATA[Part 2/2: Deploy Scalable NestJs Chat App to Kubernetes]]></title><description><![CDATA[<p>In our <a href="https://dev.to/zenstok/how-to-scale-a-chat-app-to-millions-of-users-in-nestjs-2k0k?ref=rabbitbyte.club">previous blog post</a>, we explored how to write a scalable NestJS chat app using a distributed architecture and WebSockets for real-time communication. Building upon that, we will now dive deeper into improving the app&apos;s efficiency and deploying it on Kubernetes using minikube. If you&apos;re</p>]]></description><link>https://rabbitbyte.club/part-2-2-deploy-scalable-nestjs-chat-app-to-kubernetes/</link><guid isPermaLink="false">649a975c220ba3034a10a644</guid><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Tue, 02 Jan 2024 08:46:00 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2023/06/kubernetes-cover.png" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2023/06/kubernetes-cover.png" alt="Part 2/2: Deploy Scalable NestJs Chat App to Kubernetes"><p>In our <a href="https://dev.to/zenstok/how-to-scale-a-chat-app-to-millions-of-users-in-nestjs-2k0k?ref=rabbitbyte.club">previous blog post</a>, we explored how to write a scalable NestJS chat app using a distributed architecture and WebSockets for real-time communication. Building upon that, we will now dive deeper into improving the app&apos;s efficiency and deploying it on Kubernetes using minikube. If you&apos;re eager to check out the source code, you can find it in the GitHub repository <a href="https://github.com/zenstok/nestjs-scalable-chat-app-example/tree/improved-version+deploy-on-kubernetes?ref=rabbitbyte.club">here</a> (make sure you check out on <strong>improved-version+deploy-on-kubernetes</strong> branch). Additionally, if you find this post useful, let me know in the comments, and I&apos;ll create another tutorial focusing on deploying a production app with Google Kubernetes Engine (GKE). This tutorial will cover essential features like configuring a static IP, associating a domain name, obtaining a TLS certificate for secure communication, and setting up a load balancer for distributing traffic between instances.</p><h2 id="improving-the-previous-model">Improving the Previous Model</h2><p>In the previous post, we stored an array of connected clients in each instance and triggered events to notify all instances about new messages. In this paradigm, we broadcast messages to all instances regardless if they have associated ws clients which needs to receive the message or not. To enhance this model and optimize the broadcasting process, we can leverage Redis. Here&apos;s how we can achieve this improvement:</p><p>Consolidating Redis Channels:<br>Instead of having three channels for different types of messages (send to all, send to many, send to one), we will use a single global channel for broadcasting global messages (send to all). Additionally, we will <strong>dynamically</strong> create user-specific channels in the format: <code>ws_socket_client/{USER_ID}</code>. Each instance that holds WebSocket clients will basically subscribe to these channels. This helps optimize the broadcasting process and ensures that messages are only sent to the relevant instances with the corresponding clients.</p><p>Implementation Changes:<br>To implement this change, we need to listen for WebSocket connect and disconnect events and subscribe/unsubscribe the Redis client to each client&apos;s respective channel. Here are the code changes:</p><ul><li>In the constructor, we now subscribe only to the channel for sending messages to all clients:</li></ul><pre><code class="language-typescript">this.subscriberRedis.subscribe(this.sendWsMessageToAllClientsRedisChannel);
</code></pre><ul><li>Create a new private method to generate the user-specific channel name:</li></ul><pre><code class="language-typescript">private getClientIdRedisChannel(userId: string) {
  return `${this.wsSocketClientRedisChannel}/${userId}`;
}
</code></pre><ul><li>Subscribe to the user-specific channel in the <code>addConnection</code> method:</li></ul><pre><code class="language-typescript">this.subscriberRedis.subscribe(this.getClientIdRedisChannel(userId));
</code></pre><ul><li>Unsubscribe from the user-specific channel in the <code>removeConnection</code> method:</li></ul><pre><code class="language-typescript">this.subscriberRedis.unsubscribe(this.getClientIdRedisChannel(client.userId));
</code></pre><ul><li>Update the subscriber client to listen to the global channel for sending messages to all clients, as well as the user-specific channels:</li></ul><pre><code class="language-typescript">this.subscriberRedis.on(&apos;message&apos;, (channel, message) =&gt; {
  const data = JSON.parse(message) as RedisPubSubMessage;
  if (data.from !== this.redisClientId) {
    switch (true) {
      case channel === this.sendWsMessageToAllClientsRedisChannel:
        this.sendMessageToAllClients(data.message, false);
        break;
      case channel.startsWith(this.wsSocketClientRedisChannel):
        this.sendMessageToClient(
          (data as RedisPubSubMessageWithClientId).clientId,
          data.message,
          false,
        );
        break;
      // no default
    }
  }
});
</code></pre><p>You will also notice that on this branch, &#xA0;I updated the chat gateway class, now it has 3 types of messages it subscribes to:</p><pre><code class="language-typescript">enum SubscribeMessageType {
  SendChatMessageToOneParticipant = &apos;send_chat_message_to_one_participant&apos;,
  SendChatMessageToManyParticipants = &apos;send_chat_message_to_many_participants&apos;,
  SendChatMessageToAllParticipant = &apos;send_chat_message_to_all_participants&apos;,
}
</code></pre><p>In order to test it in the browser, you can follow the <a href="https://dev.to/zenstok/how-to-scale-a-chat-app-to-millions-of-users-in-nestjs-2k0k?ref=rabbitbyte.club">previous tutorial</a> where I discussed how to set up the database and how to obtain bearer tokens for authenticated users.<br>Also, the content of the websocket message should follow the format of our DTO classes so it can be received correctly:</p><pre><code class="language-typescript">export class SendMessageToAllDto {
  @IsString()
  @Length(1, 5000)
  message: string;
}
</code></pre><pre><code class="language-typescript">export class SendMessageToManyDto extends SendMessageToAllDto {
  @IsArray()
  @IsUUID(undefined, { each: true })
  participantIds: string[];
}
</code></pre><pre><code class="language-typescript">export class SendMessageToOneDto extends SendMessageToAllDto {
  @IsUUID()
  participantId: string;
}
</code></pre><p>For example if you want to send multiple messages you can use something like this in the browser:</p><pre><code class="language-typescript">users.forEach(user =&gt; ws.send(JSON.stringify({event: &apos;send_chat_message_to_one_participant&apos;, data: {message: &apos;test&apos;, participantId: user.id}})));
</code></pre><p>To test this improvement, connect and disconnect multiple clients in the browser and observe the behavior in the Docker console.</p><h2 id="deploying-the-stack-on-minikube">Deploying the Stack on Minikube</h2><p>To deploy the chat app on Minikube for local development and testing, follow these steps:</p><p>Install Minikube by referring to the official tutorial at<br><a href="https://kubernetes.io/docs/tasks/tools/?ref=rabbitbyte.club">https://kubernetes.io/docs/tasks/tools/</a>.</p><p>Once Minikube is installed, start the Minikube cluster using the command:</p><pre><code>minikube start
</code></pre><p>Inside the <a href="https://github.com/zenstok/nestjs-scalable-chat-app-example/tree/improved-version%2Bdeploy-on-kubernetes/k8s?ref=rabbitbyte.club">k8s</a> directory, you&apos;ll find the deployment files for the backend, Redis, and PostgreSQL, along with the corresponding services and volumes. We have set up environment variables within the backend deployment to ensure connectivity with Redis and PostgreSQL. The deployment is configured with five replicas to create a total of five backend pods. To test the deployment locally, the backend service is set as a node port, allowing it to be exposed to your local machine.</p><p>To deploy the app, navigate to the project directory and run the following command:</p><pre><code>kubectl apply -f k8s
</code></pre><p>This will set up the entire app along with its dependencies. Please note that we are using the <code>zenstok/scalable-chat-app-example-backend</code> image, which is built from the <code>improved-version+deploy-on-kubernetes</code> branch available at <a href="https://github.com/zenstok/nestjs-scalable-chat-app-example?ref=rabbitbyte.club">https://github.com/zenstok/nestjs-scalable-chat-app-example</a>.</p><p>Initiate database inside kubernetes</p><p>Find a backend pod name:</p><pre><code>kubectl get pods
</code></pre><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/chx6szzbg6r4cdavc081.png" class="kg-image" alt="Part 2/2: Deploy Scalable NestJs Chat App to Kubernetes" loading="lazy"></figure><p>Access a shell inside a backend pod:</p><pre><code>kubectl exec -it backend-deployment-7ddd9c4769-fxqxg -- sh
</code></pre><p>Run db init command:</p><pre><code>yarn db-init
</code></pre><p>If you want to port-forward kubernetes db to inspect it, you can do so by running:</p><pre><code>kubectl port-forward deployment/postgres-deployment 5432:5432
</code></pre><p>To access the deployed service locally, expose it using the command:</p><pre><code>minikube service backend-service
</code></pre><p>Interact with the deployed app locally and verify its behavior.</p><p>You will get something like this in the console:</p><figure class="kg-card kg-image-card"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wtx49avwnuzw2h0o2jnr.png" class="kg-image" alt="Part 2/2: Deploy Scalable NestJs Chat App to Kubernetes" loading="lazy"></figure><p>It will start a tunnel for backend-service which can be accessed locally at this address (beware that the port might be different on your machine):</p><pre><code>http://127.0.0.1:61939
</code></pre><p>Now you can interact with the app exactly as you interacted with the docker configured app.<br>For example, to access swagger docs, you just hit this in the browser:</p><pre><code>http://127.0.0.1:61939/docs
</code></pre><p>Also, if you want to inspect the logs from all backend pods, you can do so by running:</p><pre><code>kubectl logs -l app=backend -f
</code></pre><p>Once you have finished testing, clean up the Minikube cluster by deleting it:</p><pre><code>minikube delete
</code></pre><p>In this blog post, we explored how to enhance the scalability of a NestJS chat app by leveraging Redis and implementing user-specific channels. By optimizing the broadcasting process, we reduced networking costs and improved overall efficiency. Additionally, we learned how to deploy the app on Kubernetes using Minikube for local development and testing.</p><p>Your feedback and comments are highly appreciated, so please share your thoughts and experiences. Stay tuned for new posts!</p>]]></content:encoded></item><item><title><![CDATA[Part 1/3: How to Scale a Chat App to Millions of Users in NestJS]]></title><description><![CDATA[<p>Hello, in this guide, I will teach you how to scale a NestJS app to millions of users.</p>
<p>Prerequisites:</p>
<ul>
<li>Experience with JavaScript.</li>
<li>Experience with NestJS.</li>
<li>Experience as a backend engineer.</li>
</ul>
<p>Access the GitHub repository for this project <a href="https://github.com/zenstok/nestjs-scalable-chat-app-example?ref=rabbitbyte.club">here</a>.</p>
<p>To scale your app, you will need to scale horizontally by creating</p>]]></description><link>https://rabbitbyte.club/part-1-2-how-to-scale-a-chat-app-to-millions-of-users-in-nestjs/</link><guid isPermaLink="false">649a95dd220ba3034a10a630</guid><dc:creator><![CDATA[Rares Tarabega]]></dc:creator><pubDate>Mon, 01 Jan 2024 08:45:00 GMT</pubDate><media:content url="https://rabbitbyte.club/content/images/2023/06/nest-logo.png" medium="image"/><content:encoded><![CDATA[<img src="https://rabbitbyte.club/content/images/2023/06/nest-logo.png" alt="Part 1/3: How to Scale a Chat App to Millions of Users in NestJS"><p>Hello, in this guide, I will teach you how to scale a NestJS app to millions of users.</p>
<p>Prerequisites:</p>
<ul>
<li>Experience with JavaScript.</li>
<li>Experience with NestJS.</li>
<li>Experience as a backend engineer.</li>
</ul>
<p>Access the GitHub repository for this project <a href="https://github.com/zenstok/nestjs-scalable-chat-app-example?ref=rabbitbyte.club">here</a>.</p>
<p>To scale your app, you will need to scale horizontally by creating multiple instances of your NestJS app.</p>
<p>Each instance of your app should have the following features:</p>
<ol>
<li>WebSocket integrations that keep track of connected clients through websockets in a singleton memory array. (this is done by default when using nestjs, reference: <a href="https://docs.nestjs.com/fundamentals/injection-scopes?ref=rabbitbyte.club">Injection Scopes</a>)</li>
<li>A Redis database publisher that announces new requests received by all other instances and a subscriber that listents to these requests. This is necessary because a client may send a request to one instance (e.g., instance number 3) and be registered as a websocket client there. However, the client may then send a request to another instance because you will have a load balancer installed (e.g., instance number 32), triggering a websocket chat notification. Unfortunately, this instance does not have the connected client in its singleton memory array. To handle this, the instance needs to announce the request to all other instances through Redis. The other instances can then look up their array of connected clients and notify them if a match is found.</li>
</ol>
<p>Here&apos;s how you can achieve this:</p>
<ol>
<li>
<p>Install websockets in your NestJS app. Follow the <a href="https://docs.nestjs.com/websockets/gateways?ref=rabbitbyte.club">official NestJS guide</a>.  If you&apos;d like to quickly jump out and see the GitHub repo for the full example, you can find it <a href="https://github.com/zenstok/nestjs-scalable-chat-app-example?ref=rabbitbyte.club">here</a>.</p>
</li>
<li>
<p>Create the WebSocket client manager service (explained below).</p>
</li>
</ol>
<p>After setting up websockets in your app, we need to define an entrypoint gateway for websockets.</p>
<pre><code class="language-typescript">@WebSocketGateway({ path: &apos;/entrypoint&apos; })
export class EntrypointGateway {}
</code></pre>
<p>Then, create a new lifecycle gateway that extends this entrypoint. This lifecycle gateway contains the logic to insert clients into the WebSocket client manager.</p>
<pre><code class="language-typescript">@Injectable()
export class LifecycleGateway
  extends EntrypointGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  private readonly logger = new Logger(LifecycleGateway.name);

  constructor(
    private readonly jwtService: JwtService,
    private readonly wsClientManager: WsClientManager,
  ) {
    super();
  }

  afterInit() {
    this.logger.debug(&apos;Websockets initialized &apos; + this.constructor.name);
  }

  handleConnection(client: any) {
    const authUserTokenData = this.getDecodedAuthToken(client);

    if (!authUserTokenData) {
      client.close();
      return;
    }

    this.wsClientManager.addConnection(client, authUserTokenData);
  }

  handleDisconnect(client: any) {
    this.wsClientManager.removeConnection(client);
  }

  getDecodedAuthToken(client: any) {
    let decodedJwt: DecodedAuthToken | null = null;

    try {
      if (client.protocol) {
        decodedJwt = this.jwtService.verify(client.protocol, {
          secret: jwtConstants.secret,
        }) as DecodedAuthToken;
      }
    } catch (e) {}

    return decodedJwt;
  }
}
</code></pre>
<p>To inject the Redis connection into our app, we utilize <code>@liaoliaots/nestjs-redis</code> package with the following yarn command:</p>
<pre><code> yarn add @liaoliaots/nestjs-redis
</code></pre>
<p>Moving forward, we will add the Redis module configuration for  subscriber and publisher channels.</p>
<pre><code class="language-typescript">RedisModule.forRootAsync({
  inject: [ConfigService],
  useFactory: async (config: ConfigService) =&gt; ({
    config: [
      {
        namespace: &apos;subscriber&apos;,
        host: config.get(&apos;REDIS_HOST&apos;),
        port: config.get(&apos;REDIS_PORT&apos;),
      },
      {
        namespace: &apos;publisher&apos;,
        host: config.get(&apos;REDIS_HOST&apos;),
        port: config.get(&apos;REDIS_PORT&apos;),
      },
    ],
  }),
}),
</code></pre>
<p>In the WebSocket client manager, you will find the following:</p>
<ul>
<li>We generate a random Redis client using the <code>crypto</code> module&apos;s <code>randomUUID</code> from the official Node.js package.</li>
</ul>
<p>Each NestJS app instance subscribes to the three Redis channels we defined:</p>
<ul>
<li>SendWsMessageToAllClients</li>
<li>SendWsMessageToSomeClients</li>
<li>SendWsMessageToOneClient</li>
</ul>
<p>When a NestJS instance receives a message, it first checks that the publisher is not the same as the subscriber. Then, it proceeds to send the message. However, this time, we set the <code>shouldPublishToRedis</code> parameter to <code>false</code> to avoid an infinite loop. The received message will be sent by each instance if it finds the clients in its singleton memory array.</p>
<p>This approach also handles cases where a single user uses the app through multiple devices simultaneously (e.g., PC, mobile app). The user will receive chat notifications on all devices because, upon inspecting the code, you will notice that we are utilizing a map object. For every user ID, we store all associated WebSocket connected clients.</p>
<pre><code class="language-typescript">@Injectable()
export class WsClientManager {
  private readonly connectedClients = new Map&lt;string, any[]&gt;();
  private readonly redisClientId = `ws_socket_client-${crypto.randomUUID()}`;

  constructor(
    @InjectRedis(&apos;subscriber&apos;) private readonly subscriberRedis: Redis,
    @InjectRedis(&apos;publisher&apos;) private readonly publisherRedis: Redis,
  ) {
    this.subscriberRedis.subscribe(
      RedisSubscribeChannel.SendWsMessageToAllClients,
    );
    this.subscriberRedis.subscribe(
      RedisSubscribeChannel.SendWsMessageToSomeClients,
    );
    this.subscriberRedis.subscribe(
      RedisSubscribeChannel.SendWsMessageToOneClient,
    );

    this.subscriberRedis.on(&apos;message&apos;, (channel, message) =&gt; {
      const data = JSON.parse(message) as RedisPubSubMessage;
      if (data.from !== this.redisClientId) {
        switch (channel) {
          case RedisSubscribeChannel.SendWsMessageToAllClients:
            this.sendMessageToAllClients(data.message, false);
            break;
          case RedisSubscribeChannel.SendWsMessageToSomeClients:
            this.sendMessageToClients(
              (data as RedisPubSubMessageWithClientIds).clientIds,
              data.message,
              false,
            );
            break;
          case RedisSubscribeChannel.SendWsMessageToOneClient:
            this.sendMessageToClient(
              (data as RedisPubSubMessageWithClientId).clientId,
              data.message,
              false,
            );
            break;
        }
      }
    });
  }

  addConnection(client: any, authUserTokenData: DecodedAuthToken) {
    const userId = authUserTokenData.sub;

    this.setUserIdOnClient(client, userId);
    const clientsPool = this.getClientsPool(client);
    this.connectedClients.set(
      userId,
      clientsPool ? [...clientsPool, client] : [client],
    );

    setTimeout(() =&gt; {
      client.close(); // This will trigger removeConnection from the LifecycleGateway&apos;s handleDisconnect
    }, this.getConnectionLimit(authUserTokenData));
  }

  removeConnection(client: any) {
    const clientsPool = this.getClientsPool(client);
    const newPool = clientsPool!.filter((c) =&gt; c !== client);

    if (!newPool.length) {
      this.connectedClients.delete(client.userId);
    } else {
      this.connectedClients.set(client.userId, newPool);
    }
  }

  private setUserIdOnClient(client: any, userId: string) {
    client.userId = userId;
  }

  private getClientsPool(client: any) {
    return this.connectedClients.get(client.userId);
  }

  private getConnectionLimit(tokenData: DecodedAuthToken) {
    return tokenData.exp * 1000 - Date.now();
  }

  getConnectedClientIds() {
    const clientIds: string[] = [];

    const iterator = this.connectedClients.keys();
    let current = iterator.next();
    while (!current.done)

 {
      clientIds.push(current.value);
      current = iterator.next();
    }

    return clientIds;
  }

  sendMessageToClient(
    clientId: string,
    message: string,
    shouldPublishToRedis = true,
  ) {
    if (shouldPublishToRedis) {
      this.publisherRedis.publish(
        RedisSubscribeChannel.SendWsMessageToOneClient,
        JSON.stringify({
          message,
          clientId,
          from: this.redisClientId,
        }),
      );
    }

    const clientPool = this.connectedClients.get(clientId);

    if (clientPool) {
      clientPool.forEach((client) =&gt; {
        client.send(message);
      });
    }
  }

  sendMessageToClients(
    clientIds: string[],
    message: string,
    shouldPublishToRedis = true,
  ) {
    if (shouldPublishToRedis) {
      this.publisherRedis.publish(
        RedisSubscribeChannel.SendWsMessageToSomeClients,
        JSON.stringify({
          message,
          clientIds,
          from: this.redisClientId,
        }),
      );
    }

    this.connectedClients.forEach((clientPool, clientId) =&gt; {
      if (clientIds.includes(clientId)) {
        clientPool.forEach((client) =&gt; {
          client.send(message);
        });
      }
    });
  }

  sendMessageToAllClients(message: string, shouldPublishToRedis = true) {
    if (shouldPublishToRedis) {
      this.publisherRedis.publish(
        RedisSubscribeChannel.SendWsMessageToAllClients,
        JSON.stringify({
          message,
          from: this.redisClientId,
        }),
      );
    }

    this.connectedClients.forEach((clientPool) =&gt; {
      clientPool.forEach((client) =&gt; {
        client.send(message);
      });
    });
  }
}
</code></pre>
<p>Now it&apos;s time to test our app. First, clone the project. Make sure you have Docker installed.</p>
<p>This is the Dockerfile for our NestJS app (represents a single instance):</p>
<p><strong>Dockerfile:</strong></p>
<pre><code class="language-dockerfile">FROM node:18.13.0-alpine AS development

WORKDIR /usr/src/app
COPY package.json ./
COPY yarn.lock ./

RUN yarn install --frozen-lockfile \
&amp;&amp; yarn cache clean

COPY . .

RUN yarn build

CMD yarn install; yarn start:debug;
</code></pre>
<p>In the docker-compose file, we use our backend image generated from the Dockerfile above. We also have an Nginx server to act as a load balancer between app instances, Postgres as the database, and Redis as the centralized database used for communication between app instances. In this example, we simulate the presence of five NestJS app instances.</p>
<p><strong>docker-compose.yml:</strong></p>
<pre><code class="language-yaml">version: &apos;3.7&apos;

services:
  backend:
    image: scalable-chat-app-example-backend
    build:
      context: ./../
      dockerfile: ./docker/Dockerfile
      target: development
    volumes:
      - ./../:/usr/src/app
      - scalable-chat-app-example-backend-node-modules:/usr/src/app/node_modules
      - scalable-chat-app-example-backend-dist:/usr/src/app/dist
    ports:
      - &apos;3000&apos;
    networks:
      - mainnet
    depends_on:
      - postgres
      - redis
    restart: unless-stopped
    scale: 5

  nginx:
    container_name: scalable-chat-app-example-nginx-load-balancer
    image: nginx:latest
    volumes:
      - ./../nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - backend
    networks:
      - mainnet
    ports:
      - &apos;3000:3000&apos;

  postgres:
    container_name: scalable-chat-app-example-postgres-db
    image: postgres:15.0
    networks:
      - mainnet
    environment:
      TZ: ${DB_TIMEZONE}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
      PG_DATA: /var/lib/postgresql/data
    ports:
      - &apos;5432:5432&apos;
    volumes:
      - scalable-chat-app-example-pgdata:/var/lib/postgresql/data

  redis:
    container_name: scalable-chat-app-example-redis-db
    image: redis:7.0.7
    networks:
      - mainnet
    expose:
      - &apos;6379&apos;
    volumes:
      - scalable-chat-app-example-redisdata:/data

networks:
  mainnet:

volumes:
  scalable-chat-app-example-pgdata:
  scalable-chat-app-example-backend-node-modules:
  scalable-chat-app-example-backend-dist:
  scalable-chat-app-example-redisdata:
</code></pre>
<p>We also need the following Nginx configuration to handle WebSocket connections:</p>
<p><strong>nginx.conf:</strong></p>
<pre><code class="language-nginx">user nginx;
events {

	worker_connections 1000;
}
http {

	upstream app {

		server scalable-chat-app-example-backend-1:3000;
		server scalable-chat-app-example-backend-2:3000;
		server scalable-chat-app-example-backend-3:3000;
		server scalable-chat-app-example-backend-4:3000;
		server scalable-chat-app-example-backend-5:3000;
	}

	server {

		listen 3000;
		location / {

			proxy_pass http://app;
			# WebSocket support
			proxy_http_version 1.1;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection $http_connection;

		}
		client_max_body_size 1000M;
	}
}
</code></pre>
<p>To set up the example, navigate to the repository directory and execute the following three commands:</p>
<p>Copy sample.env:</p>
<pre><code>cp sample.env .env
</code></pre>
<p>Run containers:</p>
<pre><code>yarn dc up 
</code></pre>
<p>Initiate docker database:</p>
<pre><code>yarn dc-db-init
</code></pre>
<p>If you want to clean the project you can run the following command:</p>
<pre><code>yarn dc-clean
</code></pre>
<p>Your database has 5 users initiated:</p>
<ul>
<li><a href="mailto:test@test.com">test@test.com</a></li>
<li><a href="mailto:test2@test.com">test2@test.com</a></li>
<li><a href="mailto:test3@test.com">test3@test.com</a></li>
<li><a href="mailto:test4@test.com">test4@test.com</a></li>
</ul>
<p>Each one of them has 1234 as password.</p>
<p>Open swagger at <a href="http://localhost:3000/docs?ref=rabbitbyte.club">http://localhost:3000/docs</a>.</p>
<p>Login multiple users so you obtain the bearer token needed for ws authentication.</p>
<p>Open as many browser tabs as you want and write the following code in each of them:</p>
<pre><code class="language-javascript">const ws = new WebSocket(&apos;ws://localhost:3000/entrypoint&apos;, &apos;introduceHereBearerTokenFromAuthLogin&apos;)
</code></pre>
<p>Check out your users table from docker database and extract them as a json so you can use the array of users in the browser.</p>
<p>Now, type the following in any browser window:</p>
<pre><code class="language-javascript"> users.forEach(user =&gt; ws.send(JSON.stringify({event: &apos;send_chat_message_to_participant&apos;, data: {message: &apos;test&apos;, participantId: user.id}})));
</code></pre>
<p>This will send individual chat message to all participants. You can also check docker logs to see it in action.</p>
<p>Regardless of which NestJS instance grabs the WebSocket connection, the messages are consistently sent to participants. This is how you scale a NestJS chat app to millions of users.</p>
<p>If you are interested in learning how to deploy this stack on Kubernetes, leave a comment and i will do the tutorial.</p>
<p>If you want to collaborate on potential start-up projects, you can reach me at: <a href="mailto:rares.tarabega27@gmail.com">rares.tarabega27@gmail.com</a></p>
<p>If you want to check out an improved and more efficient model for broadcasting messages between instances, as well as how to deploy this stack on Kubernetes, you can find Part 2 of this series <a href="https://dev.to/zenstok/part-22-deploy-scalable-nestjs-chat-app-to-kubernetes-59o0?ref=rabbitbyte.club">here</a>.</p>
]]></content:encoded></item></channel></rss>