Build a Desktop App with Wails #2: Project Structure and the Dev Loop — wails dev and Bindings

6 min read

In the last post we created a project with wails init and opened the first window. This post opens up the files that make up that project, one at a time. There are only a handful of them, but knowing exactly what each one does means you will not have to hunt around when later parts add features.

It runs in eight parts (six main plus two deep-dives).

  • #1 What Wails is — lightweight desktop apps in Go
  • #2 Project structure and the dev loop — wails dev and bindings ← this post
  • #3 Connecting Go and the frontend — method bindings and events
  • #4 System integration — dialogs, menus, and window control
  • #5 Real-world features — persisting settings and error handling
  • #6 Build and distribution — packaging per platform

What wails init created #

Start with the structure at the project root.

Project structure
hello-wails/
├── main.go          App entry point — window options and startup
├── app.go           Go code exposed to the frontend
├── wails.json       Project settings (name, build commands, etc.)
├── go.mod           Go module definition
├── build/           Icons and per-platform build resources
└── frontend/        The entire web frontend
    ├── index.html
    ├── src/
    ├── dist/        Frontend build output
    └── wailsjs/     Auto-generated bindings (do not edit)

The division of labor is clear. Go-side logic goes in app.go, window and startup options in main.go, and the UI in frontend/. wails.json holds settings such as the project name and the frontend build commands, and build/ is where the app icon and installer resources live — we come back to it in #6.

Dissecting main.go #

main.go is the entry point that defines the window and runs the app.

main.go — the core part
//go:embed all:frontend/dist
var assets embed.FS

func main() {
	app := NewApp()

	err := wails.Run(&options.App{
		Title:  "hello-wails",
		Width:  1024,
		Height: 768,
		AssetServer: &assetserver.Options{
			Assets: assets,
		},
		OnStartup: app.startup,
		Bind: []interface{}{
			app,
		},
	})
	if err != nil {
		println("Error:", err.Error())
	}
}

There are four main options.

  • Title, Width, Height — the window title and initial size.
  • AssetServer — serves the frontend output embedded in the app. The go:embed directive puts frontend/dist inside the binary, which is what makes single-file distribution possible.
  • OnStartup — the function called when the app starts. The context received here is used by the events in #3 and the runtime calls in #4.
  • Bind — the list of structs to expose to the frontend. This is the entrance to bindings.

app.go — where bindings start #

app.go holds the code the frontend will call. The template gives you this baseline.

app.go
type App struct {
	ctx context.Context
}

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

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

func (a *App) Greet(name string) string {
	return fmt.Sprintf("Hello %s, It's show time!", name)
}

Public methods attached to App, like Greet, become callable functions on the frontend through the Bind setting. The rules and details are the subject of #3, so for now it is enough to hold on to the idea that adding a Go method gives the frontend one more function.

Template options #

The frontend template you pass to wails init -t is a matter of preference.

TemplateContents
vanilla / vanilla-tsPlain HTML/JS(TS) with no framework
react / react-tsReact + Vite
vue / vue-tsVue + Vite
svelte / svelte-tsSvelte + Vite

From the point of view of Wails, the frontend is just a web project that produces static output, so the Go side is identical no matter which template you pick. From the next part on, this series writes its examples against the react-ts template. If you have been through the React basics course, you can read the frontend code as-is.

wails dev — the dev loop #

During development, wails dev alone drives the whole loop.

Running the dev server
wails dev

The behavior differs by file type.

  • Frontend changes — Vite’s hot reload works as usual, so saving is reflected in the window immediately.
  • Go changes — Wails detects the change, rebuilds the backend, and restarts the app.

One useful fact: in dev mode the same app is also served to a browser.

Part of the wails dev output
To develop in the browser and call your bound Go methods,
navigate to: http://localhost:34115

Open this address in a browser and you get the same app as the native window, and the bound Go methods can be called there too. You get the browser devtools console, network, and element inspector, so frontend debugging is more comfortable on this side.

Tip
You can also open the devtools (Inspect Element) from the native window itself via the context menu or a shortcut. It is enabled by default in dev mode, so use it when you want to check things right in the window.

wailsjs — the auto-generated binding code #

Tracing how the greeting feature in the template app works reveals what bindings really are. The frontend code pulls in the Go method like this.

frontend/src/main.js — the calling side
import { Greet } from '../wailsjs/go/main/App';

Greet(name).then((result) => {
    // result holds the string returned by Go
});

The frontend/wailsjs/ directory is code that wails dev generates automatically by reading the structs registered in Bind. Each Go method gets a JavaScript function of the same name, and the call result comes back as a Promise. It is not a file you edit by hand; change the Go methods and it is regenerated on the next build.

To sum up the dev loop: add a method to app.go and a function appears in wailsjs; import and call it from the frontend and the result comes back as a Promise. Extending that flow into real features is what the next post is about.

Wrapping up #

This post comes down to three points.

  • The project splits its roles across main.go (window and startup options), app.go (logic to expose), frontend/ (the UI), and wails.json (settings).
  • wails dev handles both frontend hot reload and Go rebuilds, and the same app can be debugged in a browser at localhost:34115.
  • frontend/wailsjs is binding code generated from the Bind list, exposing Go methods as Promise-based functions.

In the next post, “Build a Desktop App with Wails #3: Connecting Go and the Frontend — Method Bindings and Events”, we pin down the binding rules precisely, build a todo-list backend, and wire it to the frontend. We also cover the event system that pushes data from Go to the frontend.

X