Real-world patterns you'll actually use in production — in pure HTML.
A complete login flow: form validation, POST to auth endpoint, save the JWT to a global store, and automatically attach the token to every subsequent request via a request interceptor.
<script>
// Attach token to every outgoing request
NoJS.interceptor('request', (url, opts) => {
const token = NoJS.store.auth.token;
if (token) opts.headers['Authorization'] = 'Bearer ' + token;
return opts;
});
</script>
<!-- Global auth store -->
<div store="auth" value="{ user: null, token: null }"></div>
<template route="/login"
guard="!$store.auth.token"
redirect="/dashboard">
<form post="/auth/login" validate
success="#auth-ok" error="#auth-err">
<input name="email" type="email" required validate="email">
<input name="password" type="password" required minlength="8">
<button type="submit"
bind-disabled="!$form.valid || $form.submitting">
<span hide="$form.submitting">Sign in</span>
<span show="$form.submitting">Signing in...</span>
</button>
</form>
<template id="auth-ok" var="res">
<script>
NoJS.store.auth.user = res.user;
NoJS.store.auth.token = res.token;
NoJS.notify(); // flush DOM bindings after external store mutation
NoJS.router.push('/dashboard');
</script>
</template>
<template id="auth-err" var="err">
<p class="error" bind="err.message" animate="shake"></p>
</template>
</template>Key concepts: interceptor('request') · store · form · validate · post · guard · redirect · success/error templates · notify()
A route guarded by the auth store, paired with a response interceptor that
acts as a control script: on every API call, if the server returns 401 or
403, the token is invalidated and the user is redirected to login
automatically — no extra code needed in the route itself.
<script>
// Control script: validate token on every response
NoJS.interceptor('response', (response) => {
if (response.status === 401 || response.status === 403) {
NoJS.store.auth.user = null;
NoJS.store.auth.token = null;
NoJS.notify(); // flush DOM bindings before redirect
NoJS.router.push('/login');
throw new Error('Session expired');
}
return response;
});
</script>
<!-- Guard: redirect to /login if no token -->
<template route="/dashboard"
guard="$store.auth.token"
redirect="/login">
<!-- Token is attached automatically via the request interceptor -->
<div get="/me/dashboard" as="data" loading="#dash-skeleton">
<h2 bind="'Welcome, ' + data.user.name"></h2>
<div each="m in data.metrics">
<span bind="m.label"></span>
<span bind="m.value | number"></span>
</div>
</div>
<button on:click="$store.auth.user = null; $store.auth.token = null">
Sign out
</button>
</template>Key concepts: interceptor('response') · guard · redirect · loading · session invalidation
The two interceptors from Examples 1 and 2 work together: the request interceptor stamps the token on the way out; the response interceptor revokes it on the way back if the server rejects it.
An instant search input that fires a debounced GET request on every keystroke,
rendering results reactively. No addEventListener, no setTimeout, no DOM
manipulation.
<div state="{ query: '' }">
<input model="query" placeholder="Search products...">
<!-- GET fires 300ms after the user stops typing -->
<div get="/products?q={query}"
watch="query"
debounce="300"
as="results">
<p show="!results.length && query">
No results for <strong bind="query"></strong>
</p>
<div each="item in results">
<span bind="item.name"></span>
<span bind="item.price | currency"></span>
</div>
</div>
</div>Key concepts: model · watch · debounce · dynamic get with interpolation · show
A global store shared between a product list and a cart badge in different parts of the page. When a product is added, the badge and the cart summary update simultaneously — no event bus, no shared component.
<!-- Global cart store -->
<div store="cart" value="{ items: [] }"></div>
<!-- Badge: lives anywhere in the DOM -->
<span class="cart-badge" bind="$store.cart.items.length"></span>
<!-- Product list -->
<div get="/products" as="products">
<div each="p in products">
<span bind="p.name"></span>
<button on:click="$store.cart.items = [...$store.cart.items, p]">
Add to cart
</button>
</div>
</div>
<!-- Cart summary: somewhere else entirely -->
<div show="$store.cart.items.length > 0">
<div each="item in $store.cart.items">
<span bind="item.name"></span>
<span bind="item.price | currency"></span>
</div>
</div>Key concepts: store · $store.* cross-component reactivity · each · show
A server-status dashboard that refreshes automatically every 5 seconds using the
refresh attribute. Conditional styling reacts instantly to the health state — no
setInterval, no fetch loop.
<!-- Refetches /api/status every 5 seconds -->
<div get="/api/status" refresh="5000" as="s">
<!-- Badge: green when healthy, red otherwise -->
<span class-success="s.healthy"
class-error="!s.healthy"
bind="s.healthy ? 'Online' : 'Degraded'">
</span>
<div each="metric in s.metrics">
<span bind="metric.label"></span>
<span bind="metric.value | number"></span>
</div>
</div>Key concepts: refresh · class-* conditional styling · bind expression
All five patterns combined into a single production-grade SPA:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.no-js.dev/"></script>
<script>
NoJS.config({
baseApiUrl: 'https://api.myapp.com/v1',
router: { useHash: false }
});
// Attach JWT to every request
NoJS.interceptor('request', (url, opts) => {
const token = NoJS.store.auth.token;
if (token) opts.headers['Authorization'] = 'Bearer ' + token;
return opts;
});
// Revoke JWT on 401/403
NoJS.interceptor('response', (response) => {
if (response.status === 401 || response.status === 403) {
NoJS.store.auth.user = null;
NoJS.store.auth.token = null;
NoJS.notify(); // flush DOM bindings before redirect
NoJS.router.push('/login');
throw new Error('Session expired');
}
return response;
});
</script>
</head>
<body>
<div store="auth" value="{ user: null, token: null }"></div>
<template route="/login" guard="!$store.auth.token" redirect="/dashboard">
<form post="/auth/login" validate success="#auth-ok" error="#auth-err">
<input name="email" type="email" required validate="email">
<input name="password" type="password" required minlength="8">
<button type="submit" bind-disabled="!$form.valid || $form.submitting">
<span hide="$form.submitting">Sign in</span>
<span show="$form.submitting">Signing in...</span>
</button>
</form>
<template id="auth-ok" var="res">
<script>
NoJS.store.auth.user = res.user;
NoJS.store.auth.token = res.token;
NoJS.notify();
NoJS.router.push('/dashboard');
</script>
</template>
<template id="auth-err" var="err">
<p bind="err.message" animate="shake"></p>
</template>
</template>
<template route="/dashboard" guard="$store.auth.token" redirect="/login">
<div get="/me/dashboard" as="data">
<h1 bind="'Welcome, ' + data.user.name"></h1>
<div each="m in data.metrics">
<span bind="m.label"></span>
<span bind="m.value | number"></span>
</div>
</div>
<button on:click="$store.auth.user = null; $store.auth.token = null">
Sign out
</button>
</template>
</body>
</html>