Skip to content

🏗️ Arquitectura y Patrones

Objetivo: Entender cuándo y cómo usar cada tipo de arquitectura, packages compartidos y patrones de comunicación en nuestro stack Vue.js 3 + Monorepo.


¿Por qué Monorepo?

  • Reutilización real: Packages compartidos entre microfrontends (Types, Utils, vue-utils)
  • Consistencia de versiones: Mismo Vue, TypeScript, Vite en todos los proyectos
  • Refactoring cross-project: Cambios en tipos compartidos se propagan automáticamente
  • Developer Experience: Un git clone, un pnpm install y todo funciona

¿Por qué pnpm?

  • Workspaces nativos: Gestión de dependencies entre packages
  • Disk efficiency: Hard links reducen espacio en disco significativamente
  • Lock file determinista: Builds reproducibles en cualquier máquina
  • Catalog feature: Centralizamos versiones de dependencies críticas

¿Por qué Pinia?

  • Composition API nativo: Integración perfecta con Vue 3
  • TypeScript first: Inferencia automática de tipos
  • DevTools: Debugging superior vs state management manual
  • Modularidad: Cada microfrontend puede tener sus propios stores

¿Por qué Vite?

  • Hot Module Reload: Desarrollo ultrarrápido
  • Tree shaking: Bundles optimizados automáticamente
  • ES modules nativo: Mejor performance que bundlers tradicionales
  • Plugin ecosystem: Integración perfecta con Vue + TypeScript
  • Separación de responsabilidades: Cada microfrontend tiene un propósito específico
  • Reutilización inteligente: Solo compartir lo que realmente aporta valor
  • Comunicación explícita: Interfaces claras entre componentes
  • Independencia controlada: Autonomía sin fragmentación
¿Se ejecuta en ASP.NET?¿Usa window.__params?¿Es reutilizable?Solución
✅ SÍ✅ SÍ❌ NOMicrofrontend Integrado
❌ NO❌ NO❌ NOMicrofrontend Independiente
❌ N/A❌ N/A✅ SÍ (con Vue)Librería Vue (vue-*)
❌ N/A❌ N/A✅ SÍ (sin Vue)Librería Utilidades

proyecto-vue/
├── 📁 @apps/ # Microfrontends
│ ├── 📁 flight-list/ # Listado de vuelos (Integrado)
│ ├── 📁 flight-review/ # Revisión de reservas (Integrado)
│ └── 📁 admin-panel/ # Panel administrativo (Independiente)
├── 📁 packages/ # Packages compartidos
│ ├── 📁 types/ # Tipos compartidos
│ ├── 📁 utils/ # Utilidades generales
│ ├── 📁 vue-utils/ # Utilidades específicas de Vue
│ └── 📁 vue-modal/ # Componentes compartidos
└── 📄 pnpm-workspace.yaml # Configuración del workspace

Catalog Strategy:

pnpm-workspace.yaml
catalog:
vue: ^3.5.13
typescript: ~5.8.3
pinia: ^2.3.0
vite: ^7.0.0

Ventajas del Catalog:

  • Versiones consistentes entre todos los proyectos
  • Actualizaciones centralizadas
  • Reducción de conflictos de dependencias
  • Bundle size optimizado
Terminal window
# Scripts por proyecto
pnpm app-flight-list dev # Desarrollo de microfrontend
pnpm lib-utils build # Build de librería
pnpm lib-vue-modal dev # Desarrollo con watch
# Scripts globales
pnpm build # Build todos los proyectos
pnpm builddev # Build desarrollo (sourcemaps)
pnpm lint # Lint todo el workspace

🔗 Microfrontends: Integrados vs Independientes

Section titled “🔗 Microfrontends: Integrados vs Independientes”

Características:

  • Se ejecutan dentro de aplicaciones ASP.NET existentes
  • Reciben configuración via window.__params
  • No tienen navegación propia (single page dentro del host)
  • Optimizados para integración seamless

Ejemplo Real: flight-list

// window.__params recibido desde ASP.NET
interface FlightListParams {
flightSearchRequest: {
isPackage: boolean;
mode: number;
tripMode: number;
startingFromAirport: string;
returningFromAirport: string;
startingFromDateTime: string;
returningFromDateTime: string;
adults: number;
kids: number;
agekids: null | number[];
site: string;
quoteList: boolean;
quoteFlight: boolean;
cacheTimeout: number;
culture: string;
fareMode: number;
simpleFlightQuotes: boolean;
step: boolean;
currency: string;
channelId: number;
CriteriaFilter: {
StopTypes: string[];
FlightTypes: string[];
DepartureTimeOfDay: string[];
ArrivalTimeOfDay: string[];
Airlines: string[];
};
CriteriaSort: {
SortBy: number;
SortDirection: number;
};
QuoteTokenFQS: string;
B2B2CToken: string;
CarrierCodesList: string[];
NonStop: boolean;
};
config: {
baseUrl: string;
showPoints: boolean;
language: string;
reviewPath: string;
lottieUrl: string;
};
}

Ejemplo Real: flight-review

// window.__params recibido desde ASP.NET
interface FlightReviewParams {
config: {
baseUrl: string;
showPoints: boolean;
language: string;
currency: string;
listPath: string;
checkoutPath: string;
lottieUrl: string;
};
reviewData: {
tripMode: string;
startingFromAirport: string;
returningFromAirport: string;
checkIn: string;
checkOut: string;
adults: number;
kids: number;
ageKids: string;
carrierCodesList: string[];
nonStop: boolean;
revalidateToken: string;
detailstoken: string;
totalAmount: number;
flightDetails: string; // JSON serializado con detalles del vuelo
};
}

Cuándo usar:

  • ✅ Reemplazar secciones específicas de aplicaciones ASP.NET existentes
  • ✅ Necesitas datos del contexto del host (usuario, sesión, configuración)
  • ✅ Navegación controlada por la aplicación host
  • ✅ UI/UX consistente con el host

Características:

  • SPAs autónomos con su propia navegación
  • No dependen de window.__params
  • Manejo completo de rutas y estado
  • Comunicación únicamente via APIs

Ejemplo: admin-panel

// main.ts - No config.window.ts
import { configEnv } from "@/config.env";
const app = await i18n(createApp(App), configEnv.I18Path, configEnv.I18Version);
app.provide("configEnv", configEnv);
// Sin window.__params
app.use(createPinia());
app.use(router); // Router completo
app.mount("#app");

Cuándo usar:

  • ✅ Aplicaciones administrativas o de gestión
  • ✅ Funcionalidades completamente independientes
  • ✅ Diferentes audiencias o niveles de acceso
  • ✅ Despliegue independiente requerido

Estrategia progresiva para convertir páginas ASP.NET:

  1. Fase 1: Identificar sección específica (ej: tabla de resultados)
  2. Fase 2: Crear microfrontend integrado
  3. Fase 3: Reemplazar sección manteniendo el host
  4. Fase 4: (Opcional) Migrar completa a independiente

📦 Packages Compartidos: Cuándo y Cómo

Section titled “📦 Packages Compartidos: Cuándo y Cómo”

✅ SÍ crear package compartido cuando:

  • La funcionalidad se usa en 2+ microfrontends
  • Es genérica, sin lógica de negocio específica
  • Tiene valor como abstracción reutilizable
  • Puede evolucionar independientemente

❌ NO crear package compartido cuando:

  • Solo se usa en un microfrontend
  • Contiene lógica de negocio muy específica
  • Es más código mantener la abstracción que duplicar
  • Crea dependencias circulares

📊 Caso Real: flight-list + flight-review

Section titled “📊 Caso Real: flight-list + flight-review”

Packages Creados Desde Cero:

1. 📋 Package types - Tipos Compartidos

packages/types/src/flight.ts
export interface FlightSearchRequest {
isPackage: boolean;
mode: number;
tripMode: number;
startingFromAirport: string;
returningFromAirport: string;
startingFromDateTime: string;
returningFromDateTime: string;
adults: number;
kids: number;
// ... resto de propiedades del ejemplo real
}
export interface FlightDetails {
departure: FlightLeg;
returning: FlightLeg;
}
export interface FlightLeg {
arrival: Airport;
departure: Airport;
flightNumbers: string[];
duration: number;
airline: Airline;
stops: number;
}

2. 🔧 Package utils - Utilidades Generales

packages/utils/src/guid.ts
export function generateGUID(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// packages/utils/src/http/WebRequestService.ts
export class WebRequestService {
async get<T>(url: string): Promise<ApiResponse<T>> {
// Manejo estandarizado de peticiones HTTP
}
async post<T>(url: string, data: unknown): Promise<ApiResponse<T>> {
// Lógica común para POST con error handling
}
}
// packages/utils/src/form/DynamicFormService.ts
export class DynamicFormService {
generateForm(action: string, data: Record<string, any>): HTMLFormElement {
const form = document.createElement("form");
form.method = "POST";
form.action = action;
Object.entries(data).forEach(([key, value]) => {
const input = document.createElement("input");
input.type = "hidden";
input.name = key;
input.value = typeof value === "object" ? JSON.stringify(value) : value;
form.appendChild(input);
});
return form;
}
submitForm(form: HTMLFormElement): void {
document.body.appendChild(form);
form.submit();
}
}

3. 🌐 Package vue-utils - Configuración Estandarizada

packages/vue-utils/src/i18n.ts
export async function i18n(
app: App,
i18Path: string,
version: string,
isDebug = false,
): Promise<App> {
// Configuración estandarizada de i18next
// Mismo patrón en todos los microfrontends
}

Packages Reutilizados de Otros Proyectos:

  • vue-modal: Componentes de modal estandarizados
  • vue-alerts: Sistema de alertas consistente
  • vue-upsell: Componentes de upselling reutilizables

Lo que NO compartieron (y por qué estuvo bien):

  • Componentes de UI específicos: Cada microfrontend tiene sus propios componentes de lista/review
  • Stores de Pinia: Cada uno maneja su propio estado
  • Lógica de validación específica: Validaciones de vuelos vs validaciones de review son diferentes

📋 Checklist para Crear Package Compartido

Section titled “📋 Checklist para Crear Package Compartido”
  • ¿Se usa en 2+ microfrontends actualmente?
  • ¿Es infraestructura/utilidad, no lógica de negocio?
  • ¿Puede evolucionar sin romper dependientes?
  • ¿Mantenerlo centralizado es menos trabajo que duplicarlo?
  • ¿No crea dependencias circulares?

1. 🪟 Host ASP.NET → Microfrontend (window.__params)

Section titled “1. 🪟 Host ASP.NET → Microfrontend (window.__params)”

Patrón establecido:

// En la página ASP.NET
<script>
window.__params = {
searchData: @Json.Serialize(Model.SearchData),
config: {
baseUrl: '@Model.BaseUrl',
currency: '@Model.Currency',
language: '@Model.Language',
listPath: '@Url.Action("List", "Flight")',
reviewPath: '@Url.Action("Review", "Flight")'
}
}
</script>
// En el microfrontend
import { configWindow } from '@/config.window'
// Acceso tipado y seguro a los parámetros

Casos de uso:

  • Pasar datos del servidor (usuario loggeado, configuración)
  • URLs de navegación entre páginas
  • Configuración específica de la sesión

2. 🔄 Microfrontend → Host ASP.NET (Form POST)

Section titled “2. 🔄 Microfrontend → Host ASP.NET (Form POST)”

Patrón real usado en flight-list → flight-review:

// Navegación desde flight-list hacia review mediante form POST
import { DynamicFormService } from "@pnpmworkspace/utils/form";
const selectFlight = (selectedFlight: FlightOption) => {
const formService = new DynamicFormService();
// Crear form dinámico con datos de la selección
const form = formService.generateForm(configWindow.config.reviewPath, {
flightSelection: JSON.stringify(selectedFlight),
searchCriteria: JSON.stringify(configWindow.flightSearchRequest),
returnUrl: window.location.href,
});
// Submit automático hacia la página de review
formService.submitForm(form);
};

Desde flight-review de regreso a flight-list:

const backToSearch = () => {
const formService = new DynamicFormService();
const form = formService.generateForm(configWindow.config.listPath, {
searchCriteria: JSON.stringify(originalSearchData),
preserveFilters: true,
});
formService.submitForm(form);
};

¿Por qué Form POST y no navigation simple?

  • Preserva datos complejos entre páginas ASP.NET
  • Mantiene estado del servidor (sesión, autenticación)
  • Permite POST data que excede límites de URL
  • Consistente con el patrón ASP.NET existente

3. 🤝 Microfrontend ↔ Microfrontend (Casos especiales)

Section titled “3. 🤝 Microfrontend ↔ Microfrontend (Casos especiales)”

En el proyecto flight-list + flight-review: No hubo comunicación directa porque son secciones completamente separadas - cada uno vive en su propia página ASP.NET.

Para casos futuros donde coexistan en la misma página:

// Microfrontend emisor
const emitFlightSelected = (flight: Flight) => {
const event = new CustomEvent("flight:selected", {
detail: { flight },
});
window.dispatchEvent(event);
};
// Microfrontend receptor
onMounted(() => {
const handleFlightSelected = (event: CustomEvent) => {
const { flight } = event.detail;
reviewStore.setSelectedFlight(flight);
};
window.addEventListener("flight:selected", handleFlightSelected);
onUnmounted(() => {
window.removeEventListener("flight:selected", handleFlightSelected);
});
});
// Usando el WebRequestService compartido
import { WebRequestService } from "@pnpmworkspace/utils/http";
const apiService = new WebRequestService();
const response = await apiService.get<Flight[]>(`${configEnv.ApiUrl}/flights`);
if (response.success) {
flightStore.setFlights(response.data);
} else {
flightStore.setError(response.error);
}

1. Separación por Responsabilidad

  • flight-list: Solo listado y selección
  • flight-review: Solo revisión y confirmación
  • Evita microfrontends “god objects”

2. Package types Centralizado

  • Consistencia automática de tipos
  • Refactorings más seguros
  • Documentación implícita de contratos

3. HTTP Service Abstracto

  • Manejo de errores consistente
  • Logging centralizado
  • Fácil testing con mocks

🔄 Refactorizaciones Durante el Desarrollo

Section titled “🔄 Refactorizaciones Durante el Desarrollo”

Problema Inicial: Duplicación de lógica de Form POST

// flight-list/src/navigation.ts ❌ (código duplicado)
const navigateToReview = (flightData: any) => {
const form = document.createElement("form");
form.method = "POST";
form.action = "/flight/review";
// Lógica duplicada de creación de inputs
Object.entries(flightData).forEach(([key, value]) => {
const input = document.createElement("input");
input.type = "hidden";
input.name = key;
input.value = JSON.stringify(value);
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
};
// flight-review/src/navigation.ts ❌ (misma lógica duplicada)
const backToList = (searchData: any) => {
// ... exactamente la misma lógica duplicada
};

Solución: DynamicFormService centralizado en utils

// packages/utils/src/form/DynamicFormService.ts ✅
export class DynamicFormService {
generateForm(action: string, data: Record<string, any>): HTMLFormElement {
// Lógica centralizada y probada
}
submitForm(form: HTMLFormElement): void {
// Manejo consistente del submit
}
}
// En ambos microfrontends ✅
import { DynamicFormService } from "@pnpmworkspace/utils/form";
const formService = new DynamicFormService();

Refactorización de HTTP handling

// Problema inicial: Cada micro manejaba errores HTTP diferente
// Solución: WebRequestService con error handling estandarizado
// Beneficios: Logging consistente, retry logic, error formatting

¿Por qué esta refactorización fue exitosa?

  • Identificamos código literalmente duplicado (no solo similar)
  • La abstracción era simple y enfocada
  • Ambos microfrontends usaban exactamente el mismo patrón
  • Fácil de testear de forma aislada

1. Package compartido para componentes UI

  • Componentes específicos de cada dominio funcionan mejor separados
  • Forzar reutilización de UI crea acoplamiento innecesario

2. Store compartido entre microfrontends

  • Cada microfrontend maneja su propio estado
  • La comunicación via eventos es más explícita

3. Routing compartido

  • Cada microfrontend independiente tiene su propio router
  • Los integrados no necesitan routing interno complejo

  • ¿Justifica ser una aplicación separada?
  • ¿Tiene responsabilidades claramente definidas?
  • ¿Integrado (ASP.NET) o independiente (SPA)?
  • ¿Qué datos necesita del host?
  • ¿Se usará en 2+ proyectos?
  • ¿Es infraestructura, no lógica de negocio?
  • ¿Puede evolucionar independientemente?
  • ¿Evita dependencias circulares?
  • Host → Micro: via window.__params
  • Micro → Host: via navigation o postMessage
  • Micro ↔ API: via packages compartidos
  • Micro ↔ Micro: excepcional, via Custom Events

  1. Templates y Scaffolding - Crear nuevos proyectos
  2. Desarrollo Día a Día - Workflows habituales