Skip to content

Instantly share code, notes, and snippets.

@garyblankenship
Created October 4, 2025 02:59
Show Gist options
  • Select an option

  • Save garyblankenship/0598d047b684b3b33e16a42f561efdbd to your computer and use it in GitHub Desktop.

Select an option

Save garyblankenship/0598d047b684b3b33e16a42f561efdbd to your computer and use it in GitHub Desktop.
SvelteKit SPA with a Go API Backend #svelte #go

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.

  • Create a .env file in your SvelteKit project's root:

    PUBLIC_API_URL=http://localhost:8080
    
  • Access it in your SvelteKit code using $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:

  1. The user submits login credentials from the SvelteKit app to a /api/login endpoint on the Go server.
  2. The Go server validates the credentials and, if successful, generates a JWT.
  3. The Go server sends this JWT back to the SvelteKit app, often in an HTTP-only cookie for better security.
  4. For subsequent requests to protected API endpoints, the browser automatically includes the cookie with the JWT.
  5. The Go backend has middleware that inspects the JWT on incoming requests to protected routes, validates it, and authorizes the request.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment