DEV Community

Cover image for Cómo construí mi portafolio en 4 frameworks (React 19, Vue 3.5, Angular 19 y Compose HTML) usando Clean Architecture
David Henry Bernardo Cardenas
David Henry Bernardo Cardenas

Posted on Originally published at davidbernardo.dev

Cómo construí mi portafolio en 4 frameworks (React 19, Vue 3.5, Angular 19 y Compose HTML) usando Clean Architecture

Como desarrollador Full Stack & Mobile Senior, a menudo me encuentro con el mismo dilema en las entrevistas técnicas:

"Veo en tu CV que manejas React, Vue, Angular y Kotlin Multiplatform... ¿pero en cuál te sientes más cómodo?"

Cualquiera puede listar una docena de tecnologías en un PDF. Pero en el mundo del desarrollo de software, el código habla más fuerte que las palabras.

Por eso me propuse un reto de ingeniería: en lugar de crear un portafolio tradicional en una sola tecnología, decidí construir una misma aplicación idéntica en los cuatro ecosistemas más potentes de la actualidad, permitiendo al usuario cambiar de framework en caliente con un solo clic, sin perder el estado ni la posición de lectura:

  1. ⚛️ React 19 (Vite + Hooks personalizados)
  2. 💚 Vue 3.5 (Composition API + Composables)
  3. 🅰️ Angular 19 (Standalone Components + Nuevas Signals)
  4. 🟣 Kotlin Multiplatform / Compose HTML (Kotlin compilado a JS/Web)

¿La trampa? Si creas 4 proyectos separados, estás cuadruplicando el esfuerzo de mantenimiento: cada vez que actualizas un empleo, una habilidad o un proyecto, tendrías que modificar 4 bases de código distintas.

Aquí es donde entra la verdadera protagonista de este proyecto: Clean Architecture (Arquitectura Limpia) en un Monorepo.

Portafolio con selector Dock de microfrontends


🏛️ La Arquitectura: Separando la lógica del framework

Para evitar duplicar código, la regla de oro fue: ningún framework debe ser dueño de la lógica ni de los datos. Los frameworks son simplemente mecanismos de entrega (presenters o adaptadores de entrada).

Estructuré el proyecto en un monorepo con Yarn Workspaces:

├── packages/
│   ├── core/                  # 🧠 Dominio puro (Entidades, Casos de Uso, Puertos)
│   ├── content-json-adapter/  # 🔌 Adaptador de infraestructura (i18n, lectura de JSON)
│   └── tokens/                # 🎨 Variables CSS de diseño y temas
├── apps/
│   ├── react-app/             # ⚛️ UI en React 19
│   ├── vue-app/               # 💚 UI en Vue 3.5
│   ├── angular-app/           # 🅰️ UI en Angular 19
│   └── compose-app/           # 🟣 UI en Kotlin Compose HTML
└── shell/                     # 🐚 Host orquestador y Dock Switcher
Enter fullscreen mode Exit fullscreen mode

1. El Núcleo de Dominio (packages/core)

Este paquete es TypeScript puro y no tiene una sola línea de código acoplada al navegador, al DOM o a librerías externas. Contiene:

  • Entidades: Modelos de dominio como Experience, Project, Profile, SkillCategory.
  • Puertos (Interfaces): Define el contrato ContentRepository:
  export interface ContentRepository {
    getProfile(): Profile;
    getExperiences(): Experience[];
    getProjects(): Project[];
    getSkills(): SkillCategory[];
  }
Enter fullscreen mode Exit fullscreen mode
  • Casos de Uso: Funciones puras con la lógica de negocio, por ejemplo getExperienceTimeline o filterProjectsByTech:
  export function getExperienceTimeline(
    repo: ContentRepository, 
    locale: SupportedLocale
  ): ExperienceVM[] {
    const raw = repo.getExperiences();
    return raw
      .sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())
      .map(exp => mapToExperienceVM(exp, locale));
  }
Enter fullscreen mode Exit fullscreen mode

2. El Adaptador de Datos (packages/content-json-adapter)

Implementa el puerto ContentRepository leyendo la fuente única de verdad (archivos JSON estructurados) y gestionando la traducción dinámica entre Español e Inglés.


⚔️ La Comparativa: ¿Cómo consume cada framework el mismo caso de uso?

Aquí viene la parte más fascinante: el consumo del caso de uso en cada tecnología.

Al estar la lógica en packages/core, los componentes de la interfaz solo tienen una responsabilidad: renderizar el ViewModel. Veamos cómo se implementa la reactividad en cada uno:

1. ⚛️ React 19: Con Custom Hooks

En React creamos un hook usePortfolio que encapsula la suscripción al idioma:

// apps/react-app/src/hooks/usePortfolio.ts
export function usePortfolio() {
  const { locale } = useI18n();
  const repo = useMemo(() => new JsonContentRepository(), []);

  const experiences = useMemo(
    () => getExperienceTimeline(repo, locale),
    [repo, locale]
  );

  return { experiences };
}
Enter fullscreen mode Exit fullscreen mode

2. 💚 Vue 3.5: Con Composition API y Composables

En Vue aprovechamos la reactividad nativa con computed:

// apps/vue-app/src/composables/usePortfolio.ts
export function usePortfolio() {
  const { locale } = useI18n();
  const repo = new JsonContentRepository();

  const experiences = computed(() => 
    getExperienceTimeline(repo, locale.value)
  );

  return { experiences };
}
Enter fullscreen mode Exit fullscreen mode

3. 🅰️ Angular 19: Con Signals Nativas

Angular 19 ha revolucionado su reactividad abandonando la complejidad excesiva de RxJS en componentes para adoptar Signals. Observa qué limpio queda:

// apps/angular-app/src/app/services/portfolio.service.ts
@Injectable({ providedIn: 'root' })
export class PortfolioService {
  private contentRepo = new JsonContentRepository();
  private i18n = inject(I18nService);

  // Signal calculada automáticamente cuando cambia locale()
  experiences = computed(() => 
    getExperienceTimeline(this.contentRepo, this.i18n.locale())
  );
}
Enter fullscreen mode Exit fullscreen mode

4. 🟣 Kotlin Compose HTML: Declaratividad en la Web

En Compose para Web utilizamos el paradigma declarativo de Jetpack Compose:

// apps/compose-app/src/jsMain/kotlin/.../App.kt
@Composable
fun App(viewModel: PortfolioViewModel) {
    val locale by viewModel.locale.collectAsState()
    val experiences = remember(locale) { 
        viewModel.getExperienceTimeline(locale) 
    }

    ExperienceSection(experiences = experiences)
}
Enter fullscreen mode Exit fullscreen mode

💡 Lección clave: Cada framework brilla a su manera, pero ninguno reinventa la rueda. Si mañana decido añadir un nuevo empleo a mi trayectoria profesional, solo edito un archivo JSON y los 4 frameworks lo reflejan al instante con sus respectivos badges (Full-time o Freelance).

Línea de tiempo de Experiencia unificada


🎨 Sistema de Diseño Agnóstico: Vanilla CSS

Uno de los errores comunes al hacer microfrontends es usar Tailwind o librerías pesadas (Material UI, Vuetify, PrimeNG) en cada aplicación, provocando colisiones de nombres de clases y una descarga masiva de megabytes innecesarios.

La solución fue Vanilla CSS moderno con Tokens de Diseño compartidos:

  • Se definieron variables CSS para colores, espaciados, fuentes y sombras (--color-primary, --color-surface, --radius-lg).
  • Se implementaron temas Oscuro y Claro (Dark/Light Mode) con color-scheme y variables en el elemento raíz.
  • Cada microfrontend utiliza exactamente las mismas clases semánticas (.timeline-card, .badge, .project-card). El resultado es una paridad visual del 100%.

🐚 El Shell Host y el Switcher Dinámico

¿Cómo orquestamos los 4 microfrontends en una sola página?

Diseñé un Shell Host ligero en TypeScript puro que:

  1. Monta la aplicación seleccionada mediante un contenedor dinámico.
  2. Dock lateral interactivo en pantallas de escritorio y Floating Action Button (FAB) flotante al estilo Android en dispositivos móviles.
  3. Sincroniza el idioma actual (es / en), el tema visual y la posición de scroll para que la transición entre frameworks sea instantánea e imperceptible.

Botón Flotante FAB en Mobile


🐳 Despliegue y Build con Docker y Nginx

Compilar 4 frameworks con herramientas tan diversas (Vite, Angular CLI y el compilador de Kotlin/Gradle) puede ser un desafío en CI/CD.

Para resolverlo:

  1. Usamos Docker para garantizar un entorno reproducible con Node.js 22 y OpenJDK 17.
  2. Un script orquestador (scripts/assemble-dist.mjs) compila cada aplicación y las ensambla en una estructura de producción limpia:
   dist/
   ├── index.html        # Shell host
   ├── react/            # Bundle de React 19
   ├── vue/              # Bundle de Vue 3.5
   ├── angular/          # Bundle de Angular 19
   └── compose/          # Bundle Wasm/JS de Compose
Enter fullscreen mode Exit fullscreen mode
  1. Un contenedor Nginx sirve los artefactos con compresión Gzip, cabeceras de caché inmutable para assets versionados y soporte de rutas SPA.

📊 Conclusiones y Aprendizajes

  1. Clean Architecture funciona de maravilla en Frontend: A menudo se piensa que la arquitectura limpia es solo para backend. Desacoplar el dominio de la vista demostró ser la mejor decisión para mantener la coherencia del proyecto.
  2. Angular 19 con Signals es un placer: La simplicidad de las Signals en Angular 19 lo acerca a la agilidad de Vue y React sin perder la robustez de TypeScript.
  3. Compose HTML / Kotlin Web es el futuro móvil/web unificado: Escribir interfaces web con la misma sintaxis que uso en Android nativo permite una reutilización de habilidades sin precedentes.

🚀 Pruébalo en vivo y Código Fuente

Puedes interactuar con el portafolio en vivo y cambiar de framework en tiempo real:

🔗 Demo en vivo: davidbernardo.dev

Repositorio en GitHub: github.com/dhbernardo/portfolio

💼 LinkedIn: linkedin.com/in/david-henry-bernardo-cardenas

¿Qué framework de los cuatro prefieres tú para tus proyectos? ¡Déjame tu opinión en los comentarios!

Top comments (0)