Spec Reference
The spec is a plain JavaScript object that defines a complete contract for a page. Pulse validates every spec at startup and rejects invalid ones before the server accepts connections. At runtime, it enforces state bounds, validation rules, and lifecycle order automatically.
Quick reference
| Field | Type | Required | Description |
|---|---|---|---|
route | string | Yes | URL pattern for this page. Supports :param segments. |
state | object | Yes | Initial client-side state. Deep-cloned on mount. |
view | function | Yes | Returns an HTML string. Receives (state, serverState). |
meta | object | No | Page metadata: title, description, styles, OG tags, schema. |
hydrate | string | No | Browser-importable path to this spec file. Enables client hydration. |
mutations | object | No | Synchronous state updaters keyed by name. |
actions | object | No | Async operations with full lifecycle hooks. |
validation | object | No | Declarative validation rules keyed by dot-path state keys. |
constraints | object | No | Min/max bounds enforced after every mutation. |
persist | string[] | No | State keys to save in localStorage. |
server | object | No | Server-side data fetcher. Result passed to view as second arg. |
store | string[] | No | Global store keys this page subscribes to. See Global Store. |
methods | string[] | No | HTTP methods this page accepts. Default ['GET', 'HEAD']. Add 'POST' etc. to opt in. |
stream | object | No | Streaming SSR config: shell + deferred segment names. |
cache | object | No | HTTP cache control headers for the page response. |
serverTtl | number | No | Seconds to cache server data in-process. |
serverTimeout | number | No | Timeout in ms for all server fetchers on this page. Overrides the global fetcherTimeout option. |
contentType | string | No | Override response Content-Type. Enables raw (non-HTML) responses. |
onViewError | function | No | Fallback renderer called when view() throws. Return an HTML string. |
route
The URL pattern this spec handles. Supports static segments and dynamic :param segments.
route: '/products/:id' // matches /products/42
route: '/blog/:year/:slug'
Dynamic segments are available in server data and actions via ctx.params. See Routing for more.
state
The initial client-side state for the page. Always a plain object. Pulse deep-clones it on every mount — mutations never affect the original spec, and state cannot leak between page loads.
state: {
count: 0,
user: { name: '', email: '' },
items: [],
}
The state object is passed as the first argument to view, and as the first argument to every mutation and action hook. See State.
view
A pure function that receives (state, serverState) and returns an HTML string. Side effects are not permitted — the same inputs must always produce the same output. Pulse uses this guarantee to diff and re-render efficiently after mutations.
view: (state, server) => `
<main>
<h1>Hello, ${state.name}</h1>
${server.items.map(item => `<p>${item.title}</p>`).join('')}
</main>
`
For streaming SSR, view can be an object of named segment functions. See Streaming SSR.
meta
Page-level metadata. All fields are optional.
meta: {
title: 'Page Title — Site Name',
description: 'Meta description for search engines.',
styles: ['/app.css', '/page.css'],
ogTitle: 'Open Graph title',
ogImage: 'https://example.com/og.jpg',
schema: { '@type': 'WebPage', name: 'Page Title' }, // ld+json
}
See Metadata & SEO for the full reference.
hydrate
A browser-importable path to this spec file. Setting this enables client-side hydration — Pulse emits a bootstrap script that imports the spec bundle and calls mount(). In production, the path is resolved automatically via manifest.json.
hydrate: '/src/pages/counter.js' // dev: source file path
// Production: resolved automatically via manifest.json
hydrate for purely server-rendered pages with no client interactivity. Pulse sends zero JavaScript to the browser — no runtime overhead, no hydration cost.mutations
Synchronous state updaters. Each mutation is a function (state, event) => partialState. The returned partial object is merged into state. See Mutations.
mutations: {
increment: (state) => ({ count: state.count + 1 }),
setName: (state, event) => ({ name: event.target.value }),
}
Mutations can return _toast to show a notification — it is stripped from state automatically. See Toast notifications.
actions
Async operations with a full lifecycle. Each action has hooks for onStart, optional validate, run, onSuccess, and onError. See Actions.
actions: {
submit: {
onStart: (state, formData) => ({ status: 'loading' }),
validate: true,
run: async (state, serverState, formData) => {
const res = await fetch('/api/submit', { method: 'POST', body: formData })
return res.json()
},
onSuccess: (state, payload) => ({ status: 'success', data: payload }),
onError: (state, err) => ({
status: 'error',
errors: err?.validation ?? [{ message: err.message }],
}),
},
}
validation
Declarative rules checked when an action has validate: true. Keys are dot-path strings into state. See Validation.
validation: {
'fields.email': { required: true, format: 'email' },
'fields.name': { required: true, minLength: 2, maxLength: 100 },
'fields.age': { required: true, min: 18, max: 120 },
}
constraints
Min/max bounds enforced automatically after every mutation. Constraints cannot be bypassed — the state is clamped before the view re-renders, regardless of what the mutation returns. See Constraints.
constraints: {
count: { min: 0, max: 100 },
quantity: { min: 1, max: 99 },
}
persist
An array of dot-path state keys to persist in localStorage. Values are restored on the next visit. See Persist.
persist: ['theme', 'user.preferences']
server
Server-only data fetching. The result is passed to view as the second argument. Not available on the client. See Server Data.
server: {
data: async (ctx) => {
const product = await db.products.find(ctx.params.id)
return { product }
}
}
stream
Enables streaming SSR. Declare which named view segments are in the shell (rendered immediately) and which are deferred (streamed when ready). See Streaming SSR.
stream: {
shell: ['header', 'nav'],
deferred: ['feed', 'sidebar'],
}
cache
HTTP Cache-Control header configuration for the page response. See Caching.
cache: {
public: true,
maxAge: 300, // seconds
staleWhileRevalidate: 86400,
}
serverTtl
Number of seconds to cache the result of server.data() in-process. Subsequent requests within the TTL skip the async data fetch and re-render the HTML with the cached data. See Caching.
serverTtl: 60 // cache server data for 60 seconds
serverTtl caches only the server data fetcher results. The HTML is re-rendered on every request (good for personalised pages where only the fetched data is stable). cache caches the complete rendered HTML and sets Cache-Control headers (good for fully public pages that are identical for all users).serverTimeout
Timeout in milliseconds for all server.* fetchers on this page. If any fetcher does not resolve within this limit, it rejects with a timeout error and the request returns a 500. Use this to prevent a slow DB query or external API from hanging the response indefinitely.
serverTimeout: 5000 // fail after 5 s — overrides createServer fetcherTimeout
A global default applies to all pages via the fetcherTimeout option in createServer. spec.serverTimeout overrides it per page.
onViewError
An optional function called when view() throws at runtime. Return an HTML string to display in place of the crashed view. Without this, the Pulse runtime renders a default inline error message and logs the error to the console.
onViewError: (err, state, serverState) => `
<div class="u-p-4 u-text-center">
<p>Something went wrong. <a href="">Reload</a></p>
</div>
`
onViewError is defined — in which case the page renders with the fallback HTML and a 200 status. On the client, the runtime always catches view errors and shows a fallback, whether or not onViewError is defined.methods
HTTP methods this page accepts. Defaults to ['GET', 'HEAD'] — all other methods return 405. Add 'POST' to handle form submissions or webhooks directly on a page route without a separate API endpoint.
methods: ['GET', 'POST']
Read the method in guard to branch on POST vs GET:
export default {
route: '/contact',
methods: ['GET', 'POST'],
guard: async (ctx) => {
if (ctx.method === 'POST') {
const data = await ctx.formData()
if (!data?.email) return { status: 422, json: { error: 'Email required' } }
await db.leads.create(data)
return { redirect: '/contact?sent=1' }
}
},
state: {},
view: (state) => `<form method="POST">...</form>`,
}
contentType set), all HTTP methods are accepted by default — methods has no effect. Use ctx.method inside render to branch.contentType
Override the response Content-Type. When set, the view function receives (ctx, serverState) and returns the raw response body — the normal HTML wrapper is bypassed. See Raw Responses.
contentType: 'application/rss+xml'