Tutorial

Construye tu primera Generative UI con Vercel AI SDK

Guía paso a paso para crear tu primera interfaz de IA con componentes en streaming.

A
Alex18 min de lectura

Prerrequisitos

Antes de empezar, asegúrate de tener:

  • Node.js 18+
  • Un proyecto Next.js 14+ con App Router
  • Una clave de API de OpenAI (o Anthropic — el SDK es compatible con ambos)
  • Conocimientos básicos de React Server Components

Si eres nuevo en RSC, dedica 15 minutos a la documentación oficial de Next.js sobre Server Components. La función streamUI del Vercel AI SDK está construida sobre RSC, y todo encajará en su lugar en cuanto entiendas ese modelo.

Qué vamos a construir

Crearemos un asistente de IA sencillo que genera interfaces interactivas a partir de los prompts del usuario. Al terminar este tutorial tendrás una funcionalidad de Generative UI en funcionamiento que:

  1. Recibe un prompt de texto del usuario
  2. Transmite componentes React desde el servidor mediante streaming
  3. Renderiza tarjetas y gráficos interactivos según las decisiones de la IA

Para el ejemplo usaremos un asistente financiero capaz de mostrar cotizaciones de acciones y datos meteorológicos — lo suficientemente simple para entenderlo rápido, y lo suficientemente complejo para mostrar patrones reales.

⚠️ AI SDK RSC y streamUI están marcados por Vercel como experimental. Para proyectos de producción, Vercel recomienda AI SDK UI (useChat de @ai-sdk/react). Este artículo muestra el patrón funcional de streaming con RSC para prototipos, demos y entornos controlados; para producción evalúa los trade-offs y consulta la sección «Cuándo Vercel AI SDK NO es tu opción». Guía de migración RSC → UI.

Paso 1: Instalar dependencias

npm install ai@^4 @ai-sdk/openai@^1 zod

Fijamos v4 — la última serie con la API RSC en formato parameters: e importación desde ai/rsc. Para v5+ consulta la nota sobre diferencias al final del artículo.

El paquete ai es el núcleo del Vercel AI SDK. @ai-sdk/openai es el proveedor de OpenAI (sustitúyelo por @ai-sdk/anthropic si prefieres Claude). zod se encarga de validar los parámetros de las herramientas — con él describes qué parámetros puede pasar la IA a cada componente.

Añade la clave de API a .env.local:

OPENAI_API_KEY=sk-...

Paso 2: Crear la biblioteca de componentes

Define los componentes que la IA podrá generar. Son componentes React normales — sin nada específico de IA. El principio de diseño clave: crea componentes útiles por sí mismos, y la IA podrá combinarlos libremente.

// components/weather-card.tsx
interface WeatherCardProps {
  city: string;
  temperature: number;
  conditions: string;
  humidity: number;
}

export function WeatherCard({ city, temperature, conditions, humidity }: WeatherCardProps) {
  return (
    <div className="rounded-lg border bg-card p-6 shadow-sm">
      <h3 className="text-lg font-semibold">{city}</h3>
      <div className="mt-2 flex items-baseline gap-2">
        <span className="text-4xl font-bold">{temperature}°C</span>
        <span className="text-muted-foreground">{conditions}</span>
      </div>
      <p className="mt-2 text-sm text-muted-foreground">
        Humidity: {humidity}%
      </p>
    </div>
  );
}
// components/stock-ticker.tsx
interface StockTickerProps {
  symbol: string;
  price: number;
  change: number;
  changePercent: number;
}

export function StockTicker({ symbol, price, change, changePercent }: StockTickerProps) {
  const isPositive = change >= 0;
  const sign = isPositive ? '+' : '';
  const color = isPositive ? 'text-green-600' : 'text-red-600';

  return (
    <div className="rounded-lg border bg-card p-6 shadow-sm">
      <div className="flex items-center justify-between">
        <h3 className="text-xl font-bold">{symbol}</h3>
        <span className={`text-sm font-medium ${color}`}>
          {sign}{changePercent.toFixed(2)}%
        </span>
      </div>
      <div className="mt-2 flex items-baseline gap-2">
        <span className="text-3xl font-bold">${price.toFixed(2)}</span>
        <span className={`text-sm ${color}`}>
          {sign}{change.toFixed(2)} today
        </span>
      </div>
    </div>
  );
}
// components/loading-skeleton.tsx
export function CardSkeleton({ height = 'h-32' }: { height?: string }) {
  return (
    <div className={`animate-pulse rounded-lg bg-muted ${height} w-full`} />
  );
}

Paso 3: Definir las herramientas de IA (Server Action)

Este es el corazón de Generative UI. Crea un server action que vincule tus componentes con la IA en forma de "herramientas" — funciones que el modelo puede invocar por su propia decisión:

// app/actions.tsx
'use server';

export const runtime = 'edge';

import { streamUI } from 'ai/rsc';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { WeatherCard } from '@/components/weather-card';
import { StockTicker } from '@/components/stock-ticker';
import { CardSkeleton } from '@/components/loading-skeleton';

export async function generateUI(prompt: string) {
  const result = await streamUI({
    model: openai('gpt-4o'),
    system: `You are a helpful financial and information assistant.
             Use the available tools to display information visually
             whenever possible. Prefer showing components over text responses.
             When asked about weather or stocks, always use the appropriate tool.`,
    prompt,
    tools: {
      showWeather: {
        description: 'Display current weather conditions for a city. Use this when the user asks about weather, temperature, or climate.',
        parameters: z.object({
          city: z.string().describe('The city name, e.g. "Paris" or "New York"'),
          temperature: z.number().describe('Current temperature in Celsius'),
          conditions: z.string().describe('Weather description, e.g. "Partly cloudy"'),
          humidity: z.number().min(0).max(100).describe('Relative humidity percentage'),
        }),
        generate: async function* (params) {
          // Yield a skeleton immediately while data "loads"
          yield <CardSkeleton height="h-36" />;
          // In a real app, you would fetch live weather data here
          return <WeatherCard {...params} />;
        },
      },
      showStock: {
        description: 'Display a stock price and daily change. Use this when the user asks about stock prices, market data, or a company\'s shares.',
        parameters: z.object({
          symbol: z.string().describe('Stock ticker symbol, e.g. "AAPL" or "TSLA"'),
          price: z.number().describe('Current stock price in USD'),
          change: z.number().describe('Price change today in USD'),
          changePercent: z.number().describe('Percentage price change today'),
        }),
        generate: async function* (params) {
          yield <CardSkeleton height="h-32" />;
          return <StockTicker {...params} />;
        },
      },
    },
  });

  return result.value;
}

Hay tres cosas importantes que entender en este código:

La función generate es un generador asíncrono. La palabra clave yield envía el skeleton inmediatamente — antes de que la IA termine de determinar los parámetros. return entrega el componente final. Así funciona el streaming en Generative UI.

Las descripciones de las herramientas son instrucciones para la IA. Los campos description son lo que el modelo lee para decidir qué herramienta invocar. Escríbelos con claridad, indicando cuándo se debe usar cada herramienta y cuándo no.

Los esquemas Zod fijan el contrato. Si defines esquemas Zod estrictos, la IA no podrá pasar parámetros inválidos. Los errores de validación se capturan antes del renderizado del componente.

Paso 4: Construir la interfaz

// app/page.tsx
'use client';

import { useState } from 'react';
import { generateUI } from './actions';

const EXAMPLE_PROMPTS = [
  "What's the weather like in Tokyo?",
  "Show me Apple's current stock price",
  "Compare the weather in London and New York",
  "How is Tesla stock doing?",
];

export default function Home() {
  const [prompt, setPrompt] = useState('');
  const [messages, setMessages] = useState<Array<{ prompt: string; ui: React.ReactNode }>>([]);
  const [loading, setLoading] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!prompt.trim() || loading) return;

    const currentPrompt = prompt;
    setPrompt('');
    setLoading(true);

    const ui = await generateUI(currentPrompt);
    setMessages(prev => [...prev, { prompt: currentPrompt, ui }]);
    setLoading(false);
  }

  return (
    <main className="mx-auto max-w-2xl p-8">
      <h1 className="text-3xl font-bold">Generative UI Demo</h1>
      <p className="mt-2 text-muted-foreground">
        Ask about weather or stocks — watch the AI generate the right interface.
      </p>

      {/* Example prompts */}
      <div className="mt-4 flex flex-wrap gap-2">
        {EXAMPLE_PROMPTS.map(p => (
          <button
            key={p}
            onClick={() => setPrompt(p)}
            className="rounded-full border px-3 py-1 text-sm hover:bg-muted"
          >
            {p}
          </button>
        ))}
      </div>

      {/* Prompt input */}
      <form onSubmit={handleSubmit} className="mt-6 flex gap-2">
        <input
          value={prompt}
          onChange={e => setPrompt(e.target.value)}
          placeholder="Ask anything..."
          className="flex-1 rounded-md border bg-background px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
        />
        <button
          type="submit"
          disabled={loading || !prompt.trim()}
          className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground disabled:opacity-50"
        >
          {loading ? 'Generating...' : 'Ask'}
        </button>
      </form>

      {/* Generated UI output */}
      <div className="mt-8 space-y-6">
        {messages.map((msg, i) => (
          <div key={i}>
            <p className="mb-2 text-sm font-medium text-muted-foreground">
              "{msg.prompt}"
            </p>
            {msg.ui}
          </div>
        ))}
      </div>
    </main>
  );
}

Paso 5: Ejecutar y probar

npm run dev

Prueba estos prompts en orden para ver comportamientos distintos:

  • "What's the weather in Paris?" — una sola tarjeta WeatherCard
  • "Show me Apple stock" — un ticker StockTicker
  • "Compare the weather in London and New York" — la IA invoca showWeather dos veces y genera dos tarjetas juntas
  • "How's Tesla doing and what's the weather in San Francisco?" — la IA invoca ambas herramientas y genera componentes de tipos distintos

El tercer prompt es la demostración clave: sin ningún código adicional, el modelo combina varios componentes por sí solo para responder a una pregunta compuesta.

Qué ocurre bajo el capó

Cuando envías un prompt:

  1. El cliente invoca el server action generateUI
  2. streamUI envía el prompt y las definiciones de herramientas a la API de OpenAI
  3. El modelo decide qué herramientas invocar y con qué parámetros
  4. La función generate de cada herramienta entrega el skeleton inmediatamente
  5. La IA termina de determinar los parámetros y se devuelve el componente final
  6. React reemplaza el skeleton por el componente listo

El protocolo de streaming RSC es lo que hace posible todo esto. El servidor serializa árboles de componentes React y los transmite al cliente en partes. Esto es fundamentalmente diferente de una API JSON — el cliente recibe componentes listos, no datos en bruto.

Manejo de errores

Los componentes generados pueden fallar donde los escritos a mano no lo harían. Es importante entender: un React error boundary solo captura errores de renderizado. Un fallo del stream (pérdida de red, timeout de OpenAI, error del tool call en el servidor) no lo capturará — ese escenario hay que manejarlo explícitamente en handleSubmit.

Protección en dos capas — try/catch alrededor del stream y error boundary alrededor del UI renderizado:

// components/genui-error-boundary.tsx
'use client';

import { Component, ReactNode } from 'react';

interface Props { children: ReactNode; fallback?: ReactNode }
interface State { hasError: boolean; error: Error | null }

export class GenUIErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? (
        <div className="rounded-lg border border-destructive/50 bg-destructive/5 p-4">
          <p className="text-sm text-destructive">
            This component could not render. The AI may have passed unexpected data.
          </p>
        </div>
      );
    }
    return this.props.children;
  }
}

Y los errores del propio stream se capturan en el manejador del cliente:

async function handleSubmit(e: React.FormEvent) {
  e.preventDefault();
  if (!prompt.trim() || loading) return;

  const currentPrompt = prompt;
  setPrompt('');
  setLoading(true);

  try {
    const ui = await generateUI(currentPrompt);
    setMessages(prev => [...prev, { prompt: currentPrompt, ui }]);
  } catch (err) {
    // Errores de red, timeouts de OpenAI, fallo del server action — todo llega aquí
    setMessages(prev => [...prev, {
      prompt: currentPrompt,
      ui: <div className="text-sm text-destructive">Stream failed. Try again.</div>,
    }]);
  } finally {
    setLoading(false);
  }
}

Envuelve el UI generado con GenUIErrorBoundary en la página — capturará errores de renderizado, y el try/catch se encarga del resto.

Cuándo Vercel AI SDK NO es tu opción

El SDK es bueno, pero no es la panacea. No lo uses si:

  • El SDK está marcado como experimental — limitaciones documentadas: imposible interrumpir el stream a través de server actions, los componentes se remontan en .done() (parpadeo), muchos <Suspense> boundaries pueden hacer crashear la página, createStreamableUI genera un volumen cuadrático de datos. Para producción, Vercel recomienda AI SDK UI.
  • No usas Next.js. streamUI está construido sobre React Server Components, que requieren Next.js App Router (o Waku u otro framework RSC). Para SPA con Vite, Remix sin RSC, Vue, Svelte, Angular — consulta las alternativas abajo.
  • Necesitas un UI fijo con datos dinámicos. Si la interfaz está definida de antemano y el LLM solo se necesita para los datos, usa generateObject normal + tu código React estático. Generative UI tiene sentido cuando la IA decide qué componentes mostrar.
  • Requisitos estrictos de privacidad o despliegue on-prem. El SDK depende de proveedores (OpenAI, Anthropic). Para LLM self-hosted es más sencillo escribir una capa delgada sobre vLLM/Ollama y tu propio registro de componentes.
  • Colaboración en tiempo real o multijugador. El stream RSC es unidireccional. Para sincronización bidireccional de UI entre usuarios se necesitan soluciones WebSocket, no RSC.
  • El presupuesto de tokens es crítico. Cada renderizado implica una llamada al LLM. Con MAU > 10k sin caché agresiva, la factura de gpt-4o puede superar $1k/mes.

Alternativas para proyectos que no usan Next.js

HerramientaStackCuándo usarla
Thesys C1 APICualquiera (HTTP API)SaaS, devuelve bloques de UI listos según esquema JSON. Ideal para equipos sin experiencia en RSC.
CopilotKitReact (Next.js + Vite + Remix)Si necesitas copilotos in-app con estado y acciones. Soporta Generative UI vía useCopilotAction.
TamboReact (universal)Catálogo de componentes como entidad de primer nivel. Funciona con Vite, no requiere RSC.
A2UICualquiera (Google)Formato JSON declarativo de UI de Google para agentes. Independiente del renderer, funciona en cualquier frontend.
assistant-uiReactBiblioteca chat-first con soporte de tool UIs. Buena base para copilotos en cualquier app React.
Capa propiaCualquieraSi necesitas 2–3 tipos de componentes y el control es crítico — registro + generateObject + un switch en el cliente ocupa ~150 líneas.

Para Vue/Svelte/Angular, a fecha de mayo 2026 no hay soluciones de producción al nivel del Vercel AI SDK — la mayoría de equipos hacen un cliente delgado a una API que devuelve una descripción JSON del componente y renderizan en el frontend por su cuenta.

Despliegue en plataformas económicas

Vercel es la opción obvia para Next.js, pero no la única. Si el presupuesto es ajustado o no quieres depender de Vercel:

  • Fly.io — $0–5/mes en planes hobby. Soporta Next.js vía Dockerfile. Regiones edge en todo el mundo. Límite del free tier — 3 máquinas × 256MB.
  • Render — el web service gratuito entra en reposo tras 15 min de inactividad (primera solicitud tras el reposo ~30 seg). Adecuado para demos y proyectos personales, no para producción.
  • Railway — $5 de créditos al mes en el plan hobby. Despliegue sencillo desde GitHub, buen DX, pero más caro que Fly.io al crecer.
  • Cloudflare Pages + Workers — gratuito hasta 100k solicitudes/día. Requiere adaptación al runtime nodejs_compat; el streaming RSC funciona con matices.
  • VPS propio + Coolify/Dokploy — desde $5/mes (Hetzner, Contabo). Control total, pero tú te encargas de las actualizaciones, SSL y monitorización.

Para la mayoría de proyectos personales, Fly.io ofrece el mejor equilibrio: inicio gratuito, camino razonable hacia producción, regiones edge sin vendor lock-in.

Qué necesita el equipo

Antes de llevar Vercel AI SDK a producción, evalúa la preparación del stack y del equipo:

Habilidades imprescindibles:

  • React Server Components — sin esto, streamUI será una caja negra al primer bug.
  • TypeScript — los esquemas Zod y los parámetros de las herramientas sin tipos se convierten en caos.
  • Generadores asíncronos (async function*, yield) — no todo desarrollador React de nivel medio ha trabajado con ellos.
  • Prompt engineering — las descripciones de herramientas y el system prompt determinan la calidad de la selección de componentes. Es una disciplina aparte.

Habilidades deseables:

  • Experiencia con LLM API (rate limits, estrategias de retry, contabilidad de tokens).
  • Comprensión del Edge runtime y sus limitaciones (sin Node.js APIs, límite de tamaño del bundle).
  • Observabilidad — logs estructurados de tool calls, trazado de solicitudes.

Tamaño del equipo y TCO (estimación, mayo 2026):

TamañoTiempo de ingenieríaCoste LLM (MAU 1k)Coste LLM (MAU 10k)¿Viable?
Solo (1 persona)2–3 semanas para MVP~$50/mes (gpt-4o-mini)~$500/mesSí, para side-project
Pequeño (2–4)4–6 semanas para v1~$150/mes (mix gpt-4o)~$1.500/mesSí, caso de uso principal
Mediano (5–15)2–3 meses para integración completa~$300/mes~$3k–5k/mesSí, si existe plataforma
Grande (15+)4–6 meses + equipo de plataformapresupuesto negociado$10k+/mesVale la pena evaluar LLM self-hosted

Los números de LLM corresponden al escenario "1 solicitud por sesión, gpt-4o para selección de herramientas, gpt-4o-mini para parámetros". Los costes reales dependen mucho de la longitud de los prompts, la frecuencia de solicitudes repetidas y la estrategia de caché.

Metodología de cálculo del TCO: cifras calculadas bajo los supuestos: prompt medio ~800 input + ~300 output tokens en gpt-4o (o ~$0,001 en gpt-4o-mini), 1 solicitud/sesión, precios de OpenAI a mayo 2026, MAU ≈ DAU × 30%. Calibra según tu workload.

Consejos de despliegue

Variables de entorno: OPENAI_API_KEY debe estar disponible en tu entorno de producción. En Vercel, añádela en la configuración del proyecto en la sección Environment Variables.

Edge runtime: la función streamUI funciona en Edge runtime, lo que reduce significativamente el tiempo de cold start. Añade export const runtime = 'edge' en el archivo del server action.

Rate limiting: sin limitación de frecuencia, un solo usuario puede generar miles de solicitudes a la IA. Añade un rate limiter antes de invocar streamUI. El paquete @upstash/ratelimit se integra bien con Next.js.

Selección del modelo: gpt-4o da los mejores resultados en la selección de componentes, pero es más caro. gpt-4o-mini cuesta unas 15× menos en input/output (openai.com/api/pricing, 2026-05) y se desenvuelve bien con conjuntos de componentes sencillos. Prueba ambos con tus definiciones de herramientas concretas.

Próximos pasos

En esta guía cubrimos los fundamentos. Para Generative UI en producción:

  • Añade nuevas herramientas — cada nuevo componente en el registro amplía las capacidades de la IA
  • Implementa caché de resultados — cachea las solicitudes frecuentes para reducir latencia y costes
  • Añade texto en streaming junto con los componentes de UI para que la IA pueda explicar lo que muestra
  • Usa structured outputs para una generación de parámetros más fiable
  • Configura observabilidad — registra cada tool call, sus parámetros y las acciones del usuario

La documentación de Vercel AI SDK describe en detalle todos estos patrones, y el repositorio de ejemplos tiene plantillas listas para producción que merece la pena explorar.

Sobre AI SDK v5/v6

Si usas versiones más recientes del SDK, las diferencias clave respecto al código de este artículo son:

  • parameters: en la definición de la herramienta → inputSchema:
  • Import import { streamUI } from 'ai/rsc'import { streamUI } from '@ai-sdk/rsc'
  • RSC sigue marcado por Vercel como experimental — para producción se recomienda AI SDK UI (useChat).

¿Quieres integrar Generative UI en tu producto? Hablamos de tu caso — por nuestra experiencia en consultoría, el stack GenUI encaja bien en dashboards y herramientas internas; para superficies reguladas y páginas públicas de alto tráfico los trade-offs generalmente no son favorables.

Divulgación: los enlaces externos a productos (Thesys, CopilotKit, Tambo, Vercel, Fly.io, Render, Railway, Cloudflare, Upstash) son recomendaciones orgánicas; no hay programas de afiliados ni publicidad. Precios actualizados a 2026-05-11.

CompartirTwitterLinkedInEmail
vercel-ai-sdkreacttutorialstreaming
A

Alex

Ingeniero y Consultor de Generative UI

Ingeniero senior especializado en interfaces con AI y sistemas Generative UI. Ayudando a equipos de producto a lanzar más rápido con el stack GenUI adecuado.

Adelántate en Generative UI

Artículos semanales, actualizaciones de frameworks y guías de implementación prácticas — directamente a tu bandeja de entrada.

Respetamos tu privacidad. Baja en cualquier momento.

¿Necesitas ayuda para implementar lo que acabas de leer?

Reserva una consulta gratuita