Programming model
Datastar Kit is built around one idea: the server can remain the source of truth for interactive UI.
Instead of moving application state and rendering logic into a large browser app, you render HTML on the server, let Datastar send small requests from browser events, and return patches that update the current document. The result still feels interactive, but the important decisions stay close to your database, session, authorization, and domain code.
The loop
Most interactions follow the same loop:
- A page renders HTML from backend state.
- Datastar attributes describe browser behavior, such as
data-on:click="@post('/todos/add')". - The browser sends a Datastar action request.
- Your handler reads input, checks policy, and reads or mutates backend resources.
- The handler returns
reply.done(),reply.patch(...),reply.signals(...), orreply.stream(...). - Datastar applies the response in the browser.
Datastar Kit exists to make steps 2, 4, and 5 pleasant in TypeScript. It does not replace the rest of your application stack.
The four shapes
Think in these four handler shapes before reaching for individual APIs:
| Shape | What it does | Typical response |
|---|---|---|
| Page | Renders the first document or a route-level page. | reply.page(...) |
| Command | Accepts user intent and may mutate backend state. | reply.done() or a focused patch |
| Query | Reads current backend state for a region. | reply.patch(...) |
| Live view | Keeps a region aligned with current backend state. | reply.stream(...) |
Commands should be small and explicit. Queries and live views should render from current backend state, not from assumptions about the browser's last known state.
Signals are input, not authority
Datastar signals are browser-side values. They are useful for form fields, filters, loading flags, and local validation feedback. Treat them like any other user input:
- decode them with
read.signals(request); - validate them with your schema library when shape matters;
- use them to decide what the user requested;
- read trusted values from backend resources before making durable changes.
A signal can say "the user typed this title." It should not be the authority for "this issue belongs to this user" or "the current count is 42."
Patches are the UI contract
An element patch sends server-rendered HTML back to Datastar. For the common case, the returned element has the same stable id as the element already on the page:
const TodoCount = (props: { count: number }) => <output id="todo-count">{props.count}</output>
return reply.patch(<TodoCount count={count} />)Use explicit selectors only when the target is a container, a sibling position, multiple matches, or an element to remove. The element patches guide covers each mode.
Reconnect safety
For live views, use a current-state rule:
A client should recover by reconnecting and rendering the latest backend state.
That rule keeps streams simple. An invalidation only means "something changed"; the live handler reloads current state and renders a fresh patch.
Route naming
Clear route names make the model easier to maintain:
GET /todosrenders the page.POST /todos/addruns a command.POST /todos/:id/toggleruns a command.GET /todos/listreturns a query patch.GET /todos/livereturns a live stream.
Next: Runtime boundaries.