Unofficial Datastar SDK for Fastify - Build reactive web applications with Server-Sent Events.
This package provides a Node.js/Fastify SDK for working with Datastar.
| Field | Value |
|---|---|
VERSION |
2.0.1 |
VERSION_date |
05/16/26 |
VERSION_mess |
Uses Datastar 1.0.1 |
- Node.js 20+ (required by Fastify 5)
- Tested with Node.js 24+
- Fastify 5.x
- Datastar 1.0.1 (client-side)
# Install from GitHub
npm install github:johntom/datastar-fastify-sdk
# Or add to package.json
"@johntom/datastar-fastify": "github:johntom/datastar-fastify-sdk"Import examples:
const { datastar } = require('@johntom/datastar-fastify');
const { PatchMode } = require('@johntom/datastar-fastify');
const { datastar, GetSSE, PostSSE, escapeHtml, PatchMode } = require('@johntom/datastar-fastify');
const { datastar, PostSSE, DeleteSSE, PatchMode, escapeHtml } = require('@johntom/datastar-fastify');const Fastify = require('fastify');
const { datastar, GetSSE, PostSSE } = require('@johntom/datastar-fastify');
const app = Fastify({ logger: true });
// Register the Datastar plugin
app.register(datastar);
// Serve HTML with Datastar attributes
app.get('/', async (request, reply) => {
const html = `
<!DOCTYPE html>
<html>
<head>
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.1/bundles/datastar.js"></script>
</head>
<body>
<div data-signals='{"count": 0}'>
<p>Count: <span data-text="$count">0</span></p>
<button data-on:click="${GetSSE('/api/increment')}">Increment</button>
</div>
</body>
</html>
`;
reply.type('text/html').send(html);
});
// Handle Datastar requests with SSE responses
app.get('/api/increment', async (request, reply) => {
const { signals } = await request.readSignals();
const newCount = (signals?.count || 0) + 1;
await reply.datastar((sse) => {
sse.patchSignals({ count: newCount });
});
});
app.listen({ port: 3000 });app.register(datastar, {
defaultRetryDuration: 1000 // Default SSE retry duration in ms
});Reads Datastar signals from the request (query params for GET, body for POST/PUT/PATCH/DELETE).
const { success, signals, error } = await request.readSignals();
if (success) {
console.log(signals.count);
}Returns true if the request includes the datastar-request: true header.
Starts an SSE stream that auto-closes after the callback completes.
await reply.datastar((sse) => {
sse.patchSignals({ count: 1 });
sse.patchElements('<div id="output">Updated!</div>');
});Options:
onError(error)- Error callbackonAbort()- Connection abort callbackkeepAlive- Keep stream open after callback (default:false)
Starts a persistent SSE stream that must be manually closed. This is the equivalent of Go SDK's datastar.NewSSE(w, r) - use it for real-time features like chat, notifications, live updates.
const sse = reply.datastarStream({
onAbort: () => console.log('Client disconnected')
});
// Send updates over time
sse.patchSignals({ status: 'processing' });
// Later...
sse.close();Options:
onAbort()- Called when client disconnects (cleanup subscriptions, timers, etc.)
Comparison with Go SDK:
| Go SDK | Fastify SDK |
|---|---|
sse := datastar.NewSSE(w, r) |
const sse = reply.datastarStream() |
sse.MarshalAndPatchSignals(...) |
sse.patchSignals(...) |
sse.PatchElementTempl(...) |
sse.patchElements(...) |
sse.ExecuteScript(...) |
sse.executeScript(...) |
sse.Send(eventType, dataLines, opts) |
sse.send(eventType, dataLines, options) |
| N/A | sse.sendEvent(eventType, data, options) |
<-r.Context().Done() |
{ onAbort: () => {...} } |
Use datastarStream() for persistent connections where you need to push updates over time:
// Simple PubSub for broadcasting
class PubSub {
constructor() { this.subs = new Set(); }
subscribe(cb) { this.subs.add(cb); return () => this.subs.delete(cb); }
publish(data) { this.subs.forEach(cb => cb(data)); }
}
const messageFeed = new PubSub();
// SSE endpoint - persistent connection
fastify.get('/sse', async (request, reply) => {
let unsubscribe = null;
let heartbeat = null;
// Create persistent SSE stream (like Go's datastar.NewSSE)
const sse = reply.datastarStream({
onAbort: () => {
// Cleanup when client disconnects
if (heartbeat) clearInterval(heartbeat);
if (unsubscribe) unsubscribe();
console.log('Client disconnected');
}
});
// Subscribe to message feed
unsubscribe = messageFeed.subscribe((msg) => {
if (sse.isClosed) return;
// Push update to client using SDK methods
sse.patchElements(`<div class="message">${msg.text}</div>`, {
selector: '#messages',
mode: 'append'
});
sse.executeScript('document.getElementById("messages").scrollTop = document.getElementById("messages").scrollHeight');
});
// Heartbeat to keep connection alive
heartbeat = setInterval(() => {
if (!sse.isClosed) {
sse.reply.raw.write(': heartbeat\n\n');
}
}, 15000);
});
// Post a message - broadcasts to all connected clients
fastify.post('/send', async (request, reply) => {
const { signals } = await request.readSignals();
// Broadcast to all subscribers
messageFeed.publish({ text: signals.message });
// Clear input for sender
await reply.datastar((sse) => {
sse.patchSignals({ message: '' });
});
});Frontend (connects via EventSource):
<div data-signals='{"message": ""}'>
<div id="messages"></div>
<input data-bind="message" data-on:keydown.enter="$message && @post('/send')" />
<button data-on:click="$message && @post('/send')">Send</button>
</div>
<script>
// Connect to SSE feed
const feed = new EventSource('/sse');
// Listen for Datastar events
feed.addEventListener('datastar-patch-elements', (e) => {
// Parse and apply (or let Datastar handle natively)
});
</script>Key Differences from reply.datastar():
reply.datastar() |
reply.datastarStream() |
|---|---|
| Auto-closes after callback | Stays open until sse.close() or client disconnect |
| For request/response patterns | For real-time push updates |
| One-shot updates | Multiple updates over time |
Patches HTML elements into the DOM.
sse.patchElements('<div id="content">Hello!</div>');
// With options
sse.patchElements('<li>New item</li>', {
selector: '#list',
mode: 'append' // outer, inner, replace, prepend, append, before, after, remove
});
// SVG (or mathml) namespace — added in Datastar 1.0.0-RC.7
sse.patchElements('<circle cx="10" cy="10" r="5" fill="red"/>', {
selector: '#vis',
mode: 'append',
namespace: 'svg' // 'html' (default), 'svg', 'mathml'
});Updates client-side signals.
sse.patchSignals({ count: 42, message: 'Hello' });
// Only set if not already present
sse.patchSignals({ defaults: true }, { onlyIfMissing: true });Executes JavaScript in the browser.
sse.executeScript('alert("Hello!")');
sse.consoleLog('Debug message');Removes elements from the DOM.
sse.removeElements('#temporary-message');Redirects the browser.
sse.redirect('/dashboard');
sse.redirectf('/users/%s', userId);Send custom SSE events (matches Go SDK's Send method). Use this for event types not covered by the standard Datastar methods.
// Send a custom event with raw data lines
sse.send('connected', ['{"clientId": "abc123", "boardId": "board1"}']);
// Send with options
sse.send('notification', ['{"message": "Hello"}'], { eventId: '123' });Convenience method that auto-serializes data to JSON.
// Send a custom event with object data (auto-serialized)
sse.sendEvent('connected', { clientId: 'abc123', boardId: 'board1' });
// Send card lock notification
sse.sendEvent('card-locked', { cardId: '123', userName: 'John' });
// Send with options
sse.sendEvent('user-joined', { name: 'Alice' }, { eventId: 'evt-001' });Comparison with Go SDK:
| Go SDK | Fastify SDK |
|---|---|
sse.Send(eventType, dataLines, opts) |
sse.send(eventType, dataLines, options) |
| N/A | sse.sendEvent(eventType, data, options) (convenience) |
Template helpers for generating Datastar attributes:
const { GetSSE, PostSSE, PutSSE, PatchSSE, DeleteSSE, escapeHtml } = require('@johntom/datastar-fastify');
// Generate action attributes
GetSSE('/api/data') // "@get('/api/data')"
PostSSE('/api/submit') // "@post('/api/submit')"
// With format strings
GetSSE('/api/users/%s', userId)
// HTML escaping
escapeHtml('<script>alert("xss")</script>')Run the included examples:
# Basic counter and message demo
npm run example
# Full TodoMVC implementation
npm run example:todoUnlike the Datastar Go SDK, this Fastify SDK does not bundle compression. The recommended approach is to layer the official @fastify/compress plugin in front of it:
const Fastify = require('fastify');
const compress = require('@fastify/compress');
const { datastar } = require('@johntom/datastar-fastify');
const app = Fastify();
await app.register(compress, {
encodings: ['br', 'gzip', 'deflate'],
threshold: 0,
customTypes: /^text\/event-stream$/,
});
app.register(datastar);Two settings are easy to miss and both matter for SSE:
threshold: 0—@fastify/compressskips small payloads by default. SSE events are typically small, so without this they ship uncompressed.customTypes: /^text\/event-stream$/— the default content-type filter excludes event streams, so they need to be opted in explicitly.
@fastify/compress negotiates the codec via the request's Accept-Encoding header, matching the Go SDK's behavior.
When this isn't enough: @fastify/compress doesn't expose per-event flush controls, so very latency-sensitive feeds may experience buffering. If that becomes a problem, full SDK-level compression with per-event flushing is tracked in todo.md (Option B).
The test server emulates the Datastar SDK for Go test suite, providing the same functionality as the Go version but implemented in Node.js with Fastify.
-
testserver.js - Main test server that mirrors the Go implementation
- Listens on port 7331 (configurable via TEST_PORT env var)
- Handles POST requests to /test endpoint
- Processes three event types:
patchElements- Patches HTML elements into the DOMpatchSignals- Updates client-side signalsexecuteScript- Executes JavaScript in the browser
- Supports all options: selector, mode, useViewTransition, onlyIfMissing, autoRemove, attributes, eventId, retryDuration
- Handles multiline scripts and signals (using signals-raw field)
-
test-request.js - Test client to verify the server works correctly
- Tests all three event types
- Tests multiple events in a single request
- Successfully validated all functionality
- Signal Reading: Uses
request.readSignals()from the Fastify SDK - SSE Streaming: Uses
reply.datastar()to create SSE connections - Event Processing: Handles arrays of events sequentially
- Connection Monitoring: Checks if SSE connection is closed before processing each event
- Error Handling: Proper error responses with appropriate HTTP status codes
- Logging: Uses Fastify's built-in logger
# Run automated tests (starts server, runs tests, stops server)
npm test
# Or manually:
# 1. Start the test server in one terminal
npm run testserver
# 2. In another terminal, run test requests
node test-request.js
# Custom port
TEST_PORT=8080 npm run testserverMIT
