Frontend Labs
⋯

45. Building a Wails app: a Go backend behind a web-based to-do board

Goal: show the "Go backend + web frontend in a native window" model, where your Go code is called directly from JavaScript with no HTTP layer in between — Wails auto-generates the JS bindings for exported Go methods.

go install github.com/wailsapp/wails/v2/cmd/wails@latest
wails init -n todoboard -t vanilla
cd todoboard

For repeatable CI builds, pin the CLI to a specific release instead of installing @latest.

// app.go
package main

import (
	"context"
	"sync"
)

type Todo struct {
	ID   int    `json:"id"`
	Text string `json:"text"`
	Done bool   `json:"done"`
}

type App struct {
	ctx   context.Context
	mu    sync.Mutex
	todos []Todo
	nextID int
}

func NewApp() *App { return &App{} }

func (a *App) startup(ctx context.Context) { a.ctx = ctx }

// Exported methods become callable directly from JS as
// window.go.main.App.AddTodo(...) etc, via Wails' generated bindings.

func (a *App) AddTodo(text string) []Todo {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.nextID++
	a.todos = append(a.todos, Todo{ID: a.nextID, Text: text})
	return a.todos
}

func (a *App) ToggleTodo(id int) []Todo {
	a.mu.Lock()
	defer a.mu.Unlock()
	for i := range a.todos {
		if a.todos[i].ID == id {
			a.todos[i].Done = !a.todos[i].Done
		}
	}
	return a.todos
}

func (a *App) ListTodos() []Todo {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.todos
}
// frontend/src/main.js
import { AddTodo, ToggleTodo, ListTodos } from '../wailsjs/go/main/App';

async function render() {
  const todos = await ListTodos();
  document.getElementById('list').innerHTML = todos
    .map(t => `<li class="${t.done ? 'done' : ''}" data-id="${t.id}">${t.text}</li>`)
    .join('');
}

document.getElementById('add').addEventListener('click', async () => {
  const input = document.getElementById('text');
  await AddTodo(input.value);
  input.value = '';
  render();
});

document.getElementById('list').addEventListener('click', async (e) => {
  const id = e.target.dataset.id;
  if (id) { await ToggleTodo(Number(id)); render(); }
});

render();
wails dev    # hot-reloading dev window
wails build  # produces a single native binary per OS with the frontend embedded

The mental model: your Go struct's exported methods are the API — no REST layer, no JSON-over-HTTP boilerplate, no CORS. Wails serializes arguments and return values across the JS↔Go boundary automatically. This is the fastest path from "I know Go and basic HTML/JS" to a shippable native desktop app.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36