Toda demo de chatbot parece instantánea hasta que la publicás. La diferencia entre un juguete que imprime la respuesta completa después de ocho segundos de silencio y un producto que los usuarios aman es una sola técnica: stremear tokens a la pantalla a medida que el modelo los genera.
En este deep-dive te muestro cómo construir un chatbot IA con streaming usando Next.js y Vercel AI SDK (v7, vigente en 2026): el route handler, el hook useChat, el modelo de partes de mensaje y el checklist de producción que uso.
1. El Problema: Las Respuestas Bloqueantes Se Sienten Rotas
Un chatbot ingenuo espera el completion completo y recién ahí lo renderiza. Con modelos reales eso significa de 3 a 10 segundos de UI muerta: sin feedback, sin progreso, sin confianza. Los usuarios reintentan, envían doble o se van — y cada reintento te factura otra generación completa.
El Error Más Común
Tratar el endpoint de chat como un endpoint REST que devuelve JSON. El chat es un stream, no un par request/response. Si tu /api/chat acumula toda la respuesta antes de responder, reconstruiste lo peor de las demos de 2023.
El streaming arregla la percepción y la ingeniería: primer token en unos 300ms, tokens renderizándose en vivo, un solo round trip HTTP sobre un ReadableStream. Vercel AI SDK se encarga de esa plomería — helpers de servidor para producir el stream y el hook useChat para consumirlo.
2. Conceptos Mínimos (Cinco Minutos)
Solo necesitás seis ideas antes de tocar código. Todo lo demás en el SDK es una variación de estas, así que aprendelas una vez y el tutorial se lee solo.
UIMessage
ai · tipo del cliente
La forma de mensaje que tu UI posee: id, rol y un array parts con texto, tool calls y reasoning en orden de generación.
streamText
ai · núcleo del servidor
Ejecuta el modelo con streaming de tokens. Devuelve un resultado cuyo stream enviás al cliente — nunca esperes el texto completo.
convertToModelMessages
ai · adaptador
Quita metadata de UI como timestamps e info del remitente, convirtiendo UIMessage[] en el ModelMessage[] que el modelo espera.
createUIMessageStreamResponse
ai · transporte
Serializa el stream del modelo al protocolo de UI-message con toUIMessageStream y lo devuelve como respuesta HTTP con streaming.
useChat
@ai-sdk/react · hook
Estado del cliente para todo el chat: messages, sendMessage, status, error. Apunta a POST /api/chat por defecto — sin código fetch.
message.parts
renderizá esto, no .content
Cada mensaje se renderiza desde su array parts: hacé switch sobre part.type (texto, tools, reasoning) y renderizá en orden.
El Modelo Mental
El cliente posee UIMessage[] y lo POSTea a /api/chat. El servidor convierte a mensajes del modelo, streamText genera tokens, el stream de UI-message vuelve en un solo round trip HTTP y useChat agrega las partes en vivo. Primer token en unos 300ms.
3. El Tutorial, Paso a Paso
De un proyecto Next.js App Router vacío a un chat con streaming en seis pasos. Verifiqué cada import contra la documentación oficial de AI SDK v7 — useChat viene de @ai-sdk/react, nunca del eliminado ai/react.
🛠️ Pasos de Construcción
Paso 1 · Scaffold
Ejecutá pnpm create next-app@latest my-ai-app, aceptá App Router y Tailwind, luego cd my-ai-app. Node 22 o superior es obligatorio — el campo engines del paquete ai lo exige.
Paso 2 · Instalación
Ejecutá pnpm add ai @ai-sdk/react zod. El provider de Gateway viene dentro de ai; agregá @ai-sdk/openai solo si llamás a OpenAI directo.
Paso 3 · API key
Creá .env.local con AI_GATEWAY_API_KEY=xxxxxxxxx para acceder a cientos de modelos con una sola key. Nunca subas este archivo a git.
Paso 4 · Route handler
Creá app/api/chat/route.ts: parseá UIMessage[], llamá streamText, devolvé createUIMessageStreamResponse. Código completo en la sección 4.
Paso 5 · UI del chat
Reemplazá app/page.tsx con un client component que use useChat y sendMessage, renderizando message.parts. Código completo en la sección 4.
Paso 6 · Ejecución
Ejecutá pnpm run dev, abrí http://localhost:3000, enviá un mensaje. Los tokens se renderizan en vivo — ese loop de streaming es toda la base.
🛡️ Endurecé Antes de Publicar
maxDuration = 30
Exportá maxDuration desde el archivo de ruta para que la función serverless viva lo suficiente para terminar el stream.
Rate limit en /api/chat
Diez requests por minuto por IP con sliding window de Upstash. Una URL de chat abierta sin límites es la API gratis de otro.
Exigí autenticación
Verificá la sesión antes de llamar streamText. Cualquier cliente que descubra la URL puede gastar tu presupuesto del modelo.
📚 Machete de la API
streamText
Generación del servidor con streaming
convertToModelMessages
UIMessage[] a ModelMessage[]
createUIMessageStreamResponse
Respuesta HTTP con streaming
toUIMessageStream
Stream del resultado a protocolo UI
useChat
messages, sendMessage, status, error
sendMessage
sendMessage({ text: input })
Tip: Gateway Primero, Provider Después
Empezá con los IDs de modelo string del AI Gateway y una sola API key. Cambiá a un provider directo solo cuando necesites opciones específicas — tu código de ruta casi no cambia.
4. Los Dos Archivos, Explicados
Dos archivos sostienen toda la funcionalidad. Leelos una vez y nunca más vas a copiar código de chatbot a ciegas — cada línea viene del quickstart oficial v7.
🖥️ Servidor: app/api/chat/route.ts
POST más UIMessage[]
Parseá { messages } del body del request. El historial le da contexto al modelo — sin base de datos para sesiones simples.
streamText
Llamalo con un ID de modelo y los mensajes convertidos. Tools y stopWhen entran después; la forma del streaming no cambia.
convertToModelMessages
El puente entre tipos de UI y tipos del modelo. Si lo omitís, el modelo se atraganta con metadata que nunca pidió.
toUIMessageStream
Convierte result.stream al protocolo UI-message que el hook entiende del otro lado.
maxDuration
Exportá maxDuration = 30 para que la plataforma no mate generaciones largas a mitad del stream.
💬 Cliente: app/page.tsx
useChat más sendMessage
El hook apunta a POST /api/chat por defecto. Un useState local guarda el input; sendMessage({ text: input }) dispara el round trip.
message.parts
Renderizá partes, nunca un string content. Hacé switch sobre part.type para que texto, tools y reasoning tengan su UI.
use client
La página debe ser un client component — la interactividad del streaming necesita JavaScript del navegador.
Status más error
El hook expone status y error. Conectalos a un indicador de escritura y un botón de reintento antes de publicar.
Código completo del servidor — app/api/chat/route.ts
import {
streamText,
UIMessage,
convertToModelMessages,
createUIMessageStreamResponse,
toUIMessageStream,
} from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: "xai/grok-4.6",
messages: await convertToModelMessages(messages),
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream }),
});
}Código completo del cliente — app/page.tsx
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
export default function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat();
return (
<div>
{messages.map((message) => (
<div key={message.id}>
{message.role === "user" ? "User: " : "AI: "}
{message.parts.map((part, i) => {
if (part.type === "text") {
return <div key={message.id + "-" + i}>{part.text}</div>;
}
return null;
})}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
}}
>
<input
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
/>
</form>
</div>
);
}¿Por qué no un fetch manual más useState?
Porque reimplementarías cancelación, batching de renders, recuperación de errores y el protocolo SSE a mano. useChat ya resuelve las cuatro — tu código queda en la capa de producto.
5. Errores Comunes (Todos de Migraciones Reales)
El SDK trajo cambios breaking en cada major desde v3, así que la mayoría de los tutoriales de 2024 hoy enseñan APIs eliminadas. Esta es la lista de correcciones que aplico al portar código viejo.
Importar useChat desde ai/react
Ese path se eliminó en v5 y nada compila. Importá { useChat } desde @ai-sdk/react — verificá la línea de import antes que nada.
Renderizar message.content
Los mensajes exponen un array parts. Mapeá message.parts y hacé switch sobre part.type — las partes de texto traen part.text en orden.
Devolver toDataStreamResponse
Los helpers de transporte de la era v4 ya no existen. Devolvé createUIMessageStreamResponse con toUIMessageStream({ stream: result.stream }).
Publicar /api/chat sin autenticación
Sin chequeo de sesión ni rate limit, cualquiera que encuentre la URL gasta tu presupuesto. Agregá ambas antes de cualquier deploy público.
Omitir maxDuration
Los timeouts por defecto pueden cortar streams largos a mitad de frase. Exportá maxDuration = 30 en el archivo de ruta y listo.
6. Checklist de Producción
Que funcione en local es el paso uno. Llegar a confiable a escala exige recorrer esta lista en orden:
✅ Antes de Publicar
- 1. API key en .env.local, nunca commiteada, validada al arrancar
- 2. La UI renderiza message.parts con texto y estados de tools, nunca .content
- 3. Indicador de escritura desde status del hook, botón de reintento desde error
- 4. Rate limit más auth en /api/chat antes de cualquier URL pública
📈 A Escala
- Timeouts: maxDuration = 30 en la ruta; reintentos con backoff o fallbacks del Gateway
- Persistencia: useChat es solo memoria — guardá los chats en Postgres o Supabase para historial entre sesiones
- Resume: Agregá streams reanudables para que recargar la página nunca pierda una generación en curso
🔁 Siguiente Nivel
- Tools: Agregá una primera tool con stopWhen para respuestas multi-paso con datos en vivo
- RAG: Embeddé la pregunta, recuperá chunks, inyectalos como contexto — el mismo pipeline de streaming
- Template: ¿Necesitás auth más persistencia más multimodal ya? Cloná el template oficial de chatbot
7. Cuándo Aplica Este Stack (y Cuándo No)
La capa UI del AI SDK es el camino más rápido al chat con streaming, pero no es para toda app. Calibrá antes de comprometerte:
✅ Usá AI SDK UI
- • Next.js App Router más UI de chat en React con streaming de tokens
- • Un provider hoy, quizás multi-provider mañana (el Gateway hace trivial el cambio)
- • Tools y agentes multi-paso como próximo hito natural
- • Prototipos que deben verse production-grade en una tarde
- • Equipos que quieren streaming, cancelación y errores resueltos — no hechos a mano
❌ Mejor evitalo
- • Apps legacy en Pages Router — el quickstart apunta a App Router
- • Backends no-Node (Django, Rails, Go): usá patrones Core, no el código de ruta de Next.js
- • Chatbots zero-code — cloná el starter template en vez de construir
- • Agentes background de larga duración — usá Workflows o Sandbox, no un hook de chat
- • Presupuestos de tokens estrictos sin control del backend — primero auth y límites
Regla de oro
Si los tokens no aparecen en pantalla unos 300ms después del envío, el bug está en tu transporte, no en tu modelo. Verificá que la ruta devuelva un stream real, que el cliente renderice partes y que nada acumule en el medio.
Conclusión
Ahora dominás todo el pipeline: UIMessage[] sale del navegador, convertToModelMessages lo adapta, streamText genera tokens, el stream de UI-message los trae de vuelta y useChat renderiza las partes en vivo. Dos archivos, sin WebSockets, sin parsear SSE a mano.
Publicá los seis pasos, aplicá el trío de endurecimiento (maxDuration, rate limit, auth) y corregí los cinco errores de migración si estás portando un tutorial viejo. Desde ahí, tools, persistencia y RAG se montan sobre la misma base — ese es el verdadero rédito de aprender la API actual en vez de copiar código de 2024.
Lo Que Construiste: Resumen
Servidor
- • app/api/chat/route.ts
- • pipeline de streamText
- • maxDuration = 30
Cliente
- • useChat + sendMessage
- • renderizado de message.parts
- • estados status + error
Próximos Pasos
- • Primera tool + stopWhen
- • Persistencia del chat
- • Retrieval con RAG
Fuentes
Cada nombre de API y patrón de arriba está verificado contra estas referencias (agosto–septiembre 2026):



