How to setup fullstack web app with golang and vite?
Build web app is getting easier these days, new tools are coming out everyday with the goals to help developers to get the job done. Every new tools offer new architecture new technology and a lot more new thing, sometime that new things not helping us to get the job done there are a lot of experimental things that hard to learn.
In this article I wanted to share my choice for fullstack web framework, or actually it's not a framework. It just golang for backend and vite for frontend.
Why choosing golang?
First of, golang is very cheap. My side project Readclip which is I built with Golang and Vite only take about 60MB image size in docker. If I compare this image size to docker for laravel, ruby or remix my stack produce much smaller size in docker size.
Well, you might think docker size is not much of concern that is okay. But for me being able to produce smaller docker image size will help me optimize my resource usage, and packaging golang app into docker is very easy if we compare it to dockerizing framework like laravel, remix or nextjs.
What about scale?
For most of the cases golang out perform popular languages php, nodejs, ruby and more. Golang is simple language even though it's not strongly typed like rust and typescript the type system of golang is enough to write performant code.
What about ecosystem?
Golang is used in a lot of place, easy to deploy easy to install dependencies and it supported by large company like goolge, uber and more.
But yeah there are a lot of debate when choosing programming language, let's just stop here and focus on building software shall we?
Why vite?
I'll just give short answer to this, vite it just works, it easy to to work with any modern frontend framework that works in client, I think that is the responsibility of frontend framework it shouldn't care what happend in the backend. Let golang deal with the backend stuff.
In this article we will use React, because that is the only frontend framework I am familiar to. But feel free to choose your favourite frontend frame works.
Setup project
Let's create new project folder.
mkdir golang-fullstack-vite
go mod init github.com/ahmadrosid/golang-fullstack-vite
In this project we will use GoFiber as our web server that we will create with golang.
Create file main.go
:
package main
import (
"log"
"github.com/gofiber/fiber/v2"
)
func main() {
app := fiber.New()
app.Get("/hello", func (c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
log.Fatal(app.Listen(":3000"))
}
Install dependencies:
go mod tidy
Create new folder for vite project let's just name the folder ui
.
npm create vite@latest
Need to install the following packages:
create-vite@4.4.1
Ok to proceed? (y) y
✔ Project name: … ui
✔ Select a framework: › React
✔ Select a variant: › TypeScript
Create file embed.go
, we will use this to include vite build file into the binary file when we compile the go program so that we don't need to include folder when deploying our app.
package ui
import "embed"
//go:embed dist/*
var Index embed.FS
Now we can serve the frontend into our go fiber web server.
import (
"log"
"io/fs"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/ahmadrosid/golang-fullstack-vite/ui"
)
func main() {
app := fiber.New()
index, err := fs.Sub(ui.Index, "dist")
if err != nil {
panic(err)
}
app.Use("/", filesystem.New(filesystem.Config{
Root: http.FS(index),
Index: "index.html",
Browse: false,
}))
app.Get("/hello", func (c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
log.Fatal(app.Listen(":3000"))
}
Problems
With this setup you might got a problem, so here's the problem I found so far and how to fix it.
Serving custom static url
By default this setup will enable singgle page application from /
end point. But if you wanted to serve custom url you ca do this.
serveUI := func(ctx *fiber.Ctx) error {
return filesystem.SendFile(ctx, http.FS(index), "index.html")
}
uiPaths := []string{
"/",
"/profile",
"/setting",
"/login",
"/register",
}
for _, path := range uiPaths {
app.Get(path, serveUI)
}
Deal with cors
In local development you might need to enable cors to access the api from frontend.
import (
...
"github.com/gofiber/fiber/v2/middleware/cors"
)
app.Use(cors.New(cors.Config{
AllowOrigins: "http://localhost:3000, http://127.0.0.1:8000",
AllowHeaders: "Origin, Content-Type, Accept",
}))
Don't crash app when fatal error occur
import (
...
"github.com/gofiber/fiber/v2/middleware/recover"
)
app.Use(recover.New())
Loading env variable
I use godotenv to parse env to struct, that env value can be set to system environment variable or .env
file.
package config
import (
"github.com/caarlos0/env"
"github.com/joho/godotenv"
)
type Config struct {
Port string `env:"PORT" envDefault:"8080"`
GoogleCredentials string `env:"GOOGLE_APPLICATION_CREDENTIALS" envDefault:""`
DatabaseUrl string `env:"DB_CONNECTION_STRING" envDefault:""`
}
func Load() *Config {
godotenv.Load()
cfg := Config{}
env.Parse(&cfg)
return &cfg
}
How to handle Authentication?
So far I use firebase for authentication it easy to setup for backend and frontend. Use gofiber-firebaseauth to validate authorization from firebase.
import (
...
gofiberfirebaseauth "github.com/sacsand/gofiber-firebaseauth"
)
app.Use(gofiberfirebaseauth.New(gofiberfirebaseauth.Config{
FirebaseApp: firebaseApp,
IgnoreUrls: []string{
"GET::/",
"GET::/login",
"GET::/register",
"GET::/health-check",
},
ErrorHandler: firebase.ErrorHandler,
}))
If you still have a problem feel free to send me a question I will try to help you to solved it.
Dockerizing
Building the docker is easy. So here's the docker file you will need.
FROM golang:1.20.1-alpine as base
RUN apk add curl bash nodejs npm make
RUN npm install --global pnpm
WORKDIR /go/src/app
COPY go.* .
RUN go mod download
COPY . .
RUN make build TARGET_DIR=/go/bin/app
FROM alpine:3.17.2
COPY --from=base /go/bin/app /app
ENV PORT=8080
CMD ["/app"]
This docker file is calling Makefile
to run the build, this is usefull because we can use Makefile to run our development server as well. Here's the Makefile config.
.PHONY: build
TARGET_DIR ?= build/app
build:
(set -e; cd ui && pnpm i && pnpm run build)
go generate ./...
CGO_ENABLED=0 go build -o ${TARGET_DIR} -buildvcs=false
start-ui-dev:
cd ui && npm run dev
dev:
@source .env && npx concurrently "cd ui && npm run dev" "go run main.go"
start:
docker compose up -d
start-clean:
docker compose up --force-recreate --build app
deploy:
flyctl deploy ## You can use any deployment scripts here