trilha

package module
v0.23.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: MIT Imports: 42 Imported by: 0

README

Trilha

🇺🇸 English · 🇧🇷 Português

ci Go Reference

Web framework for Go with file-based routing. Nested layouts, API routes, per-folder middleware, a dev server with live reload and a single production binary. Zero dependencies outside the standard library. The folder layout follows the model popularized by Next.js*, translated into Go conventions.

app/
├── layout.go            → root <html> (wraps everything)
├── page.go              → GET /
├── middleware.go        → runs on every request
├── not_found.go         → 404 page
├── error.go             → 500 page
├── setup.go             → startup (database, cache...)
├── blog/
│   ├── layout.go        → wraps /blog/**
│   ├── page.go          → GET /blog
│   ├── new/page.go      → GET /blog/new  (+ the form's POST)
│   └── slug_/page.go    → GET /blog/{slug}
├── docs/path__/page.go  → GET /docs/{path...}
├── marketing-/          → group: not part of the URL
│   ├── layout.go        → wraps /pricing and /about
│   ├── pricing/page.go  → GET /pricing
│   └── about/page.go    → GET /about
├── admin/
│   ├── middleware.go    → only for /admin/**
│   └── page.go
└── api/posts/route.go   → GET/POST /api/posts
public/style.css         → served at /style.css

Documentation

https://emersonjoe.github.io/trilha — the "Learn" track (from trilha new to deploy, with challenges) and a per-package "Reference". The site is a Trilha app, exported with trilha export, in English (/) and Portuguese (/pt).

Getting started

go install github.com/emersonjoe/trilha/cmd/trilha@latest
trilha new my-app && cd my-app
trilha dev              # → http://localhost:3000, reloads on save
trilha build            # → bin/my-app, with public/ embedded

Not published yet? Use a local copy: trilha new my-app --trilha-dir ../trilha. The CLI speaks English by default and Portuguese with TRILHA_LANG=pt (or a pt_* LANG); trilha new --lang pt generates the project texts in Portuguese.

Conventions

File Exports Signature
page.go Page func(c *trilha.Ctx) (h.Node, error)
page.go POST, PUT, PATCH, DELETE (optional) func(c *trilha.Ctx) error — forms, with CSRF
route.go GET, POST, PUT, PATCH, DELETE func(c *trilha.Ctx) error — API
layout.go Layout func(c *trilha.Ctx, children h.Node) (h.Node, error)
middleware.go Middleware func(c *trilha.Ctx, next trilha.Next) error
not_found.go (root) NotFound func(c *trilha.Ctx) (h.Node, error)
error.go (root) Error func(c *trilha.Ctx, err error) (h.Node, error)
setup.go (root) Setup func(a *trilha.App) error
setup.go (optional) Config func(cfg *trilha.Config), before trilha.New

Folders become segments: blog/blog; slug_/{slug}; path__/{path...} (catch-all, must be a leaf); marketing-route group: not part of the URL, but its layout.go/middleware.go apply to everything below (the equivalent of Next.js's (marketing)). [slug] and (group) are not valid in a Go import path, hence the _ and - suffixes. Folders starting with _ or . are ignored. Two folders producing the same URL are a generation error (E_DUPLICATE_ROUTE).

Execution order for GET /admin: middleware(app) → middleware(app/admin) → Page → layout(app/admin)? → layout(app).

A page

package about

import (
	"github.com/emersonjoe/trilha"
	"github.com/emersonjoe/trilha/h"
)

func Page(c *trilha.Ctx) (h.Node, error) {
	c.SetTitle("About")
	return h.Main(
		h.H1(h.Text("About")),
		h.P(h.Textf("You are request %s.", c.RequestID())),
	), nil
}

h is a typed HTML DSL: elements and attributes are functions, text is escaped by default and h.Raw is the only unescaped door. h.If, h.Map and h.Fragment cover control flow.

Prefer html/template? The tmpl package plugs templates into the same pipeline (layouts, title, the contextual escaping of html/template itself):

//go:embed report.html
var files embed.FS
var t = tmpl.Must(files, "*.html") // fails at startup, never during a request

func Page(c *trilha.Ctx) (h.Node, error) {
	return tmpl.Node(t, "report", data), nil
}

Forms and APIs

// app/blog/new/page.go
func Page(c *trilha.Ctx) (h.Node, error) {
	return h.Form(h.Method("post"), trilha.CSRFInput(c),
		h.Input(h.Name("title")), h.Button(h.Text("Publish"))), nil
}

func POST(c *trilha.Ctx) error {
	p := posts.Create(c.Form("title"), c.Form("body"))
	return c.Redirect("/blog/" + p.Slug) // 303: POST → redirect → GET
}

// app/api/posts/route.go
func GET(c *trilha.Ctx) error { return c.JSON(200, posts.All()) }
func POST(c *trilha.Ctx) error {
	var in struct{ Title string `json:"title"` }
	if err := c.BindJSON(&in); err != nil { return err } // 400 / 413
	return c.JSON(201, posts.Create(in.Title, ""))
}

Errors are values: trilha.ErrNotFound → 404 (HTML or JSON depending on the route), trilha.Redirect(url) → 303, trilha.Errorf(422, "...") → status with a message, any other error → 500 with a stack only in dev. Methods you do not export answer 405 with Allow.

Middleware

// app/admin/middleware.go
func Middleware(c *trilha.Ctx, next trilha.Next) error {
	if ck, err := c.Cookie("session"); err != nil || ck.Value != "ok" {
		return trilha.RedirectCode("/login", 302)
	}
	c.Set("user", "admin") // pages read it with c.Get("user")
	return next()
}

UI

New projects ship with the ui kit: typed components (ui.Button, ui.Card, ui.Field, ui.Tabs, ui.Dialog...) over a prefixed CSS and 200 lines of JS, both copied to public/ and yours to edit. The theme uses the same variables as shadcn/ui (MIT): paste a ready-made theme into public/ui.theme.css and nothing in Go changes. trilha ui updates the kit without touching your theme.

ui.Card(
	ui.CardHeader(ui.CardTitle("New post")),
	ui.CardContent(h.Form(h.Method("post"), trilha.CSRFInput(c),
		ui.Field("title", "Title", ui.Input(h.ID("title"), h.Name("title"), h.Required())),
		ui.Submit(h.Text("Publish")))),
)

Examples

Level Folder Teaches
Basic examples/blog conventions, layouts, API, middleware, session
Medium examples/cadastro a form with rules: conditional fields, per-field validation (c.Bind, trilha.FieldErrors, c.Render), dependent select, disappearing toast
Complex examples/orcamento tree-shaped chart of accounts, drill-down, recursive components, dialog, CSV
SSO examples/sso OpenID Connect login (Entra ID/Keycloak), protected area, required role
AI examples/assistente streaming chat, agent with tools, MCP

The example apps are written in Portuguese (identifiers and UI texts); the code is the same Trilha documented here in English.

AI and agents

ai speaks OpenAI's chat protocol (works with OpenAI, Groq, Mistral, OpenRouter, Ollama, LM Studio, vLLM...), with typed tools, agents, handoffs and streaming; ai/mcp uses and exposes tools through the Model Context Protocol. All without external dependencies.

weather := ai.NewTool("weather", "Temperature in a city.",
    ai.Schema(`{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}`),
    ai.Typed(func(ctx context.Context, in struct{ City string }) (string, error) {
        return fetchTemperature(ctx, in.City)
    }))
agent := &ai.Agent{Name: "Assistant", Instructions: "Answer briefly.", Tools: []*ai.Tool{weather}}
res, err := ai.Run(ctx, ai.NewFromEnv(), agent, "Is it cold in Curitiba?")

See examples/assistente (streaming chat with c.Stream(), handoff to a translator and an MCP server at /mcp) and the chapter AI and agents.

How it works

trilha gen scans app/ with go/ast and writes trilha_gen.go (committed): a package main that imports each route package and calls a.Register(...) with types checked by the compiler. No reflect, no runtime magic; go build . works without the CLI. The router is Go 1.22+'s http.ServeMux.

trilha dev listens on :3000, compiles the app on an internal port, proxies to it and injects a live-reload script (SSE). On save: regenerate, recompile, swap the process and notify the browser — about 1 s in the example. Changes only in public/ do not recompile: the browser reloads in tens of milliseconds. A compile error becomes a page with the output of go build that goes away on its own when you fix it.

Secure by default: HTML escaping, nosniff/X-Frame-Options/Referrer-Policy, body limit (1 MiB), CSRF by double-submit cookie on forms, static files without path traversal, slog logs without body or cookies.

Health and observability

Every app already answers /_trilha/health/live (the process is up) and /_trilha/health/ready (the dependencies you registered with a.Check answer, with a deadline and a cache). For anyone not authorized the response is only {"status":"fail"}: dependency names and error messages stay in the log, not on the wire.

a.Check("db", func(ctx context.Context) error { return db.PingContext(ctx) })

The metrics endpoint is opt-in (TRILHA_METRICS or Observability.Metrics) and requires a token or a trusted network. It speaks the Prometheus text format, with requests, latency, in-flight requests, security events, panics and the Go runtime — plus your own (a.Metrics().Counter(...)). The route label is always the registered pattern, never the concrete path. Details in Health and observability.

Corporate login

The auth package does OpenID Connect (Entra ID, Keycloak or any conforming provider) with the standard library: PKCE, state, nonce, id_token validation with JWKS and key rotation, session in a signed cookie and roles read from wherever each provider keeps them.

// app/login/route.go — the whole flow is three two-line routes
func GET(c *trilha.Ctx) error { return sso.Start(c) }

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

An anonymous browser goes to the login; an API call gets 401. Someone logged in without the required role gets 403. Runnable example in examples/sso, details in Authentication.

Performance

make bench measures Trilha's cost over net/http + html/template (separate bench/ module). Summary on the reference machine: h renders the example page ~34 % faster than html/template; the fixed cost per request (id, CSP nonce, headers, structured logging) is ~3 µs, and the metrics instrumentation, when on, adds no allocation. Methodology, numbers and a comparison of approach with other frameworks in Performance and comparison.

Where it is going

ROADMAP.md (in Portuguese) answers an external review item by item: what already exists, what is planned (issues labeled roadmap, grouped by phase) and what we decided not to do, with the reason. The biggest acknowledged gap is interactivity without turning into an SPA; OIDC authentication (spec 016) is already in.

Out of scope (for now)

Client components/hydration and parallel routes. Client-side interactivity lives in public/*.js (or htmx).

License

MIT (LICENSE). The spec-kit files in .specify/ and .claude/skills are MIT by GitHub, Inc.; see THIRD_PARTY_NOTICES.md.

* Next.js is a trademark of Vercel, Inc. Trilha is an independent project, not affiliated, and contains no Next.js code.

Contributions are welcome: see CONTRIBUTING.md, the code of conduct, the security policy and the governance (Portuguese translations in docs/pt-BR/). Behavior changes follow the spec-kit flow in specs/.

Development

make test        # gofmt + vet + go test ./... (includes the CLI e2e and the examples)
make dev-example # trilha dev in examples/blog
make reload      # measures the edit→see cycle

Spec-kit driven project: see specs/ (one folder per spec, from 001 core to 015 i18n) and .specify/memory/constitution.md. Specs and the constitution are written in Brazilian Portuguese; everything public (site, README, CLI) is English by default with a Portuguese translation.

Documentation

Overview

Package trilha is a file-based web framework for Go: pages, layouts, API routes and middleware are discovered from the app/ directory tree by the trilha CLI, which generates a typed registration file; this package is the runtime those generated files call into.

Index

Constants

View Source
const (
	// StatusPass means every check passed.
	StatusPass = "pass"
	// StatusFail means at least one check failed.
	StatusFail = "fail"
)
View Source
const CSRFCookie = "trilha_csrf"

CSRFCookie is the name of the double-submit cookie.

View Source
const CSRFField = "_csrf"

CSRFField is the hidden form field name.

View Source
const CSRFHeader = "X-CSRF-Token"

CSRFHeader is the header accepted instead of the form field.

View Source
const MinSecretLen = 32

MinSecretLen is the minimum accepted secret size in bytes.

View Source
const NoTimeout time.Duration = -1

NoTimeout disables a Timeouts field (becomes 0 in http.Server).

View Source
const Off = "off"

Off disables a security header when assigned to its field.

View Source
const ProblemMediaType = "application/problem+json"

ProblemMediaType is what an API error is sent as (RFC 9457).

Variables

View Source
var BindInvalid = "invalid value"

BindInvalid is the FieldErrors message Bind records when a value cannot be converted to the field's type. Change it to localise.

View Source
var ErrNoSecret = errors.New("trilha: TRILHA_SECRET not set; signed cookies unavailable")

ErrNoSecret is returned by SetSigned when no secret is configured.

View Source
var ErrNotFound = errors.New("trilha: not found")

ErrNotFound makes the framework respond with 404 using the app's not-found page (HTML routes) or a JSON error (API routes).

View Source
var ErrRateLimited = &HTTPError{Code: http.StatusTooManyRequests, Message: "too many requests"}

ErrRateLimited is the 429 error; handlers may return it themselves.

View Source
var ProblemType func(status int) string

ProblemType gives the "type" URI of a status, for an app that documents its errors ("https://example.com/probs/404"). nil keeps "about:blank".

View Source
var ValidationMessages = map[string]string{
	"required": "required",
	"min":      "must be {param} or more",
	"max":      "must be {param} or less",
	"minlen":   "must have at least {param} characters",
	"maxlen":   "must have at most {param} characters",
	"minitems": "choose at least {param}",
	"maxitems": "choose at most {param}",
	"mindate":  "must not be before {param}",
	"maxdate":  "must not be after {param}",
	"len":      "must have exactly {param} characters",
	"lenitems": "choose exactly {param}",
	"email":    "invalid e-mail",
	"url":      "invalid URL",
	"oneof":    "invalid option",
	"eqfield":  "does not match",
	"filemax":  "file must be at most {param}",
	"filetype": "file type not allowed",
}

ValidationMessages is the message of each rule, in English. Change an entry, swap the map, or call UseValidationPTBR; "{param}" is replaced by what came after the "=" in the tag.

Functions

func AddRule added in v0.18.0

func AddRule(name string, fn func(Field) bool)

AddRule registers a rule for the validate tag, usually in Setup. It panics on a name that already exists: two meanings for one word in a tag is a bug nobody would find later.

trilha.AddRule("cep", func(f trilha.Field) bool { return cepValido(f.Text) })
trilha.ValidationMessages["cep"] = "CEP inválido"

func CSRFInput

func CSRFInput(c *Ctx) h.Node

CSRFInput renders the hidden input for forms: h.Form(..., trilha.CSRFInput(c), ...).

func CompileErrorPage

func CompileErrorPage(output string) string

compileErrorPage is used by the CLI dev server; exported for reuse.

func Errorf

func Errorf(code int, format string, a ...any) error

Errorf builds an HTTPError with a formatted client-visible message.

func Fatal

func Fatal(err error)

Fatal logs a fatal error and exits, ignoring the normal server-closed error.

func NonceAttr added in v0.2.0

func NonceAttr(c *Ctx) h.Node

NonceAttr renders the nonce attribute for an inline <script>: h.Script(trilha.NonceAttr(c), h.Raw(js)).

func PublicFS

func PublicFS(embedded fs.FS, dir string) fs.FS

PublicFS returns the static file system for the public directory: the embedded copy in prod, the on-disk directory in dev (so edits show up without a rebuild).

func Redirect

func Redirect(url string) error

Redirect returns a 303 See Other redirect error (POST → redirect → GET).

func RedirectCode

func RedirectCode(url string, code int) error

RedirectCode returns a redirect error with a custom 3xx status.

func Run added in v0.2.0

func Run(a *App)

Run serves the app, or exports it when TRILHA_EXPORT names a directory. The generated main calls this.

func UseValidationPTBR added in v0.18.0

func UseValidationPTBR()

UseValidationPTBR switches the validation messages, BindInvalid included, to Brazilian Portuguese. Call it in Setup. These messages are read by the person filling the form, not by the developer, which is why they are the one piece of the runtime that comes translated.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is a configured Trilha application.

func New

func New(cfg Config) *App

New creates an App. Zero values in cfg receive defaults.

func (*App) AddExportPath added in v0.2.0

func (a *App) AddExportPath(paths ...string)

AddExportPath registers extra paths to render on Export, typically pages under dynamic routes (e.g. "/blog/ola"). Call it from Setup.

func (*App) Asset added in v0.8.0

func (a *App) Asset(p string) string

Asset returns the URL of a file in Config.Public carrying a version derived from its content:

h.Link(h.Rel("stylesheet"), h.Href(c.Asset("/site.css")))
// → /site.css?v=8f3a1c92

The point is the address changing when the content changes: a CDN or browser holding the old file is asked for a URL it has never seen, so a deploy cannot leave someone with new HTML and old CSS. It also makes a long StaticCacheControl safe.

BasePath is applied, so Asset replaces Base()+path. An unknown file returns the path unchanged (with a warning in the log) rather than breaking the page.

func (*App) BasePath added in v0.2.0

func (a *App) BasePath() string

BasePath returns the URL prefix the app is served under ("" or "/docs").

func (*App) Check added in v0.6.0

func (a *App) Check(name string, fn func(context.Context) error)

Check registers a readiness check: a dependency the app needs in order to serve (database, cache, queue). It runs on /_trilha/health and /_trilha/health/ready, with the configured timeout, and its result is cached for Observability.CacheFor. Liveness never runs checks, so a dependency blinking does not restart the process.

func (*App) Config added in v0.2.0

func (a *App) Config() *Config

Config returns the live configuration for adjustment in Setup. Every field may be changed there: per-request fields (Security, Public, MaxBodyBytes, CSRFForAPI, BasePath, OnSecurityEvent, Static*) are read on each request; derived fields (Logger, Secret/PreviousSecret, RateLimit, TrustedProxies) are reapplied when serving starts (ListenAndServe, Handler, Export); Addr and Timeouts are read by ListenAndServe. To build the Config before New, export func Config(cfg *trilha.Config) in app/setup.go.

func (*App) Env

func (a *App) Env() Env

Env returns the runtime environment.

func (*App) Export added in v0.2.0

func (a *App) Export(dir string) error

Export renders the app as a static site into dir: one index.html per path, 404.html from the not-found page, and a copy of public/. The directory is emptied first, but only if Export created it before.

func (*App) ExportPaths added in v0.2.0

func (a *App) ExportPaths() []string

ExportPaths lists what Export will render: static page routes plus the paths added with AddExportPath, sorted and deduplicated.

func (*App) Handler

func (a *App) Handler() http.Handler

Handler returns the root http.Handler (useful for tests and embedding). Like ListenAndServe, it reapplies Config changes made in Setup.

func (*App) HealthReport added in v0.6.0

func (a *App) HealthReport(ctx context.Context) HealthReport

HealthReport runs the readiness checks and returns the full report, including error messages. Use it from your own code (a status page, a startup gate); the HTTP endpoint decides what to reveal.

func (*App) ListenAndServe

func (a *App) ListenAndServe() error

ListenAndServe serves until SIGINT/SIGTERM, then shuts down gracefully.

func (*App) Logger

func (a *App) Logger() *slog.Logger

Logger returns the app logger.

func (*App) Metrics added in v0.6.0

func (a *App) Metrics() *Metrics

Metrics returns the process metric registry. It always exists; set Config.Observability.Metrics to expose it over HTTP.

func (*App) OnShutdown added in v0.3.0

func (a *App) OnShutdown(fn func(*App) error)

OnShutdown registers fn to run after the server stopped accepting requests (close pools, flush logs). Hooks run in reverse registration order; setup.go may export func Shutdown(a *trilha.App) error, which the generated main registers for you.

func (*App) Register

func (a *App) Register(r Route)

Register adds a route. It is normally called only by trilha_gen.go.

func (*App) Routes

func (a *App) Routes() map[string][]string

Routes lists registered patterns (sorted) with their methods.

func (*App) Security added in v0.2.0

func (a *App) Security() *Security

Security returns the security settings for adjustment in Setup.

func (*App) SetErrorPage

func (a *App) SetErrorPage(e ErrorPageFunc)

SetErrorPage sets the page rendered on 500 (app/error.go).

func (*App) SetNotFound

func (a *App) SetNotFound(p PageFunc)

SetNotFound sets the page rendered on 404 (app/not_found.go).

func (*App) SetRootLayout

func (a *App) SetRootLayout(l LayoutFunc)

SetRootLayout sets the layout used by the not-found and error pages.

func (*App) Values

func (a *App) Values() map[string]any

Values is a process-wide bag filled by Setup (database pools, caches...). Prefer package-level variables in your own packages; this exists for glue.

type CORS added in v0.20.0

type CORS struct {
	// Origins may call this app. Empty disables CORS; "*" (alone) allows any.
	Origins []string
	// Methods the other origin may use (default GET, HEAD, POST, PUT, PATCH,
	// DELETE).
	Methods []string
	// Headers the other origin may send (default Content-Type, Authorization,
	// X-CSRF-Token, Trilha-Fragment).
	Headers []string
	// Expose lists the response headers the other origin's script may read.
	Expose []string
	// Credentials allows cookies and Authorization. Incompatible with "*".
	Credentials bool
	// MaxAge is how long the browser may cache the preflight (zero omits the
	// header, and the browser uses its own short default).
	MaxAge time.Duration
}

CORS is the cross-origin policy of the app, in Config.CORS. The zero value is off: no header is added and OPTIONS keeps reaching the router.

CORS: trilha.CORS{
	Origins:     []string{"https://app.example.com"},
	Credentials: true,
	MaxAge:      10 * time.Minute,
}

Origins are exact ("scheme://host[:port]", no path, no trailing slash), or the single entry "*" for a public API. An unsafe or malformed policy panics in New: a CORS mistake that only shows up on the first request from outside is a mistake nobody sees in development.

type CheckResult added in v0.6.0

type CheckResult struct {
	Name       string  `json:"name"`
	Status     string  `json:"status"`
	DurationMS float64 `json:"duration_ms"`
	Error      string  `json:"error,omitempty"`
}

CheckResult is the outcome of one readiness check. Error is only ever sent to an authorized client; anonymous callers see the status alone.

type Config

type Config struct {
	// Addr is the listen address (default ":3000").
	Addr string
	// Env selects dev (stack traces, live reload, no static cache) or prod.
	Env Env
	// MaxBodyBytes limits request bodies (default 1 MiB).
	MaxBodyBytes int64
	// Logger receives structured request logs (default slog.Default()).
	Logger *slog.Logger
	// Public serves static files at the root. nil disables static files.
	Public fs.FS
	// Mounts serve static trees at URL prefixes, for the app whose disk tree
	// is not shaped like its URL tree. They match before Public, longest
	// prefix first, and fall through to it when the file is not there.
	Mounts map[string]fs.FS
	// CSRFForAPI also enforces CSRF tokens on route.go handlers.
	CSRFForAPI bool
	// BasePath is the URL prefix the app is served under (e.g. "/docs" on
	// GitHub Pages). Read it with Ctx.Base when building links.
	BasePath string
	// Security tunes the hardening headers (zero value = defaults).
	Security Security
	// TrustedProxies lists CIDRs whose X-Forwarded-For/Proto are honoured.
	TrustedProxies []string
	// RateLimit enables a global per-client limit (zero = off).
	RateLimit RateLimit
	// Secret signs cookies (TRILHA_SECRET); PreviousSecret still verifies.
	Secret, PreviousSecret []byte
	// Timeouts protect the server from slow clients.
	Timeouts Timeouts
	// StaticCacheControl replaces the production Cache-Control of files in
	// Public (default "public, max-age=3600"; dev always sends no-cache).
	StaticCacheControl string
	// StaticHeaders runs for every file served from Public, after the
	// defaults, and may set any header (immutable for hashed assets, CORP...).
	StaticHeaders func(name string, hdr http.Header)
	// LogRequest decides, with the response already written, whether a
	// request enters the access log. nil logs every one of them.
	LogRequest func(c *Ctx, status int, dur time.Duration) bool
	// OnSecurityEvent is called for blocked requests (CSRF, 401/403, 413, 429, panic).
	OnSecurityEvent func(SecurityEvent)
	// DevReload controls the live-reload script injected in Dev pages; Off
	// disables it (snapshot tests, HTML diffs). TRILHA_DEV_RELOAD=off does the same.
	DevReload string
	// Observability configures the health probes, the metrics endpoint and
	// what each of them reveals.
	Observability Observability
	// CORS allows other origins to call this app (zero value = off).
	CORS CORS
}

Config configures an App.

func ConfigFromEnv

func ConfigFromEnv() Config

ConfigFromEnv builds a Config from ADDR/PORT and TRILHA_ENV.

type Counter added in v0.6.0

type Counter struct {
	// contains filtered or unexported fields
}

Counter only ever grows: requests served, errors, events.

func (*Counter) Add added in v0.6.0

func (c *Counter) Add(v float64)

Add increases the counter; negative values panic (a counter never falls).

func (*Counter) Inc added in v0.6.0

func (c *Counter) Inc()

Inc adds one.

func (*Counter) With added in v0.6.0

func (c *Counter) With(values ...string) *Counter

With binds label values, in the order they were declared.

type Ctx

type Ctx struct {
	// contains filtered or unexported fields
}

Ctx wraps one request/response pair. It is created per request and is not safe for use from other goroutines after the handler returns.

func (*Ctx) Accepts added in v0.21.0

func (c *Ctx) Accepts(offers ...string) string

Accepts returns the offer the client prefers, according to the Accept header and its q values, or "" when it accepts none of them. An absent or */* Accept is not a preference: the first offer wins, so put your default first.

switch c.Accepts("text/html", "application/json") {
case "application/json": return c.JSON(200, v)
default:                 return c.Render(200, page(v))
}

func (*Ctx) AllowBody added in v0.15.0

func (c *Ctx) AllowBody(n int64)

AllowBody raises (or lowers) the body limit for this request only, leaving Config.MaxBodyBytes in place for every other route. Call it before reading the body:

func POST(c *trilha.Ctx) error {
	c.AllowBody(8 << 20)
	…
}

Called after the body started being read, the new limit counts from there. Going over it still answers 413.

func (*Ctx) App

func (c *Ctx) App() *App

App returns the application.

func (*Ctx) Asset added in v0.8.0

func (c *Ctx) Asset(p string) string

Asset is App.Asset for a request; see it for details.

func (*Ctx) Base added in v0.2.0

func (c *Ctx) Base() string

Base returns the app's base path for building links: c.Base()+"/aprender".

func (*Ctx) Bind added in v0.5.0

func (c *Ctx) Bind(v any) error

Bind fills a struct from the request: JSON when the Content-Type is application/json (see BindJSON), otherwise form fields and query string. Fields are matched by the `form:"name"` tag (or the field name). Supported types: string, []string, bool (checkbox: on/true/1), int, int64, float64, time.Time (2006-01-02 or 2006-01-02T15:04) and pointers to them (nil when absent). A nested struct is flattened: its fields are read with the struct's tag as prefix (`Cobranca Endereco `+"`"+`form:"cob_"`+"`"+` reads cob_cep...), or with no prefix when it has no tag. Values that fail to convert are reported as FieldErrors, after every field was tried, so all messages reach the user at once.

var in struct {
	Nome  string  `form:"nome"`
	Idade int     `form:"idade"`
	Ativo bool    `form:"ativo"`
}
if err := c.Bind(&in); err != nil { return err }

func (*Ctx) BindJSON

func (c *Ctx) BindJSON(v any) error

BindJSON decodes the request body into v. Returns an HTTPError 400 on malformed JSON and 413 when the body exceeds the limit.

func (*Ctx) CSRFToken

func (c *Ctx) CSRFToken() string

CSRFToken returns the request's CSRF token, creating the cookie on first use. Put it in forms with CSRFInput or send it in the X-CSRF-Token header.

func (*Ctx) CacheControl added in v0.17.0

func (c *Ctx) CacheControl(v string)

CacheControl sets the response policy. A page carrying data that belongs to one person needs private in it: without that, a shared cache is allowed to hand one visitor's page to the next one.

func (*Ctx) ClearCookie added in v0.2.0

func (c *Ctx) ClearCookie(name string)

ClearCookie expires a cookie.

func (*Ctx) ClientIP added in v0.2.0

func (c *Ctx) ClientIP() string

ClientIP returns the client address: RemoteAddr, or the right-most untrusted entry of X-Forwarded-For when the peer is a trusted proxy.

func (*Ctx) Context

func (c *Ctx) Context() context.Context

Context returns the request context.

func (*Ctx) Cookie

func (c *Ctx) Cookie(name string) (*http.Cookie, error)

Cookie returns a request cookie.

func (*Ctx) ETag added in v0.17.0

func (c *Ctx) ETag(tag string) bool

ETag declares the version of what this route is about to answer and reports whether the browser already has it:

func Page(c *trilha.Ctx) (h.Node, error) {
	p, ok := posts.Get(c.Param("slug"))
	if !ok {
		return nil, trilha.ErrNotFound
	}
	if c.ETag(p.Rev) {
		return nil, nil // 304: nothing else to answer
	}
	return pagina(p), nil
}

The tag is whatever identifies the version of the data — a revision, an updated_at, a hash of the row. It is not the hash of the response: the CSP nonce changes on every request, so a body hash would never match twice.

Quotes are added when missing, and a tag already written as "abc" or W/"abc" is sent as it is. Only GET and HEAD answer 304; on any other method the header is written and the return is false.

func (*Ctx) Env

func (c *Ctx) Env() Env

Env returns the runtime environment.

func (*Ctx) File added in v0.19.0

func (c *Ctx) File(field string, rules FileRules) (*Upload, error)

File reads one file from a multipart form and answers with it only if it passes the rules: size, media type detected in the content (never the extension, never what the client announced) and a name that cannot walk out of a directory.

up, err := c.File("file", trilha.FileRules{
	MaxSize: 2 << 20,
	Accept:  []string{"image/png", "image/jpeg"},
})

A rule that fails is a FieldErrors under the field's name, the same answer Bind gives, so the form shows the message where the person is looking. Any other error (a broken body, a full disk) comes back as itself.

func (*Ctx) Form

func (c *Ctx) Form(name string) string

Form returns a form field (POST body or query string). Returns "" if the form cannot be parsed; use FormErr to distinguish.

func (*Ctx) FormErr

func (c *Ctx) FormErr() error

FormErr returns the error from parsing the form, if any (413 or 400).

func (*Ctx) Fragment added in v0.10.0

func (c *Ctx) Fragment() string

Fragment returns the part of the page this request asked for, or "" on a normal navigation. It is the whole protocol: the same route serves the page and the piece, and decides what to return.

func Page(c *trilha.Ctx) (h.Node, error) {
	lista := listaDe(c)
	if c.Fragment() == "lista" {
		return lista, nil // sem layouts
	}
	return h.Div(busca(), lista), nil
}

A fragment response carries no layout, no document envelope and no dev script; every HTML response gets Vary: Trilha-Fragment so a cache never serves one in place of the other.

func (*Ctx) Get

func (c *Ctx) Get(key string) any

Get reads a per-request value; nil when absent.

func (*Ctx) HTML

func (c *Ctx) HTML(code int, n h.Node) error

HTML renders a node as the whole response, without layouts.

func (*Ctx) Header

func (c *Ctx) Header(k, v string)

Header sets a response header.

func (*Ctx) Hijack added in v0.15.0

func (c *Ctx) Hijack() (net.Conn, *bufio.ReadWriter, error)

Hijack takes the connection over, so a handler can speak another protocol on it — WebSocket, above all. Trilha does not implement WebSocket: it is a transport, it touches nothing else in the framework, and a library in the app's own go.mod does it better than 400 lines here would. What Trilha owes is the way out, and this is it.

The read and write deadlines are cleared before the connection is returned (an idle socket is a WebSocket's normal state, not a stuck request), and the framework writes nothing else to the response: the log records 101 and the connection is the handler's until it closes it. Middleware, CSRF and authentication have already run.

func (*Ctx) Island added in v0.13.0

func (c *Ctx) Island(src string, props any, children ...h.Node) h.Node

Island renders an interactive region of a page that stays static: the server sends the fallback HTML, and a module in public/ takes over on the client. There is no global hydration and no bundler — src is a file in public/, addressed through Asset so it gets the content hash:

c.Island("/editor.js", map[string]any{"wpm": 200},
	h.Class("editor"), ui.Textarea(h.Name("corpo")))

The module's default export is the mount function, called once with the element and the props already parsed:

export default function (el, props) { ... }

props is anything encoding/json can serialize, or nil. What the server sends is data, never markup: it is escaped as an attribute and read back with JSON.parse. The children are the fallback, so the page works with the script blocked, failing to load, or still on its way.

func (*Ctx) JSON

func (c *Ctx) JSON(code int, v any) error

JSON writes a JSON response.

func (*Ctx) LastModified added in v0.17.0

func (c *Ctx) LastModified(t time.Time) bool

LastModified declares when what this route answers last changed, and reports whether the browser already has that version. Same contract as ETag, with two differences: the comparison happens at the second, which is all an HTTP date carries, and a request that brought If-None-Match is left to it — a strong validator is not overruled by a weak one (RFC 9110 §13.1.3).

func (*Ctx) Log added in v0.6.0

func (c *Ctx) Log() *slog.Logger

Log returns a logger already carrying request_id and, when present, trace_id, so every line of one request can be found together (NIST SP 800-53 AU-3).

func (*Ctx) NoReadDeadline added in v0.15.0

func (c *Ctx) NoReadDeadline() error

NoReadDeadline disables the server read timeout for this request; call it before reading a body that may take longer than Timeouts.Read (an upload on a slow link).

func (*Ctx) NoWriteDeadline added in v0.2.0

func (c *Ctx) NoWriteDeadline() error

NoWriteDeadline disables the server write timeout for this response; call it before streaming (SSE, long downloads). A writer with no deadline to remove (a test recorder, an export) is not an error.

func (*Ctx) Nonce added in v0.2.0

func (c *Ctx) Nonce() string

Nonce returns the per-request CSP nonce for inline scripts.

func (*Ctx) Param

func (c *Ctx) Param(name string) string

Param returns a path parameter ({slug} or {path...}).

func (*Ctx) Query

func (c *Ctx) Query(name string) string

Query returns the first value of a query-string parameter.

func (*Ctx) Redirect

func (c *Ctx) Redirect(url string) error

Redirect returns a redirect error (303). Use as `return c.Redirect("/x")`.

func (*Ctx) Render added in v0.5.0

func (c *Ctx) Render(code int, node h.Node) error

Render writes node as a page with the route's layouts applied (innermost first), like GET does. Use it in POST handlers to answer a form with validation errors inside the same layouts:

if errs := validate(in); errs.Any() {
	return c.Render(422, formulario(c, in, errs))
}

func (*Ctx) Request

func (c *Ctx) Request() *http.Request

Request returns the underlying *http.Request.

func (*Ctx) RequestID

func (c *Ctx) RequestID() string

RequestID returns the X-Request-ID header or a generated id.

func (*Ctx) Set

func (c *Ctx) Set(key string, v any)

Set stores a per-request value (typically from middleware).

func (*Ctx) SetContext added in v0.2.0

func (c *Ctx) SetContext(ctx context.Context)

SetContext replaces the request context, so a middleware can pass values to code that only receives *http.Request (templates, stdlib helpers).

func (*Ctx) SetCookie

func (c *Ctx) SetCookie(ck *http.Cookie)

SetCookie adds a Set-Cookie header.

func (*Ctx) SetRequest added in v0.2.0

func (c *Ctx) SetRequest(r *http.Request)

SetRequest replaces the request (rewritten URL, wrapped body...). Values already read from the old request (form, request id) are kept.

func (*Ctx) SetSigned added in v0.2.0

func (c *Ctx) SetSigned(name, value string, ttl time.Duration) error

SetSigned stores a tamper-proof cookie (HttpOnly, SameSite=Lax, Secure on HTTPS) that expires after ttl. Returns ErrNoSecret without a secret.

func (*Ctx) SetTitle

func (c *Ctx) SetTitle(t string)

SetTitle sets the page title; layouts read it with Title.

func (*Ctx) Signed added in v0.2.0

func (c *Ctx) Signed(name string) (value string, ok bool)

Signed reads a cookie written by SetSigned; ok is false when missing, tampered or expired.

func (*Ctx) Status

func (c *Ctx) Status(code int)

Status sets the status code used by the next page render.

func (*Ctx) Stream added in v0.2.0

func (c *Ctx) Stream() *Stream

Stream switches the response to text/event-stream, disables the write deadline and returns a writer for events. Use it from a GET route:

func GET(c *trilha.Ctx) error {
	s := c.Stream()
	for chunk := range chunks {
		if err := s.Send("delta", chunk); err != nil { return err }
	}
	return s.Send("done", "")
}

Compression is skipped for streams, and each event is flushed right away.

func (*Ctx) Text

func (c *Ctx) Text(code int, s string) error

Text writes a plain-text response.

func (*Ctx) Title

func (c *Ctx) Title() string

Title returns the page title set by the page (for layouts).

func (*Ctx) TraceID added in v0.6.0

func (c *Ctx) TraceID() string

TraceID returns the trace identifier the caller propagated in the traceparent header, or "" when it is absent or malformed. Trilha only carries the identifier into the logs; it does not sample or export spans.

func (*Ctx) Writer

func (c *Ctx) Writer() http.ResponseWriter

Writer returns the underlying http.ResponseWriter.

func (*Ctx) Written

func (c *Ctx) Written() bool

Written reports whether the response has already started.

type Env

type Env string

Env is the runtime environment.

const (
	Dev  Env = "dev"
	Prod Env = "prod"
)

type ErrorPageFunc

type ErrorPageFunc func(*Ctx, error) (h.Node, error)

ErrorPageFunc renders the 500 page (error.go: Error).

type Field added in v0.18.0

type Field struct {
	Name  string // the form name, prefix included ("cob_cep")
	Param string // what came after "=" in the tag ("3" in min=3)
	Text  string // the value as text
	Value any    // the converted value
	// contains filtered or unexported fields
}

Field is what a validation rule sees. Value is the converted value (string, bool, int64, float64, time.Time, []string, or nil when the field was not sent), and Text is the same thing as text — which is all most rules need.

func (Field) Other added in v0.18.0

func (f Field) Other(name string) string

Other returns another field's value as text, for a rule that compares two fields. Unknown names give "".

type FieldErrors added in v0.5.0

type FieldErrors map[string]string

FieldErrors collects validation messages by form field. Returned from a handler it answers 422: JSON with a "fields" object on API routes, or the error page on pages — though a form usually re-renders itself with Ctx.Render and shows each message next to its field.

func (FieldErrors) Add added in v0.5.0

func (e FieldErrors) Add(field, msg string)

Add records a message for field (the first one wins).

func (FieldErrors) Any added in v0.5.0

func (e FieldErrors) Any() bool

Any reports whether there is at least one error.

func (FieldErrors) Error added in v0.5.0

func (e FieldErrors) Error() string

Error joins the messages, sorted by field, for logs and API responses.

func (FieldErrors) Get added in v0.5.0

func (e FieldErrors) Get(field string) string

Get returns the message for field, or "".

func (FieldErrors) Has added in v0.5.0

func (e FieldErrors) Has(field string) bool

Has reports whether field has an error.

func (FieldErrors) OrNil added in v0.5.0

func (e FieldErrors) OrNil() error

OrNil returns e as an error, or nil when empty, for `return errs.OrNil()`.

type FileRules added in v0.19.0

type FileRules struct {
	// MaxSize is the limit for this file, in bytes, apart from
	// Config.MaxBodyBytes. Zero leaves the body limit doing the work.
	MaxSize int64
	// Accept lists the media types allowed, matched against the type detected
	// in the content: "image/png", "application/pdf", or "image/*". Empty
	// accepts anything.
	Accept []string
	// Optional makes an absent field return (nil, nil) instead of an error.
	Optional bool
}

FileRules is what a route accepts in one form field. The zero value accepts any type and any size the body limit allows, and requires the field.

type Gauge added in v0.6.0

type Gauge struct {
	// contains filtered or unexported fields
}

Gauge goes up and down: queue depth, connections in use.

func (*Gauge) Add added in v0.6.0

func (g *Gauge) Add(v float64)

Add moves the value (use a negative number to go down).

func (*Gauge) Dec added in v0.6.0

func (g *Gauge) Dec()

Dec subtracts one.

func (*Gauge) Inc added in v0.6.0

func (g *Gauge) Inc()

Inc adds one; Dec subtracts one.

func (*Gauge) Set added in v0.6.0

func (g *Gauge) Set(v float64)

Set replaces the value.

func (*Gauge) With added in v0.6.0

func (g *Gauge) With(values ...string) *Gauge

With binds label values, in the order they were declared.

type HTTPError

type HTTPError struct {
	Code    int
	Message string
}

HTTPError carries an HTTP status and a message safe to show to the client.

func (*HTTPError) Error

func (e *HTTPError) Error() string

type HandlerFunc

type HandlerFunc func(*Ctx) error

HandlerFunc handles an API method or a form submission.

type HealthReport added in v0.6.0

type HealthReport struct {
	Status        string        `json:"status"`
	Checks        []CheckResult `json:"checks,omitempty"`
	UptimeSeconds float64       `json:"uptime_seconds,omitempty"`
}

HealthReport is the readiness of the application.

type Histogram added in v0.6.0

type Histogram struct {
	// contains filtered or unexported fields
}

Histogram counts observations per bucket: durations, sizes.

func (*Histogram) Observe added in v0.6.0

func (h *Histogram) Observe(v float64)

Observe records one value.

func (*Histogram) With added in v0.6.0

func (h *Histogram) With(values ...string) *Histogram

With binds label values, in the order they were declared.

type LayoutFunc

type LayoutFunc func(*Ctx, h.Node) (h.Node, error)

LayoutFunc wraps rendered children (layout.go: Layout).

type Metrics added in v0.6.0

type Metrics struct {
	// MaxSeries caps the number of label combinations per metric (default
	// 1000). Extra combinations are folded into a single "other" series.
	MaxSeries int
	// contains filtered or unexported fields
}

Metrics is the process metric registry, exposed in the Prometheus text format. Get it with App.Metrics; it exists even when no endpoint serves it.

func (*Metrics) Counter added in v0.6.0

func (m *Metrics) Counter(name, help string, labels ...string) *Counter

Counter returns (creating on first use) a counter. labels declares the dimension names; bind the values with With. Invalid names panic: it is a programming error, caught on the first run.

func (*Metrics) Gauge added in v0.6.0

func (m *Metrics) Gauge(name, help string, labels ...string) *Gauge

Gauge returns (creating on first use) a gauge.

func (*Metrics) Histogram added in v0.6.0

func (m *Metrics) Histogram(name, help string, buckets []float64, labels ...string) *Histogram

Histogram returns (creating on first use) a histogram. buckets are the upper bounds in ascending order; nil uses the default duration buckets.

type MiddlewareFunc

type MiddlewareFunc func(*Ctx, Next) error

MiddlewareFunc intercepts a subtree (middleware.go: Middleware).

func Limit added in v0.2.0

func Limit(rps float64, burst int) MiddlewareFunc

Limit returns a middleware applying its own per-client limit to a subtree: put `var limit = trilha.Limit(2, 5)` in middleware.go and call it.

type Next

type Next func() error

Next continues the middleware chain.

type Observability added in v0.6.0

type Observability struct {
	// Health is the base path of the probes (default "/_trilha/health",
	// with /live and /ready under it). Off removes them.
	Health string
	// Metrics is the path of the Prometheus endpoint. Empty (the default)
	// means no endpoint and no request instrumentation.
	Metrics string
	// Token authorizes the detailed health and the metrics
	// (Authorization: Bearer ...). It must have at least 32 bytes; shorter
	// tokens never authorize anything. Read from TRILHA_OBS_TOKEN.
	Token string
	// Trusted lists CIDRs (or plain IPs) that skip the token, for a scraper
	// on a private network. "0.0.0.0/0" plus "::/0" opens it to everyone:
	// only do that when something in front already restricts access.
	Trusted []string
	// Details is Off to never reveal check names and errors, even to an
	// authorized client. Empty means dev shows them and prod requires
	// authorization.
	Details string
	// Timeout is the deadline of each readiness check (default 2s).
	// NoTimeout waits forever (not recommended: the probe holds a connection).
	Timeout time.Duration
	// CacheFor is how long a readiness result is reused (default 1s), so a
	// flood of probes cannot amplify into a flood of database queries.
	// NoTimeout disables the cache.
	CacheFor time.Duration
}

Observability configures the health, metrics and tracing surface. The zero value serves the health probes and nothing else: metrics have to be turned on explicitly, and outside dev the detailed health needs authorization (NIST SP 800-53 AU-9, OWASP API Security 2023 API8).

type PageFunc

type PageFunc func(*Ctx) (h.Node, error)

PageFunc renders a page (page.go: Page).

type Problem added in v0.21.0

type Problem struct {
	// Type is a URI identifying the kind of problem — usually a page of yours
	// documenting it. "about:blank" means "nothing beyond the status".
	Type string `json:"type,omitempty"`
	// Title is the short, human-readable summary, the same for every
	// occurrence of this Type.
	Title string `json:"title"`
	// Status is the HTTP status code.
	Status int `json:"status"`
	// Detail explains this occurrence. It is read by a person: never put a
	// stack trace, a query or a DSN in it.
	Detail string `json:"detail,omitempty"`
	// Instance identifies this occurrence (default: the request path).
	Instance string `json:"instance,omitempty"`
	// Fields carries the per-field messages of a 422, unchanged.
	Fields FieldErrors `json:"fields,omitempty"`
	// Extra members are merged into the top-level object.
	Extra map[string]any `json:"-"`
}

Problem is an API error in the RFC 9457 shape. Return one from a handler to say more than a status code:

return &trilha.Problem{
	Type:   "https://example.com/probs/out-of-credit",
	Title:  "Out of credit",
	Status: http.StatusPaymentRequired,
	Detail: "The account has $3 and the operation costs $10.",
	Extra:  map[string]any{"balance": 300},
}

Empty fields are filled in by the framework: Type becomes "about:blank" (or what ProblemType returns), Title the status text, Instance the request path, and request_id the id of the request. Extra members are written next to the standard ones, at the top level, which is what the RFC calls an extension.

func (*Problem) Error added in v0.21.0

func (p *Problem) Error() string

func (*Problem) MarshalJSON added in v0.21.0

func (p *Problem) MarshalJSON() ([]byte, error)

MarshalJSON writes the standard members and then the extension ones, so Extra reads like part of the object instead of a bag inside it.

type RateLimit added in v0.2.0

type RateLimit struct {
	// RPS is the sustained requests per second per client.
	RPS float64
	// Burst is the bucket size (requests allowed at once).
	Burst int
}

RateLimit configures the per-client token bucket. Zero disables it.

type RedirectError

type RedirectError struct {
	URL  string
	Code int
}

RedirectError is returned by handlers to redirect the client.

func (*RedirectError) Error

func (e *RedirectError) Error() string

type Route

type Route struct {
	// Pattern is the path pattern, e.g. "/blog/{slug}" or "/docs/{path...}".
	Pattern string
	// Page renders GET for page routes; nil for API routes.
	Page PageFunc
	// Methods maps HTTP methods to handlers (route.go, or form methods in page.go).
	Methods map[string]HandlerFunc
	// Layouts wrap the page, innermost first.
	Layouts []LayoutFunc
	// Middlewares run before the handler, outermost first.
	Middlewares []MiddlewareFunc
	// Kind decides how errors are rendered (HTML page or JSON) and whether
	// CSRF applies. KindAuto: page.go routes are pages; route.go routes are
	// APIs, except that a browser navigation (Accept: text/html, outside
	// /api/) gets HTML error pages. route.go may export `var Kind = trilha.KindPage`.
	Kind RouteKind
}

Route is one entry produced by the generator for App.Register.

type RouteKind added in v0.3.0

type RouteKind int

RouteKind is the error/CSRF behaviour of a Route; see Route.Kind.

const (
	// KindAuto derives the kind from the files (page.go → page, route.go → API).
	KindAuto RouteKind = iota
	// KindPage renders errors as HTML pages and enforces CSRF on body methods.
	KindPage
	// KindAPI renders errors as JSON, whatever the Accept header says.
	KindAPI
)

type Security added in v0.2.0

type Security struct {
	// CSP is the Content-Security-Policy. Empty = default policy with a
	// per-request nonce for scripts; the text may contain {nonce}.
	CSP string
	// CSPExtra adds sources to directives of the default policy, e.g.
	// {"style-src": {"https://fonts.googleapis.com"}}.
	CSPExtra map[string][]string
	// HSTS is sent only over HTTPS (TLS or a trusted proxy saying so).
	HSTS string
	// PermissionsPolicy restricts browser features.
	PermissionsPolicy string
	// COOP is Cross-Origin-Opener-Policy.
	COOP string
	// FrameOptions is X-Frame-Options.
	FrameOptions string
	// Referrer is Referrer-Policy.
	Referrer string
}

Security configures the hardening headers sent with every response. The zero value means "defaults"; set a field to Off to drop that header.

type SecurityEvent added in v0.2.0

type SecurityEvent struct {
	// Kind is one of csrf, auth, body, rate, panic.
	Kind      string
	Status    int
	Method    string
	Path      string
	IP        string
	RequestID string
}

SecurityEvent describes a request the framework blocked or flagged.

type Signer added in v0.2.0

type Signer struct {
	// contains filtered or unexported fields
}

Signer signs and verifies values with HMAC-SHA256. The first key signs; every key verifies, so a previous key can be kept during rotation.

func NewSigner added in v0.2.0

func NewSigner(keys ...[]byte) *Signer

NewSigner creates a signer; the first key signs, the rest only verify.

func (*Signer) Sign added in v0.2.0

func (s *Signer) Sign(value string, exp time.Time) (string, error)

Sign returns value|expiry|mac, safe for a cookie.

func (*Signer) Verify added in v0.2.0

func (s *Signer) Verify(token string, now time.Time) (string, bool)

Verify checks the signature and expiry with any key, returning the value.

type Stream added in v0.2.0

type Stream struct {
	// contains filtered or unexported fields
}

Stream is a Server-Sent Events writer returned by Ctx.Stream.

func (*Stream) Comment added in v0.2.0

func (s *Stream) Comment(text string) error

Comment sends a comment line (keeps proxies from timing out the stream).

func (*Stream) Done added in v0.2.0

func (s *Stream) Done() <-chan struct{}

Done reports when the client went away.

func (*Stream) Flush added in v0.2.0

func (s *Stream) Flush()

Flush pushes buffered bytes to the client.

func (*Stream) JSON added in v0.2.0

func (s *Stream) JSON(event string, v any) error

JSON marshals v and sends it as the event data.

func (*Stream) Send added in v0.2.0

func (s *Stream) Send(event, data string) error

Send writes one event. An empty name sends an unnamed message (received by EventSource.onmessage). Multi-line data is split into several data: lines.

type TestClient added in v0.23.0

type TestClient struct {
	// contains filtered or unexported fields
}

TestClient sends requests through the app keeping the cookies, so a login and the page after it are two lines.

func NewTestClient added in v0.23.0

func NewTestClient(t TestingT, a *App) *TestClient

NewTestClient builds a client over the app's handler.

func (*TestClient) Get added in v0.23.0

func (c *TestClient) Get(target string, opts ...TestOption) *TestResponse

Get sends a GET.

func (*TestClient) PostForm added in v0.23.0

func (c *TestClient) PostForm(target string, form url.Values, opts ...TestOption) *TestResponse

PostForm sends a form, CSRF included.

func (*TestClient) PostJSON added in v0.23.0

func (c *TestClient) PostJSON(target string, v any, opts ...TestOption) *TestResponse

PostJSON sends a JSON body.

func (*TestClient) Request added in v0.23.0

func (c *TestClient) Request(method, target string, opts ...TestOption) *TestResponse

Request sends one request and stores the cookies the app set.

type TestOption added in v0.23.0

type TestOption func(*testOptions)

TestOption changes one request. The options are shared by every helper.

func WithApp added in v0.23.0

func WithApp(a *App) TestOption

WithApp runs the route on this app instead of the throwaway one, so the route sees the real Config (secret, Public, CSRFForAPI). Only TestRoute and TestPage read it, and they register the route on the app, so give a fresh app: registering the same pattern twice panics in net/http.

func WithBody added in v0.23.0

func WithBody(contentType, body string) TestOption

WithBody sends the body as it is.

func WithCookie added in v0.23.0

func WithCookie(name, value string) TestOption

WithCookie sends a cookie, replacing the one the client holds.

func WithForm added in v0.23.0

func WithForm(form url.Values) TestOption

WithForm sends the values as an HTML form.

func WithHeader added in v0.23.0

func WithHeader(name, value string) TestOption

WithHeader adds a request header.

func WithJSON added in v0.23.0

func WithJSON(v any) TestOption

WithJSON sends v as a JSON body.

func WithSigned added in v0.23.0

func WithSigned(name, value string) TestOption

WithSigned sends a cookie signed like Ctx.SetSigned would, valid for an hour. It is how a test opens a session route without replaying the login.

func WithoutCSRF added in v0.23.0

func WithoutCSRF() TestOption

WithoutCSRF drops the CSRF cookie and header, so the request exercises the rejection the browser would get.

type TestResponse added in v0.23.0

type TestResponse struct {
	*httptest.ResponseRecorder
	// Request is what was sent, already carrying cookies and CSRF.
	Request *http.Request
	// Node is the node the page returned, before the layouts. Only TestPage
	// fills it in.
	Node h.Node
	// contains filtered or unexported fields
}

TestResponse is what the app answered. It embeds the recorder, so Code, Body and Header are the usual ones; the Want methods stop the test with the body in the message.

func TestPage added in v0.23.0

func TestPage(t TestingT, r Route, target string, opts ...TestOption) *TestResponse

TestPage renders one page with its layouts and fills TestResponse.Node with what the page returned, so a test can look at the node instead of the HTML.

func TestRequest added in v0.23.0

func TestRequest(t TestingT, a *App, method, target string, opts ...TestOption) *TestResponse

TestRequest sends one request through the whole app: middlewares, CSRF, error pages, everything ListenAndServe would run.

func TestRoute added in v0.23.0

func TestRoute(t TestingT, r Route, method, target string, opts ...TestOption) *TestResponse

TestRoute sends one request to a single route with its middlewares, without generating the app. Pass WithApp to give it a Config of its own.

func (*TestResponse) Cookie added in v0.23.0

func (r *TestResponse) Cookie(name string) *http.Cookie

Cookie returns the cookie the response set, or nil.

func (*TestResponse) JSON added in v0.23.0

func (r *TestResponse) JSON(v any) *TestResponse

JSON decodes the body into v.

func (*TestResponse) WantContains added in v0.23.0

func (r *TestResponse) WantContains(subs ...string) *TestResponse

WantContains fails unless every string is in the body.

func (*TestResponse) WantHeader added in v0.23.0

func (r *TestResponse) WantHeader(name, want string) *TestResponse

WantHeader fails unless the response header matches.

func (*TestResponse) WantStatus added in v0.23.0

func (r *TestResponse) WantStatus(code int) *TestResponse

WantStatus fails unless the status matches.

type TestingT added in v0.23.0

type TestingT interface {
	Helper()
	Fatalf(format string, args ...any)
}

TestingT is the part of *testing.T these helpers use. It exists so the runtime never imports testing: importing trilha in production must not drag the test flags into the binary.

type Timeouts added in v0.2.0

type Timeouts struct {
	ReadHeader     time.Duration // 10s
	Read           time.Duration // 30s
	Write          time.Duration // 60s (use Ctx.NoWriteDeadline for streams)
	Idle           time.Duration // 120s
	MaxHeaderBytes int           // 64 KiB
	// Shutdown is how long ListenAndServe waits for in-flight requests after
	// SIGINT/SIGTERM before closing (5s).
	Shutdown time.Duration
}

Timeouts are the http.Server limits. Zero fields get defaults; NoTimeout disables one (large uploads, long polls). Write applies to the whole response: streams should call Ctx.Stream or Ctx.NoWriteDeadline instead of disabling it globally.

type Upload added in v0.19.0

type Upload struct {
	// Name is the file name, sanitised: no directory, no separator, no
	// control character, at most 100 characters, never empty. It still comes
	// from the client, so it says nothing about the content — Ext does.
	Name string
	// MIME is the media type detected in the first 512 bytes of the file,
	// never what the client announced.
	MIME string
	// Ext is the extension that matches MIME (".pdf"), which may disagree
	// with the one in Name.
	Ext string
	// Size is the file size in bytes.
	Size int64
	// File is the file itself, positioned at the start. Close it, or call
	// Upload.Close.
	File multipart.File
}

Upload is a file that already passed the rules.

func (*Upload) Close added in v0.19.0

func (u *Upload) Close() error

Close closes the underlying file.

func (*Upload) Save added in v0.19.0

func (u *Upload) Save(dir string) (string, error)

Save writes the file inside dir, creating the directory if needed, with mode 0600 and a name that is free: nota.pdf, then nota-1.pdf, and so on. It returns the path written. The name cannot escape dir — that is the whole reason this exists instead of a filepath.Join in the handler.

type Validator added in v0.18.0

type Validator interface{ Validate() error }

Validator is a type that checks itself. Bind calls it on a field, right after the value was converted, and on the whole struct at the end — the second one only when every field passed, because a check that reads two fields needs both of them to be there.

type CPF string

func (c CPF) Validate() error {
	if !cpfValido(string(c)) {
		return errors.New("CPF inválido")
	}
	return nil
}

The error message goes to FieldErrors as it is, so write it in the language of the app. A struct's Validate may return FieldErrors to say which field is at fault; any other error is returned by Bind untouched.

Directories

Path Synopsis
ai
Package ai talks to chat-completion APIs that follow the OpenAI protocol (OpenAI, Azure, Ollama, LM Studio, Groq, OpenRouter, vLLM...) and builds agents on top of them: tools, handoffs, parallel and chained agents.
Package ai talks to chat-completion APIs that follow the OpenAI protocol (OpenAI, Azure, Ollama, LM Studio, Groq, OpenRouter, vLLM...) and builds agents on top of them: tools, handoffs, parallel and chained agents.
mcp
Package mcp implements the Model Context Protocol (JSON-RPC 2.0 over stdio or Streamable HTTP) as a client, to bring external tools into an ai.Agent, and as a server, to expose your app's tools to MCP hosts.
Package mcp implements the Model Context Protocol (JSON-RPC 2.0 over stdio or Streamable HTTP) as a client, to bring external tools into an ai.Agent, and as a server, to expose your app's tools to MCP hosts.
Package auth adds OpenID Connect login, signed sessions and role checks to a Trilha app, using only the standard library.
Package auth adds OpenID Connect login, signed sessions and role checks to a Trilha app, using only the standard library.
Package cache is an in-memory cache with TTL, tags and explicit invalidation.
Package cache is an in-memory cache with TTL, tags and explicit invalidation.
cmd
trilha command
Command trilha is the CLI: new, gen, dev, build, routes, export, openapi, audit, ui.
Command trilha is the CLI: new, gen, dev, build, routes, export, openapi, audit, ui.
examples
assistente command
assistente/app/api/chat
Package chat streams agent runs to the browser over Server-Sent Events.
Package chat streams agent runs to the browser over Server-Sent Events.
assistente/app/mcp
Package mcp exposes the example's tools to MCP hosts (Streamable HTTP).
Package mcp exposes the example's tools to MCP hosts (Streamable HTTP).
assistente/internal/ferramentas
Package ferramentas holds the tools, agents and MCP server of the example.
Package ferramentas holds the tools, agents and MCP server of the example.
blog command
blog/app/anexos
Package anexos shows an upload with progress that still works without JavaScript, checked by c.File: limite por arquivo, tipo lido no conteúdo e nome sem caminho.
Package anexos shows an upload with progress that still works without JavaScript, checked by c.File: limite por arquivo, tipo lido no conteúdo e nome sem caminho.
blog/app/api/posts
Package posts exposes the JSON API at /api/posts.
Package posts exposes the JSON API at /api/posts.
blog/app/marketing-
Package marketing is a route group: its layout wraps /precos and /sobre without adding a URL segment (folder name ends with "-").
Package marketing is a route group: its layout wraps /precos and /sobre without adding a URL segment (folder name ends with "-").
blog/app/painel-
Package painel is a route group for the app area (/painel, /relatorio).
Package painel is a route group for the app area (/painel, /relatorio).
blog/app/painel-/relatorio
Package relatorio renders a page from an html/template file instead of the h DSL, using the tmpl adapter.
Package relatorio renders a page from an html/template file instead of the h DSL, using the tmpl adapter.
blog/internal/anexos
Package anexos guarda os anexos enviados.
Package anexos guarda os anexos enviados.
blog/internal/icones
Package icones embute os ícones do site.
Package icones embute os ícones do site.
blog/internal/posts
Package posts is an in-memory post store for the example app.
Package posts is an in-memory post store for the example app.
cadastro command
cadastro/app/api/cidades
Package cidades serves the dependent select: GET /api/cidades?uf=SP.
Package cidades serves the dependent select: GET /api/cidades?uf=SP.
cadastro/internal/clientes
Package clientes holds the domain of the example: the form model, its validation rules and an in-memory store.
Package clientes holds the domain of the example: the form model, its validation rules and an in-memory store.
orcamento command
orcamento/app/api/relatorio.csv
Package relatoriocsv exports the month as CSV at /api/relatorio.csv?mes=.
Package relatoriocsv exports the month as CSV at /api/relatorio.csv?mes=.
orcamento/app/lancamentos
Package lancamentos has the standalone entry page (works without JS) and the POST every entry form submits to (dialog included).
Package lancamentos has the standalone entry page (works without JS) and the POST every entry form submits to (dialog included).
orcamento/internal/componentes
Package componentes holds the reusable, nested UI pieces of the budget example.
Package componentes holds the reusable, nested UI pieces of the budget example.
orcamento/internal/plano
Package plano is the domain of the budget example: a chart of accounts (tree), monthly budgets on analytic accounts, and entries.
Package plano is the domain of the budget example: a chart of accounts (tree), monthly budgets on analytic accounts, and entries.
sso command
sso/app/api
Package api agrupa as rotas de API.
Package api agrupa as rotas de API.
sso/app/api/eu
Package eu devolve a sessão como JSON.
Package eu devolve a sessão como JSON.
sso/app/entrar
Package entrar começa o login.
Package entrar começa o login.
sso/app/entrar/retorno
Package retorno termina o login.
Package retorno termina o login.
sso/app/painel
Package painel é a área que exige sessão.
Package painel é a área que exige sessão.
sso/app/painel/relatorio
Package relatorio exige um papel, não só sessão.
Package relatorio exige um papel, não só sessão.
sso/app/sair
Package sair encerra a sessão.
Package sair encerra a sessão.
sso/internal/sso
Package sso monta o fluxo OpenID Connect a partir do ambiente e expõe funções finas para as rotas em app/.
Package sso monta o fluxo OpenID Connect a partir do ambiente e expõe funções finas para as rotas em app/.
h
Package h is a small, dependency-free HTML DSL: every element, attribute and piece of text is a Node that knows how to render itself to an io.Writer.
Package h is a small, dependency-free HTML DSL: every element, attribute and piece of text is a Node that knows how to render itself to an io.Writer.
internal
dev
Package dev implements `trilha dev`: a polling file watcher, a builder and a supervisor that runs the app behind a reverse proxy with live reload.
Package dev implements `trilha dev`: a polling file watcher, a builder and a supervisor that runs the app behind a reverse proxy with live reload.
gen
Package gen turns a scan.Result into the source of trilha_gen.go.
Package gen turns a scan.Result into the source of trilha_gen.go.
openapi
Package openapi turns a scanned app into an OpenAPI 3.1 document.
Package openapi turns a scanned app into an OpenAPI 3.1 document.
scaffold
Package scaffold writes a new project from embedded templates.
Package scaffold writes a new project from embedded templates.
scan
Package scan walks an app/ directory and turns its file conventions into a list of routes, validating them along the way.
Package scan walks an app/ directory and turns its file conventions into a list of routes, validating them along the way.
app
Package app is the documentation site of Trilha, built with Trilha.
Package app is the documentation site of Trilha, built with Trilha.
internal/demos
Package demos holds the "code → result" examples used by the home page and by chapters.
Package demos holds the "code → result" examples used by the home page and by chapters.
internal/docs
Package docs loads the Markdown content of the documentation site in every locale, builds the navigation and renders pages.
Package docs loads the Markdown content of the documentation site in every locale, builds the navigation and renders pages.
internal/home
Package home renders the landing page of the site in each locale.
Package home renders the landing page of the site in each locale.
internal/md
Package md is a deliberately small Markdown-to-HTML converter for the documentation site: headings with ids, paragraphs, lists, fenced code (with Go highlighting), tables, blockquotes, callouts (:::nome), inline code, emphasis and links.
Package md is a deliberately small Markdown-to-HTML converter for the documentation site: headings with ids, paragraphs, lists, fenced code (with Go highlighting), tables, blockquotes, callouts (:::nome), inline code, emphasis and links.
internal/ui
Package ui holds the shared building blocks of the documentation site: the document shell, sidebar, table of contents and helpers.
Package ui holds the shared building blocks of the documentation site: the document shell, sidebar, table of contents and helpers.
Package tmpl adapts html/template to the h.Node pipeline, for developers who prefer template files over the Go DSL.
Package tmpl adapts html/template to the h.Node pipeline, for developers who prefer template files over the Go DSL.
Package ui is Trilha's default, customizable UI kit: typed components that render classes consumed by public/ui.css, plus a small ui.js for behavior.
Package ui is Trilha's default, customizable UI kit: typed components that render classes consumed by public/ui.css, plus a small ui.js for behavior.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL