Streaming SSE con ChatGPT y Claude en Node.js: Respuestas en Tiempo Real
Implementa streaming de respuestas de IA con Server-Sent Events (SSE) en Node.js. Tutorial paso a paso con OpenAI, Claude y Express. Código listo para producción.
Tabla de contenidos
Cuando tu aplicación llama a la API de OpenAI o Claude sin streaming, el usuario ve una pantalla en blanco durante 5-15 segundos hasta que el modelo termina de generar toda la respuesta. Con streaming, el texto aparece token a token en tiempo real — exactamente como en chat.openai.com.
En este tutorial implementamos streaming completo: desde la API de IA hasta el navegador del usuario, usando Server-Sent Events (SSE) con Node.js y Express.
Arquitectura
[Navegador] ←SSE← [Express/Node.js] ←stream← [API OpenAI/Claude]
EventSource Tu servidor Modelo de IA
El flujo es: tu servidor abre un stream con la API de IA, recibe tokens incrementales, y los reenvía al navegador como eventos SSE. El navegador los pinta conforme llegan.
Paso 1: Streaming desde la API de OpenAI
import OpenAI from 'openai';
const openai = new OpenAI();
async function* streamOpenAI(messages) {
const stream = await openai.chat.completions.create({
model: 'gpt-4.1-mini',
messages,
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) yield content;
}
}
stream: true cambia la respuesta de un objeto JSON completo a un flujo de chunks. Cada chunk contiene un delta con el siguiente fragmento de texto.
Paso 2: Streaming desde la API de Claude
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
async function* streamClaude(messages) {
const stream = anthropic.messages.stream({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
messages,
});
for await (const event of stream) {
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
yield event.delta.text;
}
}
}
Si todavía no tienes acceso a la API, revisa cómo usar la API de ChatGPT y Claude gratis — tienes $5 de créditos al registrarte en ambas plataformas.
Paso 3: Endpoint SSE con Express
Server-Sent Events es el estándar HTTP para streaming unidireccional. El servidor envía eventos, el navegador los recibe con EventSource. No necesitas WebSockets.
import express from 'express';
const app = express();
app.use(express.json());
app.post('/api/chat', async (req, res) => {
const { messages, provider = 'openai' } = req.body;
// Cabeceras SSE
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // Desactiva buffering en Nginx/proxies
});
try {
// Elegir proveedor
const streamer = provider === 'claude'
? streamClaude(messages)
: streamOpenAI(messages);
for await (const token of streamer) {
// Formato SSE: "data: contenido\n\n"
res.write(`data: ${JSON.stringify({ token })}\n\n`);
}
// Señal de fin
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
res.end();
} catch (error) {
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
});
app.listen(3000, () => console.log('Server en http://localhost:3000'));
¿Por qué SSE y no WebSockets?
| Característica | SSE | WebSockets |
|---|---|---|
| Dirección | Servidor → Cliente | Bidireccional |
| Protocolo | HTTP estándar | Protocolo propio (ws://) |
| Reconexión | Automática | Manual |
| Proxies/CDN | Funciona sin config | Puede requerir config |
| Complejidad | Baja | Media-alta |
| Para streaming IA | ✅ Perfecto | ⚠️ Overkill |
El streaming de IA es unidireccional: la IA responde, el usuario lee. SSE es la herramienta exacta para esto.
Paso 4: Cliente en el navegador
async function streamChat(messages, onToken, onDone) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Guardar línea incompleta
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = JSON.parse(line.slice(6));
if (data.done) {
onDone();
return;
}
if (data.error) {
throw new Error(data.error);
return;
}
onToken(data.token);
}
}
}
// Uso en tu UI
const outputEl = document.getElementById('output');
outputEl.textContent = '';
streamChat(
[{ role: 'user', content: '¿Qué es Node.js?' }],
(token) => { outputEl.textContent += token; }, // cada token
() => { console.log('Respuesta completa'); } // fin
);
Nota: Usamos
fetch+ReadableStreamen vez deEventSourceporqueEventSourcesolo soporta GET y necesitamos enviar el body con POST.
Paso 5: Efecto de escritura (typewriter)
Para un efecto visual profesional, renderiza con un pequeño delay entre tokens:
function createTypewriter(element) {
const queue = [];
let isProcessing = false;
async function process() {
if (isProcessing) return;
isProcessing = true;
while (queue.length > 0) {
const token = queue.shift();
element.textContent += token;
element.scrollIntoView({ behavior: 'smooth', block: 'end' });
// Micro-delay para efecto visual (los tokens llegan más rápido de lo que se lee)
await new Promise(r => setTimeout(r, 15));
}
isProcessing = false;
}
return {
add(token) {
queue.push(token);
process();
}
};
}
const typewriter = createTypewriter(document.getElementById('output'));
streamChat(
[{ role: 'user', content: 'Explica qué es SSE' }],
(token) => typewriter.add(token),
() => console.log('Hecho')
);
Manejo de errores en producción
Desconexión del cliente
Si el usuario cierra la pestaña mientras se genera la respuesta, debes abortar el stream para no desperdiciar tokens:
app.post('/api/chat', async (req, res) => {
const abortController = new AbortController();
// Si el cliente se desconecta, abortar el stream de la IA
req.on('close', () => {
abortController.abort();
});
// OpenAI soporta signal para abortar
const stream = await openai.chat.completions.create({
model: 'gpt-4.1-mini',
messages: req.body.messages,
stream: true,
}, { signal: abortController.signal });
// ... continúa con el streaming
});
Timeout y reconexión
// Cliente: reintentar si el stream se corta
async function streamWithRetry(messages, onToken, onDone, maxRetries = 2) {
let fullText = '';
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
await streamChat(
messages,
(token) => {
fullText += token;
onToken(token);
},
onDone
);
return; // Éxito
} catch (error) {
if (attempt === maxRetries) throw error;
console.warn(`Stream cortado, reintentando (${attempt + 1})...`);
}
}
}
Si los reintentos generan errores 429, necesitas implementar backoff exponencial. Consulta la guía completa de Error 429 en APIs de IA.
Streaming + JSON estructurado
¿Necesitas streaming Y respuesta en JSON? Puedes hacer streaming del texto y parsear al final:
let fullResponse = '';
for await (const token of streamOpenAI(messages)) {
fullResponse += token;
// Enviar token al cliente para visualización
res.write(`data: ${JSON.stringify({ token })}\n\n`);
}
// Al terminar, parsear el JSON completo
import { parseAIJson } from './utils.js'; // de nuestro artículo de parseo JSON
const data = parseAIJson(fullResponse);
Para técnicas avanzadas de parseo de JSON de IA (incluyendo streaming), revisa el artículo dedicado a parsear JSON de IA sin errores.
Alternativa: NestJS para producción
Si tu backend es más complejo (auth, base de datos, múltiples endpoints), Express se queda corto. NestJS ofrece estructura, inyección de dependencias y soporte nativo para SSE:
// NestJS: SSE nativo con @Sse()
@Controller('chat')
export class ChatController {
@Post('stream')
@Sse()
stream(@Body() dto: ChatDto): Observable<MessageEvent> {
return new Observable((subscriber) => {
const streamer = this.aiService.stream(dto.messages);
(async () => {
for await (const token of streamer) {
subscriber.next({ data: { token } } as MessageEvent);
}
subscriber.complete();
})();
});
}
}
¿Por qué NestJS en vez de Express para SaaS? Lo explico en detalle en por qué NestJS sobre Express para un SaaS.
Checklist de implementación
- Endpoint SSE con cabeceras correctas (
text/event-stream,no-cache) - Desactivar buffering en proxy (
X-Accel-Buffering: no) - Abortar stream si el cliente se desconecta
- Señal de fin de stream (
data: { done: true }) - Manejo de errores enviado como evento SSE (no como HTTP 500)
- Reconexión automática en el cliente
- Efecto typewriter para UX
Conclusión
El streaming con SSE transforma la experiencia de tu aplicación de IA: de “esperar 10 segundos sin feedback” a “ver la respuesta escribirse en tiempo real”.
La implementación es más sencilla de lo que parece: stream: true en la API, cabeceras SSE en Express, y ReadableStream en el cliente. Tres piezas que conectas en una tarde.
Si quieres conectar el streaming con herramientas externas (bases de datos, APIs, búsqueda web), el siguiente paso es implementar function calling.