Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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"))
        if errors.Is(err, services.ErrNoteNotFound) {
            return models.Note{}, fuego.NotFoundError{Detail: "note not found"}
        }
        return note, err
    }, fuego.OptionOperationID("getNote"))
  5. Regenerate and use it:

    mise run openapi
    import { getNote } from '$lib/sdk';
    const note = await getNote(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

Task What it does
mise run dev Run both dev servers
mise run openapi Regenerate the spec and the SDK client
mise run build Build the frontend and the server binary
mise run lint go vet + prettier + eslint
mise run check Type-check the frontend
mise run format Format Go and web code
mise run test Run the Go tests
mise tasks List 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
Variable Default Description
PORT 3000 Port 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

Watchers

Forks

Releases

Packages

Contributors

Languages