go 39 lines · 7 steps

Serving a single-page app with Gin

Wire a Gin engine to serve static assets and fall back to index.html for client-side routes.

Explained by highlit
1package server
2 
3import (
4 "net/http"
5 "os"
6 "path/filepath"
7 "strings"
8 
9 "github.com/gin-gonic/gin"
10)
11 
12func RegisterSPA(r *gin.Engine, distDir string) {
13 indexPath := filepath.Join(distDir, "index.html")
14 
15 r.Static("/assets", filepath.Join(distDir, "assets"))
16 r.StaticFile("/favicon.ico", filepath.Join(distDir, "favicon.ico"))
17 r.StaticFile("/robots.txt", filepath.Join(distDir, "robots.txt"))
18 
19 r.NoRoute(func(c *gin.Context) {
20 if strings.HasPrefix(c.Request.URL.Path, "/api/") {
21 c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
22 return
23 }
24 
25 requested := filepath.Join(distDir, filepath.Clean(c.Request.URL.Path))
26 if !strings.HasPrefix(requested, filepath.Clean(distDir)) {
27 c.Status(http.StatusForbidden)
28 return
29 }
30 
31 if info, err := os.Stat(requested); err == nil && !info.IsDir() {
32 c.File(requested)
33 return
34 }
35 
36 c.Header("Cache-Control", "no-cache")
37 c.File(indexPath)
38 })
39}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1SPAs need a catch-all route that returns index.html so the client router can handle unknown paths.
  2. 2Always confirm a joined filesystem path still lives inside its base directory to block traversal attacks.
  3. 3Distinguish API 404s from asset requests so missing endpoints don't accidentally return HTML.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Serving a single-page app with Gin — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code