SvelteKit SPA with a Go API Backend
Building a SvelteKit SPA with a Go API Backend: A Comprehensive Brainstorm
Combining a SvelteKit Single-Page Application (SPA) with a Go backend offers a powerful, performant, and cost-effective stack. This approach gives you the rich, modern developer experience of SvelteKit for the frontend, while leveraging Go's speed, simplicity, and concurrency for the backend API. Here's a detailed breakdown of how to approach building such an application, drawing on community discussions and best practices.
Core Architecture: Decoupled Frontend and Backend
The fundamental concept is to create two distinct applications:
- SvelteKit Frontend (SPA): A client-side rendered application that handles all the UI and user interactions. It will be built into a set of static HTML, CSS, and JavaScript files.
- Go Backend (API): A server-side application that exposes a REST or GraphQL API for the SvelteKit frontend to consume. It will also be responsible for serving the static files of the SvelteKit application in a production environment.
This decoupled architecture provides flexibility in development, deployment, and scaling of both the frontend and backend independently.
Part 1: Setting up the SvelteKit Frontend as an SPA
SvelteKit is a full-stack framework by default, but it can be configured to output a client-side rendered SPA. This is achieved by using the adapter-static
.
1. Initialize a new SvelteKit project:
npm create svelte@latest my-svelte-app
cd my-svelte-app
npm install
2. Install adapter-static
:
This adapter will build your SvelteKit app into a collection of static files.
npm i -D @sveltejs/adapter-static
3. Configure svelte.config.js
for SPA mode:
Modify your svelte.config.js
to use the static adapter and specify a fallback page. The fallback page is crucial for an SPA as it allows the client-side router to handle all routes.
import adapter from '@sveltejs/adapter-static';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'index.html', // or 200.html depending on your hosting provider
precompress: false,
strict: true
})
}
};
export default config;
4. Disable Server-Side Rendering (SSR):
To ensure your application is a true SPA, disable SSR in your root layout file (src/routes/+layout.js
or src/routes/+layout.ts
). This tells SvelteKit to only render on the client-side.
// src/routes/+layout.js
export const ssr = false;
With this setup, when you run npm run build
, SvelteKit will generate a build
directory containing the static assets for your SPA.
Part 2: Building the Go API Backend
Your Go backend will have two primary responsibilities: providing the API endpoints and serving the SvelteKit SPA.
1. Project Structure:
A common approach is to have a monorepo structure:
/my-project
/frontend // Your SvelteKit app
/backend // Your Go app
2. Creating a simple Go web server:
You can use the standard library's net/http
package or a popular router like gorilla/mux
or chi
.
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
// API routes
http.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from the Go API!")
})
// Serve the SvelteKit SPA
fs := http.FileServer(http.Dir("./frontend/build"))
http.Handle("/", fs)
log.Println("Listening on :8080...")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}
3. Handling SPA Routing in Go:
A key challenge with SPAs is that refreshing the page on a route other than the root (e.g., /profile
) will result in a 404 error if the server doesn't know how to handle it. The solution is to have your Go server redirect all non-API, non-file requests to the index.html
of your SvelteKit app.
Here's a more robust way to handle this:
package main
import (
"log"
"net/http"
"os"
"path/filepath"
"strings"
)
func main() {
// API handler
http.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello from Go!"))
})
// Static file server for the SvelteKit app
staticDir := "./frontend/build"
fileServer := http.FileServer(http.Dir(staticDir))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// If the request is for an API endpoint, let the API handler take over
if strings.HasPrefix(r.URL.Path, "/api") {
http.NotFound(w, r) // Or handle with your API router
return
}
// Check if the requested file exists in the static directory
_, err := os.Stat(filepath.Join(staticDir, r.URL.Path))
if os.IsNotExist(err) {
// If the file doesn't exist, serve the index.html for client-side routing
http.ServeFile(w, r, filepath.Join(staticDir, "index.html"))
return
}
// Otherwise, serve the static file
fileServer.ServeHTTP(w, r)
})
log.Println("Starting server on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
Part 3: Connecting SvelteKit to the Go API
1. Data Fetching in SvelteKit:
You can fetch data from your Go API within your Svelte components using the browser's fetch
API, typically within the onMount
lifecycle function or in a load
function in a +page.js
file.
<!-- src/routes/+page.svelte -->
<script>
import { onMount } from 'svelte';
let message = 'Loading...';
onMount(async () => {
const response = await fetch('/api/hello');
message = await response.text();
});
</script>
<h1>{message}</h1>
2. Managing Environment Variables:
To avoid hardcoding your API URL, use environment variables in SvelteKit.
.env
file in your SvelteKit project's root:
PUBLIC_API_URL=http://localhost:8080
$env/dynamic/public
:
import { env } from '$env/dynamic/public';
const apiUrl = env.PUBLIC_API_URL;
3. Handling CORS (Cross-Origin Resource Sharing):
During development, your SvelteKit dev server (e.g., on port 5173
) and your Go backend (e.g., on port 8080
) will be on different origins. This will cause browsers to block requests due to CORS policy. To fix this, you need to enable CORS on your Go backend.
Here's an example using the rs/cors
library in Go:
// In your Go main function
import (
"net/http"
"github.com/rs/cors"
)
func main() {
// ... your handlers
c := cors.New(cors.Options{
AllowedOrigins: []string{"http://localhost:5173"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"*"},
})
handler := c.Handler(http.DefaultServeMux)
log.Fatal(http.ListenAndServe(":8080", handler))
}
Part 4: Authentication
Authentication between a SvelteKit SPA and a Go backend is typically handled using tokens (like JWTs) stored in cookies or local storage.
A common flow:
/api/login
endpoint on the Go server.There are several tutorials and examples available that demonstrate session-based authentication with Go and SvelteKit.
Part 5: Deployment
A significant advantage of this stack is the ease of deployment. You can compile your Go backend and embed the static SvelteKit frontend files into a single binary.
1. Build the SvelteKit App:
cd frontend
npm run build
2. Embed the Frontend in the Go Binary:
Use Go's embed
package to bundle the SvelteKit build output into your Go executable.
package main
import (
"embed"
"io/fs"
"log"
"net/http"
)
//go:embed all:frontend/build
var embeddedFiles embed.FS
func main() {
// ... your API handlers
// Create a sub-filesystem that serves from the 'frontend/build' directory
// within the embedded files.
subFS, err := fs.Sub(embeddedFiles, "frontend/build")
if err != nil {
log.Fatal(err)
}
http.Handle("/", http.FileServer(http.FS(subFS)))
log.Println("Listening on :8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Now, when you build your Go application (go build
), the resulting binary will contain your entire frontend and can be deployed as a single file. This simplifies deployment to services like Fly.io, a VPS, or any platform that can run a Go binary.