Skip to content

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

ts
// 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

ts
// 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

ts
// 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

ts
// 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:

ts
// 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 renderToResponse works in three runtimes.
  • Web standard primitivesRequest / Response cross the runtime boundary unchanged.
  • Hydration parity — markup keys generated by renderToResponse line up with hydrateMount() 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

Released under the MIT License.