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 all, Go (or Golang) is very efficient. My side project Readclip, which I built with Go and Vite, only takes up around 60MB in Docker image size. If I compare this image size to Docker images for Laravel, Ruby, or Remix, my stack produces a much smaller image size.

Well, You might think docker image size isn't a big concern, which is fair. However, for me, being able to produce smaller docker images helps optimize resource usage. Plus, packaging a Go app into a docker container is relatively straightforward compared to dockerizing frameworks like Laravel, Remix, or Next.js.

What about scale?

For most cases, Golang outperforms popular languages like PHP, Node.js, and Ruby. While Golang is not as strongly typed as languages like Rust and TypeScript, its type system is sufficient to write performant code. The language is simple yet effective.

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 give a short answer to this - Vite just works, and it's easy to work with any modern frontend framework that runs in the client. I think it's the responsibility of the frontend framework, and it shouldn't care about what happens in the backend. Let Golang handle 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