SSR + hydration on Node, Bun, and Deno
bQuery ships runtime-agnostic SSR primitives. The same template renders identically under Node 24+, Bun 1.3+, and Deno 2, and the examples/ folder contains a runnable app per runtime.
Shared template
// shared/app.ts
export const template = `
<main>
<h1 bq-text="title"></h1>
<p bq-text="message"></p>
</main>
`;
export const initialData = (req: Request) => ({
title: 'Hello SSR',
message: `You requested ${new URL(req.url).pathname}`,
});Node
// examples/ssr-node/serve.ts
import { createServer } from 'node:http';
import { renderToResponse } from '@bquery/bquery/ssr';
import { template, initialData } from '../shared/app';
createServer(async (req, res) => {
const request = new Request(`http://localhost${req.url ?? '/'}`);
const response = await renderToResponse(template, await initialData(request));
res.writeHead(response.status, Object.fromEntries(response.headers));
res.end(await response.text());
}).listen(3000);Run with node --experimental-strip-types examples/ssr-node/serve.ts.
Bun
// examples/ssr-bun/serve.ts
import { renderToResponse } from '@bquery/bquery/ssr';
import { template, initialData } from '../shared/app';
Bun.serve({
port: 3000,
async fetch(request) {
return renderToResponse(template, await initialData(request));
},
});Run with bun examples/ssr-bun/serve.ts.
Deno
// examples/ssr-deno/serve.ts
import { renderToResponse } from '@bquery/bquery/ssr';
import { template, initialData } from '../shared/app.ts';
Deno.serve({ port: 3000 }, async (request) => {
return renderToResponse(template, await initialData(request));
});Run with deno run -A examples/ssr-deno/serve.ts.
Hydration
The client-side hydration code is identical across runtimes:
// src/entry-client.ts
import { hydrateMount } from '@bquery/bquery/ssr';
const data = JSON.parse(document.getElementById('__BQUERY__')!.textContent!);
hydrateMount('main', data, { hydrate: true });Inject data into the server response inside <script id="__BQUERY__" type="application/json">…</script> and the client picks it up on first paint.
What you exercised
- Runtime-agnostic SSR — the same
renderToResponseworks in three runtimes. - Web standard primitives —
Request/Responsecross the runtime boundary unchanged. - Hydration parity — markup keys generated by
renderToResponseline up withhydrateMount()automatically.
Cross-runtime CI
.github/workflows/ssr-cross-runtime.yml builds the library once and runs tests/cross-runtime/run.mjs against Node 24, Bun 1.3, and Deno 2 to guard this public surface. Copy that workflow into your own app to lock down runtime parity.
Next steps
- Add cache + streaming with Streaming SSR with
flushBoundary. - Layer a server with routing, body parsing, and WebSockets via the Backend API + WebSocket workflow.
- Deploy to an edge runtime using
createEdgeHandler.