<?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: Magic.rb</title>
    <description>The latest articles on DEV Community by Magic.rb (@magicnull).</description>
    <link>https://dev.to/magicnull</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1193208%2F58f4d0d3-98d2-4bf1-87a1-a6e5114a294c.jpg</url>
      <title>DEV Community: Magic.rb</title>
      <link>https://dev.to/magicnull</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/magicnull"/>
    <language>en</language>
    <item>
      <title>I built a free, open-source App Store Screenshot Generator</title>
      <dc:creator>Magic.rb</dc:creator>
      <pubDate>Tue, 10 Feb 2026 19:34:50 +0000</pubDate>
      <link>https://dev.to/magicnull/i-built-a-free-open-source-app-store-screenshot-generator-39ko</link>
      <guid>https://dev.to/magicnull/i-built-a-free-open-source-app-store-screenshot-generator-39ko</guid>
      <description>&lt;p&gt;As an indie developer, I was tired of paying $15+/month for screenshot tools or spending hours in Figma every time I needed App Store screenshots. So I built my own — and open-sourced it.&lt;/p&gt;

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

&lt;p&gt;🔗 &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://appshots.appstate.xyz/" rel="noopener noreferrer"&gt;appshots.appstate.xyz&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/oyeolamilekan/appshots" rel="noopener noreferrer"&gt;github.com/oyeolamilekan/appshots&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is it?
&lt;/h2&gt;

&lt;p&gt;A browser-based editor that lets you create professional, high-converting screenshots for the Apple App Store and Google Play Store in minutes. No accounts. No cloud. No tracking. Everything runs locally in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  📱 Device Frames
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6 realistic device mockups&lt;/strong&gt; — iPhone 15 Pro Max, iPhone 15 Pro, iPhone 14, iPad Pro 12.9", Samsung Galaxy S24 Ultra, Samsung Galaxy Tab S9&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple color options&lt;/strong&gt; per device — Black Titanium, Natural, Blue, White, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flat 2D &amp;amp; 3D rendering modes&lt;/strong&gt; — toggle between a classic frame and a perspective 3D view with visible device edges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3D rotation controls&lt;/strong&gt; — adjust Rotate Y and Rotate X for the perfect angle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accurate camera elements&lt;/strong&gt; — Dynamic Island, notch, and punch-hole matching each device&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎨 Backgrounds &amp;amp; Appearance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solid color backgrounds&lt;/strong&gt; with a full color picker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 gradient presets&lt;/strong&gt; — Sunset, Ocean, Mint, Berry, Royal, Rose&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global text color picker&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📝 Rich Text &amp;amp; Fonts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rich text editor&lt;/strong&gt; for headlines and subheadlines — bold, italic, underline, text color, alignment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Fonts integration&lt;/strong&gt; — search and preview hundreds of fonts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent sizing&lt;/strong&gt; for headline and subheadline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Width control&lt;/strong&gt; per text block&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drag-to-reposition&lt;/strong&gt; text anywhere on the canvas&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🖼️ Overlay Images
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unlimited overlay images&lt;/strong&gt; — upload badges, logos, arrows, or decorations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drag, resize, and rotate&lt;/strong&gt; each image independently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer management&lt;/strong&gt; — place behind or in front of the device, reorder freely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-image shadow&lt;/strong&gt; with color, blur, and offset controls&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📐 Layout &amp;amp; Positioning
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8 position presets&lt;/strong&gt; — Centered, Bleed Bottom, Bleed Top, Float Center, Float Bottom, Tilt Left, Tilt Right, Perspective&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device size&lt;/strong&gt; slider (scale %)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device vertical position&lt;/strong&gt; slider (offset %)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device shadow&lt;/strong&gt; — toggle on/off with color, blur, and vertical offset controls&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📋 Project Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple projects&lt;/strong&gt; — create, rename, switch between, and delete&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-save&lt;/strong&gt; — all settings persist to localStorage across sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reset to defaults&lt;/strong&gt; — clear everything and start fresh&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📦 Export
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch export&lt;/strong&gt; — export all screenshots at once (ZIP for multiple, PNG for single)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4 export size presets&lt;/strong&gt; — 6.7" iPhone, 6.5" iPhone, 5.5" iPhone, 12.9" iPad Pro&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full 3D support&lt;/strong&gt; — perspective, edges, and shadows preserved in exports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pixel-perfect&lt;/strong&gt; output matching the on-screen preview&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🖥️ Editor Experience
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-screenshot gallery&lt;/strong&gt; with horizontal carousel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time preview&lt;/strong&gt; — all changes update instantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drag-and-drop&lt;/strong&gt; repositioning on the canvas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Element selection&lt;/strong&gt; with visual feedback&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dark mode UI&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to use it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Select a device&lt;/strong&gt; — pick from iPhones, iPads, or Samsung devices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload a screenshot&lt;/strong&gt; — add your app's screenshot to the device screen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit text&lt;/strong&gt; — add headlines and subheadlines, format with the rich text toolbar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a font&lt;/strong&gt; — browse Google Fonts for the perfect typeface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a background&lt;/strong&gt; — choose a solid color or gradient&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Position the device&lt;/strong&gt; — use presets or manually adjust size, position, rotation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch to 3D&lt;/strong&gt; — toggle 3D mode and tweak perspective angles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add overlays&lt;/strong&gt; — upload badges or logos and layer them around the device&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export&lt;/strong&gt; — download all screenshots at App Store resolution&lt;/li&gt;
&lt;/ol&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React 19&lt;/td&gt;
&lt;td&gt;UI framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;Type safety&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind CSS 4&lt;/td&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vite 7&lt;/td&gt;
&lt;td&gt;Build tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TanStack Router&lt;/td&gt;
&lt;td&gt;Routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shadcn/ui&lt;/td&gt;
&lt;td&gt;UI components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lucide React&lt;/td&gt;
&lt;td&gt;Icons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Fonts API&lt;/td&gt;
&lt;td&gt;Font loading&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Run it locally
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/oyeolamilekan/app-screenshot-generator.git
&lt;span class="nb"&gt;cd &lt;/span&gt;app-screenshot-generator
bun &lt;span class="nb"&gt;install
&lt;/span&gt;bun run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opens at &lt;code&gt;http://localhost:5173&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open source?
&lt;/h2&gt;

&lt;p&gt;App Store screenshot tools shouldn't cost a monthly subscription. This tool is &lt;strong&gt;MIT licensed&lt;/strong&gt; — use it, fork it, modify it, ship it. No strings attached.&lt;/p&gt;

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

&lt;p&gt;I'm actively working on this and would love your input. If you have feature requests or find bugs, &lt;a href="https://github.com/oyeolamilekan/app-screenshot-generator/issues" rel="noopener noreferrer"&gt;open an issue on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you find it useful, a ⭐ on GitHub would mean a lot!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Made with ❤️ for iOS and Android developers&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>Building a Style-Aware AI Image Generator with Nano Bana, React, and Hono.</title>
      <dc:creator>Magic.rb</dc:creator>
      <pubDate>Wed, 17 Dec 2025 00:38:42 +0000</pubDate>
      <link>https://dev.to/magicnull/building-a-style-aware-ai-image-generator-with-nano-bana-react-and-hono-1a8d</link>
      <guid>https://dev.to/magicnull/building-a-style-aware-ai-image-generator-with-nano-bana-react-and-hono-1a8d</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s2kz7nk82sn7u3xkyzy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s2kz7nk82sn7u3xkyzy.png" alt=" " width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Out of sheer curiosity to test the limits of the new Google Gemini models, I built &lt;strong&gt;Amaris&lt;/strong&gt;—a full-stack SaaS application that lets you generate images with specific artistic styles using a simple chat interface. No complex workflows, just upload a reference image, type a prompt, and let the AI do the heavy lifting!&lt;/p&gt;

&lt;p&gt;It's built with the "Better-T-Stack" (Bun, Hono, React 19) and uses &lt;strong&gt;Google's Gemini 2.5 Flash Image&lt;/strong&gt; model via the Vercel AI Gateway. If you're interested in building modern AI apps, learning about monorepos, or just want to see how Hono handles AI streams, this project is a great sandbox.&lt;/p&gt;

&lt;p&gt;Demo video:&lt;br&gt;
&lt;a href="https://x.com/devguy_007/status/1997485877325394061?s=20" rel="noopener noreferrer"&gt;https://x.com/devguy_007/status/1997485877325394061?s=20&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check it out here:&lt;br&gt;
🔗 &lt;strong&gt;[&lt;a href="https://github.com/oyeolamilekan/amaris-app" rel="noopener noreferrer"&gt;https://github.com/oyeolamilekan/amaris-app&lt;/a&gt;]&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  ✨ Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  💬 &lt;strong&gt;Chat-Based Interface&lt;/strong&gt; – Generate images in a conversational flow.&lt;/li&gt;
&lt;li&gt;  🎨 &lt;strong&gt;Style Transfer&lt;/strong&gt; – Upload a reference image (like a sketch or painting) and the AI mimics that style.&lt;/li&gt;
&lt;li&gt;  ⚡ &lt;strong&gt;Blazing Fast Backend&lt;/strong&gt; – Powered by Hono and running on Bun.&lt;/li&gt;
&lt;li&gt;  🧠 &lt;strong&gt;Smart Vision&lt;/strong&gt; – Uses Gemini to "see" your style references before generating.&lt;/li&gt;
&lt;li&gt;  🔐 &lt;strong&gt;Full Auth System&lt;/strong&gt; – Secure login with Better-Auth.&lt;/li&gt;
&lt;li&gt;  💳 &lt;strong&gt;Credit System&lt;/strong&gt; – Built-in logic for managing user credits and usage limits.&lt;/li&gt;
&lt;li&gt;  🌑 &lt;strong&gt;Modern UI&lt;/strong&gt; – Dark mode, Tailwind CSS 4, and shadcn/ui components.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  🧰 The Complete Tech Stack
&lt;/h2&gt;

&lt;p&gt;I threw the kitchen sink of modern web dev at this project. Here is everything powering Amaris:&lt;/p&gt;
&lt;h3&gt;
  
  
  Core &amp;amp; Runtime
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bun&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ultra-fast JavaScript runtime &amp;amp; package manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;For type safety across the entire monorepo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Frontend (apps/web)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;React 19&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The latest version of the library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;React Router 7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;For declarative, file-based routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tailwind CSS 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The newest utility-first CSS engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;shadcn/ui&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Beautiful, accessible UI components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TanStack Query&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;For async state management and caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TanStack Form&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;For handling complex forms with ease&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lucide React&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clean, consistent icons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sonner&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;For those nice toast notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Backend (apps/server)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hono&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lightweight, standards-based web framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zod&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema validation for API requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hono/Zod-Validator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Middleware to validate incoming data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Data &amp;amp; Authentication
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The relational database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Drizzle ORM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TypeScript-first ORM for type-safe SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Better-Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Comprehensive authentication (Email/Pass, Sessions)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  AI &amp;amp; Services
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google Gemini&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.5 Flash Image (Generation) &amp;amp; 2.5 Flash (Vision)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vercel AI SDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI Gateway for caching, rate-limiting, and analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudinary&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;For storing and optimizing generated images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Polar.sh&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;For handling subscriptions and payments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  🚀 Getting Started Locally
&lt;/h2&gt;

&lt;p&gt;Here's how to get Amaris running on your machine:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Clone the Repo&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;git clone https://github.com/oyeolamilekan/amaris-app
&lt;span class="nb"&gt;cd &lt;/span&gt;amaris
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Install Dependencies&lt;/strong&gt;&lt;br&gt;
We use Bun for speed!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Set Up Environment&lt;/strong&gt;&lt;br&gt;
You'll need a PostgreSQL DB and a Google AI Studio key. Create your &lt;code&gt;.env&lt;/code&gt; files:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;apps/server/.env&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL=postgresql://user:pass@localhost:5432/amaris
GOOGLE_GENERATIVE_AI_API_KEY=your_gemini_key
CORS_ORIGIN=http://localhost:5173
CLOUDINARY_CLOUD_NAME=your_cloud_name
POLAR_ACCESS_TOKEN=your_polar_token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Push the Database Schema&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;bun run db:push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Start the Dev Server&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;bun run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;http://localhost:5173&lt;/code&gt; to start generating!&lt;/p&gt;

&lt;h2&gt;
  
  
  🧪 How It Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Upload a Style:&lt;/strong&gt; You drop an image into the chat (e.g., a pixel art character).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Vision Analysis:&lt;/strong&gt; The backend uses Gemini's vision capabilities to understand the "vibe" of that image.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Prompting:&lt;/strong&gt; You type "A futuristic city."&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Generation:&lt;/strong&gt; The app combines your prompt with the style reference and sends it through the Vercel AI Gateway to Google Gemini.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Result:&lt;/strong&gt; Seconds later, you get a futuristic city rendered in pixel art style!&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  💭 Why I Built This
&lt;/h2&gt;

&lt;p&gt;I wanted to move beyond simple "text-to-image" scripts and build a real, production-ready architecture. I was specifically interested in how &lt;strong&gt;Hono&lt;/strong&gt; (the backend framework) pairs with &lt;strong&gt;React 19&lt;/strong&gt; in a monorepo setup, and how to integrate payment flows like &lt;strong&gt;Polar.sh&lt;/strong&gt; alongside complex AI logic.&lt;/p&gt;

&lt;p&gt;Plus, the new Gemini 2.5 models are incredibly fast and cheap, making them perfect for experimenting with style transfer without breaking the bank.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧑‍💻 Try It, Fork It, Break It!
&lt;/h2&gt;

&lt;p&gt;This project is open-source and meant to be hacked on. Feel free to clone the repo, swap out the database, try a different AI model, or turn it into a comic book generator.&lt;/p&gt;

&lt;p&gt;I'd love to see what you build with it!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; [&lt;a href="https://github.com/oyeolamilekan/amaris-app" rel="noopener noreferrer"&gt;https://github.com/oyeolamilekan/amaris-app&lt;/a&gt;]&lt;/p&gt;

&lt;h2&gt;
  
  
  🙌 Let's Connect
&lt;/h2&gt;

&lt;p&gt;If you try it out or have questions about the stack, drop a comment or tag me!&lt;/p&gt;

&lt;p&gt;Happy hacking! 👨🏽‍💻✨&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>bunjs</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building an Open Source Real-Time Crypto Price Tracker with Bun's Native WebSocket</title>
      <dc:creator>Magic.rb</dc:creator>
      <pubDate>Sun, 02 Nov 2025 13:32:51 +0000</pubDate>
      <link>https://dev.to/magicnull/building-an-open-source-real-time-crypto-price-tracker-with-buns-native-websocket-2kba</link>
      <guid>https://dev.to/magicnull/building-an-open-source-real-time-crypto-price-tracker-with-buns-native-websocket-2kba</guid>
      <description>&lt;p&gt;I built &lt;a href="https://coinstate.co" rel="noopener noreferrer"&gt;Coinstate&lt;/a&gt;, a real-time cryptocurrency price tracker that aggregates data from 10+ exchanges using &lt;strong&gt;Bun's native WebSocket&lt;/strong&gt;. It's 5x faster than traditional Node.js solutions, open-source (Apache 2.0), and built as a modern monorepo.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://coinstate.co" rel="noopener noreferrer"&gt;Live Demo&lt;/a&gt;&lt;/strong&gt; | 💻 &lt;strong&gt;&lt;a href="https://github.com/oyeolamilekan/coinstate" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




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

&lt;p&gt;If you've ever tried to compare cryptocurrency prices across exchanges, you know the pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔥 &lt;strong&gt;Tab hell&lt;/strong&gt; - Opening 10+ browser tabs for different exchanges&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Manual refresh&lt;/strong&gt; - Constantly clicking refresh to see prices&lt;/li&gt;
&lt;li&gt;⏰ &lt;strong&gt;Missed opportunities&lt;/strong&gt; - By the time you check all exchanges, it's too late&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;No overview&lt;/strong&gt; - Can't see the market at a glance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted a single dashboard showing real-time prices from all major exchanges. So I built Coinstate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Coinstate?
&lt;/h2&gt;

&lt;p&gt;Coinstate is a real-time cryptocurrency price tracker that:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Aggregates prices&lt;/strong&gt; from 10+ exchanges (Binance, Coinbase, KuCoin, Kraken, etc.)&lt;br&gt;
✅ &lt;strong&gt;Streams live updates&lt;/strong&gt; via WebSocket with sub-second latency&lt;br&gt;
✅ &lt;strong&gt;Displays everything&lt;/strong&gt; in a clean, responsive interface&lt;br&gt;
✅ &lt;strong&gt;Compares prices&lt;/strong&gt; side-by-side across exchanges&lt;br&gt;
✅ &lt;strong&gt;Runs blazingly fast&lt;/strong&gt; using Bun's native WebSocket&lt;/p&gt;

&lt;p&gt;Think of it as a Bloomberg Terminal for crypto, but free and open-source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhaq4mwum7lb08hhasufp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhaq4mwum7lb08hhasufp.png" alt="Coinstate Screenshot" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Tech Stack
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Why Bun? 🚀
&lt;/h3&gt;

&lt;p&gt;I chose Bun for the backend because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Native WebSocket&lt;/strong&gt; - Built-in, no external dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10x Faster&lt;/strong&gt; - Installation, startup, and execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript Native&lt;/strong&gt; - No compilation step needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern API&lt;/strong&gt; - Clean, simple, performant&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Performance Comparison
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node.js + ws package:
├─ WebSocket handshake: ~15ms
├─ Message latency: ~5ms
└─ Memory per connection: ~50KB

Bun native WebSocket:
├─ WebSocket handshake: ~3ms (5x faster ⚡)
├─ Message latency: ~1ms (5x faster ⚡)
└─ Memory per connection: ~20KB (60% less 📉)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Backend Stack
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Bun + Hono + Redis + Bull&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The backend connects to multiple exchange APIs simultaneously:&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;// Native Bun WebSocket - No external dependencies!&lt;/span&gt;
&lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Client connected&lt;/span&gt;
      &lt;span class="nx"&gt;wsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;connected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome to Coinstate!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="c1"&gt;// Handle subscriptions&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="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;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;wsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&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;subscriptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Client disconnected&lt;/span&gt;
      &lt;span class="nx"&gt;wsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🎯 &lt;strong&gt;Smart Throttling&lt;/strong&gt; - Limits updates to 10/sec per market&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Deduplication&lt;/strong&gt; - Skips identical updates (saves ~40% bandwidth)&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Redis Caching&lt;/strong&gt; - &amp;lt;1ms cache hits for instant responses&lt;/li&gt;
&lt;li&gt;🔁 &lt;strong&gt;Retry Logic&lt;/strong&gt; - Automatic reconnection on failures&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Frontend Stack
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;React 19 + Vite + TailwindCSS 4 + TanStack Query&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// WebSocket for real-time updates&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;isConnected&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useWebSocket&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// React Query for server state&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;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&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;rates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchAllRates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;refetchInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Group by currency&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grouped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;groupBy&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;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;market&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="mi"&gt;0&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;container mx-auto p-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StatusBar&lt;/span&gt; &lt;span class="nx"&gt;connected&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grouped&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prices&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CurrencySection&lt;/span&gt;
          &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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;h2&gt;
  
  
  Interesting Technical Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. 🔌 Handling Exchange API Differences
&lt;/h3&gt;

&lt;p&gt;Each exchange has a different API format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binance: &lt;code&gt;BTCUSDT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Coinbase: &lt;code&gt;BTC-USD&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Kraken: &lt;code&gt;XBTUSD&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Adapter pattern that normalizes all exchanges:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;NormalizedPrice&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;exchange&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;market&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;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;high24h&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;low24h&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;volume24h&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timestamp&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Each exchange has an adapter&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;fetchBinancePrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;symbol&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="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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BINANCE_API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/ticker/24hr?symbol=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;symbol&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;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;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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;binance&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&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;lastPrice&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;high24h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&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;highPrice&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;low24h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&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;lowPrice&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;volume24h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&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;volume&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&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="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. 📊 WebSocket Bandwidth Optimization
&lt;/h3&gt;

&lt;p&gt;Broadcasting every price update to every client would waste bandwidth. Most updates are tiny changes (e.g., $50,000.12 → $50,000.13).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Three-layer optimization:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Throttling&lt;/strong&gt; - Max 10 updates/sec per market per client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplication&lt;/strong&gt; - Skip if price hasn't actually changed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscriptions&lt;/strong&gt; - Only send markets the client cares about
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebSocketManager&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;broadcastPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NormalizedPrice&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;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Check subscription&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shouldSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&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="c1"&gt;// Check deduplication&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&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="c1"&gt;// Check throttle&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;canSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pendingUpdates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&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="c1"&gt;// Send update&lt;/span&gt;
      &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;
      &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; 60% less bandwidth without losing important updates! 🎉&lt;/p&gt;

&lt;h3&gt;
  
  
  3. 🛡️ Graceful Degradation
&lt;/h3&gt;

&lt;p&gt;Exchanges go down. APIs rate-limit. Networks fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Multi-tier fallback system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exchange&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;market&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="c1"&gt;// 1. Check Redis cache (&amp;lt; 1ms)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`price:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;exchange&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;market&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;isFresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&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;cached&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="c1"&gt;// 2. Try primary endpoint&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;fetchFromExchange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;market&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="c1"&gt;// 3. Try alternative endpoint&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;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alternativeUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchFromAlternative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;market&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// 4. Return last known price&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. 📦 Monorepo with Bun Workspaces
&lt;/h3&gt;

&lt;p&gt;Converted from separate npm packages to unified Bun monorepo:&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cross-router/          # Backend (npm)
frontend/              # Frontend (npm)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/
  ├── backend/         # @coinstate/backend (bun)
  └── frontend/        # @coinstate/frontend (bun)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;✅ Shared dependencies&lt;/li&gt;
&lt;li&gt;✅ Single &lt;code&gt;bun install&lt;/code&gt; (11x faster than npm!)&lt;/li&gt;
&lt;li&gt;✅ Unified scripts&lt;/li&gt;
&lt;li&gt;✅ Better developer experience
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before (npm): ~45 seconds&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# After (bun): ~4 seconds! 🚀&lt;/span&gt;
bun &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance Benchmarks 📈
&lt;/h2&gt;

&lt;p&gt;Real-world production metrics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket handshake&lt;/td&gt;
&lt;td&gt;3ms ⚡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message latency&lt;/td&gt;
&lt;td&gt;1ms ⚡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP request (cached)&lt;/td&gt;
&lt;td&gt;&amp;lt;1ms ⚡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP request (uncached)&lt;/td&gt;
&lt;td&gt;~200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent connections&lt;/td&gt;
&lt;td&gt;100,000+ 🔥&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput&lt;/td&gt;
&lt;td&gt;500,000 msg/sec 🔥&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory usage&lt;/td&gt;
&lt;td&gt;100MB + 20KB/conn&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Compared to Node.js:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;11x faster installation&lt;/li&gt;
&lt;li&gt;40x faster hot reload&lt;/li&gt;
&lt;li&gt;5x faster WebSocket&lt;/li&gt;
&lt;li&gt;60% less memory per connection&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned 💡
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Bun Is Production-Ready ✅
&lt;/h3&gt;

&lt;p&gt;I was skeptical, but Bun has been rock-solid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero stability issues in production&lt;/li&gt;
&lt;li&gt;Native WebSocket is flawless&lt;/li&gt;
&lt;li&gt;TypeScript works out of the box&lt;/li&gt;
&lt;li&gt;npm packages work fine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; Some Node.js packages (like &lt;code&gt;ws&lt;/code&gt;) aren't needed with Bun's native APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Real-Time Is Hard ⚠️
&lt;/h3&gt;

&lt;p&gt;Building a real-time system taught me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always have fallbacks&lt;/strong&gt; - Networks fail&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache aggressively&lt;/strong&gt; - Redis is your friend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throttle intelligently&lt;/strong&gt; - Not every update matters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with load&lt;/strong&gt; - 10 vs 10,000 connections is very different&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Documentation Matters 📚
&lt;/h3&gt;

&lt;p&gt;Spent 20% of dev time on documentation. Worth it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;800+ lines of backend docs&lt;/li&gt;
&lt;li&gt;900+ lines of frontend docs&lt;/li&gt;
&lt;li&gt;Migration guides&lt;/li&gt;
&lt;li&gt;Quick start guide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; First contributor PR within 24 hours! 🎉&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Open Source Everything 🌍
&lt;/h3&gt;

&lt;p&gt;Apache 2.0 license means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anyone can use it freely&lt;/li&gt;
&lt;li&gt;Companies can fork it&lt;/li&gt;
&lt;li&gt;Contributions come back&lt;/li&gt;
&lt;li&gt;Trust through transparency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Already seeing interest from trading bots and portfolio trackers.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Live Demo
&lt;/h3&gt;

&lt;p&gt;Visit &lt;strong&gt;&lt;a href="https://coinstate.co" rel="noopener noreferrer"&gt;coinstate.co&lt;/a&gt;&lt;/strong&gt; to see it in action!&lt;/p&gt;

&lt;h3&gt;
  
  
  Run Locally
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone&lt;/span&gt;
git clone https://github.com/oyeolamilekan/coinstate
&lt;span class="nb"&gt;cd &lt;/span&gt;coinstate

&lt;span class="c"&gt;# Install (takes ~4 seconds!)&lt;/span&gt;
bun &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Start Redis&lt;/span&gt;
redis-server

&lt;span class="c"&gt;# Start backend&lt;/span&gt;
bun dev:backend

&lt;span class="c"&gt;# Start frontend (new terminal)&lt;/span&gt;
bun dev:frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;http://localhost:5173&lt;/code&gt; and watch prices update in real-time! ⚡&lt;/p&gt;

&lt;h3&gt;
  
  
  API Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get Bitcoin price from Binance&lt;/span&gt;
curl http://localhost:3000/api/binance/BTC-USDT

&lt;span class="c"&gt;# WebSocket stream&lt;/span&gt;
wscat &lt;span class="nt"&gt;-c&lt;/span&gt; ws://localhost:3000/ws

&lt;span class="c"&gt;# Subscribe to markets&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;:&lt;span class="s2"&gt;"subscribe"&lt;/span&gt;,&lt;span class="s2"&gt;"subscriptions"&lt;/span&gt;:[&lt;span class="s2"&gt;"BTC-USDT"&lt;/span&gt;,&lt;span class="s2"&gt;"ETH-USDT"&lt;/span&gt;&lt;span class="o"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Roadmap for v2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] 🔔 &lt;strong&gt;Price alerts&lt;/strong&gt; - Get notified at target prices&lt;/li&gt;
&lt;li&gt;[ ] 📈 &lt;strong&gt;Historical charts&lt;/strong&gt; - View price trends&lt;/li&gt;
&lt;li&gt;[ ] 🌐 &lt;strong&gt;More exchanges&lt;/strong&gt; - Add DEXs and regional exchanges&lt;/li&gt;
&lt;li&gt;[ ] 💼 &lt;strong&gt;Portfolio tracking&lt;/strong&gt; - Track your holdings&lt;/li&gt;
&lt;li&gt;[ ] 📱 &lt;strong&gt;Mobile apps&lt;/strong&gt; - iOS and Android&lt;/li&gt;
&lt;li&gt;[ ] 🤖 &lt;strong&gt;Trading signals&lt;/strong&gt; - ML-based predictions&lt;/li&gt;
&lt;li&gt;[ ] 🔌 &lt;strong&gt;Public API&lt;/strong&gt; - Let others build on top&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Contributing 🤝
&lt;/h2&gt;

&lt;p&gt;The project is open-source and looking for contributors!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll work with:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bun runtime&lt;/li&gt;
&lt;li&gt;WebSocket architecture&lt;/li&gt;
&lt;li&gt;Real-time systems&lt;/li&gt;
&lt;li&gt;React 19&lt;/li&gt;
&lt;li&gt;TailwindCSS 4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ways to contribute:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add exchanges (~50 lines per adapter)&lt;/li&gt;
&lt;li&gt;Improve UI/UX&lt;/li&gt;
&lt;li&gt;Write tests&lt;/li&gt;
&lt;li&gt;Fix bugs&lt;/li&gt;
&lt;li&gt;Add features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out the &lt;a href="https://github.com/oyeolamilekan/coinstate/blob/master/CONTRIBUTING.md" rel="noopener noreferrer"&gt;contributing guide&lt;/a&gt; to get started!&lt;/p&gt;

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

&lt;p&gt;Building Coinstate taught me that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bun is ready for production&lt;/strong&gt; - Don't be afraid to use it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native APIs are powerful&lt;/strong&gt; - Less dependencies = less complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time is achievable&lt;/strong&gt; - With the right architecture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source works&lt;/strong&gt; - Share your code, grow together&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The entire stack is modern, fast, and maintainable. If you're building a real-time application, definitely consider Bun's native WebSocket—it's a game-changer! 🚀&lt;/p&gt;




&lt;h2&gt;
  
  
  Links &amp;amp; Resources 🔗
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://coinstate.co" rel="noopener noreferrer"&gt;coinstate.co&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/oyeolamilekan/coinstate" rel="noopener noreferrer"&gt;github.com/oyeolamilekan/coinstate&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📧 &lt;strong&gt;Email&lt;/strong&gt;: &lt;a href="mailto:johnsonoye34@gmail.com"&gt;johnsonoye34@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;strong&gt;Q: Why not Node.js?&lt;/strong&gt;&lt;br&gt;
A: Bun is 10x faster with native WebSocket support. No external packages needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why not Rust/Go?&lt;/strong&gt;&lt;br&gt;
A: JavaScript/TypeScript is more accessible for contributors. Bun gives near-native performance with better DX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why not serverless?&lt;/strong&gt;&lt;br&gt;
A: WebSocket requires persistent connections. Dedicated servers are more cost-effective for high-frequency updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is it safe for trading?&lt;/strong&gt;&lt;br&gt;
A: This is for informational purposes only, not financial advice. Always verify prices on official exchanges before trading.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Questions? Comments?&lt;/strong&gt; Drop them below! 👇&lt;/p&gt;

&lt;p&gt;If you found this interesting, give it a ⭐ on &lt;a href="https://github.com/oyeolamilekan/coinstate" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built with ❤️ using Bun, React, and too much coffee.&lt;/strong&gt; ☕&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>opensource</category>
      <category>bunjs</category>
    </item>
    <item>
      <title>How My Weekend Project Went Viral on Hacker News</title>
      <dc:creator>Magic.rb</dc:creator>
      <pubDate>Sat, 31 May 2025 12:20:44 +0000</pubDate>
      <link>https://dev.to/magicnull/how-my-weekend-project-went-viral-on-hacker-news-3bhb</link>
      <guid>https://dev.to/magicnull/how-my-weekend-project-went-viral-on-hacker-news-3bhb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqrd0c3jw9yokmmdknlc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqrd0c3jw9yokmmdknlc.png" alt="Image description" width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sometimes the best ideas come from simple problems. A couple of months ago, I was annoyed by something most developers deal with. I never knew how my website links would look when I shared them online. Would the image show up on Twitter? Does it look good on Facebook? What about WhatsApp?&lt;/p&gt;

&lt;p&gt;Instead of checking each site manually (which takes forever), I decided to build a tool to fix this problem. What started as a fun weekend project ultimately went viral on Hacker News.&lt;/p&gt;

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

&lt;p&gt;MetaCheck is a simple web app that shows you how your website links will look on different platforms. Just paste in any URL and see how it appears on Google, Twitter, Facebook, WhatsApp, LinkedIn, and more - all at once.&lt;/p&gt;

&lt;p&gt;The idea was simple: help people see their link previews without having to post test links everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;The app is pretty straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shows previews on 9 platforms&lt;/strong&gt; - See your link on Google, Twitter, Facebook, WhatsApp, LinkedIn, Telegram, DuckDuckGo, Discord, and Mastodon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gets your website info automatically&lt;/strong&gt; - Pulls your title, description, image, and favicon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixes URLs for you&lt;/strong&gt; - Adds https:// and cleans up messy links&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works on mobile&lt;/strong&gt; - Looks good on phones and computers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shows helpful errors&lt;/strong&gt; - Tells you what went wrong if something breaks&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I kept the tech simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; - For the website framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; - To catch bugs early&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; - For quick, clean styling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Query&lt;/strong&gt; - To load data efficiently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheerio&lt;/strong&gt; - To read website HTML on the server&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how it works: You paste a URL, my server grabs the webpage, reads all the important info (like title and image), then shows you how that info looks on each platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Viral
&lt;/h2&gt;

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

&lt;p&gt;I didn't expect much when I posted it on Hacker News. But the project hit the front page and stayed there for hours. Thousands of people visited the site, and the comments were amazing.&lt;/p&gt;

&lt;p&gt;Developers shared their own tips, suggested new features, and told me they'd been looking for exactly this tool. It was cool to see how many people had the same problem I did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Response
&lt;/h2&gt;

&lt;p&gt;The numbers were crazy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thousands of visitors in one day&lt;/li&gt;
&lt;li&gt;Front page of Hacker News&lt;/li&gt;
&lt;li&gt;Tens of comments and discussions&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Building MetaCheck taught me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Simple ideas work best&lt;/strong&gt; - The problem wasn't hard, but everyone had it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekend projects can become big&lt;/strong&gt; - Don't overthink it, just build&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developers help each other&lt;/strong&gt; - The Hacker News community was super supportive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link previews matter&lt;/strong&gt; - Way more people care about this than I thought&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Everything is open source on GitHub. The code is clean and easy to understand. If you want to add new platforms, improve how it works, or just learn from it, it's all there.&lt;/p&gt;

&lt;p&gt;Key parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server gets website data safely&lt;/li&gt;
&lt;li&gt;Handles broken links well&lt;/li&gt;
&lt;li&gt;Caches results so it's fast&lt;/li&gt;
&lt;li&gt;Each platform has its own preview design&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The viral response showed me people want tools that solve real problems. MetaCheck was just the start - it reminded me how much I love building useful things.&lt;/p&gt;

&lt;p&gt;Ideas for version 2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check multiple URLs at once&lt;/li&gt;
&lt;li&gt;Compare how previews looked before vs now&lt;/li&gt;
&lt;li&gt;Let developers use it in their own apps&lt;/li&gt;
&lt;li&gt;Add more platforms&lt;/li&gt;
&lt;li&gt;Give tips to make previews better&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the biggest thing I learned is that I need to keep building. There are so many small problems we deal with every day that could be fixed with simple tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm back to building&lt;/strong&gt;, and I can't wait to see what happens with the next weekend project. Sometimes the best tools come from fixing your own problems and sharing them with everyone.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Check out MetaCheck at &lt;a href="https://metacheck.appstate.co/" rel="noopener noreferrer"&gt;metacheck.appstate.co&lt;/a&gt; and see the code on &lt;a href="https://github.com/oyeolamilekan/metacheck" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Read the &lt;a href="https://news.ycombinator.com/item?id=43336892" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt; that started it all. Built with ❤️ in a weekend.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>programming</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>AI-Powered Virtual Try-On App</title>
      <dc:creator>Magic.rb</dc:creator>
      <pubDate>Sat, 31 May 2025 11:59:00 +0000</pubDate>
      <link>https://dev.to/magicnull/ai-powered-virtual-try-on-app-3c7d</link>
      <guid>https://dev.to/magicnull/ai-powered-virtual-try-on-app-3c7d</guid>
      <description>&lt;p&gt;Out of sheer curiosity, I built an AI-powered virtual try-on app a lightweight open-source project that lets you upload your photo and a clothing item and see what it might look like on you. No fancy cameras or 3D scanners, just a bit of AI magic under the hood!&lt;/p&gt;

&lt;p&gt;It's built with Next.js and uses Google's Gemini API (experimental Flash model) to generate try-on previews. If you're curious about generative AI, want to learn about handling images in React/Next.js, or just enjoy experimenting, this project is a fun place to start.&lt;/p&gt;

&lt;p&gt;Check it out here:&lt;br&gt;
🔗 &lt;a href="https://github.com/oyeolamilekan/gemini-ai-tryon" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  ✨ Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;👤 &lt;strong&gt;User Image Upload&lt;/strong&gt; – Add your own photo.&lt;/li&gt;
&lt;li&gt;👕 &lt;strong&gt;Clothing Image Upload&lt;/strong&gt; – Upload a shirt, jacket, or any apparel.&lt;/li&gt;
&lt;li&gt;🖼️ &lt;strong&gt;Image Previews&lt;/strong&gt; – See your selected images before generating results.&lt;/li&gt;
&lt;li&gt;🧠 &lt;strong&gt;AI-Powered Try-On&lt;/strong&gt; – Uses Google Gemini to generate a virtual try-on.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Result Display&lt;/strong&gt; – Get a final AI-generated image preview.&lt;/li&gt;
&lt;li&gt;⏳ &lt;strong&gt;Loading Indicator&lt;/strong&gt; – Visual feedback while the AI does its work.&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Reset Button&lt;/strong&gt; – Quickly clear everything and start fresh.&lt;/li&gt;
&lt;li&gt;🎨 &lt;strong&gt;Clean UI&lt;/strong&gt; – Built with Tailwind CSS for simplicity and responsiveness.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  🧰 Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Next.js 13+&lt;/td&gt;
&lt;td&gt;App Router + React&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;Safer, typed JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind CSS&lt;/td&gt;
&lt;td&gt;Utility-first CSS styling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini API&lt;/td&gt;
&lt;td&gt;Flash experimental model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@google/gemini SDK&lt;/td&gt;
&lt;td&gt;For interacting with the API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lucide React&lt;/td&gt;
&lt;td&gt;Clean, minimal icons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;td&gt;Package manager&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  🚀 Getting Started Locally
&lt;/h2&gt;

&lt;p&gt;Here's how to get this running on your machine:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Clone the Repo
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/oyeolamilekan/gemini-ai-tryon.git
&lt;span class="nb"&gt;cd &lt;/span&gt;gemini-ai-tryon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or if you already have the code locally, navigate to the root directory.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Install Dependencies
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  3. Add Your Gemini API Key
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;.env.local&lt;/code&gt; file in the root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env.local
GEMINI_API_KEY=YOUR_GOOGLE_GEMINI_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_GOOGLE_GEMINI_API_KEY&lt;/code&gt; with a real key from Google AI Studio.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Start the Dev Server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; to view the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧪 How to Use
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Upload your photo.&lt;/li&gt;
&lt;li&gt;Upload a clothing item image.&lt;/li&gt;
&lt;li&gt;Click "Try It On!"&lt;/li&gt;
&lt;li&gt;Wait a few seconds while the AI works.&lt;/li&gt;
&lt;li&gt;View your AI-generated try-on image.&lt;/li&gt;
&lt;li&gt;Click Reset to start over.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🧵 API Details
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Endpoint:&lt;/strong&gt; &lt;code&gt;POST /api/tryon&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request Body:&lt;/strong&gt; FormData containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;userImage&lt;/code&gt;: The user's photo&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clothingImage&lt;/code&gt;: The clothing item photo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Success Response:&lt;/strong&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="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data:image/png;base64,..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Optional description"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Error Response:&lt;/strong&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="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Error message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Optional additional details"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  💭 Why I Built This
&lt;/h2&gt;

&lt;p&gt;I've been curious about how AI could be applied to fashion and e-commerce. What started as an experimental project ended up being a fun way to learn how to combine frontend frameworks, generative models, and image processing into a single app.&lt;/p&gt;

&lt;p&gt;It's open-source, simple, and meant to be hacked on.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧑‍💻 Try It, Fork It, Break It!
&lt;/h2&gt;

&lt;p&gt;Feel free to clone the repo, tinker with the UI, swap in a different AI model, or build your own version of a virtual fitting room. I'd love to see what you make!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/oyeolamilekan/gemini-ai-tryon" rel="noopener noreferrer"&gt;github.com/oyeolamilekan/gemini-ai-tryon&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🙌 Let's Connect
&lt;/h2&gt;

&lt;p&gt;If you try it out or build something cool with it, tag me or drop a comment!&lt;/p&gt;

&lt;p&gt;Happy hacking! 👨🏽‍💻✨&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How I stop users with a disposable email from creating an account on appstate.co</title>
      <dc:creator>Magic.rb</dc:creator>
      <pubDate>Sat, 31 May 2025 11:47:16 +0000</pubDate>
      <link>https://dev.to/magicnull/how-i-stop-users-with-a-disposable-email-from-creating-an-account-on-appstateco-d12</link>
      <guid>https://dev.to/magicnull/how-i-stop-users-with-a-disposable-email-from-creating-an-account-on-appstateco-d12</guid>
      <description>&lt;h1&gt;
  
  
  Simple Guide: Building an Email Checker in Rails
&lt;/h1&gt;

&lt;p&gt;Let's create a tool that checks if an email is from a disposable email service. This can help keep your user list clean and reduce spam.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;We'll make a service that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks an email address&lt;/li&gt;
&lt;li&gt;Tells us if it's from a disposable email service&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Steps to Build It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Set Up Your Files
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Make a new folder for our service:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   mkdir -p app/services/utils
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Create a new file for our code:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   touch app/services/utils/email_checker_service.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Write the Basic Code
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;email_checker_service.rb&lt;/code&gt; and add this code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Utils&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailCheckerService&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationService&lt;/span&gt;
    &lt;span class="no"&gt;DISPOSABLE_DOMAINS_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.json'&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
      &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;disposable_domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_disposable_domains&lt;/span&gt;
      &lt;span class="n"&gt;disposable_domains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_domain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'@'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_disposable_domains&lt;/span&gt;
      &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'disposable_email_domains'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;expires_in: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DISPOSABLE_DOMAINS_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;timeout: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="no"&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="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success?&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;StandardError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
      &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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="s2"&gt;"Error getting disposable domains: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Understand the Code
&lt;/h3&gt;

&lt;p&gt;Let's break it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;initialize(email)&lt;/code&gt;: This starts our service with an email to check.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;call&lt;/code&gt;: This is the main method that does the checking.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_domain&lt;/code&gt;: This gets the domain part of the email (the part after @).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_disposable_domains&lt;/code&gt;: This gets a list of known disposable email domains.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Add HTTParty
&lt;/h3&gt;

&lt;p&gt;We use HTTParty to get the list of disposable domains. Add it to your &lt;code&gt;Gemfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'httparty'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Create a Base Service Class
&lt;/h3&gt;

&lt;p&gt;Create a new file &lt;code&gt;app/services/application_service.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationService&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&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;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets us use our service more easily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Use the Service
&lt;/h3&gt;

&lt;p&gt;Here's how to use it in your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SignupsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Utils&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EmailCheckerService&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="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s1"&gt;'Please use a permanent email address'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="c1"&gt;# Sign up the user&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7: Test It
&lt;/h3&gt;

&lt;p&gt;Let's add a simple test. Create &lt;code&gt;spec/services/utils/email_checker_service_spec.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rails_helper'&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Utils&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EmailCheckerService&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'detects disposable emails'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;allow_any_instance_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;described_class&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:get_disposable_domains&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;and_return&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'disposable.com'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;described_class&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="s1"&gt;'test@disposable.com'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;described_class&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="s1"&gt;'test@gmail.com'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rspec spec/services/utils/email_checker_service_spec.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;We made a service that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Takes an email address&lt;/li&gt;
&lt;li&gt;Checks if it's from a list of known disposable email services&lt;/li&gt;
&lt;li&gt;Tells us if it's disposable or not&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This helps keep your user list clean and can reduce fake signups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;To make it even better, you could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Update the list of disposable domains regularly&lt;/li&gt;
&lt;li&gt;Add more checks, like making sure the email format is correct&lt;/li&gt;
&lt;li&gt;Allow some disposable emails if needed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it! You now have a simple way to check for disposable emails in your Rails app.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
