<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shuvo</title>
    <description>The latest articles on DEV Community by Shuvo (@0shuvo0).</description>
    <link>https://dev.to/0shuvo0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F728097%2F22055fc3-95c3-4684-8344-ac7727d2dc3a.png</url>
      <title>DEV Community: Shuvo</title>
      <link>https://dev.to/0shuvo0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/0shuvo0"/>
    <language>en</language>
    <item>
      <title>I'm Too Lazy to Write Alt Text, So Gemma Does It For Me</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Sun, 25 Oct 2026 04:14:14 +0000</pubDate>
      <link>https://dev.to/0shuvo0/im-too-lazy-to-write-alt-text-so-gemma-does-it-for-me-6ji</link>
      <guid>https://dev.to/0shuvo0/im-too-lazy-to-write-alt-text-so-gemma-does-it-for-me-6ji</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Every codebase I've touched has the same line in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/imgs/cookie.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It's rarely because we don't care about accessibility. Describing forty images is just the most boring job in front-end, so it gets pushed to "later."&lt;/p&gt;

&lt;p&gt;So I built a VS Code extension that does it for me. Hover over any &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; with a missing or empty &lt;code&gt;alt&lt;/code&gt;, click &lt;strong&gt;Generate alt text&lt;/strong&gt;, and the description is written straight into your code.&lt;/p&gt;

&lt;p&gt;Feeling really lazy? Press &lt;code&gt;Cmd+Shift+A&lt;/code&gt; (&lt;code&gt;Ctrl+Shift+A&lt;/code&gt; on Windows/Linux) and it fills in every empty alt in the file at once.&lt;/p&gt;

&lt;p&gt;A few small rules keep it safe to trust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;never overwrites&lt;/strong&gt; alt text you already wrote.&lt;/li&gt;
&lt;li&gt;Failures are reported per image, and the successful ones are still applied.&lt;/li&gt;
&lt;li&gt;Output is escaped, so a stray quote can't break your markup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F66g5wwn8w1klvstnbrml.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F66g5wwn8w1klvstnbrml.gif" alt="Extension demo" width="600" height="367"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/0shuvo0" rel="noopener noreferrer"&gt;
        0shuvo0
      &lt;/a&gt; / &lt;a href="https://github.com/0shuvo0/ai-img-alt-text-ext" rel="noopener noreferrer"&gt;
        ai-img-alt-text-ext
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Generate accessible image alt text in HTML, JSX, and TSX files using a locally running Ollama vision model. Image contents are sent only to the Ollama server you configure.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;AI Image Alt Text&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Generate accessible image alt text in HTML, JSX, and TSX files using a locally running Ollama vision model. Image contents are sent only to the Ollama server you configure.&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/0shuvo0/ai-img-alt-text-ext/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0shuvo0%2Fai-img-alt-text-ext%2FHEAD%2Fdemo.gif" alt="demo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Hover over an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; element with an empty or missing &lt;code&gt;alt&lt;/code&gt; attribute and select &lt;strong&gt;Generate alt text&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Cmd+Shift+A&lt;/strong&gt; (or &lt;strong&gt;Ctrl+Shift+A&lt;/strong&gt; on Windows/Linux) to fill empty or missing alt attributes in the current file.&lt;/li&gt;
&lt;li&gt;Supports multiline tags, other attributes, workspace-relative image files, and HTTP(S) image URLs.&lt;/li&gt;
&lt;li&gt;Existing non-empty alt text is never overwritten. Bulk generation reports individual image failures and applies successful results together.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Requirements&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://ollama.com/" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt; running locally or at a server you can access.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A vision-capable model installed in Ollama, for example:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;ollama pull gemma3:latest&lt;/pre&gt;

&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first generation prompts for the Ollama server URL (for example &lt;code&gt;http://localhost:11434&lt;/code&gt;) and model name. These values are saved in the VS Code user settings. To change them…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/0shuvo0/ai-img-alt-text-ext" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The whole thing runs on &lt;strong&gt;Gemma 3 through Ollama&lt;/strong&gt;, entirely on my own machine.&lt;/p&gt;

&lt;p&gt;When you click the button, the extension:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finds the image. It checks the workspace root first, then common static folders like &lt;code&gt;public/&lt;/code&gt;, so &lt;code&gt;/imgs/cookie.png&lt;/code&gt; just works. Remote URLs work too.&lt;/li&gt;
&lt;li&gt;Sends it to the local Gemma model with a short prompt asking for a concise, useful description.&lt;/li&gt;
&lt;li&gt;Writes the result back into the &lt;code&gt;alt&lt;/code&gt; attribute.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Setup is two steps: &lt;code&gt;ollama pull gemma3:latest&lt;/code&gt;, then point the extension at your Ollama URL. The model name is a setting, so you can swap in any other vision model without touching the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Alt text feels like a perfect job for a hosted API, until you think about what you'd be sending it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your images stay yours.&lt;/strong&gt; Client mockups, unreleased products and internal dashboards often sit in a repo long before anyone should see them. With a local model, nothing leaves the laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing per image.&lt;/strong&gt; A big project can have hundreds of images. Running them through Gemma costs electricity, not a bill that grows with your codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's swappable.&lt;/strong&gt; If a better open vision model shows up next month, it's a one-line settings change. I'm not tied to one provider's pricing or availability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest trade-off:&lt;/strong&gt; a local model is slower than a hosted API. But this is a job you start and walk away from, so for this use case slow and steady is a fine deal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;I ran it on 20 images from one of my projects.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; just 3 minutes, versus roughly 10 minutes writing them by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usable as-is:&lt;/strong&gt; 18 out of 20 descriptions needed no editing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where it struggled:&lt;/strong&gt; Animated or vector images eg &lt;code&gt;gif&lt;/code&gt; files&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; every description is generated by Gemma 3, running locally through Ollama.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Chat with client In -&gt; Invoice Out</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Thu, 15 Oct 2026 06:30:00 +0000</pubDate>
      <link>https://dev.to/0shuvo0/chat-with-client-in-invoice-out-4odg</link>
      <guid>https://dev.to/0shuvo0/chat-with-client-in-invoice-out-4odg</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Every freelancer knows this moment.&lt;/p&gt;

&lt;p&gt;The client says &lt;em&gt;"Great work, can you send the invoice?"&lt;/em&gt; and suddenly you're digging through 200 WhatsApp messages to find the rate, the hours, the email address, and that one message where the price changed from $250 to $300.&lt;/p&gt;

&lt;p&gt;Then you open a template, retype everything, export a PDF, and email it. Ten minutes gone, for something that's already written down in the chat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chat2Invoice&lt;/strong&gt; fixes that. You paste the conversation, and a local AI turns it into a ready-to-send invoice PDF.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Paste&lt;/strong&gt; your WhatsApp, Slack, iMessage, or email conversation (optionally attach a contract, a receipt, or a mockup).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review&lt;/strong&gt; what the AI found. Anything missing gets flagged, and everything is editable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send&lt;/strong&gt; a clean A4 PDF. Download it or email it straight to the client.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Who would pay for this?&lt;/strong&gt; Anyone who gets paid directly, outside platforms that auto-generate invoices: freelance designers, developers, writers, consultants, and small agencies who negotiate in DMs and then have to turn those DMs into paperwork.&lt;/p&gt;

&lt;p&gt;The best part is that the chat never has to leave your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Paste the chat&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Step 2: Review what the AI found&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The AI shows what it extracted and highlights anything it couldn't find. Here it caught one field was not found in chat.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Step 3: Preview, download, send&lt;/strong&gt;&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;h2&gt;
  
  
  Struggling to learn Git?
&lt;/h2&gt;

&lt;p&gt;Learn Git by solving problems on &lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;GitMission.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5629n4cm1n7gxrhe6fat.png" alt="GitMission preview" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/0shuvo0" rel="noopener noreferrer"&gt;
        0shuvo0
      &lt;/a&gt; / &lt;a href="https://github.com/0shuvo0/chat2invoice" rel="noopener noreferrer"&gt;
        chat2invoice
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Chat2Invoice&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Paste a client chat → local AI (Ollama) extracts invoice details → review/edit → generate an A4 PDF → download or email it.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/0shuvo0/chat2invoice/preview.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0shuvo0%2Fchat2invoice%2FHEAD%2Fpreview.gif" alt="demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; Next.js 14 (App Router) + TypeScript + Tailwind CSS, jsPDF (PDF generation), pdfjs-dist (PDF text extraction), Inter font bundled locally (@fontsource).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline:&lt;/strong&gt; everything runs locally. Text/PDF reading and PDF generation happen in the browser; the AI runs in your local Ollama. Only "Send via Email" needs internet (Resend).&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Requirements&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Node.js &lt;strong&gt;18.18+&lt;/strong&gt; (20 LTS recommended)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ollama.com/download" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; 1. Install Ollama, then pull the model (one time, ~3 GB)&lt;/span&gt;
ollama pull gemma3:latest

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; 2. Install dependencies&lt;/span&gt;
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; chat2invoice
npm install

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; 3. Environment&lt;/span&gt;
cp .env.example .env.local     &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; edit if needed&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;code&gt;.env.local&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-dotenv notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-v"&gt;OLLAMA_AI_MODEL&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;gemma3:latest&lt;/span&gt;
&lt;span class="pl-v"&gt;OLLAMA_HOST&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;http://127.0.0.1:11434&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Optional – the "Send Invoice via Email" section only appears when BOTH are set:&lt;/span&gt;
&lt;span class="pl-v"&gt;RESEND_API_KEY&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;re_xxx&lt;/span&gt;
&lt;span class="pl-v"&gt;RESEND_SENDER_EMAIL&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;invoices@your-verified-domain.com&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;(Shell environment variables work too.) RESEND_SENDER_EMAIL must…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/0shuvo0/chat2invoice" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The open-source AI part:&lt;/strong&gt; &lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 3&lt;/a&gt; running locally through &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;. No API key, no per-request bill, no server in the middle. The app talks to Ollama on &lt;code&gt;127.0.0.1:11434&lt;/code&gt;, and that's where the "understanding" happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 14&lt;/strong&gt; (App Router) + &lt;strong&gt;TypeScript&lt;/strong&gt; + &lt;strong&gt;Tailwind CSS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;jsPDF&lt;/strong&gt; to generate the invoice PDF in the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pdfjs-dist&lt;/strong&gt; to read text out of attached PDF contracts, also in the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inter&lt;/strong&gt; font bundled locally, so nothing is fetched from a CDN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend&lt;/strong&gt; for the optional "Send via Email" feature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Input.&lt;/strong&gt; Paste the chat or hit "Paste Clipboard". An optional "Advanced &amp;amp; Attachments" section lets you add notes that &lt;em&gt;override&lt;/em&gt; the chat (like "Net 15, apply 10% holiday discount"), plus PDF/TXT files and images. Text is extracted in the browser, and images go to Gemma 3's vision capability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review.&lt;/strong&gt; Missing required fields appear under &lt;strong&gt;Action Needed&lt;/strong&gt;, and found ones are shown as editable fields. Optional extras include discount, amount due now, tax note, currency, company names, and payment instructions. The invoice ID (like &lt;code&gt;INV-2026-OCT-13-JD2N&lt;/code&gt;) and issue date are auto-generated, and "Due Now" defaults to amount minus discount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready.&lt;/strong&gt; Preview the invoice, download the PDF, or send it to the client with an editable message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I intentionally put a human checkpoint in the middle. The AI proposes, and &lt;em&gt;you&lt;/em&gt; approve. A small local model will occasionally misread a message, and an invoice is a document with money on it, so the review step is a feature rather than a limitation. Missing details get flagged and asked for instead of invented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running it yourself&lt;/strong&gt; takes a minute. Pull a model with Ollama, then create &lt;code&gt;.env.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OLLAMA_AI_MODEL=gemma3:latest
OLLAMA_HOST=http://127.0.0.1:11434

# Optional: the "Send Invoice via Email" section only appears when BOTH are set
RESEND_API_KEY=re_xxx
RESEND_SENDER_EMAIL=invoices@your-verified-domain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RESEND_SENDER_EMAIL&lt;/code&gt; must be on a domain verified in your Resend account. Skip these two lines and the app simply hides the email section, so everything else works fully offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Client chats are some of the most sensitive text a freelancer owns. They contain real names, email addresses, rates, project details, and sometimes things covered by an NDA. Pasting that into a closed cloud API just to get a PDF is a bad trade.&lt;/p&gt;

&lt;p&gt;Open-weight models changed the deal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy by architecture.&lt;/strong&gt; The chat is processed by Gemma on your own machine. There's no "we promise not to store it" because there's no server to store it on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Costs nothing to run.&lt;/strong&gt; No tokens, no subscription, no usage cap. A tool for a side hustle shouldn't eat into the side hustle's income.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works offline.&lt;/strong&gt; Text extraction, AI, and PDF generation all happen locally. Only the optional email step needs internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; The model is one environment variable (&lt;code&gt;OLLAMA_AI_MODEL&lt;/code&gt;). If a better open model shows up next month, swapping it in is a one-line change, with no pricing changes or deprecations to worry about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A closed API could do the extraction, but it couldn't make the promise that matters most to the person using this tool: &lt;em&gt;your client's conversation never left your laptop.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;My friend who is in final year in college and he is super busy trying to manage study and freelance work at the same time. So this will save him a few hours monthly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 3 runs locally via Ollama and handles both text extraction and image understanding for receipts and mockups.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built a Group Chat Where Only AIs Can Talk (and They Roast Each Other)</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Wed, 14 Oct 2026 05:10:34 +0000</pubDate>
      <link>https://dev.to/0shuvo0/i-built-a-group-chat-where-only-ais-can-talk-and-they-roast-each-other-2bfk</link>
      <guid>https://dev.to/0shuvo0/i-built-a-group-chat-where-only-ais-can-talk-and-they-roast-each-other-2bfk</guid>
      <description>&lt;p&gt;Imagine a group chat like WhatsApp or Discord, but no human can send messages. Only AI models can. They talk to each other, bully each other, roast each other, and react to each other's messages.&lt;/p&gt;

&lt;p&gt;That was my idea, and it turned out to be a lot of fun.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Jjmaq4meZYM" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Here's how I built it, including the one trick that made it far more entertaining: giving every model its own voice.&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/0shuvo0/AI-Group-Chat" rel="noopener noreferrer"&gt;github.com/0shuvo0/AI-Group-Chat&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;It's a custom app where I can add a bunch of AI models to a shared chat. Once they're in, they read what the others wrote and respond on their own. I just sit back and watch.&lt;/p&gt;

&lt;p&gt;The first question is how to get the models into the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: Too many APIs
&lt;/h2&gt;

&lt;p&gt;I don't want to open chatgpt.com and paste messages by hand. I want my app to send a question to a model and get the answer back programmatically. That's what an &lt;strong&gt;API&lt;/strong&gt; is for. Gemini, Claude, Grok, and ChatGPT all have their own.&lt;/p&gt;

&lt;p&gt;The catch: I wanted &lt;em&gt;lots&lt;/em&gt; of models, and API access is paid. Five models would mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;five separate accounts,&lt;/li&gt;
&lt;li&gt;five separate billing setups,&lt;/li&gt;
&lt;li&gt;five API keys to manage,&lt;/li&gt;
&lt;li&gt;a minimum top-up on each, even though I'd barely use any tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's complicated and not cost effective.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: OpenRouter
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; gives you access to a huge catalog of models through a single API. You add credit to one account, use whichever models you like, and it bills you per model usage.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;one service, one API key, one balance&lt;/strong&gt;. Swapping or adding a model is just a change to the model name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: Getting them to actually chat
&lt;/h2&gt;

&lt;p&gt;Left alone, models will happily ramble about whatever they want, and that's rarely entertaining. So I added a &lt;strong&gt;theme&lt;/strong&gt;. When I open the app, I can give the chat a topic, and the models riff around it.&lt;/p&gt;

&lt;p&gt;Each model gets a prompt like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
You are &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ai, chatting in a group chat with these AIs &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;others&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;.
Talk the way people do in a casual group chat.

&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`\nTheme of this chat: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

Rules:
- Write only your own messages, never lines for the others.
- Send 1 to 3 short messages. Put each message on its own line.
- Keep each message to a sentence or two. Do not write paragraphs.
- No markdown, no quotes, and do not start with your name or a label.
- An emoji-only message is fine now and then.
- Respond to what others said, by name if it helps. Add something new instead of just agreeing or repeating.
- Do not say goodbye or try to wrap up the conversation.
- Talk like carefree young generation: relaxed, fun, expressive, emojis

&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;
  &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`Chat so far:
&amp;lt;chat&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
&amp;lt;/chat&amp;gt;

If you have nothing to add right now, reply with exactly: &amp;lt;SKIP&amp;gt;
Otherwise, send your next message(s).`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Nobody has said anything yet, so you are sending the first message.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details make this work well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role clarity.&lt;/strong&gt; Each model is told it's in a group chat and who else is there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict format rules.&lt;/strong&gt; Short messages, no markdown, no name labels. Without these, models write essays or script lines for the other participants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared history.&lt;/strong&gt; Previous messages are passed in a &lt;code&gt;&amp;lt;chat&amp;gt;&lt;/code&gt; block so replies actually respond to what was said.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An escape hatch.&lt;/strong&gt; Models can answer &lt;code&gt;&amp;lt;SKIP&amp;gt;&lt;/code&gt; if they have nothing to add, which keeps the conversation from feeling forced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personality.&lt;/strong&gt; The casual, emoji-friendly tone instruction is what gives the chat its vibe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app itself was simple to build. I described what I wanted to Claude, and a few prompts later I had a working version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: It's too fast to read
&lt;/h2&gt;

&lt;p&gt;The app worked and looked good, but it wasn't as fun as I'd hoped. Messages arrive so quickly that reading them all becomes a chore.&lt;/p&gt;

&lt;p&gt;So I gave the models a voice.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: Kokoro.js
&lt;/h3&gt;

&lt;p&gt;For text-to-speech I used &lt;strong&gt;&lt;a href="https://huggingface.co/hexgrad/Kokoro-82M" rel="noopener noreferrer"&gt;Kokoro&lt;/a&gt;&lt;/strong&gt;, an open model with just 82 million parameters, around 300 MB in size. That's small enough to run directly in the browser, so there are no extra server calls or per-character fees, and it's fast.&lt;/p&gt;

&lt;p&gt;I used its JavaScript library, &lt;a href="https://www.npmjs.com/package/kokoro-js" rel="noopener noreferrer"&gt;kokoro-js&lt;/a&gt;, and plugged it into the web app. Each incoming AI message is converted to audio locally and played back. Now the models don't just type, they &lt;em&gt;talk&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The final result
&lt;/h2&gt;

&lt;p&gt;Watching a handful of AIs bicker, joke, and roast each other out loud is oddly entertaining.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Tech stack recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multiple AI models, one API&lt;/td&gt;
&lt;td&gt;&lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-speech in the browser&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://huggingface.co/hexgrad/Kokoro-82M" rel="noopener noreferrer"&gt;Kokoro&lt;/a&gt; via &lt;a href="https://www.npmjs.com/package/kokoro-js" rel="noopener noreferrer"&gt;kokoro-js&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaffolding the app&lt;/td&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The full code is on GitHub: &lt;a href="https://github.com/0shuvo0/AI-Group-Chat" rel="noopener noreferrer"&gt;0shuvo0/AI-Group-Chat&lt;/a&gt;. Clone it, add your OpenRouter key, pick a theme, and let the models loose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Struggling with git?
&lt;/h2&gt;

&lt;p&gt;Learn git by playing games and solving missions with &lt;a href="https://gitmission.com/" rel="noopener noreferrer"&gt;GitMission.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdh8fieg1pcprtbas1rgi.png" alt="GitMission Preview" width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>showdev</category>
    </item>
    <item>
      <title>People Won't Go Outside so I made this</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Wed, 07 Oct 2026 01:38:29 +0000</pubDate>
      <link>https://dev.to/0shuvo0/people-wont-go-outside-so-i-made-this-20j4</link>
      <guid>https://dev.to/0shuvo0/people-wont-go-outside-so-i-made-this-20j4</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/3kbQoHIhxB0" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://goseek.ai.studio/" rel="noopener noreferrer"&gt;GoSeek&lt;/a&gt;&lt;/strong&gt; is a mobile scavenger hunt that gets you outside by giving you a riddle instead of an instruction.&lt;/p&gt;

&lt;p&gt;The problem I started with: if an app says &lt;em&gt;"go outside!"&lt;/em&gt;, nobody goes. People don't like being told what to do, but they love solving puzzles. So GoSeek hands you this instead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Small furry apex predator that domesticated itself, loves afternoon sunbeams, and leaves silent pawprints in the garden."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can't solve that from the couch. You have to go find the thing, and the app only accepts a photo taken live in the app. No gallery uploads.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqz5bd8kv3lcnwcdmavs.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyqz5bd8kv3lcnwcdmavs.gif" alt="App demo" width="480" height="1040"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The loop:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tap &lt;strong&gt;Touch Grass&lt;/strong&gt; and get a riddle for a random everyday object (a bench, a pigeon, a manhole cover).&lt;/li&gt;
&lt;li&gt;Go outside and work out what it is.&lt;/li&gt;
&lt;li&gt;Open the camera and take a photo.&lt;/li&gt;
&lt;li&gt;A vision model running &lt;strong&gt;on your phone&lt;/strong&gt; checks whether it matches.&lt;/li&gt;
&lt;li&gt;Match confirmed: confetti, points, and the photo joins your &lt;strong&gt;Trophy Gallery&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Harder objects are worth more points, and points level you up from &lt;em&gt;Wanderer&lt;/em&gt; to &lt;em&gt;Explorer&lt;/em&gt; to &lt;em&gt;Seeker&lt;/em&gt; to &lt;em&gt;Trailblazer&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is it for?&lt;/strong&gt; Anyone, especially city dwellers. The object list leans urban (street signs, power lines, shop signs), with nature finds like butterflies and pine cones as harder, higher-reward targets. The screen is the shortest part: read, walk, snap, done.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🌿 &lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://goseek.ai.studio/" rel="noopener noreferrer"&gt;goseek.ai.studio&lt;/a&gt; (best on your phone)&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/0shuvo0" rel="noopener noreferrer"&gt;
        0shuvo0
      &lt;/a&gt; / &lt;a href="https://github.com/0shuvo0/goseek" rel="noopener noreferrer"&gt;
        goseek
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      GoSeek is a mobile-first outdoor scavenger hunt game. Each round gives you a riddle clue, and you must find the matching object in the real world by taking a photo with your camera or uploading an image.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/0shuvo0/goseek/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0shuvo0%2Fgoseek%2FHEAD%2Fdemo.gif" alt="Demo video"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;GoSeek&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;GoSeek is a mobile-first outdoor scavenger hunt game. Each round gives you a riddle clue, and you must find the matching object in the real world by taking a photo with your camera or uploading an image.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/0shuvo0/goseek/demo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0shuvo0%2Fgoseek%2FHEAD%2Fdemo.png" alt="Demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Try it: &lt;a href="https://goseek.ai.studio/" rel="nofollow noopener noreferrer"&gt;goseek.ai.studio&lt;/a&gt;
&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;The app picks a random object from &lt;code&gt;src/data/objects.json&lt;/code&gt; and shows a clue/riddle.&lt;/li&gt;
&lt;li&gt;You open the mission screen, then use the live camera or a file upload to capture a photo.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/services/aiModel.ts&lt;/code&gt; validates the image against the target object using a local Hugging Face vision pipeline and a fallback visual heuristic.&lt;/li&gt;
&lt;li&gt;If the object matches, the app awards points, stores the discovery, and updates your run stats.&lt;/li&gt;
&lt;li&gt;Progress is saved in the browser using IndexedDB (&lt;code&gt;src/services/db.ts&lt;/code&gt;), so your streak, score, and discovery history persist locally.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Main flow&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/App.tsx&lt;/code&gt; manages the dashboard, active mission, score updates, and success/reset modals.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/components/ActiveMission.tsx&lt;/code&gt; handles the camera, verification loop, save…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/0shuvo0/goseek" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Design: Google Stitch → Figma
&lt;/h3&gt;

&lt;p&gt;I described the app to &lt;a href="https://stitch.withgoogle.com/" rel="noopener noreferrer"&gt;Google Stitch&lt;/a&gt;, imported the generated design into Figma, simplified it, and edited the copy.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Stitch prompt
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP: GoSeek
1.
when user opens the app and clicks a button first we choose an everyday common object that can be easily found outside(We will have an array of those things with difficulty level, more common = less difficult and more difficult = more points)
2.
app uses ai to describe the object as a mystery/riddle eg. instead of saying 'cat' it says 'small furry apex predator that domesticated itself'
3.
the use has to go out find the object and take a photo(use can only take photo on the app, website not upload old or downloaded photos)
4.
then the app uses AI to analyze the image and tells use if the photo is of correct thing or not
if correct so a nice animation and add points to user

design a mvp for it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




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

&lt;p&gt;Google stitch output: &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcbf7872j08xz4ghz9yjx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcbf7872j08xz4ghz9yjx.png" alt="Google stitch output" width="799" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After simplifying on figma:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff1tzq15axt8tz7aa1znm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff1tzq15axt8tz7aa1znm.png" alt="After simplifying on" width="799" height="415"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The architecture problem
&lt;/h3&gt;

&lt;p&gt;My first plan: keep a list of objects and have an on-device model write riddles on the fly. That fails. A model good enough for &lt;em&gt;clever&lt;/em&gt; riddles is too big for most phones, and this is an outdoor game that has to run on the device you actually carry.&lt;/p&gt;

&lt;p&gt;So I split the job and put each task where it fits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Runs on&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Write creative riddles&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 3 4B&lt;/strong&gt; (open-weight)&lt;/td&gt;
&lt;td&gt;My computer, via Ollama&lt;/td&gt;
&lt;td&gt;Once, ahead of time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge the photo&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;SmolVLM-500M&lt;/strong&gt; (open-weight)&lt;/td&gt;
&lt;td&gt;The user's phone, in the browser&lt;/td&gt;
&lt;td&gt;Every play&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The heavy creative model runs &lt;strong&gt;once&lt;/strong&gt; and produces a static &lt;code&gt;objects.json&lt;/code&gt;. The tiny vision model handles the one thing that must happen live.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Generating riddles with Gemma
&lt;/h3&gt;

&lt;p&gt;I pulled &lt;code&gt;gemma3:4b&lt;/code&gt; with Ollama and wrote a small script that asks for five riddles per object. The key is the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You write riddles for an outdoor scavenger hunt game for all ages.
Write 5 different riddles, each describing "${obj.name}" WITHOUT ever saying the word "${obj.name}" (or its plural, or any word in it).
Rules:
- Each riddle is 1-2 sentences, maximum 30 words.
- Each riddle uses a different angle (looks, purpose, location, sound, behavior, feel).
- Family-friendly, no mention of people or danger.
- Do not start with "I am" every time.

Output ONLY a JSON array of 5 strings. No explanation, no markdown.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Asking for &lt;strong&gt;a different angle per riddle&lt;/strong&gt; keeps replays fresh, and the "never say the word" rule stops riddles from giving the answer away.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Full generation script (ai.ts + index.ts)
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ai.ts&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;askGemma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:11434/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemma3:4b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userPrompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Local AI request failed (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;). Make sure Ollama is running.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Local AI returned an empty recommendation. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;askGemma&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;askGemma&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./ai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Difficulty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;easy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GameObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Difficulty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;easy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tree&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OUTPUT_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;objects.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Generating "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`...the prompt above...`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;askGemma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/``&lt;/span&gt;&lt;span class="err"&gt;`
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;```/gi, "").trim();
    const riddles = JSON.parse(cleaned);

    results.push({ name: obj.name, difficulty: obj.difficulty, riddles });
  }

  await fs.writeFile(OUTPUT_FILE, JSON.stringify(results, null, 2), "utf8");
  console.log(`&lt;/span&gt;&lt;span class="err"&gt;✅&lt;/span&gt; &lt;span class="nx"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Saved&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;objects&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;OUTPUT_FILE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`);
}

run().catch(console.error);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




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

&lt;p&gt;The output is a plain &lt;code&gt;objects.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"road"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"difficulty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"easy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"riddles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"A long ribbon of black stone that carries wheels from where you are to where you're going, never moving itself"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Painted with dashed lines and arrows, it tells travelers where to go, yet it has no voice"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Building the app in Google AI Studio
&lt;/h3&gt;

&lt;p&gt;I opened &lt;a href="https://aistudio.google.com/apps" rel="noopener noreferrer"&gt;Google AI Studio's app builder&lt;/a&gt;, attached the design screenshots and &lt;code&gt;objects.json&lt;/code&gt;, and gave it a single detailed prompt: game loop, data models, tech stack (React, TypeScript, Tailwind, Transformers.js, IndexedDB), and camera rules. A few follow-up prompts later the app worked, and one click on &lt;strong&gt;Publish&lt;/strong&gt; made it live.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  AI Studio prompt
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role &amp;amp; Goal: Act as an expert frontend developer. Build a mobile-first, PWA-ready web application called "GoSeek," an outdoor scavenger hunt game where users find real-world objects based on riddles.

UI/UX Design
Match the layout, dark theme, and typography strictly to the provided design mockups in attached photos. The app must be highly responsive on mobile devices.

Core Game Loop
Model Initialization: On first load, download and initialize the SmolVLM-500M AI model locally on the device (show a progress bar during this step).
Mission Generation: Select a random everyday outdoor object from objects.json. Display one of its associated riddles to the user (e.g., "small furry apex predator that domesticated itself" instead of "cat").
Capture: The user clicks "Open camera" to access the device's live camera. Constraint: Users must capture a live photo; file uploads from the gallery are strictly disabled.
Verification: Pass the captured frame to the local SmolVLM-500M model to verify if the object matches the riddle.
Success State: If matched, trigger a celebratory animation (e.g., confetti or a success modal) and award points based on the object's difficulty.
Gallery &amp;amp; State: Downscale the verified image and save it locally along with the object name, timestamp, and points earned. Display these in a scrollable "Trophy Gallery &amp;amp; Log" history section.

Data Structures &amp;amp; Logic
export type Difficulty = 'easy' | 'medium' | 'hard';
export interface GameObject {
  name: string;
  difficulty: Difficulty;
  riddles: string[];
}
export const points: Record&amp;lt;Difficulty, number&amp;gt; = { easy: 10, medium: 20, hard: 30 }
export const level: Record&amp;lt;string, number&amp;gt; = {
  wanderer: 0,      // 0-49 points
  explorer: 50,     // 50-99 points
  seeker: 100,      // 100-149 points
  trailblazer: 150  // 150+ points
}

Technical Requirements
Tech Stack: Use React with TypeScript and Tailwind CSS for styling.
AI Integration: Use Hugging Face transformers.js to load and run SmolVLM-500M entirely on-device via WebGPU/WebAssembly.
Storage: Use IndexedDB (via a wrapper like idb or localforage) rather than localStorage to save the downscaled user photos and history state without hitting quota limits.
Camera API: Use the HTML5 navigator.mediaDevices.getUserMedia API facing the environment (rear camera). Fall back to typical file upload if it fails.
Handle edge cases and figure stuff out if missing in description.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




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

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

&lt;h3&gt;
  
  
  The stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Design&lt;/td&gt;
&lt;td&gt;Google Stitch → Figma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Riddle generation&lt;/td&gt;
&lt;td&gt;Gemma 3 4B via Ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-device vision&lt;/td&gt;
&lt;td&gt;SmolVLM-500M via Transformers.js (WebGPU/WASM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;React + TypeScript + Tailwind CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;IndexedDB (photos + history)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getUserMedia&lt;/code&gt;, rear camera, live capture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build + hosting&lt;/td&gt;
&lt;td&gt;Google AI Studio&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How It Went Outside
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;So I played with it outsider for 20-0 minutes and it was a blast&lt;/li&gt;
&lt;li&gt;One of the riddle I got was: &lt;em&gt;No back cushions, no screen, just a place to rest, feed the pigeons, or wait for someone&lt;/em&gt; and very interesting I must way&lt;/li&gt;
&lt;li&gt;The SmolVLM-500M is small but so far it has been working decent]&lt;/li&gt;
&lt;li&gt;It runs very smoothly on my iPhone 14&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Open models didn't just make GoSeek cheaper. They made the architecture possible.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Photos never leave your phone.&lt;/strong&gt; GoSeek points a camera at your street and neighborhood. Because SmolVLM-500M is open-weight and runs in the browser, verification happens on the device: no upload, no server, no account. A closed vision API would receive every photo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works where the game happens.&lt;/strong&gt; After the one-time model download, checking a photo doesn't need a network round-trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing per play.&lt;/strong&gt; Every verification is free, where a hosted vision API charges per image. Running Gemma locally made generating all the riddles free too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right model, right place.&lt;/strong&gt; Open weights let me run a bigger model once for a creative task and a tiny one live on the phone. That split is the core design decision of the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything is swappable.&lt;/strong&gt; A better small vision model next month? Swap it in. Riddles in another language or tone? Regenerate &lt;code&gt;objects.json&lt;/code&gt; with a new prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Honest trade-offs:&lt;/strong&gt; The first load need internet connection for downloading the AI model, but after that it can run completely offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 3 4B, run locally through Ollama, writes every riddle in the game.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading, and now go touch some grass. 🌿&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Struggling to learn Git?
&lt;/h2&gt;

&lt;p&gt;Learn Git by solving problems on &lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;GitMission.com&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5629n4cm1n7gxrhe6fat.png" alt="GitMission preview" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>My friend's Game Boy got stolen so I built him one</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Sat, 03 Oct 2026 06:45:29 +0000</pubDate>
      <link>https://dev.to/0shuvo0/my-friends-game-boy-got-stolen-so-i-built-him-one-3lnn</link>
      <guid>https://dev.to/0shuvo0/my-friends-game-boy-got-stolen-so-i-built-him-one-3lnn</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My friend's Game Boy Color got stolen. Rude. Then he went looking for a replacement and found out that decent used ones are now rare, overpriced, or "mint condition" in the way that means "mint-flavored lie."&lt;/p&gt;

&lt;p&gt;So I built him a new one that can't be stolen: a &lt;strong&gt;Game Boy Color in the browser&lt;/strong&gt;. It's teal, it has a D-pad that clicks, and it plays real Game Boy games.&lt;/p&gt;

&lt;p&gt;The twist is that he never has to scroll through a ROM list. He just tells it what he's in the mood for:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I want to play a game involving Pokémon."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A small AI reads the request, picks a game from the collection, and boots it. He can type the request or tap the mic and say it out loud. A bit like a game store clerk who lives inside the console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff82oi9rintgdt2uyaqvb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff82oi9rintgdt2uyaqvb.gif" alt="Game demo" width="600" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/0shuvo0" rel="noopener noreferrer"&gt;
        0shuvo0
      &lt;/a&gt; / &lt;a href="https://github.com/0shuvo0/Game-boy-color-emulator" rel="noopener noreferrer"&gt;
        Game-boy-color-emulator
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Browser-based Gameboy Color Emulator with AI game search capabilities using Google gemma 4b
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/0shuvo0/Game-boy-color-emulator/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2F0shuvo0%2FGame-boy-color-emulator%2FHEAD%2Fdemo.gif" alt="Demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Browser-based Gameboy Color Emulator&lt;/h1&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;with AI game search capabilities using Google gemma 4b&lt;/h2&gt;

&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: For the AI feature to work you need to have &lt;a href="https://ollama.com/library/gemma3:4b" rel="nofollow noopener noreferrer"&gt;https://ollama.com/library/gemma3:4b&lt;/a&gt; installed and running on your machine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;in the package.json file there is a script called "start:ai" which will start the ollama server.&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/0shuvo0/Game-boy-color-emulator" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Emulator:&lt;/strong&gt; The mGBA core compiled to WebAssembly, wired up in a Vite + TypeScript project. It loads ROMs straight from &lt;code&gt;public/gameroms&lt;/code&gt;, including ones packed inside ZIP files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controls:&lt;/strong&gt; Keyboard (&lt;code&gt;Z&lt;/code&gt;/&lt;code&gt;X&lt;/code&gt;/&lt;code&gt;Enter&lt;/code&gt;/&lt;code&gt;Shift&lt;/code&gt;), touch buttons on the on-screen console, and gamepad support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AI picker:&lt;/strong&gt; Gemma 3 (4B), running locally through &lt;strong&gt;Ollama&lt;/strong&gt;. When he makes a request, the app sends it along with the list of available games. Gemma picks the best match and the emulator loads it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice:&lt;/strong&gt; The browser's built-in speech recognition fills in the prompt and submits it when he stops talking. It's not an AI model, but it makes the whole thing feel like magic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escape hatch:&lt;/strong&gt; If he knows what he wants, "Choose a game instead" opens a normal list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt is simple: "here's what the player wants, here's the game list, pick one." The game list is just a file (&lt;code&gt;src/games.ts&lt;/code&gt;), so adding a game means dropping a ROM in a folder and adding its name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;This project needed an AI that could do one small job well: match a vibe to a filename. A giant cloud model would be overkill, and a cloud API would have made it worse in a few ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing.&lt;/strong&gt; No API key, no per-request bill, no "you've hit your limit" while someone is trying to play Pokémon. For a gift to a friend, that matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It runs locally.&lt;/strong&gt; His requests never leave the machine. Nobody needs to know what he plays at 2 AM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's swappable.&lt;/strong&gt; Gemma 3 4B was small and fast enough. If a different model picks better, it's one line to change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't break on me.&lt;/strong&gt; No deprecation emails, no pricing changes, no surprise outages. The model is on the disk, and it stays there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 4B open model on a normal machine turned out to be plenty for this. It does the job without anyone's permission or a credit card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 3 (4B) running locally through Ollama powers the natural-language game picker.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hacktoberfest 2026, what changed?</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Thu, 01 Oct 2026 04:06:27 +0000</pubDate>
      <link>https://dev.to/0shuvo0/hacktoberfest-2026-what-changed-1g3g</link>
      <guid>https://dev.to/0shuvo0/hacktoberfest-2026-what-changed-1g3g</guid>
      <description>&lt;h1&gt;
  
  
  Hacktoberfest 2026 Is Not What You Remember (And That's Actually Good News)
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Last updated: October 1, 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've done Hacktoberfest before, you probably remember the drill: open four pull requests, wait for the merge notifications, and wear your free T-shirt with quiet pride.&lt;/p&gt;

&lt;p&gt;Forget all of that. &lt;strong&gt;Pull requests no longer count toward Hacktoberfest rewards.&lt;/strong&gt; The whole event has been rebuilt from the ground up, and this year it's about learning and building with open-source AI.&lt;/p&gt;

&lt;p&gt;Don't close the tab yet. The new version is free, open to beginners, and has more ways to earn swag than the old one did. This guide walks through what changed, why, and exactly how to take part, online or in person.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 60-second version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pull requests are out.&lt;/strong&gt; Neither PRs nor merge requests count toward rewards anymore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The theme is open-source AI.&lt;/strong&gt; The tagline is "AI belongs to everyone," and the focus is open-weight models, agents, and the tools around them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There are two ways to join:&lt;/strong&gt; 300+ one-day in-person events called &lt;strong&gt;Fests&lt;/strong&gt;, and an &lt;strong&gt;online program&lt;/strong&gt; with livestreams, challenges, and a week-long event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewards run on stickers.&lt;/strong&gt; You earn virtual stickers for activities, and real stickers get mailed to you once you collect enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's free&lt;/strong&gt;, and you need to be 13 or older.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MLH and DEV run it now&lt;/strong&gt;, with DigitalOcean (the original creator) as presenting partner.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Old vs. new: what actually changed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;The old Hacktoberfest (2014-2025)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hacktoberfest 2026&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core task&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open 4 pull requests on participating repos&lt;/td&gt;
&lt;td&gt;Learn and build with open-source AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;How progress is measured&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PR count&lt;/td&gt;
&lt;td&gt;Virtual stickers from activities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Main format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Online, solo, on GitHub&lt;/td&gt;
&lt;td&gt;300+ in-person Fests plus an online program&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reward&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;T-shirt (or a tree planted, in some years)&lt;/td&gt;
&lt;td&gt;Sticker pack by mail, with extra tiers up to a raffle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Who runs it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DigitalOcean&lt;/td&gt;
&lt;td&gt;MLH and DEV, with DigitalOcean presenting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open-source contributions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The entire point&lt;/td&gt;
&lt;td&gt;Still welcome, just not rewarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Community element&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mostly social media and Discord&lt;/td&gt;
&lt;td&gt;Real events in real cities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T-shirt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The headline prize&lt;/td&gt;
&lt;td&gt;Only at Fests, while supplies last, not guaranteed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A small correction to a common memory: older editions let you contribute to &lt;em&gt;any&lt;/em&gt; public repo that opted in, not just a short curated list. The 2026 change is bigger than swapping the list. The counter itself is gone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why they changed it
&lt;/h2&gt;

&lt;p&gt;Short answer: the spam got out of hand.&lt;/p&gt;

&lt;p&gt;The organizers say it's now easier than ever to churn out low-effort pull requests, and maintainers kept paying the price in review time and burnout. Their own history page describes the 2015-2025 era as one where thousands of people made a first contribution, but the flood of low-quality PRs eventually became too much for the people on the receiving end.&lt;/p&gt;

&lt;p&gt;So rather than patching the rules again, they dropped the incentive. The mission page puts it simply: open source was never defined by a PR counter, and this year they'd rather help people learn to build with open models and tools.&lt;/p&gt;

&lt;p&gt;You can agree or disagree with the call. But the reasoning is easy to follow, and anyone who has ever maintained a popular repo in October will probably nod along.&lt;/p&gt;




&lt;h2&gt;
  
  
  Meet the new cast: MLH, DEV, and DigitalOcean
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.mlh.com/" rel="noopener noreferrer"&gt;Major League Hacking (MLH)&lt;/a&gt;&lt;/strong&gt; runs the Fests and the sign-in system (MyMLH).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to"&gt;DEV&lt;/a&gt;&lt;/strong&gt; runs the online challenges and hands out badges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.digitalocean.com/" rel="noopener noreferrer"&gt;DigitalOcean&lt;/a&gt;&lt;/strong&gt; started Hacktoberfest in 2014 and is still on board as presenting partner.&lt;/li&gt;
&lt;li&gt;A long sponsor list backs the swag and events, including GitHub, MongoDB, Snowflake, Google Cloud, IBM, Qualcomm, Arduino, and many others.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Two ways to take part
&lt;/h2&gt;

&lt;p&gt;You can do either one, or both. They stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 1: Go to a Fest (in person)
&lt;/h3&gt;

&lt;p&gt;A Fest is a free, one-day, locally organized event. Almost all of them are free, though a few pop-ups happen inside ticketed conferences. There are two flavors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hack Day&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Meetup&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vibe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mini hackathon&lt;/td&gt;
&lt;td&gt;Community hangout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What you do&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Build a project with open-weight models or open-source AI tools, then demo it&lt;/td&gt;
&lt;td&gt;Talks, workshops, panels, and meeting people&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Team?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Solo or any team size (prizes cover up to four members)&lt;/td&gt;
&lt;td&gt;No team needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prizes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, usually a swag bag&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Laptop and charger&lt;/td&gt;
&lt;td&gt;Curiosity, plus a laptop only if the host asks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Beginners are welcome.&lt;/strong&gt; You don't need to build an AI project to attend a Meetup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everyone on a team must register and check in.&lt;/strong&gt; One person submits the project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Projects must be finished and submitted on the day,&lt;/strong&gt; before the host's deadline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every in-person attendee gets an official participation certificate,&lt;/strong&gt; available in your My Hacktoberfest dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swag varies by Fest.&lt;/strong&gt; T-shirts and other goodies are limited and not guaranteed. The local host decides how to hand them out. Select Hack Days also have Arduino boards available while supplies last.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to join a Fest:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://hacktoberfest.com/fests/" rel="noopener noreferrer"&gt;Find a Fest&lt;/a&gt; and search by city, country, or name (or use your location).&lt;/li&gt;
&lt;li&gt;Open the Fest's page and read the details: date, venue, food, accessibility, and what to install or bring.&lt;/li&gt;
&lt;li&gt;Register on that Fest's own page with a free MyMLH account. Every Fest has a separate registration.&lt;/li&gt;
&lt;li&gt;Show up, check in with the host, and say hi to a stranger.&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;Contact Host&lt;/strong&gt; button on the page for anything about logistics, accommodations, or late arrival.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Path 2: Join online (from anywhere)
&lt;/h3&gt;

&lt;p&gt;The online program runs all month and has three main pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Livestreams.&lt;/strong&gt; Sessions on open-weight models, agents, and tools. You check in with a code shown on screen to collect a sticker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEV Challenges.&lt;/strong&gt; A weekend kickoff challenge plus four weekly open-source AI rounds, all labeled as cash-prize challenges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Hack Week: Hacktoberfest.&lt;/strong&gt; A week-long virtual festival of livestreams and challenges, run by MLH.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are the dates as listed on DEV's challenge hub and in community guides. Always double-check the deadline printed on each official challenge page, because that's the one that counts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;Dates (UTC, as reported)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Weekend Challenge&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 2 (02:00) to Oct 5 (06:59)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Open-Source AI Challenge, Week 1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 5 to Oct 11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://events.mlh.com/events/14553-global-hack-week-hacktoberfest" rel="noopener noreferrer"&gt;Global Hack Week&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 9 to Oct 15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Open-Source AI Challenge, Week 2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 12 to Oct 18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Open-Source AI Challenge, Week 3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 19 to Oct 25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Open-Source AI Challenge, Week 4&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Oct 26 to Oct 31&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full online calendar lives on the &lt;a href="https://hacktoberfest.com/schedule/" rel="noopener noreferrer"&gt;official schedule page&lt;/a&gt; and shows times in your own time zone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; the exact prize amounts and the actual challenge prompts weren't public when I researched this. The prompts are revealed when each challenge launches, so don't start building a "winning entry" before you've read the real brief. Challenge announcements go out under DEV's &lt;a href="https://dev.to/t/hf26challenge"&gt;&lt;code&gt;#hf26challenge&lt;/code&gt; tag&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The sticker system, explained
&lt;/h2&gt;

&lt;p&gt;This is the new reward engine, and it's friendlier than it sounds.&lt;/p&gt;

&lt;p&gt;There are &lt;strong&gt;24 virtual stickers&lt;/strong&gt; in total: 22 you can earn online, plus 2 you earn in person (attend a Fest, or host one). Each one is tied to an activity.&lt;/p&gt;

&lt;h3&gt;
  
  
  The reward tiers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stickers collected&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A real, mailed Hacktoberfest 2026 sticker pack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A bonus holographic sticker added to your pack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;17&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Completionist&lt;/strong&gt; status, plus automatic entry into a raffle for a Hacktoberfest 2026 T-shirt or an Arduino Uno Q board&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two stickers are &lt;strong&gt;required&lt;/strong&gt; before anything ships: signing in to Hacktoberfest.com, and adding your mailing address to your MyMLH account. Do those first. They also count toward your total, so you're already two-thirds of the way to the first reward after five minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the rest come from
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Examples of what earns a sticker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Livestreams&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Check into 1, 3, or 5 livestreams, or the launch livestream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DEV Challenges&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Connect your DEV account, then submit to the Launch Weekend and Weeks 1 to 4 challenges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Global Hack Week&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complete registration challenges, check into a session, and hit 15, 30, or 75 points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Install and log in to DevRelay, join the MLH Community Discord, connect your DigitalOcean account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Surveys&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fill out the pre-event and post-event surveys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;In person&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Attend a Fest, or host one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full, up-to-date list is on the &lt;a href="https://hacktoberfest.com/activities/" rel="noopener noreferrer"&gt;Activities page&lt;/a&gt;, and your personal progress lives in &lt;a href="https://hacktoberfest.com/my/" rel="noopener noreferrer"&gt;My Hacktoberfest&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Good to know about swag
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Online participants don't get T-shirts.&lt;/strong&gt; Those are a Fest thing, and even there they aren't guaranteed. Completionists can still win one in the raffle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailing takes a while.&lt;/strong&gt; The main site says stickers and prizes go out 8 to 12 weeks after Hacktoberfest ends, while the FAQ says packs should arrive at most destinations within 30 to 60 days of shipping starting. Plan for a wait either way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activity done early still counts.&lt;/strong&gt; The FAQ says eligible activity completed before milestones were announced is credited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing in alone won't earn you a pack.&lt;/strong&gt; You need to do at least a few activities.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Your quick-start checklist
&lt;/h2&gt;

&lt;p&gt;If you only do one section of this article, make it this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Today (5 to 10 minutes):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in at &lt;a href="https://hacktoberfest.com/my/" rel="noopener noreferrer"&gt;My Hacktoberfest&lt;/a&gt; with a free MyMLH account.&lt;/li&gt;
&lt;li&gt;Add your mailing address to MyMLH.&lt;/li&gt;
&lt;li&gt;Connect your DEV account (optional, but it earns a sticker and a DEV badge, and it's needed for DEV Challenges credit).&lt;/li&gt;
&lt;li&gt;Join the &lt;a href="https://discord.mlh.io" rel="noopener noreferrer"&gt;MLH Community Discord&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;This week:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Register for &lt;a href="https://events.mlh.com/events/14553-global-hack-week-hacktoberfest" rel="noopener noreferrer"&gt;Global Hack Week: Hacktoberfest&lt;/a&gt;, which is free.&lt;/li&gt;
&lt;li&gt;Follow each &lt;a href="https://dev.to/challenges/hacktoberfest"&gt;DEV Hacktoberfest challenge&lt;/a&gt; so you get launch notifications.&lt;/li&gt;
&lt;li&gt;Check the &lt;a href="https://hacktoberfest.com/schedule/" rel="noopener noreferrer"&gt;schedule&lt;/a&gt; and mark a few livestreams.&lt;/li&gt;
&lt;li&gt;Look up &lt;a href="https://hacktoberfest.com/fests/" rel="noopener noreferrer"&gt;Fests near you&lt;/a&gt; and register if one fits.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;All month:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check into livestreams and submit to challenges as they open.&lt;/li&gt;
&lt;li&gt;Fill out the pre-event survey early and the post-event survey at the end.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What should you build? Ideas to get started
&lt;/h2&gt;

&lt;p&gt;The official line is that instead of counting PRs, you'll write your first &lt;code&gt;skills.md&lt;/code&gt;, build an open-source agent, fine-tune an open-weight model, or follow your curiosity. Here are some starter ideas by difficulty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beginner-friendly&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A reusable agent &lt;strong&gt;skill&lt;/strong&gt;: a folder with a &lt;code&gt;SKILL.md&lt;/code&gt;, a README, a license, and a couple of examples.&lt;/li&gt;
&lt;li&gt;A small local chatbot running an open-weight model through a local runtime such as Ollama or llama.cpp.&lt;/li&gt;
&lt;li&gt;A tiny &lt;strong&gt;evaluation script&lt;/strong&gt; that checks whether a model gives consistent answers to the same prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Intermediate&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small &lt;strong&gt;open-source agent&lt;/strong&gt; that combines a local model, a few tools (files, APIs, MCP servers), and simple memory.&lt;/li&gt;
&lt;li&gt;A CPU-friendly &lt;strong&gt;fine-tuning experiment&lt;/strong&gt; using Hugging Face's &lt;a href="https://github.com/huggingface/peft" rel="noopener noreferrer"&gt;PEFT&lt;/a&gt; or &lt;a href="https://github.com/huggingface/trl" rel="noopener noreferrer"&gt;TRL&lt;/a&gt;. Keep it small. A tiny project that works beats a giant one that doesn't.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;tool-call correctness test suite&lt;/strong&gt; or prompt regression tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ambitious&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improve an open-source &lt;strong&gt;agent harness or SDK&lt;/strong&gt;: better error messages, safer permissions, clearer setup on Windows, macOS, or Linux.&lt;/li&gt;
&lt;li&gt;Build an &lt;strong&gt;evaluation dashboard&lt;/strong&gt; that compares open-weight models side by side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good project checklist before you submit anything: what real problem does it solve, which license applies, which model and under what terms, can someone else run it without hidden steps, is there a short demo or screenshot, and are the limitations written down.&lt;/p&gt;




&lt;h2&gt;
  
  
  But I still want to contribute to open source!
&lt;/h2&gt;

&lt;p&gt;You absolutely can, and nobody is stopping you. The organizers say outright that open source runs 365 days a year and that you can contribute anytime. They just aren't giving swag for it anymore. They point newcomers to this &lt;a href="https://dev.to/opensauced/open-source-101-a-beginners-guide-to-getting-started-37fb"&gt;beginner's guide to getting started&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're going to send a PR this October, here's how to be the kind of contributor maintainers love:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; first.&lt;/strong&gt; Some projects require you to be assigned the issue, get design approval, sign a CLA, or follow an AI-disclosure policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check that the issue is still open and unclaimed,&lt;/strong&gt; with no competing PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce the problem yourself.&lt;/strong&gt; A fix without a reproduction is a guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the change small and focused,&lt;/strong&gt; and add a test that fails before your fix and passes after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be honest about AI help.&lt;/strong&gt; Many repos now ask for disclosure. Never submit code you can't explain or test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Say what you're about to do&lt;/strong&gt; before writing a lot of code. A short comment like "I reproduced this, here's my plan, anyone already on it?" saves everyone time.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where to look
&lt;/h3&gt;

&lt;p&gt;Hacktoberfest has no official participating-repo list this year. These are active projects in the open-AI orbit that have accepted outside contributions. Treat them as places to browse, not guaranteed openings, since issues get claimed quickly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/modelcontextprotocol/typescript-sdk" rel="noopener noreferrer"&gt;MCP TypeScript SDK&lt;/a&gt; and &lt;a href="https://github.com/modelcontextprotocol/python-sdk" rel="noopener noreferrer"&gt;MCP Python SDK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/huggingface/peft" rel="noopener noreferrer"&gt;Hugging Face PEFT&lt;/a&gt; and &lt;a href="https://github.com/huggingface/trl" rel="noopener noreferrer"&gt;TRL&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Giskard-AI/giskard-oss" rel="noopener noreferrer"&gt;Giskard&lt;/a&gt; (AI evaluation)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mdn/content" rel="noopener noreferrer"&gt;MDN content&lt;/a&gt; (documentation, with low setup cost)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/browser-use/browser-use" rel="noopener noreferrer"&gt;Browser Use&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/PrefectHQ/fastmcp" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A smaller, well-scoped issue in a project with a clear guide is almost always a better bet than a big feature nobody has approved.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to host a Fest?
&lt;/h2&gt;

&lt;p&gt;Anyone can. If your city has no Fest yet, it could be you.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hack Day&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Meetup&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attendees build projects&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workshops and speakers&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prizes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MLH funding&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Applications are open and are confirmed on a rolling basis, within about a week of a completed application.&lt;/li&gt;
&lt;li&gt;Hosts get stickers, swag, and prizes for participants, a listing in the searchable directory, programming help, and (for Hack Days) funding with reimbursement for certain event expenses.&lt;/li&gt;
&lt;li&gt;Start here: &lt;a href="https://hacktoberfest.com/host/" rel="noopener noreferrer"&gt;Host a Fest&lt;/a&gt;, then read the &lt;a href="https://mlh.gitbook.io/mlh-hacktoberfest-organizer-guide" rel="noopener noreferrer"&gt;host handbook&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Hosting also earns you a sticker and a DEV badge.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do pull requests count at all?&lt;/strong&gt;&lt;br&gt;
No. They don't count toward rewards, and the organizers say that's deliberate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I still get a T-shirt?&lt;/strong&gt;&lt;br&gt;
Maybe, but only at a Fest while supplies last (the host decides), or through the Completionist raffle if you hit 17 stickers. Online participants aren't promised one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who can join?&lt;/strong&gt;&lt;br&gt;
People aged 13 and over, subject to country eligibility rules. Individual Fests can have their own age limits, and if you're under 18 you should ask the host about parental permission. DEV Challenges have separate contest eligibility rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it free?&lt;/strong&gt;&lt;br&gt;
Yes, including nearly all Fests and Global Hack Week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to build an AI project to take part?&lt;/strong&gt;&lt;br&gt;
No. You can watch livestreams, attend a Meetup, join the Discord, and do surveys. Those all count toward stickers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a GPU?&lt;/strong&gt;&lt;br&gt;
Not for everything. Plenty of beginner-friendly work (documentation, SDK bugs, evaluation tools, small fine-tuning experiments) runs fine on a laptop CPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who do I contact with problems?&lt;/strong&gt;&lt;br&gt;
Use &lt;strong&gt;Contact Host&lt;/strong&gt; on a Fest's page for event questions, the relevant challenge page for DEV Challenge questions, and the support emails listed on the &lt;a href="https://hacktoberfest.com/questions/" rel="noopener noreferrer"&gt;official FAQ&lt;/a&gt; for swag, prizes, or safety concerns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;It's okay to feel a little nostalgic. The four-PR challenge got a lot of people into open source, and it's strange to see it retired.&lt;/p&gt;

&lt;p&gt;But the new format has a lot going for it. Maintainers get a break from the spam. Beginners get real events and a clear, low-stakes path to a reward. And if you've been meaning to try out open-weight models or build your first agent, October now comes with a built-in excuse, a community, and stickers.&lt;/p&gt;

&lt;p&gt;So sign in, add your address, grab those first two stickers, and see where the month takes you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hacktoberfest.com/" rel="noopener noreferrer"&gt;Hacktoberfest 2026 home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hacktoberfest.com/questions/" rel="noopener noreferrer"&gt;Official FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hacktoberfest.com/online/" rel="noopener noreferrer"&gt;Attend online&lt;/a&gt; and the &lt;a href="https://hacktoberfest.com/activities/" rel="noopener noreferrer"&gt;Activities page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hacktoberfest.com/in-person/" rel="noopener noreferrer"&gt;Attend in person&lt;/a&gt; and &lt;a href="https://hacktoberfest.com/fests/" rel="noopener noreferrer"&gt;Find a Fest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hacktoberfest.com/host/" rel="noopener noreferrer"&gt;Host a Fest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hacktoberfest.com/mission/" rel="noopener noreferrer"&gt;Mission and history&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/challenges/hacktoberfest"&gt;DEV Hacktoberfest challenges&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/aniruddhaadak/hacktoberfest-2026-what-online-builders-can-join-win-and-claim-4fge"&gt;Hacktoberfest 2026: what online builders can join, win, and claim&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hacktoberfest</category>
      <category>hackathon</category>
      <category>devchallenge</category>
    </item>
    <item>
      <title>You Can't Even Make an AI Wrapper Chat App 😆🫵</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Fri, 05 Jun 2026 05:48:23 +0000</pubDate>
      <link>https://dev.to/0shuvo0/you-cant-even-make-an-ai-wrapper-chat-app-3ph5</link>
      <guid>https://dev.to/0shuvo0/you-cant-even-make-an-ai-wrapper-chat-app-3ph5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;"It's just a fetch call, bro." — Every junior dev, moments before learning a hard lesson.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So you want to build a ChatGPT wrapper. Fair enough. It sounds embarrassingly simple — you've got an API key, a text box, and ambition. How hard can it be?&lt;/p&gt;

&lt;p&gt;Turns out: pretty hard. Not &lt;em&gt;rocket science&lt;/em&gt; hard, but &lt;em&gt;"oh god why is my chat broken on refresh"&lt;/em&gt; hard.&lt;/p&gt;

&lt;p&gt;Let's walk through the three stages of enlightenment.&lt;/p&gt;




&lt;h2&gt;
  
  
  🟢 Stage 1: The Junior Dev Move
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Ship it. It works on my machine."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The junior dev opens a blank React project, installs &lt;code&gt;axios&lt;/code&gt; (or doesn't, and just uses &lt;code&gt;fetch&lt;/code&gt;), and writes something like this:&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;setReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Deployed. LinkedIn post drafted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem?&lt;/strong&gt; You're waiting for the &lt;em&gt;entire&lt;/em&gt; AI response to generate before anything shows up on screen. For a short reply, fine. For a longer one? You're staring at a blank screen for 10–15 seconds while your users quietly close the tab.&lt;/p&gt;

&lt;p&gt;It feels like dial-up internet. In 2025.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  🟡 Stage 2: The Mid-Level Dev Move
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"I've heard of this 'streaming' thing."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The mid-level dev has seen the ChatGPT UI. Tokens appear one by one, like the AI is &lt;em&gt;thinking&lt;/em&gt;. That's streaming — and it's a vastly better experience.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;setReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now tokens stream in real-time. The app &lt;em&gt;feels&lt;/em&gt; alive. Users are happy. You are happy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem?&lt;/strong&gt; Hit refresh. &lt;/p&gt;

&lt;p&gt;Gone. All of it. Every conversation, vanished. The app has the memory of a goldfish. Each new page load is a blank slate — no history, no context, no continuity.&lt;/p&gt;

&lt;p&gt;You've built a chat app that forgets you exist the moment you leave. That's not a chat app. That's a very expensive &lt;code&gt;alert()&lt;/code&gt; box.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔴 Stage 3: The Senior Dev Move
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;"Streaming is table stakes. Persistence is the real work."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The senior dev knows that a chat app without history isn't a product — it's a demo. Real users expect to close the tab, come back tomorrow, and pick up where they left off. They expect their conversation to &lt;em&gt;exist&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This means the stream itself needs to be saved &lt;em&gt;as it arrives&lt;/em&gt; — not after it's done, not on the next request, but &lt;strong&gt;token by token&lt;/strong&gt;, persisted to a database in real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend — save while streaming:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Save user message immediately&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;fullResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fullResponse&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Save complete assistant response after stream finishes&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;conversationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fullResponse&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Frontend — load history on mount:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/conversations/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/messages`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when a user refreshes? Their chat is still there. When they come back on mobile? Still there. When they share a conversation link? You get the idea.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But wait, there's more.&lt;/strong&gt; The senior dev also thinks about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending the full conversation history back to the AI (so it has &lt;em&gt;actual&lt;/em&gt; context)&lt;/li&gt;
&lt;li&gt;Handling mid-stream disconnects gracefully (what if the user closes the tab at 50%?)&lt;/li&gt;
&lt;li&gt;Debouncing saves so you're not hammering your DB on every single token&lt;/li&gt;
&lt;li&gt;Auth, so users only see &lt;em&gt;their&lt;/em&gt; conversations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is glamorous. It's just... what production looks like.&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;What they build&lt;/th&gt;
&lt;th&gt;What's missing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Junior&lt;/td&gt;
&lt;td&gt;Request → Response&lt;/td&gt;
&lt;td&gt;Speed, UX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid&lt;/td&gt;
&lt;td&gt;Streaming response&lt;/td&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Senior&lt;/td&gt;
&lt;td&gt;Streaming + saved history&lt;/td&gt;
&lt;td&gt;(probably something else they'll discover next week)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap between a junior and senior implementation isn't really about the AI part at all — the API call is the easy bit. It's about treating the &lt;em&gt;surrounding infrastructure&lt;/em&gt; with the same seriousness as the feature itself.&lt;/p&gt;

&lt;p&gt;So next time someone says "I built an AI chat app over the weekend" — ask them what happens when they refresh the page.&lt;/p&gt;

&lt;p&gt;The silence will tell you everything.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Now go add persistence to your side project. You know who you are.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Before AI we had ...</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Fri, 22 May 2026 04:12:37 +0000</pubDate>
      <link>https://dev.to/0shuvo0/before-ai-we-had--5a0g</link>
      <guid>https://dev.to/0shuvo0/before-ai-we-had--5a0g</guid>
      <description>&lt;p&gt;Nowadays you will hear a lot of people saying &lt;strong&gt;Why should I hire a software developer when I can vibecode it my self with about $100 worth of AI tools?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Well they aren't wrong about that!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But getting an app made and running was never the hard part. Before &lt;strong&gt;AI or vibe coding&lt;/strong&gt; we had platforms like &lt;strong&gt;Fiverr/UpWork&lt;/strong&gt; where people could just pay a third world country developer &lt;strong&gt;$50-$100&lt;/strong&gt; to build them their app.&lt;/p&gt;

&lt;p&gt;So getting a &lt;strong&gt;working APP&lt;/strong&gt; or at least &lt;strong&gt;MVP&lt;/strong&gt; was never really very hard or expensive rather its the &lt;strong&gt;distribution&lt;/strong&gt; part that kills 99% of the apps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;And for handling complex apps or codebases &lt;strong&gt;cheap developers&lt;/strong&gt; were not reliable then, &lt;strong&gt;AI tools&lt;/strong&gt; are not reliable now!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>discuss</category>
      <category>coding</category>
    </item>
    <item>
      <title>Frontend rate limiting can save you $10,000</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Thu, 21 May 2026 04:04:07 +0000</pubDate>
      <link>https://dev.to/0shuvo0/frontend-rate-limiting-can-save-you-10000-272l</link>
      <guid>https://dev.to/0shuvo0/frontend-rate-limiting-can-save-you-10000-272l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Fastest Requests Are the Ones You Never Make&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While backend rate limiting is absolutely crucial, &lt;strong&gt;it comes at a cost&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For backend rate limiting, we usually store a user's IP and request timestamps in &lt;strong&gt;memory&lt;/strong&gt; or a &lt;strong&gt;Redis database&lt;/strong&gt;. If your app has millions of users, you end up storing a lot of data in memory or a database — and that can get expensive fast.&lt;/p&gt;

&lt;p&gt;But what if &lt;strong&gt;we block requests &lt;em&gt;before&lt;/em&gt; they ever leave the browser/client&lt;/strong&gt;? By handling rate limiting on the client side, we can save a significant number of unnecessary request and read/write operations on the backend. Depending on how many users your app has, this can translate to anywhere between &lt;strong&gt;hundreds to thousands of dollars saved per year&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1: Basic frontend rate limiter example:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// utils/rate-limiter.ts&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_REQUESTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// Maximum number of requests allowed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TIME_WINDOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// Time window in milliseconds (1 minute)&lt;/span&gt;

&lt;span class="c1"&gt;// Store timestamps of recent requests&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="c1"&gt;// Check if a new request is within the allowed limit&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canMakeRequest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Remove timestamps that have fallen outside the time window&lt;/span&gt;
    &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;TIME_WINDOW&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_REQUESTS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Drop-in fetch wrapper with rate limiting&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;RequestInit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;canMakeRequest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rate limit exceeded. Please try again later.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Better rate limiter with cooldown time and localstorage implementation so user can't just reset limit by refreshing the page
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Front end rate limiter&lt;/span&gt;
&lt;span class="c1"&gt;// Max number of requests allowed per time window&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_REQUESTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Maximum number of requests allowed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TIME_WINDOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Time window in milliseconds (e.g., 1 minute)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COOLDOWN_TIME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Time to wait after hitting the rate limit before requests are allowed again&lt;/span&gt;

&lt;span class="c1"&gt;// localStorage keys for persisting state across page refreshes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STORAGE_KEY_TIMESTAMPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate_limiter_timestamps&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STORAGE_KEY_COOLDOWN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate_limiter_cooldown_until&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// --- localStorage helpers ---&lt;/span&gt;

&lt;span class="c1"&gt;// Load request timestamps from localStorage, falling back to an empty array&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadTimestamps&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY_TIMESTAMPS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Persist the current timestamps array to localStorage&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveTimestamps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY_TIMESTAMPS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Silently fail if localStorage is unavailable (e.g. private browsing restrictions)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Load the cooldown expiry timestamp (ms since epoch), or 0 if none is set&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadCooldownUntil&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY_COOLDOWN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Persist the cooldown expiry timestamp to localStorage&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveCooldownUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY_COOLDOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;until&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Silently fail if localStorage is unavailable&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// --- Core rate-limit logic ---&lt;/span&gt;

&lt;span class="c1"&gt;// Function to check if a request can be made&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canMakeRequest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// If an active cooldown exists, block all requests until it expires&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cooldownUntil&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadCooldownUntil&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cooldownUntil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cooldownUntil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Load persisted timestamps so a page refresh doesn't reset the counter&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadTimestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Remove timestamps that are outside the time window&lt;/span&gt;
    &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;TIME_WINDOW&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Persist the cleaned-up timestamps&lt;/span&gt;
    &lt;span class="nf"&gt;saveTimestamps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Check if the number of requests in the time window is less than the maximum allowed&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_REQUESTS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Function to make a request&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;RequestInit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;canMakeRequest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Inform the caller how long (in seconds) they need to wait&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;loadCooldownUntil&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Rate limit exceeded. Please try again in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s.`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Load, update, and persist timestamps before making the request&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadTimestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;saveTimestamps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// If this request tips us over the limit, start a cooldown period.&lt;/span&gt;
    &lt;span class="c1"&gt;// The cooldown begins NOW and lasts COOLDOWN_TIME ms, after which&lt;/span&gt;
    &lt;span class="c1"&gt;// canMakeRequest() will unblock and the sliding window will have cleared.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestTimestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_REQUESTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;saveCooldownUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;COOLDOWN_TIME&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Make the actual fetch request&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$fetch&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Whenever you need to make a request to the backend, just use &lt;code&gt;$fetch&lt;/code&gt; instead of the native &lt;code&gt;fetch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// example usage&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;$fetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utils/rate-limiter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;$fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/get-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; This will prevent the vast majority of users from flooding your backend, but a determined minority and AI bots may attempt to bypass client-side checks entirely. &lt;strong&gt;Backend rate limiting must still be in place&lt;/strong&gt; as your true line of defense. Think of frontend rate limiting as an optimization layer, not a security layer.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  When to Use Frontend Rate Limiting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your app has a high volume of users and you want to reduce unnecessary backend load&lt;/li&gt;
&lt;li&gt;The endpoint being called is expensive (e.g., hits a third-party API, runs a heavy DB query)&lt;/li&gt;
&lt;li&gt;You want to improve UX by giving users instant feedback instead of waiting for a backend &lt;code&gt;429&lt;/code&gt; response&lt;/li&gt;
&lt;li&gt;You want to reduce cloud costs on services that charge per request or per DB operation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When &lt;em&gt;Not&lt;/em&gt; to Use Frontend Rate Limiting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;As a &lt;strong&gt;replacement&lt;/strong&gt; for backend rate limiting — it is never a substitute&lt;/li&gt;
&lt;li&gt;For protecting sensitive or security-critical endpoints (authentication, payments, etc.)&lt;/li&gt;
&lt;li&gt;When your users are primarily API consumers or developers who interact directly with your backend&lt;/li&gt;
&lt;li&gt;When the client environment is untrusted (e.g., mobile apps that can be reverse-engineered or patched)&lt;/li&gt;
&lt;li&gt;When accurate rate limiting is a hard requirement — client-side state can be reset simply by refreshing the page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>coding</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Your trycatch sucks - lets fix it</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Thu, 21 May 2026 03:19:50 +0000</pubDate>
      <link>https://dev.to/0shuvo0/your-trycatch-sucks-lets-fix-it-37e9</link>
      <guid>https://dev.to/0shuvo0/your-trycatch-sucks-lets-fix-it-37e9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;You're not handling errors. You're &lt;em&gt;hiding&lt;/em&gt; them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every app crashes. Every API fails. Every database hiccups at 2am on a Friday.&lt;br&gt;
The difference between a good dev and a great one? &lt;strong&gt;What happens next.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's roast your error handling — then make it legendary.&lt;/p&gt;


&lt;h2&gt;
  
  
  🤦 Level 0: The "Trust Me Bro" Dev
&lt;/h2&gt;

&lt;p&gt;No try/catch at all. Just vibes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 💥 TypeError: Cannot read properties of undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The crime:&lt;/strong&gt; One bad response nukes the entire app. Users see a white screen. You get a 3am Slack ping.&lt;/p&gt;




&lt;h2&gt;
  
  
  🐣 Level 1: The Junior — "I Googled try/catch"
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 👈 and... that's it. shipped.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's wrong here?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;console.log&lt;/code&gt; in production helps &lt;em&gt;nobody&lt;/em&gt; — users still see a broken UI&lt;/li&gt;
&lt;li&gt;No distinction between a 404 and a 500 — every error is treated the same&lt;/li&gt;
&lt;li&gt;The error disappears into the void (or a DevTools tab nobody has open)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;err&lt;/code&gt; might be &lt;code&gt;null&lt;/code&gt;, a string, or an &lt;code&gt;Error&lt;/code&gt; object — you're not checking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The mindset:&lt;/strong&gt; &lt;em&gt;"At least it won't crash."&lt;/em&gt; — Yeah, it just silently breaks instead. Cool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📈 Level 2: The Mid-Level — "I've Been Burned Before"
&lt;/h2&gt;

&lt;p&gt;Now we're thinking. You've seen production fires. You have trust issues with APIs. Good.&lt;/p&gt;

&lt;h3&gt;
  
  
  2a — Typed errors, real messages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;NetworkError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Connection lost. Check your internet.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User not found.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something went wrong. We're on it.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[fetchUserData]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// 👈 goes to Sentry/Datadog&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Users get &lt;em&gt;useful&lt;/em&gt; feedback, not a frozen screen&lt;br&gt;
✅ Engineers get &lt;em&gt;structured logs&lt;/em&gt;, not a haystack of &lt;code&gt;console.log&lt;/code&gt;s&lt;/p&gt;


&lt;h3&gt;
  
  
  2b — Undo previous operations (the "atomic mindset")
&lt;/h3&gt;

&lt;p&gt;Imagine you're updating a user's profile &lt;em&gt;and&lt;/em&gt; their avatar. Step 1 succeeds. Step 2 fails.&lt;br&gt;
Congrats — your user now has a corrupted half-state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;previousProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;previousProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// snapshot&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newProfileData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// step 1&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;uploadAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newAvatar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// step 2 💥 fails here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Profile update failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// ↩️ Roll back step 1 since step 2 failed&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previousProfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;previousProfile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;showToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Update failed. Your profile has been restored.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Users never see broken half-state&lt;br&gt;
✅ Rollback is &lt;em&gt;explicit&lt;/em&gt;, not accidental&lt;/p&gt;


&lt;h3&gt;
  
  
  2c — Wrap it in a clean utility (stop repeating yourself)
&lt;/h3&gt;

&lt;p&gt;Tired of writing &lt;code&gt;try/catch&lt;/code&gt; 50 times? Make a helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// utils/tryCatch.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tryCatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage — clean, flat, readable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;tryCatch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;showToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Couldn't load user.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ No more deeply nested try/catch pyramids&lt;br&gt;
✅ Forces you to &lt;em&gt;handle&lt;/em&gt; the error at call site — can't ignore it&lt;/p&gt;


&lt;h2&gt;
  
  
  🧠 Level 3: The Senior — "I've Seen Things"
&lt;/h2&gt;

&lt;p&gt;You don't just &lt;em&gt;catch&lt;/em&gt; errors. You &lt;em&gt;anticipate&lt;/em&gt; them. You build systems that heal themselves.&lt;/p&gt;
&lt;h3&gt;
  
  
  3a — Retry queue with exponential backoff
&lt;/h3&gt;

&lt;p&gt;Networks are flaky. Don't give up on the first failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchWithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isLast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isRetryable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;NetworkError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLast&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isRetryable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// don't retry 401s or 404s&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 500ms → 1s → 2s&lt;/span&gt;
      &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Attempt &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed. Retrying in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithRetry&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Temporary blips are invisible to users&lt;br&gt;
✅ Smart: retries server errors, not client errors (no point retrying a 401)&lt;/p&gt;


&lt;h3&gt;
  
  
  3b — Circuit breaker (stop hammering a dead service)
&lt;/h3&gt;

&lt;p&gt;A retry queue is great — unless the &lt;em&gt;whole service is down&lt;/em&gt;. Then you're just DDoS-ing a corpse.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CircuitBreaker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CLOSED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// CLOSED = healthy, OPEN = tripped, HALF_OPEN = testing&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextAttempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OPEN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextAttempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Circuit open — service unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HALF_OPEN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recordFailure&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;recordFailure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failures&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OPEN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextAttempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;🔴 Circuit breaker TRIPPED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CLOSED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userServiceBreaker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userServiceBreaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Failing fast is &lt;em&gt;kind&lt;/em&gt; — users get an error immediately, not after 10s of retrying&lt;br&gt;
✅ Gives the downstream service a breather to recover&lt;/p&gt;


&lt;h3&gt;
  
  
  3c — Structured error classes (errors that &lt;em&gt;mean something&lt;/em&gt;)
&lt;/h3&gt;

&lt;p&gt;Stop throwing raw strings or generic &lt;code&gt;Error&lt;/code&gt;s. Give your errors context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;retryable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AppError&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;retryable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Subclass for specificity&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AUTH_ERROR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;retryable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ServiceUnavailableError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is unavailable`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SERVICE_DOWN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;retryable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Throwing&lt;/span&gt;
&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ServiceUnavailableError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UserService&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Catching&lt;/span&gt;
&lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;AuthError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;redirectToLogin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;AppError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;retryQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;showToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unexpected error. Engineers notified.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Catch blocks can make &lt;em&gt;decisions&lt;/em&gt;, not just log and pray&lt;br&gt;
✅ Every error carries its own context — no more guessing what happened&lt;/p&gt;




&lt;h2&gt;
  
  
  🗺️ The Full Picture
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;th&gt;Junior&lt;/th&gt;
&lt;th&gt;Mid&lt;/th&gt;
&lt;th&gt;Senior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catch errors&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inform the user&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send to a logger&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typed/structured errors&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ partial&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback on failure&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry transient errors&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Circuit breaker&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Errors are self-describing&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ✅ The Golden Rules
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never swallow errors silently.&lt;/strong&gt; A hidden bug is a time bomb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always tell the user &lt;em&gt;something&lt;/em&gt;.&lt;/strong&gt; Frozen UI is worse than an error message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log with context&lt;/strong&gt;, not just a message — &lt;em&gt;what&lt;/em&gt; failed, &lt;em&gt;who&lt;/em&gt; triggered it, &lt;em&gt;when&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not all errors are equal&lt;/strong&gt; — 404 ≠ 500 ≠ NetworkError. Handle them differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retryable ≠ always retry&lt;/strong&gt; — client errors (4xx) should fail fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave the system in a valid state.&lt;/strong&gt; Roll back or compensate when operations are partial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your &lt;code&gt;catch&lt;/code&gt; block is business logic&lt;/strong&gt; — treat it that way.&lt;/li&gt;
&lt;/ol&gt;




&lt;blockquote&gt;
&lt;p&gt;"The mark of a great engineer isn't writing code that never fails.&lt;br&gt;&lt;br&gt;
It's writing code that &lt;em&gt;fails gracefully&lt;/em&gt;."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now go fix your try/catches. 🛠️&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt="GitMission Preview" width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>coding</category>
      <category>programming</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I Built a Fully Automated YouTube Channel That Uploads Every Day - Without Touching It</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Wed, 20 May 2026 04:57:27 +0000</pubDate>
      <link>https://dev.to/0shuvo0/i-built-a-fully-automated-youtube-channel-that-uploads-every-day-without-touching-it-1a2i</link>
      <guid>https://dev.to/0shuvo0/i-built-a-fully-automated-youtube-channel-that-uploads-every-day-without-touching-it-1a2i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Every morning, a video goes live on my YouTube channel. I didn't film it. I didn't edit it. My computer did all of it while I slept.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the story of how I built &lt;strong&gt;&lt;a href="https://www.youtube.com/@ProgressOfYear" rel="noopener noreferrer"&gt;Progress Bar of the Year&lt;/a&gt;&lt;/strong&gt; - a fully automated YouTube Shorts channel that posts a daily update on how much of the current year has passed - and the surprisingly elegant tech stack behind it.&lt;br&gt;
&lt;a href="https://www.youtube.com/@ProgressOfYear" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wk671r70fzcqoj6kz8z.png" alt="YouTube channel preview" width="799" height="671"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The Idea: Simple, But Strangely Satisfying
&lt;/h2&gt;

&lt;p&gt;The premise couldn't be simpler: every day, a short video goes up showing &lt;strong&gt;what percentage of the year has elapsed&lt;/strong&gt;, paired with a fun historical fact about something that happened on this same day in a previous year.&lt;/p&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;But there's something almost meditative about watching the year tick forward - and something genuinely cool about a channel that &lt;em&gt;runs itself&lt;/em&gt;. No burnout, no filming schedule, no editor. Just code, running quietly on a small computer in the corner.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Tech Stack at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core logic&lt;/td&gt;
&lt;td&gt;JavaScript / TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historical facts&lt;/td&gt;
&lt;td&gt;Google Gemini API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video generation&lt;/td&gt;
&lt;td&gt;Remotion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upload&lt;/td&gt;
&lt;td&gt;YouTube Data API v3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automation&lt;/td&gt;
&lt;td&gt;Bash + crontab&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's break down each piece.&lt;/p&gt;



&lt;p&gt;&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzzlb8ltgzwelahgpuyy.png" alt=" " width="799" height="471"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: The Math - How Much of the Year Has Passed?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Calculates today's date, the total days in the current year, and the percentage of the year that's gone by.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; This is pure JavaScript - no libraries needed. The logic boils down to three things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startOfYear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endOfYear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startOfYear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;endOfYear&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startOfYear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;percentage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also accounts for &lt;strong&gt;leap years&lt;/strong&gt; automatically, since &lt;code&gt;endOfYear - startOfYear&lt;/code&gt; will naturally be 366 days in a leap year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this approach:&lt;/strong&gt; JavaScript's &lt;code&gt;Date&lt;/code&gt; object handles all the heavy lifting. There's no edge case to manually code - the language figures out whether it's February 28th or 29th, whether we're in a leap year, all of it. Simple and bulletproof.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt; A number like &lt;code&gt;38.21&lt;/code&gt; - meaning 38.21% of 2026 has already passed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: The History Hook - Google Gemini API
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Fetches a genuinely interesting historical event that happened on &lt;strong&gt;this exact calendar day&lt;/strong&gt; in a previous year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why AI instead of a static database?&lt;/strong&gt; A hardcoded database of historical facts would work - but it would be finite, brittle, and boring to maintain. The Gemini API gives us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An effectively &lt;strong&gt;unlimited&lt;/strong&gt; and varied set of historical facts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural language output&lt;/strong&gt; ready to display on screen&lt;/li&gt;
&lt;li&gt;The ability to request facts in a specific format or tone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; A prompt is sent to the Gemini API each day with the current month and day, asking for a notable historical event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  Give me one interesting historical event that happened on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; 
  in any previous year. Keep it to 1-2 sentences. Be specific with the year.
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gemini&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returns something like: &lt;em&gt;"49 years ago on this day, the first public screening of Star Wars generated enormous excitement among audiences who had never seen anything like it."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters for the channel:&lt;/strong&gt; It transforms what could be a dry statistics update into something actually worth watching. Every video has a little historical surprise baked in. It gives viewers a reason to come back — not just for the percentage, but for the story of the day.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Video Generation - Remotion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Takes the percentage and the historical fact and renders them into a polished, animated MP4 video - entirely in code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Remotion?&lt;/strong&gt; Remotion is a framework that lets you &lt;strong&gt;write videos using React&lt;/strong&gt;. Instead of opening Premiere Pro or After Effects, you define your video as a React component. Animations, timing, transitions - all written in TypeScript. Then you render it to an actual video file using Node.js.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Remotion over other tools?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full programmatic control&lt;/strong&gt; - Every pixel, every animation, every timing decision is in code. No manual steps, no GUI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent output&lt;/strong&gt; - The same code produces the same visual style every single day, automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React-based&lt;/strong&gt; - If you already know React, you're immediately productive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High quality&lt;/strong&gt; - Remotion renders frame-by-frame at whatever resolution and framerate you specify. The output is a proper video file, not a screen recording.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What the video looks like:&lt;/strong&gt; A dark, minimal design with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The year progress percentage in large type&lt;/li&gt;
&lt;li&gt;A smooth animated progress bar filling to the correct point&lt;/li&gt;
&lt;li&gt;Today's date&lt;/li&gt;
&lt;li&gt;The historical fact rendered as clean text below&lt;/li&gt;
&lt;li&gt;A short looping background animation for visual polish&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire &lt;code&gt;VideoTemplate.tsx&lt;/code&gt; component is just a React component with Remotion's &lt;code&gt;&amp;lt;AbsoluteFill&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;spring&amp;gt;&lt;/code&gt;, and timing hooks to drive the animation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VideoTemplate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;VideoProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;percentage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCurrentFrame&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useVideoConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spring&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;damping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AbsoluteFill&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0f0f1a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProgressBar&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;percentage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PercentageText&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;percentage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HistoricalFact&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;fact&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AbsoluteFill&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the component is ready, rendering is a single CLI command (or programmatic call):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx remotion render VideoTemplate out/video.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Uploading to YouTube - YouTube Data API v3
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Takes the rendered MP4 and automatically uploads it to the YouTube channel with a title, description, and the correct Shorts formatting - no human involvement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the YouTube Data API works:&lt;/strong&gt; Google provides an official API for interacting with YouTube programmatically. After a one-time OAuth2 authentication setup (where you grant your app permission to upload to your channel), you can upload videos, set metadata, manage playlists, and more - all from code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The upload flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;youtube&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;youtube&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;oauth2Client&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;youtube&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;part&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;snippet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Year Progress Update - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;formattedDate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;percentage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;% of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has passed.\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;historicalFact&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yearProgress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;motivation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;history&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;22&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;privacyStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createReadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;out/video.mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Making it a YouTube Short:&lt;/strong&gt; YouTube Shorts are simply vertical videos (9:16 aspect ratio) under 60 seconds. Remotion renders the video at 1080×1920, and that's all YouTube needs to classify it as a Short - no special API flag required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The token refresh problem:&lt;/strong&gt; OAuth2 tokens expire. To keep the automation running forever without manually re-authenticating, the app stores refresh tokens in &lt;code&gt;tokens.json&lt;/code&gt; and automatically refreshes the access token before each upload. This is the kind of detail that separates a weekend project from something that actually keeps running for months.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: The Bash Script - Gluing It All Together
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; &lt;code&gt;run-daily.sh&lt;/code&gt; is the conductor. It runs once a day and orchestrates the entire pipeline end-to-end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a bash script?&lt;/strong&gt; The core logic is in Node.js/TypeScript, but crontab (which triggers the daily run) works at the OS level. A bash script acts as the perfect bridge - it can set up the environment, invoke Node, handle errors, and maintain a log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the script does, step by step:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;PROJECT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/Users/mdshuvo/yearlyprogress"&lt;/span&gt;
&lt;span class="nv"&gt;STATE_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/last-upload-date.txt"&lt;/span&gt;
&lt;span class="nv"&gt;LOG_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/log.txt"&lt;/span&gt;

&lt;span class="nv"&gt;TODAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Read the last run date&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATE_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;LAST_RUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATE_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nv"&gt;LAST_RUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Skip if already ran today&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LAST_RUN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TODAY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="c"&gt;# Run the main script, append output to log&lt;/span&gt;
/Users/mdshuvo/.nvm/versions/node/... &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1

&lt;span class="c"&gt;# Trim the log to keep only the last 2000 lines&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 2000 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key design decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency&lt;/strong&gt; - The state file check ensures the script never accidentally uploads twice on the same day, even if crontab triggers it multiple times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging&lt;/strong&gt; - All output is appended to &lt;code&gt;log.txt&lt;/code&gt; for easy debugging if something goes wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log trimming&lt;/strong&gt; - The log is kept to 2,000 lines so it doesn't grow forever and fill up the disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nvm path&lt;/strong&gt; - The full path to the Node binary is specified, because crontab runs in a minimal environment that doesn't load your shell's &lt;code&gt;.bashrc&lt;/code&gt; or &lt;code&gt;.zshrc&lt;/code&gt;. This is a classic gotcha that trips up many automation projects.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 6: The Crontab - Set It and Forget It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Tells my computer(Mac mini) to run &lt;code&gt;run-daily.sh&lt;/code&gt; automatically at a set time every day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is crontab?&lt;/strong&gt; It's a Unix job scheduler built into macOS and Linux. You define a schedule using a simple syntax, point it at a script, and the OS handles the rest - forever, or until you delete the entry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The crontab entry:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0 9 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /Users/mdshuvo/yearlyprogress/run-daily.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs the script at &lt;strong&gt;9:00 AM every day&lt;/strong&gt;. The five fields represent: minute, hour, day of month, month, day of week. Five stars with the first two set means "every day at 9:00."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; Every morning at 9 AM, without any human action, computer runs the script, generates the video, and uploads it to YouTube. By the time most people are having their morning coffee, the day's video is live.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes This Project Special
&lt;/h2&gt;

&lt;p&gt;Beyond the individual pieces, what's interesting here is how each layer is chosen for a specific reason:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript for logic&lt;/strong&gt; - Universal, fast to write, perfect for date manipulation and API calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini for content&lt;/strong&gt; - Turns a static metric into a story worth watching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remotion for video&lt;/strong&gt; - Treats video like software, making it reproducible and automatable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YouTube Data API for distribution&lt;/strong&gt; - The last mile, handled entirely in code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bash for orchestration&lt;/strong&gt; - Lightweight, reliable, and plays perfectly with crontab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;crontab for scheduling&lt;/strong&gt; - The simplest possible scheduler, built into the OS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No overengineering. No cloud infrastructure to manage. No monthly bills for a complex pipeline. Just a small computer, some clever code, and a crontab entry.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;A few directions this project could grow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple languages&lt;/strong&gt; - Generate the same video in Bengali, Spanish, French, etc. to reach broader audiences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Milestone alerts&lt;/strong&gt; - Special videos at 25%, 50%, 75%, and 99% with more elaborate animations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thumbnail generation&lt;/strong&gt; - Use a canvas library or Puppeteer to auto-generate custom thumbnails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics tracking&lt;/strong&gt; - Pull view counts back from the YouTube API and log the growth over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-channel&lt;/strong&gt; - Clone the pipeline for a "Day of the Year" or "Days Until New Year" variant.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;All the core tools used are free or have generous free tiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://remotion.dev" rel="noopener noreferrer"&gt;Remotion&lt;/a&gt; - Free for open-source and personal projects&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ai.google.dev" rel="noopener noreferrer"&gt;Google Gemini API&lt;/a&gt; - Free tier available&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/youtube/v3" rel="noopener noreferrer"&gt;YouTube Data API v3&lt;/a&gt; - Free with a Google account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to watch the year tick away one percentage point at a time, the channel is live at &lt;strong&gt;&lt;a href="https://www.youtube.com/@ProgressOfYear" rel="noopener noreferrer"&gt;youtube.com/@ProgressOfYear&lt;/a&gt;&lt;/strong&gt;. A new video drops every morning.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built with JavaScript, Remotion, Gemini&lt;/em&gt;&lt;/p&gt;

</description>
      <category>coding</category>
      <category>programming</category>
      <category>automation</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why Traditional Tutorials Don't Work for Learning Git</title>
      <dc:creator>Shuvo</dc:creator>
      <pubDate>Wed, 13 May 2026 04:57:00 +0000</pubDate>
      <link>https://dev.to/0shuvo0/why-traditional-tutorials-dont-work-for-learning-git-36j3</link>
      <guid>https://dev.to/0shuvo0/why-traditional-tutorials-dont-work-for-learning-git-36j3</guid>
      <description>&lt;p&gt;If you've tried learning Git through videos or articles and still feel confused, you're not alone. The problem isn't you - it's the way Git is usually taught.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fln6va7gb9hatyiij747b.png" alt="GitMission CTA" width="219" height="47"&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The Problem with Traditional Git Tutorials
&lt;/h2&gt;

&lt;p&gt;Most tutorials focus on explaining commands instead of helping you understand how Git actually works in real situations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They are passive (watching instead of doing)&lt;/li&gt;
&lt;li&gt;They focus on syntax, not workflows&lt;/li&gt;
&lt;li&gt;No real problem-solving scenarios&lt;/li&gt;
&lt;li&gt;No feedback when you make mistakes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why This Approach Fails
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Git is Conceptual, Not Just Commands
&lt;/h3&gt;

&lt;p&gt;Git involves understanding history, branches, and changes - not just memorizing commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. No Muscle Memory
&lt;/h3&gt;

&lt;p&gt;Watching tutorials doesn't build the habit of actually using Git commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No Real Context
&lt;/h3&gt;

&lt;p&gt;Real development involves conflicts, mistakes, and workflows - tutorials rarely simulate this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Traditional Learning vs Effective Learning
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Watching tutorials&lt;/td&gt;
&lt;td&gt;Temporary understanding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading docs&lt;/td&gt;
&lt;td&gt;Good reference, low retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Interactive practice&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Real understanding&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  A Better Way to Learn Git
&lt;/h2&gt;

&lt;p&gt;The most effective way to learn Git is through &lt;strong&gt;hands-on, interactive practice&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Learn a concept&lt;/li&gt;
&lt;li&gt;Apply it immediately&lt;/li&gt;
&lt;li&gt;Make mistakes and fix them&lt;/li&gt;
&lt;li&gt;Repeat with real workflows&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Stop Watching, Start Practicing
&lt;/h2&gt;

&lt;p&gt;If you want to truly understand Git, you need to use it - not just watch someone else use it.&lt;br&gt;
&lt;a href="https://www.gitmission.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fln6va7gb9hatyiij747b.png" alt="GitMission CTA" width="219" height="47"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Are tutorials useless for learning Git?&lt;/strong&gt;&lt;br&gt;
Not useless, but incomplete. They should be combined with hands-on practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does Git feel confusing?&lt;/strong&gt;&lt;br&gt;
Because it involves abstract concepts that require practice to fully understand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the best way to learn Git?&lt;/strong&gt;&lt;br&gt;
The best way is practicing real workflows instead of relying only on passive learning.&lt;/p&gt;

</description>
      <category>git</category>
      <category>github</category>
      <category>coding</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
