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.