AI APIs & Structured Outputs· Lesson 2 of 7
- 1The First Call — Reaching the Model From Code7 min
- 2Streaming — The Reply Arrives Word by Word7 min
- 3Structured Outputs — JSON You Can Rely On8 min
- 4Errors & Retries — When the Call Fails9 min
- 5Rate Limits — Your Ceiling per Minute6 min
- 6API Keys & Secrets — Guarding Your Key6 min
- 7Timeouts & Idempotency — Requests That Don't Double-Fire8 min
Streaming — The Reply Arrives Word by Word
In one line · Instead of waiting for the whole reply to finish, streaming sends it to you token by token as it's generated — that ChatGPT-style typing feel.
Users feel a product is far faster when text appears immediately, even if the total time is the same. It transforms the experience of any chat.
It's live broadcast versus downloading a whole video: instead of staring at a spinner until everything's ready, you see the first sentence while the rest is still coming.
Like a simultaneous interpreter: they don't wait for the whole speech to end before translating — they hand you word after word as it comes, so you follow from the first second.
You set stream: true, so instead of one response you get a series of SSE events, each a data: line carrying a small text delta — you concatenate and render them as they arrive until a [DONE] line signals the end.
You add stream: true to the request, so instead of one JSON blob the connection stays open and the server sends SSE events: each data: line carries a small chunk with a text delta inside. You read the body as a stream, split it into lines, parse each chunk, and append the deltas in the UI as they arrive. When the [DONE] line shows up you close and treat the reply as complete — any drop before it means what you have is partial.
const res = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ model: "model-name", stream: true, messages }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let text = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split("\n")) {
if (!line.startsWith("data: ") || line.includes("[DONE]")) continue;
const delta = JSON.parse(line.slice(6)).choices[0].delta.content ?? "";
text += delta; // render incrementally here
}
}🧪 Try it yourself: set stream: true and render the reply word by word in a terminal or simple page until [DONE] arrives. Then cut the connection mid-stream (with AbortController) and see how your code handles the partial reply.
(1) Buffering the whole stream into a variable and rendering at the end — you paid streaming's complexity with zero user benefit. (2) Not handling a mid-stream drop: the connection can die halfway, and storing that fragment as a complete reply corrupts your data. (3) Calling JSON.parse on the whole body — a stream is separate SSE lines, not one JSON document.
Any chat or long reply shown to a user waiting at the screen — an early first word changes how the whole product feels.
Background batch jobs, or replies your code parses whole such as structured JSON — there non-streaming is far simpler and nobody is watching anyway.