Build a SPA with Go and embedded SvelteKit
Nov 9, 2024
Motivation
The purpose of this article is to harness the power of Sveltekit for building dynamic frontend applications and serve it using Golang to deliver a single binary for our SPA.
The step-by-step
First, create a directory and initialize a Go project in it.
mkdir go-sveltekit-example
cd ./go-sveltekit-example
go mod init github.com/brequet/go-sveltekit-example
Building the frontend
Let’s focus on the SvelteKit application for now. In the project directory, instantiate a brand new SvelteKit project:
pnpx sv create frontend
# Follow the installation steps and choose the configuration that suits you
cd ./frontend
Install the svelte static adapter and tweak your svelte.config.js
file to make use of it:
pnpm i -D @sveltejs/adapter-static
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
precompress: false,
strict: true,
fallback: 'index.html',
}),
}
};
export default config;
Next add this file at ./src/routes/layout.ts
export const prerender = false;
export const csr = true;
export const ssr = false;
We’re almost there! Let’s add some static and dynamic routes to have something to test:
- First, a static route by creating
./src/routes/about/+page.svelte
:
This is the about page.
- Next, a dynamic route with
./src/routes/[slug]/+page.svelte
:
<script>
import { page } from '$app/stores';
</script>
This route is dynamic: {$page.params.slug}.
- Edit
./src/routes/+page.svelte
:
<h1 class="text-3xl">Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<div class="flex flex-col">
<a href="about" class="text-blue-500">About</a>
<a href="dynamic-route" class="text-blue-500">Dynamic route</a>
</div>
From here, we can test if everything in our frontend works by building and previewing our app:
pnpm build
pnpm preview
Go to http://localhost:4173/
, and see that all the pages are rendering smoothly:



Serving the frontend using Go
Now to serve the built SvelteKit frontend using Go, there are a few things to keep in mind. The incoming request URL may represent:
- one of our application route (e.g.
/about
), the web page will be responsible for handling redirection. - an asset (e.g.
/favicon.png
, or/_app/immutable/entry/start.CgSmxp2P.js
) that we have to serve to the client.
Taking that into account, let’s create a file at ./frontend/frontend.go
:
package frontend
import (
"embed"
"io/fs"
"net/http"
"path/filepath"
"strings"
)
//go:embed build/**
var buildDir embed.FS
func Handler() http.Handler {
stripped, err := fs.Sub(buildDir, "build")
if err != nil {
panic(err)
}
fileServer := http.FileServer(http.FS(stripped))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "" || r.URL.Path == "/" {
w.Header().Set("Content-Type", "text/html")
index, err := fs.ReadFile(stripped, "index.html")
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Write(index)
return
}
// Serve assets from the build directory
if strings.HasPrefix(r.URL.Path, "/_app/") || filepath.Ext(r.URL.Path) != "" {
fileServer.ServeHTTP(w, r)
return
}
// For all other paths, serve index.html to let SvelteKit handle routing
w.Header().Set("Content-Type", "text/html")
index, err := fs.ReadFile(stripped, "index.html")
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Write(index)
})
}
We can now use this handler to serve our frontend, at the project root create a main.go
:
package main
import (
"log"
"net/http"
"github.com/brequet/go-sveltekit-example/frontend"
)
func main() {
http.Handle("/", http.StripPrefix("/", frontend.Handler()))
log.Println("Server starting on localhost:8080...")
if err := http.ListenAndServe("localhost:8080", nil); err != nil {
log.Fatal(err)
}
}
And you are set! You can test it by running the Golang application and navigating to the URL.
From here, you can add your API endpoints to build your SPA, e.g.:
func main() {
http.Handle("/api/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!"))
}))
http.Handle("/", http.StripPrefix("/", frontend.Handler()))
log.Println("Server starting on localhost:8080...")
if err := http.ListenAndServe("localhost:8080", nil); err != nil {
log.Fatal(err)
}
}
Serve the frontend using a base path
In some cases, you will want to serve your frontend with a base path, like https://my-website/app/..
. This requires tweaking a few things, let’s see how to achieve this.
Edit your svelte.config.js
to add this field in the kit
object:
paths: {
base: '/app'
},
Warning: do not forget to update any hard-coded paths or redirects in your frontend!
Edit your frontend handler at ./src/frontend.go
:
...
path := strings.TrimPrefix(r.URL.Path, "/app")
if path == "" || path == "/" {
w.Header().Set("Content-Type", "text/html")
index, err := fs.ReadFile(stripped, "index.html")
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Write(index)
return
}
// Serve assets from the build directory
if strings.HasPrefix(path, "/_app/") || filepath.Ext(path) != "" {
r.URL.Path = path
fileServer.ServeHTTP(w, r)
return
}
...
And finally, adapt your server in the main file:
http.Handle("/", http.RedirectHandler("/app", http.StatusMovedPermanently))
http.Handle("/app/", http.StripPrefix("/app", frontend.Handler()))
And this is it, you’ve got an embedded SvelteKit application with Go!

What’s next
You’ve learned how to ship a simple Go binary embedding a SvelteKit application, using Go stdlib only. This example is meant to be quite direct and minimalist, it is now up to you to refactor this code if you would like, add API endpoints to it and create a true SPA!
You can find the final code at https://github.com/brequet/go-sveltekit-example.