Saltar al contenido principal

Clean Code en la era del Vibe Coding

2026-04-20
Madrid, España
Codemotion 2026
Clean CodeIAArchitectureFrontend

El 20 de Abril de 2026 repetí charla en Codemotion, esta vez para mostrar mi experiencia con el uso de la IA Generativa y el Vibe Coding, y de como poder usarla para diferenciarte del resto de desarrolladores y perder el miedo al futuro de nuestro trabajo. Para ello el secreto es aportar ese toque de calidad de código, arquitectura y una buena batería de tests, todo esto puesto en la práctica con un ejemplo sencillo, la migración de mi web personal de Astro a Next.js. Fue una experiencia increíble poder compartir conocimientos con la comunidad técnica en uno de los eventos más importantes de la comunidad tech de diferentes países.

A continuación comparto con vosotros el contenido que comenté en la charla.


1. Intro: La Promesa vs. La Realidad

Seguro que habéis visto el término por Twitter: Vibe Coding. Andrej Karpathy lo definió como ese estado de flujo donde "simplemente dices cosas y pasan cosas". Suena al futuro, ¿verdad? Casi parece que los ingenieros sobramos.

Yo quise ponerlo a prueba. Mi misión: migrar mi portfolio personal (ascinfo.dev) de Astro a Next.js. Quería un diseño "Bento Grid" moderno, oscuro, complejo. Como desarrollador que hace tiempo que no toco frontend, diseñar ese CSS desde cero me tomaría semanas de peleas con Tailwind.

¿Por qué Next.js? Si voy a usar IA para generar UI, necesito estar en el ecosistema donde mejor rinde. Y ese ecosistema hoy es React con Next.js. Hay librerías de componentes (Shadcn, Framer Motion) pensadas para este ecosistema que agilizan el desarrollo mucho más que mantenerme en mi stack anterior.

Así que organicé mi equipo de desarrolladores modernos, 3 herramientas de IA:

  1. Gemini: El Arquitecto (Planning a alto nivel y Prompt Engineer).

  2. v0 (Vercel): El Diseñador UX/UI (Generación de JSX).

  3. Claude: El Desarrollador (Planning técnico, Refactor y Clean Code).

¿El resultado visual? Increíble en 5 minutos. ¿El código? Bueno... hoy vamos a ver lo que nadie enseña en Twitter: el código que hay detrás del "Vibe".


2. El "Antes": La Inocencia de Astro

Astro es maravilloso. Es HTML con esteroides y zero JavaScript por defecto. Mi web antigua era sencilla y limpia. Mirad este Layout:

javascript
// Archivo: src/layouts/Layout.astro
---
import {ViewTransitions} from 'astro:transitions';
import NavBar from "@components/astro/NavBar.astro";

const {title, description} = Astro.props;
---

<!DOCTYPE html>
<html lang="es">
<head>
    <meta property="og:title" content={title}/>
    <ViewTransitions/>
</head>
<body class="bg-black text-white">
    <NavBar/>
    <slot/>
</body>
</html>

Incluso tenía un intento de abstracción para los datos:

javascript
// Archivo: src/pages/proyectos/index.astro
---
import {GetAllProjects} from "@application/project/getAllProjects";
import {ProjectAstroContentAdapter} from "@infrastructure/adapter/ProjectAstroContentAdapter";

const allProjects = await new GetAllProjects(new ProjectAstroContentAdapter()).execute()
---

<section>
{ allProjects.map(project => <ProjectCard title={project.title} />) }
</section>

Conclusión: La arquitectura de datos estaba bien — de hecho, los Use Cases y adaptadores los pude reutilizar casi tal cual. Lo que era rígido era la capa de presentación: componentes .astro sin ecosistema de UI rico, sin Framer Motion, sin Shadcn. Para el salto visual que quería, reescribir el frontend era inevitable. Y si vas a reescribir de todos modos, tiene sentido hacerlo en el ecosistema donde la IA rinde mejor: React.


3. El Flujo de Trabajo: Orquestando IAs

Dicho esto, así fue la dinámica. No fue lanzar prompts al azar. Fue un proceso de ingeniería, y la clave es entender una cosa: cada IA tiene un punto dulce, y si le pides que salga de ahí, la calidad se desploma. Por eso no usé solo Claude para todo.

  1. Conversación inicial con Gemini: Le expliqué mi situación. Quería un toque minimalista, moderno y un Bento Grid.

  2. Plan de implementación: Gemini me ofreció el plan, decidimos las tecnologías y le pedí que actuara como mi "Prompt Engineer". Él redactó los prompts específicos para v0.

  3. v0 al ataque: Con los prompts de Gemini, v0 generó la UI. Salió a la primera. Fue maravilloso.

  4. Claude como filtro de calidad: Abrí el proyecto en local y usé a Claude para que, con el contexto de la web antigua, iterara los resultados de v0 y les diera forma de producción.

4. El "Vibe": El Espejismo de v0

Le pedí a v0: "Genera un portfolio estilo Bento Grid, oscuro, para un arquitecto de software, con acentos naranjas". ¡Boom! En 30 segundos tenía una UI que yo tardaría días en hacer. Se veía preciosa.

[Slide 6: El Horror del Código Generado - Componente Bloque]

Narrativa: Pero como ingenieros, nuestra responsabilidad es abrir el capó. Y lo que encontré fue "Spaghetti UI". Mirad este componente LatestArticleBlock:

typescript
// Archivo: components/bento/latest-article-block.tsx (Generado por v0)
"use client" // ❌ Innecesario para contenido estático

import Link from "next/link"

export function LatestArticleBlock() {
  return (
    <article className="flex h-full min-h-[280px] flex-col rounded-xl border border-white/5 bg-[#222222] p-6 transition-all duration-300 hover:-translate-y-1">
        <div className="mt-4 flex-1">
          {/* 🚨 DATOS HARDCODEADOS - El pecado capital del Vibe Coding */}
          <span className="bg-[#FCA311]/10 text-[#FCA311] px-3 py-1 rounded-full">
            Spring Boot
          </span>

          <h2 className="text-xl font-semibold mt-4">
            Arquitectura Hexagonal en Spring  {/* 👈 Título fijo! */}
          </h2>

          <p className="text-sm text-muted-foreground mt-3">
            Cómo desacoplar tu dominio del framework...
          </p>
        </div>
    </article>
  )
}

Análisis:

  1. Datos Hardcodeados: Si quiero escribir un post nuevo, ¿tengo que editar el código y redesplegar? Inaceptable.

  2. Sin Props: Este componente es un "dibujo", no es reutilizable.

  3. Estado fijo: En otros bloques, como el de proyectos, el estado "Active" estaba clavado en el HTML.

Conclusión: El "Vibe Coding" es genial para prototipar (0 a 1), pero terrible para mantener (1 a N). Aquí termina el trabajo del "Designer" y empieza el del "Architect".


5. Hallucination Corner: Cuando los agentes pierden los papeles

Antes de pasar al resultado final, hablemos de la cara B. Porque si pensáis que tener a 4 agentes trabajando para vosotros es el paraíso, es que no habéis visto a un Agente Revisor y a un Agente Dev entrar en un bucle infinito de reproches.

1. El Desastre de la Hidratación

v0 tiene una obsesión con el tiempo real. Me generó un componente que hacía esto:

typescript
// Lo que v0 pensó que era buena idea
export function PostDate() {
  return <span>Publicado el: {new Date().toLocaleDateString()}</span>
  // 🚨 ERROR: Hydration Mismatch de manual
}

El resultado: La web renderizaba una fecha en el servidor y otra en el cliente, petando por todos lados. La IA "vibeaba" tanto que se olvidó de cómo funciona el renderizado de Next.js. Este es el tipo de error que te parece obvio cuando lo lees, pero que no lo es tanto cuando confías ciegamente en lo que te genera.

2. La Librería Fantasma

El Agente de Backend, en un ataque de optimismo, decidió que la mejor forma de gestionar los metadatos era usar una librería llamada next-super-mdx-optimizer.

  • Problema: Esa librería no existe. Se la inventó mezclando tres nombres distintos.

  • Lección: "Trust, but verify". Si la IA te propone una dependencia nueva, búscala en NPM antes de darle al npm install. Conclusión: Trabajar con agentes es como dirigir a un equipo de genios con amnesia. Son increíblemente brillantes, pero si les quitas el ojo de encima un segundo, intentarán construir un rascacielos empezando por el tejado o usando materiales que no existen en este universo.


6. La Ingeniería: Clean Architecture

Aquí es donde entra Claude (guiado por mí). El objetivo: Refactorizar el caos de v0 usando Clean Architecture y principios SOLID.

El "War Room": El Setup de Agentes

Para arreglar lo generado por v0 y migrar la lógica, no usé un solo chat de Claude. Monté un Setup de Agentes Dedicados:

  1. Planificador (Planning Agent): Analizaba el código de Astro y V0 y definía el paso a paso.

  2. Dev de Backend (Logic Agent): Encargado de los Use Cases y Repositorios.

  3. Dev de Frontend (UI Refactor Agent): Encargado de recibir el JSX de v0 y "limpiarlo".

  4. Revisor (Code Reviewer): Su única misión era buscar "bad smells" y recordarle al Dev que usara tipos de TypeScript.

El flujo era:

  1. Los planificadores analizaban la situación y el contexto para desarrollar el paso a paso desde cada lado.

  2. Si el plan no me convencía, iteraba hasta tener un plan sólido.

  3. Con el plan aprobado, los developers se ponían manos a la obra.

  4. Una vez terminado, el revisor redactaba los code smells o cosas a mejorar y vuelta a empezar hasta llegar a un punto válido.


Ejemplo Práctico

Veamos cómo resultó con el componente que veíamos antes.

Primero, convertimos el dibujo en un componente inteligente y tipado. Extraemos las props. Ahora al componente le da igual si el post es de Spring o de .NET.

typescript
// Archivo: components/bento/latest-article-block.tsx (Refactorizado)

// ✅ Definición de Tipos Explícitos
type LatestArticleBlockProps = {
  slug: string
  title: string
  excerpt: string
  tags: string[]
}

export function LatestArticleBlock({
  slug, title, excerpt, tags
}: LatestArticleBlockProps) {
  return (
    <div className="flex flex-col rounded-xl border border-white/5 bg-[#222222] transition-all duration-300 hover:-translate-y-1">
      <Link href={`/blog/${slug}`}>
        <h2 className="text-xl font-semibold">{title}</h2> {/* ✅ Dinámico */}
        <p className="mt-3 text-muted-foreground">{excerpt}</p>

        <div className="mt-4 flex gap-2">
            {tags.map(tag => (
                <span key={tag} className="rounded-full bg-orange-500/10">
                    {tag}
                </span>
            ))}
        </div>
      </Link>
    </div>
  )
}

¿De dónde salen los datos? Creamos un Caso de Uso agnóstico que no sepa nada de Next.js ni de v0.

typescript
// Archivo: src/lib/content/application/use-cases/posts/GetAllPosts.ts
import type { ContentRepository } from "@/content/domain/repositories/ContentRepository"

export class GetAllPosts {
  // ✅ Inyección de Dependencias
  constructor(private readonly contentRepository: ContentRepository) {}

  async execute(): Promise<PostDto[]> {
    const rawPosts = await this.contentRepository.readAll(POSTS_DIR) // Configuración externa
    // Lógica de ordenación y mapeo...
    return rawPosts.map(p => p.toDto())
  }
}

Definimos el contrato (Repository) y usamos un Container para resolver las dependencias. Esto me permite cambiar de MDX local a Notion API simplemente cambiando una variable de entorno.

typescript
// Archivo: src/lib/content/infrastructure/Container.ts
function createContentRepository(): ContentRepository {
  return process.env.CMS_PROVIDER === "notion"
    ? new NotionContentRepository()
    : new MDXContentRepository()
}

export const posts = {
  getAll: new GetAllPosts(createContentRepository()),
}

Una nota técnica: esto es un patrón factory más que inyección de dependencias pura. Para un portfolio personal, la complejidad de un container de DI real no aporta valor. Lo importante es que el principio se cumple: los casos de uso no saben de dónde vienen los datos.


7. Resultado Final: El Nuevo Rol del Dev (5 min)

Asi es cómo queda la página principal. Limpio, asíncrono y seguro.

typescript
// Archivo: app/page.tsx (Final)
import { posts } from "@/src/lib/content"
import { LatestArticleBlock } from "@/components/bento/latest-article-block"

export default async function Home() {
  const featuredPost = await posts.getFeatured.execute()
  const featuredPostDto = featuredPost?.toDto()

  return (
    <main>
      {featuredPostDto && (
        <LatestArticleBlock
          slug={featuredPostDto.slug}
          title={featuredPostDto.title}
          excerpt={featuredPostDto.excerpt}
          tags={featuredPostDto.tags}
        />
      )}
    </main>
  )
}

Estructura de Carpetas

plain
📦 ascinfo.dev
├── 📂 app/                # Next.js App Router (UI & Glue Code)
├── 📂 components/ui/       # Componentes "tontos" (v0)
└── 📂 src/lib/content/     # 🏛️ El Núcleo (Hexagonal)
    ├── 📂 domain/          # Interfaces
    ├── 📂 application/     # Use Cases
    └── 📂 infrastructure/  # MDX / Notion

¿El balance final? La IA me ahorró unas 20 horas de CSS, boilerplate y migración de contenido. Pero de esas 20 horas, probablemente invertí 8 en revisar, refactorizar y corregir lo que la IA generó. El ahorro neto es brutal, pero no es gratis. El "Vibe Coding" sin ingeniería detrás es deuda técnica a crédito.

8. Pro-tips para sobrevivir a la IA

Antes de terminar me gustaría compartir con vosotros un par de tips que me hubiera gustado saber antes de empezar:

  1. No aceptéis el primer código que funcione. El LatestArticleBlock de v0 funcionaba, pero era un dibujo con datos clavados. Si lo hubiera aceptado tal cual, cada post nuevo sería un redespliegue.

  2. La arquitectura es vuestro escudo contra las alucinaciones. Si tenéis una capa de dominio bien definida, cuando la IA genere código raro, lo detectáis al instante porque no encaja con vuestros contratos.

  3. Dividid los roles de la IA. No le pidáis que sea un maestro supremo que diseña, programa y revisa a la vez. Pedid que sea experta en una sola cosa. Un agente planifica, otro ejecuta, otro revisa. Igual que un equipo real.


Conclusión Final

La IA me ahorró 20 horas de CSS y boilerplate. Pero la arquitectura la puse yo. No os conforméis con ser Vibe Coders. Usad la IA para ir rápido, pero usad vuestra experiencia de ingeniería para ir lejos.

Esto para mí es lo que convierte a un dev en alguien que multiplica su impacto con IA, en vez de ser reemplazado por ella. No se trata de competir contra la máquina, sino de elevar tu nivel de abstracción: pasar de albañil de código a arquitecto que orquesta herramientas.