Serve a frontend
If you're building a single-page application (SPA), you can embed the frontend build into your Go binary and serve it directly from the httpserver.
CreateEmbeddedStaticServe
Use CreateEmbeddedStaticServe with Go's embed.FS to serve static files:
//go:embed public
var publicFs embed.FS
router.UseFactory(httpserver.CreateEmbeddedStaticServe(publicFs, "public", "/api"))
The three arguments are:
| Argument | Description |
|---|---|
files | The embed.FS containing your static files |
dir | The subtree root within the embed.FS (e.g., "public") |
excludes... | Path prefixes to skip (e.g., "/api" — API routes handle these) |
How it works
- For each incoming request, the middleware checks if the path starts with any excluded prefix
- If excluded, the request passes through to the next handler
- If not excluded, it looks for a matching file in the embedded filesystem
- If the file has no extension, it falls back to serving
index.html(SPA routing) - If no file is found, it returns 404
This means your API routes under /api/* are handled by your handlers, and everything else falls through to the SPA.
Typical setup
The common pattern is:
- API routes handle
/api/*paths - The static serve middleware handles everything else
- The middleware is registered after API routes so API paths take priority
httpserver.RunDefaultServer(func(ctx context.Context, config cfg.Config, logger log.Logger, router *httpserver.Router) error {
// API routes
router.GET("/api/status", func(ginCtx *gin.Context) {
ginCtx.JSON(200, gin.H{"status": "ok"})
})
// Static file serving (falls back to index.html for SPA routing)
router.UseFactory(httpserver.CreateEmbeddedStaticServe(publicFs, "public", "/api"))
return nil
})
Directory structure
Your project should look like this:
backend/
├── main.go # //go:embed public
├── config.dist.yml
└── public/
├── index.html
├── assets/
│ ├── index.js
│ └── index.css
└── favicon.ico
The public/ directory is populated by your frontend build (e.g., npm run build).
Complete example
main.go
main.go
package main
import (
"context"
"embed"
"io/fs"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gosoline-project/httpserver"
"github.com/justtrackio/gosoline/pkg/cfg"
"github.com/justtrackio/gosoline/pkg/log"
)
//go:embed public
var publicFs embed.FS
func main() {
httpserver.RunDefaultServer(func(ctx context.Context, config cfg.Config, logger log.Logger, router *httpserver.Router) error {
router.GET("/api/status", func(ginCtx *gin.Context) {
ginCtx.JSON(200, gin.H{"status": "ok"})
})
router.UseFactory(httpserver.CreateEmbeddedStaticServe(publicFs, "public", "/api"))
return nil
})
}
var _ = fs.FS(nil)
var _ = http.StatusOK
config.dist.yml
config.dist.yml
app:
env: dev
name: static-serve
httpserver:
default:
port: 8088
Test:
# API endpoint
curl http://localhost:8088/api/status
# {"status":"ok"}
# Frontend
curl http://localhost:8088/
# <!DOCTYPE html>
# <html>...