CookbookStream in Node.js

Stream in Node.js

Node.js 18 and later ship fetch with streaming response bodies, so no library is needed. The second tab uses the openai package pointed at Syntic.

// stream.mjs  (node stream.mjs)
const res = await fetch('https://app.syntic.ai/v1/chat/completions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SYNTIC_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'Amara S1.2',
    messages: [{ role: 'user', content: 'Describe a lighthouse at night in four sentences.' }],
  }),
})
if (!res.ok) {
  console.error(res.status, await res.text())
  process.exit(1)
}
console.error('request id:', res.headers.get('x-syntic-request-id'))
 
const decoder = new TextDecoder()
let buffer = ''
let usage = null
let done = false
 
for await (const bytes of res.body) {
  buffer += decoder.decode(bytes, { stream: true })
  const lines = buffer.split('\n')
  buffer = lines.pop() // keep a partial line for the next chunk
  for (const line of lines) {
    if (!line.startsWith('data: ')) continue
    const payload = line.slice(6)
    if (payload === '[DONE]') { done = true; continue }
    const event = JSON.parse(payload)
    if (event.error) throw new Error(`stream failed: ${event.detail}`)
    const piece = event.choices?.[0]?.delta?.content
    if (piece) process.stdout.write(piece)
    if (event.usage) usage = event.usage
  }
}
process.stdout.write('\n')
if (!done) console.error('[stream ended early; reply is incomplete]')
console.error('usage:', usage)

Forward the stream to a browser

In a server route, pass text through as it arrives instead of collecting it first. With Express:

app.post('/api/ask', async (req, res) => {
  const upstream = await fetch('https://app.syntic.ai/v1/chat/completions', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.SYNTIC_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ model: 'Amara Quick', messages: [{ role: 'user', content: String(req.body.q) }] }),
  })
  if (!upstream.ok) return res.status(502).json({ error: 'upstream', status: upstream.status })
  res.setHeader('Content-Type', 'text/event-stream')
  res.setHeader('Cache-Control', 'no-cache')
  for await (const bytes of upstream.body) res.write(bytes)   // events pass through unchanged
  res.end()
})

The browser can then read /api/ask with the same parsing loop as the fetch tab. The API key stays on the server.

Notes

  • The buffer logic matters: a network chunk can end in the middle of a line, and parsing half an event throws.
  • Check res.ok before reading. Errors before the stream starts are ordinary JSON.
  • usage arrives near the end of the stream; keep the last value.
Was this page helpful?