Frameworks & Deployment
Express & Plain Node
Bridge Node's req/res to the Web-standard handler - streaming included, so SSE works.
Express and plain Node's http module predate the Web Request/Response
types, so they need a small bridge. Streaming the response body is what
makes /stream (SSE) work - don't buffer it.
Express
import express from "express";
import { chat } from "./chat";
const app = express();
const chatHandler = chat.handler();
app.use("/api/chat", async (req, res) => {
// req.originalUrl matters under Express mounts
const url = `http://${req.headers.host}${req.originalUrl}`;
const chunks = [];
for await (const c of req) chunks.push(c);
const request = new Request(url, {
method: req.method,
headers: Object.entries(req.headers).flatMap(([k, v]) =>
v === undefined ? [] : Array.isArray(v) ? v.map((x) => [k, x]) : [[k, v]],
),
body: chunks.length ? new Uint8Array(Buffer.concat(chunks)) : null,
});
const response = await chatHandler.fetch(request);
res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
if (response.body) {
const reader = response.body.getReader();
req.on("close", () => void reader.cancel().catch(() => {}));
for (;;) {
const { done, value } = await reader.read();
if (done) break;
res.write(value);
}
}
res.end();
});
app.listen(3000);The pieces that matter:
req.originalUrl- under an Express mount (app.use("/api/chat", ...)),req.urlhas the prefix stripped;originalUrlkeeps the full path the handler needs for routing.- Streaming the body with a reader loop -
res.write(value)per chunk is what lets SSE events flow as they happen. req.on("close")→reader.cancel()- cleans up the server-side stream when the client disconnects.
Plain Node
A complete, runnable plain-Node version - including in-memory or Postgres
storage and a full curl walkthrough with SSE reconnect -
lives at
examples/node-server.
Node 18+ has Request/Response globals built in - no polyfill needed (the repo requires
Node >= 18).