You wrote an EventSource client, a fetch-stream reader, or a progressive-rendering list. Now you need a URL that actually streams β and the real streaming backend is, naturally, the part that doesn't exist yet. Faking text/event-stream by hand means standing up a server, getting the framing right (event:/data:/blank line, ids, retry), and keeping it running somewhere your teammates and CI can reach.
Mockbird does it with a query parameter: add ?mock_sse=1 to any mock list endpoint and the records stream as Server-Sent Events β one event per record, paced on a timer, closed with a done event. No signup, no server, no code.
Run this against the public demo project (the -N flag turns off curl's buffering so you see events arrive live):
curl -N 'https://HOST/m/demo/products?mock_sse=1&limit=3&mock_stream_interval=300'
You get correctly-framed SSE, one event every 300 ms:
retry: 60000
id: 1
event: products
data: {"id":1,"name":"Money Light Mountain Water Story","price":280.06, β¦}
id: 2
event: products
data: {"id":2,"name":"World World Bridge","price":901.93, β¦}
id: 3
event: products
data: {"id":3,"name":"Eye Member Member Area Money","price":478.72, β¦}
event: done
data: {"total":30,"sent":3}
The event name is the resource name, the id: field increments from 1 (so lastEventId works), and the final done event tells you the total so your client knows the stream ended on purpose.
const es = new EventSource("https://HOST/m/demo/products?mock_sse=1&limit=10");
es.addEventListener("products", (e) => {
render(JSON.parse(e.data)); // called once per record, 500 ms apart
});
es.addEventListener("done", (e) => {
console.log(JSON.parse(e.data)); // {"total":30,"sent":10}
es.close(); // important β see the reconnect note below
});
es.onerror = () => showRetryBanner();
done handler. Auto-reconnect is EventSource's defining behavior: when the stream ends, the browser will reconnect and replay unless you close(). (The stream sends retry: 60000 so an accidentally-forgotten tab reconnects lazily, not in a hot loop β but your client should still close.) That reconnect behavior is itself worth testing: kill the listener mid-stream and verify your UI survives the replay.Not every streaming client is EventSource. If you're testing a fetch() + ReadableStream reader (the pattern used for newline-delimited JSON APIs), use ?mock_stream=1 instead β same records, one JSON object per line, Content-Type: application/x-ndjson:
curl -N 'https://HOST/m/demo/products?mock_stream=1&limit=5'
const res = await fetch("https://HOST/m/demo/products?mock_stream=1&limit=20");
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buf = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += value;
const lines = buf.split("\n"); buf = lines.pop();
for (const line of lines) if (line) render(JSON.parse(line));
}
?mock_stream=sse is an alias for mock_sse=1 if you prefer one parameter.
The demo is read-mostly. Your own project takes one curl and streams the same way:
> curl -s -X POST https://HOST/api/projects \
-H 'content-type: application/json' \
-d '{"name":"my-stream","preset":"ecommerce"}'
# β {"id":"abc123xyz9","adminKey":"β¦", β¦}
> curl -N 'https://HOST/m/abc123xyz9/orders?mock_sse=1&sortBy=createdAt&order=desc'
Everything composes before streaming: filters (?status=shipped, price_gte=100), full-text q=, sortBy/order, page/limit, field projection (select=name,price), nested routes (/products/1/reviews?mock_sse=1 streams one product's reviews), even snapshot pinning. Whatever the query would have returned as an array is what streams.
| What you're testing | URL |
|---|---|
| Slow trickle (skeleton screens, "still loading" states) | ?mock_sse=1&mock_stream_interval=2000 |
| Firehose (does your renderer batch, or thrash?) | ?mock_sse=1&mock_stream_interval=0&limit=100 |
| Slow first byte (connection spinner) | ?mock_sse=1&mock_delay=3000 |
Server rejects the stream β onerror fires | ?mock_sse=1&mock_status=503 |
| Stream sometimes fails (reconnect logic) | ?mock_sse=1&mock_chaos=0.3 |
mock_stream_interval is the per-record gap in ms (default 500, max 5000; total stream time is capped at 60 s, so the interval shrinks automatically for big pages). mock_status and mock_chaos win over streaming β they return a normal JSON error with that status, which is exactly what makes EventSource.onerror and reconnect paths fire. More failure-testing recipes here.
If your project is in protected mode, there's a wrinkle: EventSource can't send an Authorization header β the browser API simply has no option for it. So protected projects also accept the JWT as a query parameter on mock endpoints:
new EventSource(`https://HOST/m/<id>/orders?mock_sse=1&mock_token=${jwt}`);
The token value is redacted in the request inspector, and this works on any mock endpoint (handy for <img>-adjacent cases too). Get a JWT from the project's own mock login endpoint.
MSW added first-class SSE mocking in v2.12.0 (sse() handler) and it's genuinely good β type-safe, standards-based, and it can intercept an EventSource pointed at your real URL. If you're writing JS unit tests, use it. The trade-off is the same as all in-process mocking: the mock exists only inside your JS test process. curl, Postman, a mobile app, a backend consumer, a teammate, or CI hitting a deployed preview get nothing β and the handlers are code you write and maintain. A hosted URL needs no process at all. (They compose fine: MSW in unit tests, a Mockbird URL in everything else.)
mocksse and similar npm packages stub the EventSource class in tests β same in-process scope, EventSource only, no NDJSON, no real network.
Roll your own β a 30-line Express/Flask server writing text/event-stream works, and is the right call if you need custom event logic. You're now running and deploying a server so CI and teammates can reach it, which is the thing you were trying to avoid.
Hosted mock services β as of July 2026 we could not find hosted SSE mock endpoints in WireMock Cloud, Postman mock servers, mockapi.io, or Mockoon Cloud (Beeceptor's streaming support targets gRPC). If you know of one, tell us and we'll list it here.
done. It is not a pub/sub broker: a write that happens mid-stream won't be pushed into an open connection (a reconnect will pick it up).curl -s -X POST https://HOST/api/projects \
-H 'content-type: application/json' \
-d '{"name":"my-api","preset":"ecommerce"}'
β¦or use the one-click dashboard link β no terminal required. Every list endpoint you create streams with ?mock_sse=1 from the first second. See the streaming docs for the full parameter reference.