<?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: Joffrey Mischler</title>
    <description>The latest articles on DEV Community by Joffrey Mischler (@joffrey_mischler_ae908ebb).</description>
    <link>https://dev.to/joffrey_mischler_ae908ebb</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%2F3515299%2F51024870-a798-497b-97b9-0dd424651a32.png</url>
      <title>DEV Community: Joffrey Mischler</title>
      <link>https://dev.to/joffrey_mischler_ae908ebb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joffrey_mischler_ae908ebb"/>
    <language>en</language>
    <item>
      <title>I Built a Self-Hostable MQTT Dashboard in Go + React — Here's How It Works</title>
      <dc:creator>Joffrey Mischler</dc:creator>
      <pubDate>Mon, 29 Jun 2026 12:19:18 +0000</pubDate>
      <link>https://dev.to/joffrey_mischler_ae908ebb/i-built-a-self-hostable-mqtt-dashboard-in-go-react-heres-how-it-works-27fa</link>
      <guid>https://dev.to/joffrey_mischler_ae908ebb/i-built-a-self-hostable-mqtt-dashboard-in-go-react-heres-how-it-works-27fa</guid>
      <description>&lt;p&gt;If you've ever worked with IoT devices over MQTT, you know the tooling situation is... acceptable at best. MQTT Explorer is great for inspecting traffic, but it's a desktop app with no dashboard capabilities. Tools like Node-RED can do dashboards, but they're heavy, opinionated, and not exactly what you'd call a developer tool.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://github.com/jmischler72/mqtt-dashboard" rel="noopener noreferrer"&gt;MQTT Dashboard&lt;/a&gt;&lt;/strong&gt; — a self-hostable, single-binary web app that combines an MQTT topic explorer with a fully customizable drag-and-drop dashboard. Here's why I built it, what's inside, and how you can run it in 30 seconds.&lt;/p&gt;




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

&lt;p&gt;Existing MQTT tools tend to fall into one of two buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pure clients&lt;/strong&gt; (like MQTT Explorer, MQTT.fx): great for debugging, no persistence, no dashboards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full platforms&lt;/strong&gt; (like Node-RED, Home Assistant): powerful, but heavy and not focused on the developer debugging workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I wanted was something in between — a lightweight self-hosted tool where I could connect to a broker, explore topics in real time, &lt;em&gt;and&lt;/em&gt; build a control panel for testing my device without switching apps.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0w9x62bomb3e7md0e01e.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%2F0w9x62bomb3e7md0e01e.png" alt="MQTT Dashboard - Dashboard page" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🧩 Drag &amp;amp; Drop Dashboard
&lt;/h3&gt;

&lt;p&gt;The dashboard is built around a grid layout engine (react-grid-layout). Panels are resizable and reorderable. You configure them per broker/topic, and the layout is persisted in SQLite so it's there when you come back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Functional panels available:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Panel&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Button&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One-click publish a preset payload to a topic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Type and send ad-hoc messages to any topic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Log&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Live message stream with history and wildcard subscriptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cron&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scheduled publishing with a visual cron builder and countdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stats&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Topic activity chart with configurable time window&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Visual panels:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Panel&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Image&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Display an image from URL or file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Separator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Horizontal/vertical divider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Text&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Markdown text for documenting your dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  🌳 Topic Explorer
&lt;/h3&gt;

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

&lt;p&gt;The topic explorer lets you browse a real-time tree of all topics active on the broker. Subscriptions support &lt;code&gt;+&lt;/code&gt; (single-level) and &lt;code&gt;#&lt;/code&gt; (multi-level) MQTT wildcards. Message history is persistent with configurable retention.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔌 Multi-Broker Support
&lt;/h3&gt;

&lt;p&gt;You can connect to multiple MQTT brokers simultaneously and switch between them from a single interface. Each broker can be configured with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plain TCP or TLS/SSL&lt;/li&gt;
&lt;li&gt;Username &amp;amp; password authentication&lt;/li&gt;
&lt;li&gt;Client certificate authentication (mTLS)&lt;/li&gt;
&lt;li&gt;QoS and retain flags&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The whole thing ships as a &lt;strong&gt;single Go binary&lt;/strong&gt; that embeds the compiled React frontend. No separate web server needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────┐         ┌──────────────────────┐
│   React Frontend (SPA)   │◄──WS──► │    Go Backend        │
│   Vite + Tailwind +      │◄──API─► │    Single Binary     │
│   DaisyUI                │         │                      │
└──────────────────────────┘         └──────────┬───────────┘
                                                │
                                     ┌──────────┴───────────┐
                                     │                      │
                              ┌──────▼──────┐    ┌──────────▼──────────┐
                              │   SQLite    │    │  MQTT Brokers (N)   │
                              │  (layouts,  │    │  TCP / TLS / Auth   │
                              │   configs,  │    └─────────────────────┘
                              │   history)  │
                              └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The Go backend uses &lt;strong&gt;&lt;a href="https://github.com/eclipse/paho.mqtt.golang" rel="noopener noreferrer"&gt;Eclipse paho.mqtt.golang&lt;/a&gt;&lt;/strong&gt; for MQTT connections, &lt;strong&gt;SQLite&lt;/strong&gt; for persistence, &lt;strong&gt;gocron&lt;/strong&gt; for the scheduled publishing feature, and &lt;strong&gt;WebSockets&lt;/strong&gt; for pushing live data to the frontend.&lt;/li&gt;
&lt;li&gt;The React frontend is built with &lt;strong&gt;Vite + TypeScript + Tailwind + DaisyUI&lt;/strong&gt;, and uses &lt;strong&gt;react-grid-layout&lt;/strong&gt; for the dashboard grid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production, the Go binary serves the embedded React build directly — no Vite dev server in the loop. Everything runs on a single port (&lt;code&gt;:8080&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech stack at a glance:&lt;/strong&gt;&lt;/p&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;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React, Vite, TypeScript, Tailwind CSS, DaisyUI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grid engine&lt;/td&gt;
&lt;td&gt;react-grid-layout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Go (Golang)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MQTT lib&lt;/td&gt;
&lt;td&gt;Eclipse paho.mqtt.golang&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;SQLite (embedded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduling&lt;/td&gt;
&lt;td&gt;gocron&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realtime&lt;/td&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td&gt;Docker (Alpine)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Docker Compose (Recommended)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mqtt-dashboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/jmischler72/mqtt-dashboard:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mqtt-dashboard&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;data_volume:/app/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;data_volume&lt;/span&gt;&lt;span class="pi"&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 shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Open &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Docker Command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;always &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; mqtt-dashboard-data:/app/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; mqtt-dashboard &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/jmischler72/mqtt-dashboard:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 3: Build from Source
&lt;/h3&gt;

&lt;p&gt;Requirements: Go 1.22+, Node.js 20+&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/jmischler72/mqtt-dashboard.git
&lt;span class="nb"&gt;cd &lt;/span&gt;mqtt-dashboard

&lt;span class="c"&gt;# Build frontend&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm ci &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ..

&lt;span class="c"&gt;# Build backend (embeds the compiled frontend)&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;backend
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; ../frontend/dist ./dist
go build &lt;span class="nt"&gt;-o&lt;/span&gt; mqtt-dashboard &lt;span class="nb"&gt;.&lt;/span&gt;
./mqtt-dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;A few things I'm working on or thinking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom panels&lt;/strong&gt; — let users build and share their own panel types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More configuration options&lt;/strong&gt; for built-in panels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full roadmap is in &lt;a href="https://github.com/jmischler72/mqtt-dashboard/blob/main/TODO.md" rel="noopener noreferrer"&gt;&lt;code&gt;TODO.md&lt;/code&gt;&lt;/a&gt; and the &lt;a href="https://github.com/jmischler72/mqtt-dashboard/blob/main/docs/PRD" rel="noopener noreferrer"&gt;&lt;code&gt;docs/PRD/&lt;/code&gt;&lt;/a&gt; folder.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This project was heavily inspired by &lt;a href="https://github.com/thomasnordquist/MQTT-Explorer" rel="noopener noreferrer"&gt;MQTT Explorer&lt;/a&gt; — if you haven't used it, check it out. MQTT Dashboard tries to extend that exploration-focused experience with persistence and a customizable control interface, all packaged in a self-hostable single binary.&lt;/p&gt;

&lt;p&gt;If you find it useful, a ⭐ on &lt;a href="https://github.com/jmischler72/mqtt-dashboard" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; goes a long way. And if you run into bugs or have feature ideas, issues and PRs are very welcome!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Go + React · GPL-3.0 · &lt;a href="https://github.com/jmischler72/mqtt-dashboard" rel="noopener noreferrer"&gt;github.com/jmischler72/mqtt-dashboard&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mqtt</category>
      <category>iot</category>
      <category>go</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>You should really use react-grid-layout, it's awesome !</title>
      <dc:creator>Joffrey Mischler</dc:creator>
      <pubDate>Fri, 05 Jun 2026 11:11:15 +0000</pubDate>
      <link>https://dev.to/joffrey_mischler_ae908ebb/you-should-really-use-react-grid-layout-its-awesome--2k6c</link>
      <guid>https://dev.to/joffrey_mischler_ae908ebb/you-should-really-use-react-grid-layout-its-awesome--2k6c</guid>
      <description>&lt;p&gt;If you do any self-hosting, home automation, or IoT projects, you know all about the MQTT protocol. It’s ultra-lightweight and robust. But on the UI side, you often end up either scripting your own custom interfaces or building dashboards by hand from scratch.&lt;/p&gt;

&lt;p&gt;That’s exactly why I developed &lt;strong&gt;mqtt-dashboard&lt;/strong&gt;, a self-hostable dashboard featuring a Go backend and a React frontend. The goal? To easily create custom panels, subscribe to MQTT topics, and display that data cleanly.&lt;/p&gt;

&lt;p&gt;But very quickly, I hit a classic UX problem: &lt;strong&gt;how do I let the user organize their panels however they want?&lt;/strong&gt; I wanted them to be able to drag, drop, and resize every single widget on the fly—true drag-and-drop—without breaking the layout or making the codebase a total nightmare to manage.&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;React-Grid-Layout&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Problem with Static Dashboards
&lt;/h3&gt;

&lt;p&gt;At first, you think, &lt;em&gt;"Easy, I'll just set up a CSS grid or use Flexbox, add some 'Move Up / Move Down' buttons, and call it a day."&lt;/em&gt; But in the real world, that user experience is awful. A real dashboard needs to feel alive. You want your CPU monitoring panel big and bold in the top left, and your light switch tucked away in a small corner.&lt;/p&gt;

&lt;p&gt;Trying to build that yourself in React (handling mouse events, recalculating X/Y coordinates in pixels, managing object collisions so panels don't overlap...) is a one-way ticket to wasting three weeks of dev time just to end up with pure spaghetti code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why React-Grid-Layout?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/react-grid-layout/react-grid-layout" rel="noopener noreferrer"&gt;React-Grid-Layout (RGL)&lt;/a&gt; is a grid layout library built specifically for React. It handles responsive design, drag-and-drop, and resizing natively out of the box.&lt;/p&gt;

&lt;p&gt;What I love about this tool is how it abstracts away all the math. Instead of dealing with pixels, you think in &lt;strong&gt;grid coordinates&lt;/strong&gt; (columns and rows).&lt;/p&gt;

&lt;p&gt;Here’s a look at the core structure I used in &lt;strong&gt;mqtt-dashboard&lt;/strong&gt; to display the widgets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;RGL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WidthProvider&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;react-grid-layout&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-grid-layout/css/styles.css&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-resizable/css/styles.css&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;ResponsiveReactGridLayout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WidthProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RGL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;widgets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onLayoutChange&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The layout defines the position and size of each panel&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;widgets&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;widget&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;widget&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="c1"&gt;// Unique widget ID&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// X position on the grid&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Y position on the grid&lt;/span&gt;
    &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Width (number of columns)&lt;/span&gt;
    &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Height (number of rows)&lt;/span&gt;
    &lt;span class="na"&gt;minW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// Safety check so widgets don't shrink into unreadability&lt;/span&gt;
    &lt;span class="na"&gt;minH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="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;ResponsiveReactGridLayout&lt;/span&gt; &lt;span class="c1"&gt;// 12-column Callback Dragging Height Standard a around className="layout" cols="{12}" draggableHandle=".widget-drag-handle" header in is layout move of onLayoutChange="{onLayoutChange}" pixels restricted row rowHeight="{50}" single split the things to when&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;widgets&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;widget&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"widget-container"&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"widget-drag-handle"&lt;/span&gt; &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Drag handle for widget"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;:: Grab me&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&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;MQTTWidgetContent&lt;/span&gt; &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{widget.topic}"&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"{widget.type}"&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="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="err"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="na"&gt;ResponsiveReactGridLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pitfalls to Avoid (The Reality Check)
&lt;/h3&gt;

&lt;p&gt;On paper, you import the package and &lt;em&gt;bam&lt;/em&gt;, it works. But as usual, once you move into production, reality kicks in. If you're planning to use it, keep these things in mind:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The React &lt;code&gt;key&lt;/code&gt; Trap
&lt;/h4&gt;

&lt;p&gt;It’s written in bold in the documentation, but we all fall for it the first time: &lt;strong&gt;the &lt;code&gt;key&lt;/code&gt; of the child component must perfectly match the &lt;code&gt;i&lt;/code&gt; property in your layout object.&lt;/strong&gt; If you use a randomly generated string or a different identifier, React-Grid-Layout loses track, the drag mechanics go crazy, and your panels start randomly teleporting across the screen.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Saving State (Without Spamming Your Backend)
&lt;/h4&gt;

&lt;p&gt;The component provides an &lt;code&gt;onLayoutChange&lt;/code&gt; callback. This fires &lt;em&gt;every single time&lt;/em&gt; a panel moves even one grid unit. If you send a &lt;code&gt;fetch&lt;/code&gt; or an HTTP request to your API for every millisecond of dragging, your backend is going to absolutely choke.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Update your local React state immediately to keep the UI smooth, but use a &lt;strong&gt;debounce&lt;/strong&gt; function before sending the final layout payload to your Go server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Fighting with Widget Content
&lt;/h4&gt;

&lt;p&gt;If your panel contains interactive elements like charts (Chart.js or Recharts) or text that users need to highlight, the global drag-and-drop layer can block interactions inside the widget.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Make good use of the &lt;code&gt;draggableHandle&lt;/code&gt; option. This restricts the dragging capability to a specific area (like a small title bar at the top of the panel), leaving the rest of the widget free for clicking buttons or scrolling.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Verdict
&lt;/h3&gt;

&lt;p&gt;If you need to build a dashboard, a monitoring SaaS, or any tool where the user needs full control over their workspace, &lt;strong&gt;React-Grid-Layout&lt;/strong&gt; saves you an insane amount of time. It runs smoothly, feels incredibly responsive, and when paired with a lightweight backend, gives you a genuinely dynamic app.&lt;/p&gt;

&lt;p&gt;If you want to see exactly how I wired this up with state management, the full code is open source here: &lt;a href="https://github.com/jmischler72/mqtt-dashboard" rel="noopener noreferrer"&gt;jmischler72/mqtt-dashboard&lt;/a&gt;. Drop by the repo, check out the code, fork it, or hit me up in the comments if you have any questions!&lt;/p&gt;

&lt;p&gt;What are you guys using to handle dynamic grid layouts in React? &lt;/p&gt;

&lt;p&gt;Take care, Ciao!&lt;/p&gt;

</description>
      <category>iot</category>
      <category>javascript</category>
      <category>react</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Just build your react frontend into your Go Backend !</title>
      <dc:creator>Joffrey Mischler</dc:creator>
      <pubDate>Mon, 01 Jun 2026 13:19:37 +0000</pubDate>
      <link>https://dev.to/joffrey_mischler_ae908ebb/just-build-your-react-frontend-into-your-go-backend--417m</link>
      <guid>https://dev.to/joffrey_mischler_ae908ebb/just-build-your-react-frontend-into-your-go-backend--417m</guid>
      <description>&lt;p&gt;The typical approach to full-stack web development involves two independent pieces: a frontend development server (or an Nginx instance in production) delivering your UI assets, and a separate backend server handling API requests.&lt;/p&gt;

&lt;p&gt;That works fine for massive corporate systems, but it adds unnecessary deployment friction for lightweight applications, internal tools, or self-hosted dashboards.&lt;/p&gt;

&lt;p&gt;While building my open-source project, &lt;a href="https://github.com/jmischler72/mqtt-dashboard" rel="noopener noreferrer"&gt;MQTT Dashboard&lt;/a&gt;, I wanted a &lt;strong&gt;zero-dependency deployment flow&lt;/strong&gt;. I wanted users to download a single file, execute it, and immediately have both the UI and backend running on port &lt;code&gt;8080&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the exact setup I used to pack a Vite + React single-page application (SPA) straight into a statically linked Go binary—without sacrificing hot-reloading during local development.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Why would I want that?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Shipping a Single Binary
&lt;/h3&gt;

&lt;p&gt;By embedding your static assets directly into your Go backend, you eliminate the need for an external web server like Nginx or a Node runtime in production. The Go binary serves the compiled JavaScript, CSS, and HTML natively out of memory. If a user wants to run your tool, they don't need to configure reverse proxies or manage frontend paths; they just run the executable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the Dockerfile Simpler
&lt;/h3&gt;

&lt;p&gt;This architectural choice heavily simplifies your production container deployment. Instead of orchestrating multiple containers or configuring complex multi-stage setups that copy code into separate runtime environments, your production &lt;code&gt;Dockerfile&lt;/code&gt; drops down to a featherweight, secure layer.&lt;/p&gt;

&lt;p&gt;You build the React app, pass the static &lt;code&gt;dist/&lt;/code&gt; directory to Go, compile a statically linked binary, and toss it into a raw, non-root Alpine image. The final production container drops to around 25-30MB, consumes minimal RAM, and runs effortlessly on low-power hardware like a home server or a Raspberry Pi.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Bonus Advantage:&lt;/strong&gt; Because the Go binary serves both the UI and your backend endpoints from the exact same port (&lt;code&gt;:8080&lt;/code&gt;), you completely bypass CORS configuration headaches and WebSocket handshake path friction.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. How to use the embed directive
&lt;/h2&gt;

&lt;p&gt;Go lets you read files and folders at compilation time and bake them straight into your application binary using the standard library's native &lt;code&gt;embed&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;Our directory layout looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── backend/
│   ├── dist/             # Where the React production build will land
│   ├── main.go
│   └── go.mod
└── frontend/
    ├── package.json
    └── dist/             # Vite's production output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Go Asset Layer
&lt;/h3&gt;

&lt;p&gt;Inside the &lt;code&gt;backend/&lt;/code&gt; directory, create a file called &lt;code&gt;assets.go&lt;/code&gt;. This acts as the bridge that reads your compiled frontend assets and exposes them to your router:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"embed"&lt;/span&gt;
    &lt;span class="s"&gt;"io/fs"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;//go:embed dist/*&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;frontendAssets&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FS&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;getFrontendFileSystem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileSystem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Strip the "dist" prefix so files are served relative to the build root&lt;/span&gt;
    &lt;span class="n"&gt;strippedFS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frontendAssets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dist"&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to initialize embedded filesystem: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&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="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strippedFS&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;Note on &lt;code&gt;fs.Sub&lt;/code&gt;:&lt;/strong&gt; By default, the embedded filesystem keeps the &lt;code&gt;dist/&lt;/code&gt; prefix in its pathing. Stripping it ensures that when a browser requests &lt;code&gt;/index.html&lt;/code&gt;, Go maps it straight to the root instead of breaking your asset resolution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Handling SPA Routing
&lt;/h3&gt;

&lt;p&gt;If you serve static assets traditionally, hitting the refresh button on a React Router path like &lt;code&gt;/dashboards/edit&lt;/code&gt; throws a &lt;strong&gt;404 Not Found&lt;/strong&gt; because the Go server looks for a physical directory named &lt;code&gt;/dashboards/edit&lt;/code&gt; on disk.&lt;/p&gt;

&lt;p&gt;To make an embedded SPA function properly, your HTTP router needs a fallback mechanism: If a requested file doesn't exist on the embedded filesystem, automatically serve &lt;code&gt;index.html&lt;/code&gt; and let React Router handle the path rendering client-side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"errors"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;setupRoutes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeMux&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// 1. Register API/WebSocket endpoints first&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`{"status":"healthy"}`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c"&gt;// 2. Setup the SPA Embedded File Server fallback&lt;/span&gt;
    &lt;span class="n"&gt;fileServer&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getFrontendFileSystem&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fsys&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;getFrontendFileSystem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c"&gt;// If the file physically exists in the embedded assets, serve it&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fsys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&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;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;fileServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&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="c"&gt;// Fallback: If file doesn't exist, serve index.html for React Router&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotExist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;
            &lt;span class="n"&gt;fileServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&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="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Internal Server Error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusInternalServerError&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="n"&gt;mux&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. How to still dev correctly with hot-reload
&lt;/h2&gt;

&lt;p&gt;Baking your assets into a binary is incredible for production, but rebuilding your entire Go binary every single time you change a pixel or a CSS class in React is a developer experience nightmare.&lt;/p&gt;

&lt;p&gt;To maintain an instant Hot Module Replacement (HMR) workflow during development, you run your tools in a decoupled dev mode, utilizing Vite's dev server for the frontend and Air for backend live-reloading.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Development Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Run &lt;code&gt;npm run dev&lt;/code&gt; in your &lt;code&gt;/frontend&lt;/code&gt; directory. Vite fires up on &lt;code&gt;http://localhost:5173&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Run &lt;code&gt;air&lt;/code&gt; in your &lt;code&gt;/backend&lt;/code&gt; directory. Air watches your &lt;code&gt;.go&lt;/code&gt; files and live-reloads your API on &lt;code&gt;http://localhost:8080&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pointing Vite to the Go Backend
&lt;/h3&gt;

&lt;p&gt;To bypass CORS issues locally and make sure your frontend API fetches seamlessly hit your Go server, configure Vite’s built-in proxy.&lt;/p&gt;

&lt;p&gt;Update your &lt;code&gt;frontend/vite.config.ts&lt;/code&gt; (or &lt;code&gt;.js&lt;/code&gt;) file to automatically proxy API and WebSocket requests back to your running Go port:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&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="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&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;@vitejs/react-swc&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Proxy standard API HTTP requests&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&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;target&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:8080&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;changeOrigin&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="c1"&gt;// Proxy live WebSocket traffic&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ws&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;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ws://localhost:8080&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ws&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="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;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;When working locally, you open your browser to &lt;code&gt;http://localhost:5173&lt;/code&gt; (Vite). Any changes you make to your React components reflect instantly in the browser via HMR. When your React components make a fetch request to &lt;code&gt;/api/v1/health&lt;/code&gt;, Vite transparently routes it back to your Go server running on &lt;code&gt;:8080&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you are ready to ship to production, you run your production build script: Vite compiles down to static optimized files inside &lt;code&gt;backend/dist/&lt;/code&gt;, and your &lt;code&gt;go build&lt;/code&gt; pipeline locks those files permanently inside your deployable executable.&lt;/p&gt;

&lt;p&gt;If you want to see this layout working in a live codebase, check out the full implementation over at &lt;a href="https://github.com/jmischler72/mqtt-dashboard" rel="noopener noreferrer"&gt;MQTT Dashboard on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Drop a comment below if you run into any issues mapping your embedded file handlers or configuring Air for your hot-reload stack!&lt;/p&gt;

</description>
      <category>go</category>
      <category>react</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
