Skip to content

Repository files navigation

stack

My personal template for building a fullstack web application with my favorite tooling and technologies. This repo and its contents are subject to change as I discover better approaches.

  • Backend: fuego for HTTP + automatic OpenAPI generation, fx for dependency injection, GORM + SQLite for persistence behind a repository layer.
  • Frontend: SvelteKit (SPA via adapter-static) with shadcn-svelte + Tailwind CSS for UI and a client generated by oazapfts.
  • Tooling: mise manages every dependency (Go, Node, pnpm, air, goreleaser) and every task.

The example API is a tiny notes CRUD, persisted to SQLite, so the moving parts are obvious. Rip it out and build your own.

The request path is layered: controller → service → repository → DB. HTTP and validation live in the controller, business logic in the service, and every database call in the repository.

Prerequisites

Install mise. It provides the exact Go, Node, pnpm, air, and goreleaser versions pinned in mise.toml:

mise install
mise run install:web

Develop

mise run dev

This runs both dev servers at once:

Open the SvelteKit server. Its Vite config proxies /api to the Go server, so the browser talks to a single origin in development just like it does in production.

Run them separately with mise run dev:server and mise run dev:web.

The OpenAPI workflow

This is the core of the template. The Go server is the single source of truth for the API shape.

  1. You write a fuego handler with typed request/response structs.
  2. mise run openapi regenerates server/openapi.json, then regenerates web/src/lib/sdk/client.ts from it.
  3. The frontend imports typed functions and types from $lib/sdk.

Both openapi.json and client.ts are committed so the frontend builds without running the server. CI fails if they drift out of sync. Always run mise run openapi after changing a handler.

Adding an endpoint

  1. Model the data. Persisted types go in server/internal/database/models/ with GORM tags; register the model with db.AutoMigrate(...) in database.go. Request-only types (e.g. a create body) go in server/internal/dtos/. Struct tags drive the spec too: validate:"required" makes a field required, json:",omitempty" makes it optional.

  2. Add a repository method in server/internal/repositories/: the only place that touches *gorm.DB.

  3. Add a service method in server/internal/services/ for the logic, calling the repository and translating errors (e.g. gorm.ErrRecordNotFound → a domain error).

  4. Register a route in a controller under server/internal/controllers/. Give every route a stable OptionOperationID: it becomes the client function name:

    fuego.Get(route, "/{id}", func(ctx fuego.ContextNoBody) (models.Note, error) {
    note, err:=c.svc.Get(ctx.PathParam("id"))
    iferrors.Is(err, services.ErrNoteNotFound) {
    return models.Note{}, fuego.NotFoundError{Detail: "note not found"}
    }
    returnnote, err
    }, fuego.OptionOperationID("getNote"))
  5. Regenerate and use it:

    mise run openapi
    import{getNote}from'$lib/sdk';constnote=awaitgetNote(id);

New controllers, services, and repositories are wired up through their Module variables (controllers.go, services.go, repositories.go) which fx assembles in main.go.

Project structure

server/ Go backend
main.go fx application wiring
server.go fuego server + OpenAPI generation
static.go serves the built frontend in production
openapi.json generated spec (committed)
internal/
config/ environment configuration
controllers/ HTTP routes (one file per resource)
dtos/ request-only types
services/ business logic
repositories/ database access (GORM)
database/ connection, migration, seed, and models/
web/ SvelteKit frontend
components.json shadcn-svelte config
src/lib/sdk/ generated client (client.ts) + wrapper (index.ts)
src/lib/components/ui/ shadcn-svelte components
src/routes/ pages + app.css (Tailwind + theme)
docs/websockets.md how to add Socket.IO push updates
.github/workflows/ build, lint, release
Dockerfile production image (assembled by goreleaser)
mise.toml tools + tasks

Tasks

TaskWhat it does
mise run devRun both dev servers
mise run openapiRegenerate the spec and the SDK client
mise run buildBuild the frontend and the server binary
mise run lintgo vet + prettier + eslint
mise run checkType-check the frontend
mise run formatFormat Go and web code
mise run testRun the Go tests
mise tasksList every task

Production

mise run build produces:

  • server/tmp/app: the API binary
  • web/build: the static frontend

The server serves the frontend when WEB_STATIC_PATH points at the build output, so a single process handles both the app and the API:

cd server
WEB_STATIC_PATH=../web/build ./tmp/app
VariableDefaultDescription
PORT3000Port the server listens on
WEB_STATIC_PATH(unset)Directory of the built frontend to serve; unset in dev
DATA_DIR.Directory for application data

CI/CD

GitHub Actions in .github/workflows/:

  • build: builds and tests the server; builds the web bundle.
  • lint: formatting, go vet, eslint, type-check, and a check that the committed OpenAPI spec and SDK client are up to date.
  • release: on a v* tag, goreleaser builds cross-platform binaries and a multi-arch Docker image pushed to GitHub Container Registry (ghcr.io/<owner>/<repo>).

Extending

  • WebSockets: see docs/websockets.md for adding Socket.IO push updates.
  • UI components: add more shadcn-svelte components with the CLI, e.g. pnpm dlx shadcn-svelte@latest add dialog. They land in src/lib/components/ui/.
  • Database: SQLite is the default so the template runs with zero setup. To use Postgres instead, swap the driver in database.go (gorm.io/driver/postgres) and point NewDatabase at a connection string; the repositories are unchanged.

Using this template

  1. Click Use this template on GitHub (or copy the files).

  2. The Go module is named app. To rename it, edit the module line in server/go.mod and update the app/internal/... imports:

    cd server
    go mod edit -module yourname
    grep -rl '"app/internal'.| xargs sed -i '''s#"app/internal#"yourname/internal#g'
  3. The release workflow derives the image name from the repository, so no changes are needed there.

About

My personal template for building a fullstack web application with my favorite tooling and technologies.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages