Skip to content
Trilha
Chapters

Learn

Examples

Complete apps in examples/, from basic to complex, and what each one teaches.

The examples are real apps, with integration tests that run in the repository's make test. Each has a short README.md. Run any of them with trilha dev inside the folder (or go run ../../cmd/trilha dev from the clone).

LevelFolderWhat it teaches
Basicexamples/blogevery file convention, nested layouts, route groups, JSON API, middleware, signed session, tmpl
Mediumexamples/cadastroa form with rules: conditional fields, server-side validation with per-field errors, dependent select, disappearing toast, responsive layout
Complexexamples/orcamentotree-shaped domain (chart of accounts), aggregation, drill-down through a dynamic route, nested and recursive components, dialog with a form, period filter, CSV
SSOexamples/ssoOpenID Connect login with Entra ID or Keycloak, protected area, required role, federated logout
AIexamples/assistentestreaming chat, agent with tools, handoff, MCP server

Medium: sign-up (cadastro)#

The form model is a struct with form tags; c.Bind(&in) fills it (nested structs are flattened, with an optional prefix):

type Cliente struct {
	Tipo     string   `form:"tipo"`      // type
	Nome     string   `form:"nome"`      // name
	Endereco Endereco            // cep, rua, uf, cidade (address)
	Cobranca Endereco `form:"cob_"` // cob_cep, cob_rua... (billing address)
	Novidades bool    `form:"novidades"` // newsletter
}

Validation is a pure function returning trilha.FieldErrors, and POST decides:

func POST(c *trilha.Ctx) error {
	var in clientes.Cliente
	if err := c.Bind(&in); err != nil {
		return err                       // invalid conversion → 422
	}
	clientes.Normalizar(&in)             // drops what the type does not use
	if errs := clientes.Validar(in); errs.Any() {
		return c.Render(422, tela(c, in, errs)) // same page, with layouts
	}
	clientes.Salvar(in)
	return c.Redirect("/?ok=1")          // PRG + disappearing toast
}

On screen, each field reads its value and its error from the same place:

ui.Field("cnpj", "CNPJ",
	ui.Input(h.ID("cnpj"), h.Name("cnpj"), h.Value(in.CNPJ), ui.InvalidIf(errs, "cnpj")),
	ui.Errors(errs, "cnpj"))

Conditional groups use ui.ShowWhen("tipo", "pj"): hidden ones are disabled and do not travel in the POST; and since anyone can craft the POST by hand, Normalizar clears what the type does not use before validating. The city <select> is filled by GET /api/cidades?uf= with 20 lines of app.js; on a 422 the server already returns the cities of the chosen state, so the page comes back complete without JavaScript.

Complex: budget (orcamento)#

The chart of accounts is a tree (Conta{Codigo, Nome, Filhos}); budgeted and actual values of a summary account are the sum of its children, computed on read. The components mirror the tree: Linha renders the account and calls itself for the children, ui.Depth(n) indents:

func Linha(c *plano.Conta, mes string, nivel, max int) h.Node {
	row := h.Tr(ui.Depth(nivel), h.Td(h.A(h.Href("/contas/"+c.Codigo), h.Text(c.Nome))), ...)
	if nivel >= max || c.Analitica() {
		return row
	}
	return h.Fragment(row, h.Map(c.Filhos, func(f *plano.Conta) h.Node {
		return Linha(f, mes, nivel+1, max)
	}))
}

The drill-down is the route app/contas/codigo_/page.go: breadcrumb with Caminho(), children (same Tabela) or entries (leaf account). The entry form is a single one (FormLancamento), used inside ui.Dialog in the overview and in the drill-down, and on its own at /lancamentos; POST validates with c.Bind + plano.Validar and, on a 422, app.js reopens the dialog because it found .ui-field-error inside it. voltar (a hidden field) says where to redirect on success. The export lives in app/api/relatorio.csv/route.go, a folder with a dot in its name.

SSO: Entra ID and Keycloak#

examples/sso is the whole login flow in three routes of two lines each. The auth package handles PKCE, state, nonce, the code exchange and id_token validation; the app only forwards:

// app/entrar/route.go  ("entrar" = sign in)
var Kind = trilha.KindPage
func GET(c *trilha.Ctx) error { return sso.Start(c) }

Protecting a subtree is a middleware.go, like any other:

// app/painel/middleware.go  ("painel" = dashboard)
func Middleware(c *trilha.Ctx, next trilha.Next) error { return sso.Require(c, next) }

// app/painel/relatorio/middleware.go — role, not just session
func Middleware(c *trilha.Ctx, next trilha.Next) error { return sso.RequireAdmin(c, next) }

Below the middleware, the page reads sso.User(c) without checking anything. An anonymous browser is sent to /entrar?next=…; a call to /api gets 401 as JSON, because redirecting an HTTP client to a form only produces a confusing parsing error.

No secret lives in the code: the provider comes from environment variables, and without them the app still starts and says what is missing.

What became framework#

Writing the two examples exposed repetition that is now API: c.Bind, trilha.FieldErrors, c.Render (a page with layouts from a POST), ui.Errors, ui.InvalidIf, ui.SelectOptions, ui.Checked. That is the constitution's criterion: an example that needs repetitive code points to a gap in Trilha, not in the example.

Challenge#

In the budget app, add a "Year" column to the drill-down that sums the account's twelve months.

Show solution
func Ano(c *plano.Conta, ano string) (orcado, real int64) {
	for m := 1; m <= 12; m++ {
		mes := fmt.Sprintf("%s-%02d", ano, m)
		orcado += plano.Orcado(c, mes)
		real += plano.Realizado(c, mes)
	}
	return
}

Call it from Linha and add the two cells; since aggregation is recursive, the column already works for summary accounts.