3 min read
How to Serve A SolidJS App With A Go Backend

Single page applications (SPAs) are generally not the first tool I reach for when building a new website. Unless I’m starved for reactivity, I find managing user credentials, securing my API, and shipping a large bundle to the client to process frankly over-stressful and unnecessary. I’d much rather use a server-side multi-page app (MPA) with HTMX or Alpine.JS to achieve dynamic hydration where it counts without having to worry about exposing logic or secrets to the client.

However, recently I started building a realtime multiplayer board game using websockets. I needed a tech stack that could handle regular state changes from the server while updating a dynamic game board. Out of everything I found, Solid JS seemed to be the simplest, most performant framework I could integrate with Go.

Setting up Vite

You can set up a new Solid project easily by running

bunx degit solidjs/templates/ts my-app

Then move package.json, tsconfig.json, vite.config.ts, index.html and postcss.config.js out of your my-app directory into the root of your Go project. This centers your root as the heart of your project and follows Go best practice. Next, modify the index html so the script tag source is accurate to your index.tsx.

<!DOCTYPE html>

<html lang="en">

  <head>

    <meta charset="utf-8" />

    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <meta name="theme-color" content="#000000" />

    <link

      rel="shortcut icon"

      type="image/ico"

      href="/client/assets/favicon.ico"

    />

    <title>My App</title>

  </head>

  <body>

    <noscript>You need to enable JavaScript to run this app.</noscript>

    <div id="root"></div>

    <script src="/my-app/index.tsx" type="module"></script>

  </body>

</html>

Finally, update your vite.config.ts’s server.proxy object to support your application’s API route:

proxy: {  

     "/api": {  

       target: "http://localhost:3000",  

       changeOrigin: true,  

     },  

     "/ws": {  

       target: "http://localhost:3000",  

       changeOrigin: true,  

     },  

   },

Now, you can configure your backend to render your app’s development server and static assets depending on your development or production environment.

Using Go Fiber

DEBUG in this example represents any methodology you might use to determine if you’re running your app in production.

	if DEBUG {  

		  

		app.Use(func(c *fiber.Ctx) error {  

			// Proxy requests to Vite dev server  

			if c.Path() == "/" || strings.HasPrefix(c.Path(), "/assets/") {  

				return proxy.Do(c, "http://localhost:5173")  

			}  

			return c.Next()  

		})

		app.Get("/*", func(c *fiber.Ctx) error {  

			// Redirect to Vite dev server  

			return c.Redirect("http://localhost:5173")  

		})  

	} else {  

		// Serve built assets in production  

		app.Use("/assets", filesystem.New(filesystem.Config{  

			Root: http.Dir("./dist/assets"),  

		}))  

		app.Get("/*", func(c *fiber.Ctx) error {  

			return c.SendFile("./dist/index.html")  

		})  

	}

Using Gin

if gin.Mode() == gin.DebugMode {  

        // Serve Vite in development  

        router.Use(func(c *gin.Context) {  

            if c.Request.URL.Path == "/" || strings.HasPrefix(c.Request.URL.Path, "/assets/") {  

                proxy := httputil.NewSingleHostReverseProxy(&url.URL{  

                    Scheme: "http",  

                    Host:   "localhost:5173",  

                })  

                proxy.ServeHTTP(c.Writer, c.Request)  

                return  

            }  

            c.Next()  

        })

        // Fallback route should be last  

        router.NoRoute(func(c *gin.Context) {  

            c.Redirect(http.StatusFound, "http://localhost:5173")  

        })  

    } else {  

        router.Use(static.Serve("/assets", static.LocalFile("./dist/assets", false)))  

        router.NoRoute(func(c *gin.Context) {  

            c.File("./dist/index.html")  

        })  

    }