Hello Doors

In this tutorial, we'll build a small weather dashboard with Doors. Along the way, you'll learn the core building blocks of the framework: rendering HTML with GoX, serving a page with Doors, wiring routes, and learning a few helpful APIs.

The full code for this tutorial is available at github.com/doors-dev/tutorial.

Finished tutorial app preview

Install

Make sure you're using Go 1.25.1 or newer:

go version

Doors is built on top of GoX, so editor support matters a lot here. Use one of these:

You can also follow the manual installation steps in the GoX README.

It is also a good idea to have the gox binary on your PATH. That gives you commands like gox fmt and gox gen, and it helps editor tooling and code agents run the same workflow explicitly when needed.

The practical workflow is:

  • write GoX source in .gox
  • use .go for ordinary Go files
  • treat .x.go as generated output and do not edit it directly

The GoX language server keeps generated .x.go files in sync when you save a .gox file. You can still run gox gen or gox fmt manually whenever you want.

Setup

Create a directory for the tutorial app:

mkdir doors-tutorial
cd doors-tutorial

Initialize a Go module and install Doors:

go mod init github.com/doors-dev/tutorial
go get github.com/doors-dev/doors

App

Create app.gox:

package main

import "github.com/doors-dev/gox"

type App struct{}

elem (a App) Main() {
	<!doctype html>
	<html lang="en">
		<head>
			<meta charset="utf-8">
			<meta name="viewport" content="width=device-width, initial-scale=1">
			<title>Hello Doors!</title>
		</head>
		<body>
			<main class="container">
				<h1>Hello Doors!</h1>
			</main>
		</body>
	</html>
}

In Doors and GoX, a component needs a Main() method that returns gox.Elem. The elem keyword lets you write HTML directly inside the function body instead of building it manually in Go.

When you save app.gox, the GoX language server generates the matching .x.go file for you.

Serve

Now create main.go:

package main

import (
	"net/http"

	"github.com/doors-dev/doors"
)

type Path struct {
	Home bool `path:"/"`
}

func main() {
	r := doors.NewRouter()

	doors.UseModel(r, func(doors.RequestModel, doors.Source[Path]) doors.Response {
		return doors.ResponseComp(App{})
	})

	if err := http.ListenAndServe(":8080", r); err != nil {
		panic(err)
	}
}

This does four things:

  1. Defines the / route with the Path struct.
  2. Creates a Doors router.
  3. Registers a handler that always renders App{}.
  4. Starts an HTTP server on port 8080.

Run the app:

go run .

Then open http://localhost:8080.

Hello Doors page

What Just Happened?

The Path struct is a Doors model. Doors uses tagged struct fields to match, decode, and encode URLs.

doors.UseModel(...) registers a handler for that model type. In this example, the handler always returns doors.ResponseComp(App{}), so the page renders the App component for /.

The doors.RequestModel argument gives access to request-specific HTTP data, and doors.Source[Path] holds the current path model value. We are not using either one yet, but they become important as soon as the page starts reacting to navigation, query params, or request state.

For the underlying model flow, see Path Model and Router.

Styles

For a decent default look, let's add Pico CSS.

You could use a normal <link href="...">, but doors.ResourceExternal(...) also lets Doors track the external URL for CSP if you enable it later.

For resource-backed styles and external URLs, see Styles.

Update app.gox like this:

package main

import (
	"github.com/doors-dev/doors"
	"github.com/doors-dev/gox"
)

type App struct{}

elem (a App) Main() {
	<!doctype html>
	<html lang="en" data-theme="dark">
		<head>
			<meta charset="utf-8">
			<meta name="viewport" content="width=device-width, initial-scale=1">
			<title>Hello Doors!</title>
			<link
				(doors.ResourceExternal("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"))
				rel="stylesheet">
		</head>
		<body>
			<main class="container">
				<h1>Hello Doors!</h1>
			</main>
		</body>
	</html>
}

doors.ResourceExternal(...) is an attribute modifier. It updates the tag's attributes and lets Doors track the external URL as part of resource metadata.

Attribute modifiers can inspect and change tag attributes. You can build your own by implementing the gox.Modify interface.

Reloading

An easy way to get live reloading for Go files is wgo:

go install github.com/bokwoon95/wgo@latest

Then run it from the project directory:

wgo -file=.go go run .

For the usual editor workflow, the GoX language server already regenerates .x.go files whenever you save .gox, so you usually do not need to watch .gox files separately. In real projects, you may also want to watch files such as .css, .js, or .ts.


Next: Integrations