Documentation menu

Thinking in Goldar

Goldar is easier to understand when you begin with the request and the document it should produce, not with a long-lived component tree. Routes coordinate work, resources own reads and mutations, templates own presentation, and browser code is an optional enhancement.

Start with working HTML

The first response should already contain the page’s meaningful content and controls. Use links for navigation and forms for mutations. A user, crawler, or test can interact with that foundation before custom elements load—or when JavaScript does not load at all.

This is not a ban on browser behavior. It is an ordering rule: deliver the document first, then add the smallest client-side capability that improves it.

A route owns one request graph

A Router maps an absolute path to a Route constructor. For every matched request, Goldar creates a fresh route and its owned resources, initializes its stores, loads its data, and renders the result.

The route is the composition boundary for that work:

  • Data owns reads.
  • FormAction owns POST mutations.
  • Store holds mutable state used during the request.
  • render() turns resolved values and action references into views or responses.

Resources are addressed by identity rather than by string names at the call site. A route can only read data and inspect actions that it owns, so crossing a route boundary fails explicitly.

class DashboardRoute extends Route {
	readonly data = {
		account: new AccountData(),
		projects: new ProjectsData(),
	};

	render(context: RouteContext) {
		return DashboardView({
			account: context.data(this.data.account),
			projects: context.data(this.data.projects),
		});
	}
}

All data owned by the route starts loading concurrently before render() runs. context.data() returns the resolved value; do not pass the Data instance itself into a view.

Reads belong in Data

Put a request-dependent read in a Data<Output> subclass. Its load() method receives the request URL, parameters, abort signal, and underlying Hono context.

class ProjectData extends Data<Project> {
	load({ params }: DataContext) {
		return projectRepository.find(params.require("projectId"));
	}
}

This keeps loading observable to the framework and replaceable in tests. It also keeps templates free of networking and persistence concerns.

Goldar does not currently turn Data into a browser-side reactive cache. A new request receives a new route graph; durable state belongs in the database, service, or platform primitive that owns it.

Mutations belong in FormAction

A FormAction receives one submitted form, validates it, performs the mutation, and returns an explicit result. It can preserve field errors, redirect, rerender after success, or return a raw Response.

class RenameProject extends FormAction {
	async submit({ form, invalid, redirect }: FormActionContext) {
		const name = form.string("name").trim();
		if (name.length === 0) {
			return invalid({ name: "Enter a project name." });
		}

		await projectRepository.rename(name);
		return redirect("/projects");
	}
}

The route exposes an owned action to the template with context.action(action). That reference contains the form method, URL, identifier, and hidden-field name. After an invalid result, context.submission(action) returns the preserved values and field errors for the same action.

Same-origin CSRF protection is enabled by default. Disabling it changes the application’s security boundary and should be an explicit adapter-level decision.

Templates own presentation

A compiled .hbs import is a function. Calling it with props returns an opaque View that a route can return or pass into another template.

const page = ProjectView({ project });
return LayoutView({ title: project.name }, page);

{{outlet}} renders those child views in order without inserting a wrapper element. Imported templates provide smaller typed presentation units inside another template.

Keep conditionals, empty states, iteration, and presentation composition in templates. Keep authorization, data access, mutations, response status, and nontrivial application decisions in routes and resources.

Expressions and normal attributes are escaped. Rendering trusted HTML requires the explicit TrustedHtml brand, and branding a string does not sanitize it. Establish the sanitization boundary before creating that value.

Browser behavior is an island

Use a custom element when a part of the document needs local browser state or interaction. Keep useful light-DOM content inside the element so the server response remains understandable.

When a template contains <quantity-stepper>, Goldar discovers one matching local quantity-stepper.ts module whose default export is its HTMLElement constructor. The generated browser entry registers it. Missing or ambiguous modules fail at build time rather than becoming a silent runtime omission.

Goldar does not currently provide client routing, hydration, or browser-lived reactive stores. Those are not hidden defaults; they are outside the implemented contract.

Hono is the HTTP escape hatch

Goldar assembles a Hono application rather than hiding HTTP behind a closed abstraction. RouteContext.hono exposes the current Hono context, and the Application server configuration hook can install middleware before routes are registered.

Return a standard Response when a route is not an HTML page. Use the framework primitives when they improve ownership and testing, and use the web platform or Hono directly when they are the clearer boundary.

A translation for component-framework users

Familiar conceptGoldar equivalent
Route loaderRoute-owned Data
Server actionRoute-owned FormAction
Render functionCallable compiled .hbs view
Stateful browser componentCustom element
Request-local service or stateStore
Framework rootApplication with a Router

The important difference is lifetime. A Goldar route, its data, its actions, and its stores belong to a request. A custom element belongs to the browser document. Treating those as separate systems keeps server authority and client behavior legible.

A practical decision order

When adding a feature, ask these questions in order:

  1. What useful HTML should the request return?
  2. Which route owns the URL and response?
  3. Which reads belong in Data?
  4. Which form submissions belong in FormAction?
  5. Which presentation belongs in typed templates?
  6. What interaction genuinely requires a custom element?

Continue with Typed Handlebars to build the presentation layer, or use the runtime reference for the exact lifecycle and result contracts.