Beyond JSON — Streams, Files, Redirects
Custom Server Handlers
Handlers can return any h3-compatible value — objects become JSON, ReadableStreams stream, raw Response objects pass through unchanged.
What you'll learn
- Return a streamed response for server-sent events
- Send a file as a downloadable stream
- Return a raw Response object for full control over the wire
Most handlers return a plain object and let Nitro serialize it as JSON. When that’s not enough —
SSE, file downloads, custom content types — handlers can return streams or raw Response objects.
Server-Sent Events
// server/api/events.ts
export default defineEventHandler((event) => {
const stream = new ReadableStream({
start(controller) {
let n = 0
const id = setInterval(() => {
controller.enqueue(`data: tick ${n++}\n\n`)
}, 1000)
event.node.req.on('close', () => {
clearInterval(id)
controller.close()
})
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
}) The browser-side EventSource('/api/events') will receive each tick.
Streaming Files
For large files, stream them instead of loading into memory:
// server/api/download.ts
import { createReadStream } from 'node:fs'
export default defineEventHandler((event) => {
setHeader(event, 'Content-Type', 'application/pdf')
setHeader(event, 'Content-Disposition', 'attachment; filename="report.pdf"')
return sendStream(event, createReadStream('/tmp/report.pdf'))
}) Note: node:fs only works on Node-based presets — skip this pattern on edge runtimes.
Raw Response
When you need full control over status, headers, and body shape, return a Web Response:
// server/api/csv.ts
export default defineEventHandler(() => {
const csv = 'id,name\n1,Ada\n2,Grace\n'
return new Response(csv, {
status: 200,
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': 'attachment; filename="users.csv"',
},
})
}) Web Streams Everywhere
ReadableStream and Response are Web standards — they work on Node, Bun, Deno, and Workers
without changes. Prefer them over Node-specific streams when targeting multiple runtimes.