{
"$type": "site.standard.document",
"content": {
"$type": "site.standard.content.markdown",
"text": "import Callout from '../../components/mdx/Callout.astro'\n\nI've recently been playing around with Go (and RPC) and have stumbled upon the `embed` package, which allows us to specify files and directory to be **embedded** (hence the name) into the final binary. You would usually use this to embed something like the db schema / migrations, but there's also a world where this can be used for a vite bundle so that we can embed a SPA (single page app) into the final executable; neat right ?\n\n## Bundling the assets\n\nLet's consider that we already have a Vite application and that we have already run an `npm run build` to generate its bundle and so we have a `dist` folder where all our front assets are.\n\nIf we have a standard http mux, we could simply add a `Handle` function. Let's see what that would look like:\n\n```go\n// this file could be called front_prod.go\n\n//go:embed dist/*\nvar embedFS embed.FS\n\nfunc Front(mux *http.ServeMux) {\n\tstaticFiles, err := fs.Sub(embedFS, \"dist\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfileServer := http.FileServer(http.FS(staticFiles))\n\tmux.Handle(\"/\", fileServer)\n\tlog.Println(\"Serving static files from embedded filesystem\")\n}\n```\n\nLet's break this down: most of the magic happens on these lines:\n\n```go\n//go:embed dist/*\nvar embedFS embed.FS\n```\n\nThese lines tell go to get the `dist` folder and all its subfolders (and files) into a structure that matches the standard library's `FS` interface and stores it in a variable called `embedFS`. Inside the `Front` function we simply target the `dist` folder (with a bit of error handling ) and we add the `FileServer` as a route to our mux.\n\n\n\n### Caveats\n\nThis is a simple and pretty \"dumb\" implementation of the \"production assets\"; if using something like React, you might need to write some logic to redirect the requests to the `index.html` file if no matching asset is found.\n\n## What about during development ?\n\nWell, I think having to build a Vite app every time you make a change during development is not the best DX, right ? What if I told you there was a way that you could have all the power of the Vite dev server (i.e. hot reload) from within your go app ?\n\n\n\n### Writing the Vite Proxy\n\nThis is the (mostly) the whole code we'll need to do this:\n\n```go\n// this file could be called front_dev.go\n\nfunc createViteProxy(target string) http.Handler {\n\turl, err := url.Parse(target)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tproxy := httputil.NewSingleHostReverseProxy(url)\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// Update the request to point to the Vite server\n\t\tr.Host = url.Host\n\t\tr.URL.Scheme = url.Scheme\n\t\tr.URL.Host = url.Host\n\t\tproxy.ServeHTTP(w, r)\n\t})\n}\n\nfunc Front(mux *http.ServeMux) {\n\tviteProxy := createViteProxy(\"http://localhost:5173\")\n\tmux.Handle(\"/\", viteProxy)\n\tlog.Info(\"proxying requests to vite dev server\")\n}\n```\n\nLet's break it down!\n\nWe have the same function signature for the `Front` function\n\n<Callout>Remember this, it will be important later on</Callout>\nThe magic happens with the `createViteProxy` function, where we return a new\n`http.HandlerFunc` that sends the request through a `SingleHostReverseProxy`\nthat points to the Vite dev server. This means that all requests the `Front`\nfunction handles will be sent to the Vite server; this allows all the hot reload\ngoodness that Vite provides to work out of the box.\n\n## How Do You Switch Between the Two ?\n\nHere's the fun part: you let the compiler do it!\n\n\nOn its latest versions, Go supports a feature called **build constraints**. These are values that you can pass your `go build` command and that can influence which files the compiler will include.\nWhen I shared the whole code file, I might have lied; there are actually three more lines you might need to have at the top of the file:\n\n```go\n//go:build dev\n//+build dev\n\n// the rest of the code goes here\n```\n\nThe two comments above are the two syntaxes for **build constraints**. These tell the compiler to only include this file if this constraint is passed to the `build` function: here's how you would run this:\n\n```bash\ngo build -tags dev\n```\n\nOnce you add the reverse condition on the \"production\" logic, you're done!\n\n<Callout>you can just add a ! before the constraint you're checking</Callout>\n\nThis is why I said it would be important to name the `Front` function the same in both files; if both files are in the same package, the rest of the program is completely oblivious to this switcharoo happening in dev or prod!\n\n## Should you do this ?\n\nWhile this might almost look like magic, there are cases in which this might not be the ideal choice:\n\n- If you need more complex routing that just redirecting to `index.html`\n\nWhile I have addressed this a bit in the related section, I just want to point this out again.\n\n- separation of concerns and scalability\n\nThis setup will obviously not work if you want to deploy your frontend code to a dedicated service (Vercel, Netflify etc), this will obviously not work.\n\nStill, there are some cases where you might want this:\n\n- offline-first\n\nWeirdly, this could work really well with an offline-first approach; download the binary, run it on your system and just browse to localhost:3000 (or any other port) and you have your whole application running, no internet required (except if you're connecting to external services, obviously)\n\n- simple deploy\n\nIt's a binary.\n\n- great DX\n\nWith this setup, you have a backend and frontend living on the same port (and/or domain), meaning you don't need to worry about CORS, proxy rules and whatever else causes mental breakdowns to developers nowadays.\n\n",
"version": "1.0"
},
"path": "/articles/go-webapp-vite",
"publishedAt": "2025-05-10T00:00:00.000Z",
"site": "at://did:plc:dgtaz4vldacvqhvvmdvoc4ad/site.standard.publication/3mfbydibiwc7f",
"tags": [
"go",
"javascript"
],
"textContent": "import Callout from '../../components/mdx/Callout.astro'\n\nI've recently been playing around with Go (and RPC) and have stumbled upon the package, which allows us to specify files and directory to be embedded (hence the name) into the final binary. You would usually use this to embed something like the db schema / migrations, but there's also a world where this can be used for a vite bundle so that we can embed a SPA (single page app) into the final executable; neat right ?\n\nBundling the assets\n\nLet's consider that we already have a Vite application and that we have already run an to generate its bundle and so we have a folder where all our front assets are.\n\nIf we have a standard http mux, we could simply add a function. Let's see what that would look like:\n\nLet's break this down: most of the magic happens on these lines:\n\nThese lines tell go to get the folder and all its subfolders (and files) into a structure that matches the standard library's interface and stores it in a variable called . Inside the function we simply target the folder (with a bit of error handling ) and we add the as a route to our mux.\n\nCaveats\n\nThis is a simple and pretty \"dumb\" implementation of the \"production assets\"; if using something like React, you might need to write some logic to redirect the requests to the file if no matching asset is found.\n\nWhat about during development ?\n\nWell, I think having to build a Vite app every time you make a change during development is not the best DX, right ? What if I told you there was a way that you could have all the power of the Vite dev server (i.e. hot reload) from within your go app ?\n\nWriting the Vite Proxy\n\nThis is the (mostly) the whole code we'll need to do this:\n\nLet's break it down!\n\nWe have the same function signature for the function\n\nRemember this, it will be important later on\nThe magic happens with the function, where we return a new\n that sends the request through a \nthat points to the Vite dev server. This means that all requests the \nfunction handles will be sent to the Vite server; this allows all the hot reload\ngoodness that Vite provides to work out of the box.\n\nHow Do You Switch Between the Two ?\n\nHere's the fun part: you let the compiler do it!\n\nOn its latest versions, Go supports a feature called build constraints. These are values that you can pass your command and that can influence which files the compiler will include.\nWhen I shared the whole code file, I might have lied; there are actually three more lines you might need to have at the top of the file:\n\nThe two comments above are the two syntaxes for build constraints. These tell the compiler to only include this file if this constraint is passed to the function: here's how you would run this:\n\nOnce you add the reverse condition on the \"production\" logic, you're done!\n\nyou can just add a ! before the constraint you're checking\n\nThis is why I said it would be important to name the function the same in both files; if both files are in the same package, the rest of the program is completely oblivious to this switcharoo happening in dev or prod!\n\nShould you do this ?\n\nWhile this might almost look like magic, there are cases in which this might not be the ideal choice:\nIf you need more complex routing that just redirecting to \n\nWhile I have addressed this a bit in the related section, I just want to point this out again.\nseparation of concerns and scalability\n\nThis setup will obviously not work if you want to deploy your frontend code to a dedicated service (Vercel, Netflify etc), this will obviously not work.\n\nStill, there are some cases where you might want this:\noffline-first\n\nWeirdly, this could work really well with an offline-first approach; download the binary, run it on your system and just browse to localhost:3000 (or any other port) and you have your whole application running, no internet required (except if you're connecting to external services, obviously)\nsimple deploy\n\nIt's a binary.\ngreat DX\n\nWith this setup, you have a backend and frontend living on the same port (and/or domain), meaning you don't need to worry about CORS, proxy rules and whatever else causes mental breakdowns to developers nowadays.",
"title": "Developing and Compiling Webapps with Vite and Go",
"updatedAt": "2026-05-17T13:09:08.730Z"
}