GenAINextVercel AI SDKLangchainGoogle Gemini

Fundamentos modernos y primera interacción con IA. Parte 2

Antonio Pérez
2025-08-13
8
Fundamentos modernos y primera interacción con IA. Parte 2

De la teoría al terminal: configurando nuestro entorno de desarrollo

La teoría nos ha proporcionado el mapa; ahora es el momento de trazar el territorio. En esta sección, traduciremos los conceptos arquitectónicos en una estructura de proyecto tangible. Configuraremos nuestro entorno, instalaremos las dependencias clave y estableceremos una base de código limpia y organizada, lista para la integración de la lógica de IA.

Podrás encontrar el código de este artículo en: https://github.com/aperezl/ai-fullstack-serie/tree/fundamentals-ai-part2

Prerrequisitos:

  • Node.js v18 o superior.
  • Una clave de API para Google AI Studio (Gemini).

Paso 1: Inicialización del proyecto Next.js

Comenzamos creando un nuevo proyecto Next.js utilizando el App Router. Abre tu terminal y ejecuta:

bash
npx create-next-app@latest ai-fullstack-serie --typescript --tailwind --eslint
cd ai-fullstack-serie

Durante la instalación se nos harán preguntas sobre la configuración que queremos, podemos elegirla a nuestro gusto.

bash
$ npx create-next-app@latest ia-fullstack-serie --typescript --tailwind --eslint
√ Would you like your code inside a `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to use Turbopack for `next dev`? ... No / Yes
√ Would you like to customize the import alias (`@/*` by default)? ... No / Yes

Esta será la raíz de nuestro proyecto durante toda esta serie de artículos.

Paso 2: Instalación de dependencias esenciales

Instalaremos las bibliotecas que formarán el núcleo de nuestra interacción con la IA y la validación de datos.

bash
pnpm install ai @ai-sdk/google zod
  • ai: El paquete principal del Vercel AI SDK.
  • @ai-sdk/google: El proveedor específico para los modelos Gemini de Google.
  • zod: Nuestra biblioteca de validación y definición de esquemas.

Paso 3: Configuración de variables de entorno

La seguridad de las claves de API es primordial. Crearemos un archivo .env.local en la raíz del proyecto para almacenar nuestras credenciales de forma segura. Este archivo nunca debe ser versionado en Git.

bash
touch .env.local

Abre .env.local y añade tu clave de API de Google:

javascript
GOOGLE_GENERATIVE_AI_API_KEY="TU_API_KEY_DE_GEMINI_AQUI"

El Vercel AI SDK detectará automáticamente esta variable de entorno.

Para obtener nuestra API_KEY de Google Gemini tendrémos que ir a Google AI Studio y seleccionar la opción Get API key

Google AI Studio

Posteriormente pulsamos sobre Crear clave de API

Google AI Studio

Ejercicio 1: Creando una arquitectura de ficheros escalable

Una decisión temprana que distingue a un proyecto senior es su estructura de directorios. En lugar de colocar toda la lógica en app/, adoptaremos una estructura más modular y mantenible.

Crea los siguientes directorios dentro del proyecto:

bash
/
|- src
|  |- app/
|  |- components/
|    |- ui/      # Para componentes de UI reusables (ej. shadcn/ui)
|  |- features/  # Para "vertical slices" o funcionalidades completas
|  |- lib/
|    |- ai/      # Lógica relacionada con la IA
|    |- utils.ts # Funciones de utilidad generales
|    |- validations/ # Esquemas de Zod
|- .env.local
|- next.config.mjs
|- package.json
...

Justificación de la arquitectura:

  • lib/: Contiene la lógica de negocio central y compartida. lib/ai albergará nuestras Server Actions que interactúan con los LLMs. lib/validations contendrá todos nuestros esquemas de Zod, centralizando la "fuente de verdad" de nuestras estructuras de datos.
  • features/: Para funcionalidades más grandes y autocontenidas. Por ejemplo, si construimos un dashboard de análisis, todo lo relacionado (componentes, hooks, API routes) podría vivir en features/analytics/. Por ahora lo dejaremos vacío.
  • components/ui/: Un lugar estándar para componentes de UI atómicos y reutilizables, compatible con la instalación por defecto de shadcn/ui, que usaremos más adelante.

Esta separación de responsabilidades (Separation of Concerns) es fundamental. Nuestra lógica de IA (lib/ai) no sabe nada sobre la UI que la consume, lo que la hace reutilizable y fácil de testear.

Ejercicio 2: Implementando nuestra primera Server Action con IA

Ahora, construiremos la pieza central de este capítulo: una Server Action que encapsula una llamada a Gemini, validada de principio a fin.

Paso 1: Definir los "contratos" con Zod

Antes de escribir la lógica, definimos las estructuras de datos. Crea el archivo src/lib/validations/schemas.ts:

typescriptsrc/lib/validations/schemas.ts
import { z } from 'zod';

// Esquema para la entrada de nuestra Server Action
export const GenerateTitlesInputSchema = z.object({
  articleUrl: z
    .string()
    .url({ message: "Por favor, introduce una URL válida." })
    .min(10, { message: "La URL parece demasiado corta." }),
});

// Esquema para la salida que esperamos del LLM
export const AITitlesOutputSchema = z.object({
  titles: z
    .array(z.string().min(10, { message: "El título es demasiado corto." }))
    .length(3, { message: "Esperábamos exactamente 3 títulos." })
    .describe("Un array de 3 títulos clickbait para el artículo."),
});

Hemos definido dos contratos claros: uno para la entrada que nuestra UI debe proporcionar y otro para la salida que exigimos al LLM. El uso de .describe() será crucial en capítulos posteriores para guiar al modelo.

Paso 2: Crear la Server Action

Ahora, la lógica principal. Crea el archivo src/lib/ai/actions.ts:

typescriptsrc/lib/ai/actions.ts
'use server';

import { google } from '@ai-sdk/google';
import { generateObject } from 'ai';
import { GenerateTitlesInputSchema, AITitlesOutputSchema } from '@/src/lib/validations/schemas';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';

// Derivamos el tipo de la salida de la IA a partir del schema de Zod
type AITitles = z.infer<typeof AITitlesOutputSchema>;

// Definimos los tipos auxiliares.
export type GenerateTitlesSuccess = {
  success: true;
  data: AITitles;
};

export type GenerateTitlesError = {
  success: false;
  error: string;
};

export type GenerateTitlesResponse = GenerateTitlesSuccess | GenerateTitlesError;

export async function generateClickbaitTitles(
  input: z.infer<typeof GenerateTitlesInputSchema>
): Promise<GenerateTitlesResponse> {
  try {
    // 1. Validar la entrada con nuestro schema. Falla rápido si no es válida.
    const validation = GenerateTitlesInputSchema.safeParse(input);
    if (!validation.success) {
      return {
        success: false,
        error: validation.error.flatten().fieldErrors.articleUrl?.[0] || 'Entrada inválida.'
      };
    }

    const { articleUrl } = validation.data;

    // Hacemos fetch a la URL, el contenido HTML de esa petición será lo que analicemos
    const response = await fetch(articleUrl)
    const articleContent = await response.text()

    // 2. Instanciar el modelo y preparar el prompt
    const model = google('gemini-2.0-flash-001');

    const prompt = `
      Eres un experto en SEO y marketing digital con 20 años de experiencia,
      especializado en crear titulares virales.
      Analiza el siguiente contenido de un artículo y genera exactamente
      3 títulos clickbait optimizados para redes sociales.
      Los títulos deben ser provocativos, cortos y generar curiosidad.
      No añadas ninguna explicación, solo devuelve el objeto JSON con la estructura requerida.
      Contenido del artículo: "${articleContent}"
    `;

    // 3. Llamar a la IA con generateObject para obtener una salida estructurada
    const { object } = await generateObject({
      model: model,
      schema: AITitlesOutputSchema, // Le decimos a la IA la "forma" que debe tener su respuesta
      prompt: prompt,
    });

    // La validación contra AITitlesOutputSchema ya la hace 'generateObject' por nosotros.
    // Si la forma no es correcta, la función lanzará un error que capturaremos.

    revalidatePath('/'); // Opcional: Invalida el caché de la página si es necesario.

    return { success: true, data: object };

  } catch (e) {
    console.error(e);
    // Para un sistema en producción, aquí registraríamos el error en un servicio de logging.
    return {
      success: false,
      error: 'Ha ocurrido un error al contactar con la IA. Por favor, inténtalo de nuevo.'
    };
  }
}

Análisis del código experto:

  • 'use server': Directiva que define este archivo como un módulo de Server Actions, ejecutable en el servidor.
  • generateObject vs generateText: Elegimos generateObject deliberadamente. En lugar de pedir al LLM que devuelva un string JSON y luego hacer JSON.parse (lo que es frágil), le pasamos nuestro AITitlesOutputSchema. El AI SDK se encarga de instruir al modelo para que genere una salida que se ajuste a ese esquema y la valida por nosotros. Esto es infinitamente más robusto.
  • Validación de Entrada Explícita: Usamos safeParse al principio. Un principio de diseño senior es "confía pero verifica", incluso en las fronteras internas de nuestro propio sistema.
  • Manejo de Errores: El bloque try...catch no es opcional. Las llamadas a la red y a la IA pueden fallar por múltiples razones. Devolvemos un objeto discriminado ({ success: true | false, ... }) que la UI podrá manejar de forma predecible.
  • Prompt Engineering: El prompt es detallado y asigna un rol (persona) al LLM. Le damos instrucciones claras y negativas ("No añadas ninguna explicación") para acotar su comportamiento.

Conclusiones de la práctica

Hemos construido el motor de nuestra primera funcionalidad de IA. Es importante destacar lo que hemos logrado: una función de backend (generateClickbaitTitles) completamente autocontenida, segura en sus tipos, robusta frente a errores y totalmente desacoplada de la interfaz de usuario.

Esta Server Action está ahora lista para ser importada y utilizada desde cualquier Client Component en nuestra aplicación, como si fuera una función local. Hemos sentado las bases arquitectónicas correctas.

Puedes encontrar el código de esta sección en: https://github.com/aperezl/ai-fullstack-serie/tree/fundamentals-ai-part2

Con el backend listo, en la siguiente sección construiremos la interfaz de usuario que consumirá esta lógica y presentará los resultados al usuario final.