Start here Mado 0.15.3
The Mado Way
One right way. Strict contracts. No magic.
Mado is a frontend framework for static sites and live SPAs that should be easy to build and boring to maintain. It provides a small set of contracts; application structure grows only when the application has a measured need.
Principles
- One way. For every task there is one right path, not five. If you write something unusual — ask yourself whether an idiomatic helper already exists.
- Explicitness over magic. No file-system scanners, no implicit globals, no hidden side-effects. Everything the framework does can be read in a single file.
- Platform first. If the browser already has a feature — use it directly.
Mado helpers may add reactivity and lifecycle ownership, but they do not hide
the native
fetch,<form>, History API or Shadow DOM contracts. - Strict types.
tsc --strict --noUncheckedIndexedAccessalways. If something cannot be typed — that is a signal the API is wrong. - No runtime dependencies in Mado core. Every framework dependency is a years-long commitment; the Web Components ecosystem does not require one.
Conventions
Project structure
src/
├── main.ts ← boot: global CSS + render router
├── app.routes.ts ← one readable route map
├── pages/ ← one *.page.ts per route
├── components/ ← autonomous Web Components
├── content/ ← optional static content
└── styles/ ← optional document/global CSS
This universal starter is the canonical minimum. Add layouts/, feature
folders, shared HTTP policy or domain modules only after repeated application
code justifies them. The modular starter is an optional architecture
experiment, not a framework contract.
One component = one file
// src/components/x-user-card.component.ts
import { component, html, css } from "@madojs/mado";
component(
"x-user-card",
() => {
return html`<div class="card"><slot /></div>`;
},
{
styles: css`
.card {
padding: 1rem;
}
`,
},
);
import "./components/x-user-card.component.js" registers the component via
customElements.define. This is a side effect. Import where the component is needed.
Data at the right level
Use browser fetch() directly for a small one-off request. Add a thin
application helper only when authentication, decoding or error policy is
actually shared.
Use resource() for reactive reads that need loading/error state,
cancellation, caching or invalidation. Use mutation() for writes whose state
or invalidation belongs in the UI lifecycle:
// reactive read
const user = resource(() => `/api/users/${id()}`, jsonFetcher());
// tracked write
const save = mutation(api.save, { invalidates: ["/api/users*"] });
Resources and effects created in a page view or component setup are disposed
with that owner. An intentionally standalone resource must have an integration
owner that eventually calls its idempotent dispose().
One way to describe a page
// src/pages/user-profile.page.ts
import { page, html, resource, jsonFetcher } from "@madojs/mado";
export default page({
title: ({ id }) => `User #${id}`,
view: ({ params }) => {
const user = resource(() => `/api/users/${params.id}`, jsonFetcher());
return html`...`;
},
});
Keep page-local signals, resources and forms inside view(). Module-wide state
is ordinary application code; introduce a service or context only when multiple
owners genuinely share it.
One way to declare routes
See 12-routing.md.
What we do NOT do
- ❌ Do not write components without a hyphen. This is the browser rule for
custom elements:
user-cardis ok,usercardis not. x-*is only a convention for Mado examples and tests, not a brand standard. In production use a domain prefix:app-*,crm-*,ticket-*,admin-*.- ❌ Do not assign
innerHTMLdirectly. Build structure withhtml``; useunsafeHTML()only for content you own or have already sanitized. - ❌ Do not start timers or raw browser subscriptions without cleanup. Pair them
with the page/component
onDispose; aneffect()may return cleanup for work owned by that effect. - ❌ Do not store global mutable state. Use signals and
context. - ❌ Do not add application packages reflexively. Prefer the platform and record the measured need; never add a runtime dependency to Mado core.
When in doubt
If you are asking "what's the best way here?" — that is a signal that:
- Either there is a built-in helper you don't know about (check
docs/). - Or this is a new situation — discuss it and record it in this document as one more convention.
"A consistent okay beats a varied ideal."