Ir al contenido

Screaming Architecture: la clave para un frontend escalable

Tu proyecto frontend debería gritar lo que hace, no con qué está hecho. Ejemplos concretos de migración, convivencia con frameworks como Nuxt y manejo de shared state.

7 minArchitecture · Frontend · Vue · Nuxt leer en inglés

El patrón que vi en todos lados

En varias empresas donde trabajé me encontré con el mismo patrón. Los equipos sabían que organizar por features era importante, la intención estaba ahí. Pero en la práctica, la feature terminaba desparramada por todo el proyecto: components/auth/, services/auth/, store/auth/, views/auth/. El nombre de la feature aparecía en cada carpeta técnica, pero nunca en un solo lugar.

src/
├── components/
│   ├── auth/          # LoginButton.vue, RegisterForm.vue
│   ├── products/      # ProductCard.vue, ProductList.vue
│   └── orders/        # OrderSummary.vue
├── views/
│   ├── auth/          # LoginView.vue, RegisterView.vue
│   ├── products/      # ProductsView.vue
│   └── orders/        # OrdersView.vue
├── services/
│   ├── auth.service.ts
│   ├── products.service.ts
│   └── orders.service.ts
├── store/
│   ├── auth.store.ts
│   ├── products.store.ts
│   └── orders.store.ts
└── utils/

A primera vista parece ordenado. Pero cuando necesitas cambiar algo de Auth, tocas 4 carpetas. Cuando un dev nuevo llega, tiene que reconstruir mentalmente qué archivos pertenecen a qué feature. Y cuando la app crece, cada carpeta técnica se convierte en un cajón de sastre con 30+ archivos.

El problema no es la falta de organización. Es que la organización grita "tecnología" en vez de gritar "negocio".

La analogía que lo explica

Imagina el plano de una casa. No ves una sección de "ladrillos", otra de "cemento" y otra de "ventanas". Ves "cocina", "dormitorio", "baño". Los planos gritan el propósito de cada espacio, no los materiales con los que está construido.

Screaming Architecture busca lo mismo para tu código. Que la estructura de carpetas de alto nivel grite las features de tu aplicación, no las tecnologías que usas.

Qué ganas con este enfoque

  • Entendimiento instantáneo: abres el proyecto y de un vistazo sabes qué hace el negocio.
  • Onboarding veloz: nuevos devs entienden la estructura del proyecto en minutos, no días.
  • Cambios localizados: modificar una feature no te obliga a tocar 4 carpetas. Todo está en un lugar.
  • Escalabilidad real: agregar una nueva feature es crear una carpeta, no insertar archivos en 6 lugares distintos.
  • Testing más fácil: cada módulo es una unidad aislada que se puede testear independiente.
  • Refactorizaciones seguras: mover o eliminar una feature entera es mover o eliminar una carpeta.

Aplicando Screaming Architecture

La idea es simple: agrupar por features o dominios de negocio. Cada carpeta contiene todo lo necesario para esa funcionalidad.

src/
├── modules/
│   ├── Auth/
│   │   ├── components/   # LoginButton.vue, RegisterForm.vue
│   │   ├── views/        # LoginView.vue, RegisterView.vue
│   │   ├── routes/       # Rutas de este módulo
│   │   ├── store/        # auth.store.ts
│   │   └── services/     # auth.service.ts
│   ├── Products/
│   │   ├── components/   # ProductCard.vue, ProductList.vue
│   │   ├── views/        # ProductsView.vue, ProductDetailView.vue
│   │   ├── store/        # products.store.ts
│   │   └── services/     # products.service.ts
│   └── Orders/
│       ├── components/
│       ├── views/
│       ├── store/
│       └── services/
├── shared/
│   ├── ui/
│   │   ├── components/   # BaseButton.vue, ModalBase.vue
│   │   └── composables/  # useModal.ts, useDarkMode.ts
│   ├── composables/      # useDebounce.ts, useLocalStorage.ts
│   ├── utils/            # formatDate.ts, validateEmail.ts
│   └── assets/           # Imágenes globales, estilos base
├── layouts/              # DefaultLayout.vue, AuthLayout.vue
├── router/               # Router general
├── app.vue
└── main.ts

No todos los módulos necesitan la misma estructura interna. Auth puede tener store/ y services/, pero un módulo de Landing puede ser solo components/ y views/. Cada módulo define lo que necesita.

De la teoría a la práctica: migrando una feature

Tomemos el ejemplo de Auth en la estructura desagregada y veamos cómo queda la migración:

Antes (4 carpetas):

src/components/auth/LoginButton.vue
src/components/auth/RegisterForm.vue
src/views/auth/LoginView.vue
src/views/auth/RegisterView.vue
src/services/auth.service.ts
src/store/auth.store.ts

Después (1 carpeta):

src/modules/Auth/components/LoginButton.vue
src/modules/Auth/components/RegisterForm.vue
src/modules/Auth/views/LoginView.vue
src/modules/Auth/views/RegisterView.vue
src/modules/Auth/services/auth.service.ts
src/modules/Auth/store/auth.store.ts

Los imports cambian, el código no. Si tu módulo de Auth necesita exponer algo al resto de la app (un guard, un composable, el estado del usuario), puedes crear un index.ts que haga de API pública del módulo:

// src/modules/Auth/index.ts
export { useAuthStore } from './store/auth.store'
export { useCurrentUser } from './composables/useCurrentUser'
export { authGuard } from './routes/guards'

Así el resto de la app importa desde @/modules/Auth, no desde las carpetas internas. Si refactorizas la estructura interna del módulo, los imports externos no se rompen.

Convivencia con frameworks

Si usas Nuxt, Next o cualquier framework con convenciones de carpetas (pages/, composables/, components/), la pregunta obvia es: ¿cómo convive esto con Screaming Architecture?

Las convenciones del framework se encargan del routing y el auto-import. Screaming Architecture se encarga de la lógica de negocio.

En Nuxt, por ejemplo, pages/ define las rutas, pero la página puede ser un wrapper liviano que importa el módulo real:

<!-- pages/auth/login.vue -->
<template>
  <LoginView />
</template>

<script setup lang="ts">
import LoginView from '~/modules/Auth/views/LoginView.vue'
</script>

Nuxt se encarga del routing (/auth/login), y tu módulo Auth se encarga de la lógica. pages/ queda liviana, solo como punto de entrada.

Lo mismo aplica para composables/. Los composables globales (auto-imported por Nuxt) van en la carpeta convencional del framework. Los composables específicos de una feature van dentro del módulo:

composables/              # Auto-imported por Nuxt (globales)
├── useTheme.ts
└── useBreakpoint.ts
modules/Auth/composables/ # Específicos de Auth (import manual)
├── useCurrentUser.ts
└── useAuthRedirect.ts

Cross-cutting concerns: qué va en shared

La pregunta más común cuando aplicas esto: ¿qué pasa cuando dos módulos necesitan lo mismo?

La regla es simple. Si algo es específico de una feature, va en su módulo. Si lo usan dos o más features, va en shared/.

shared/ui/ es para componentes de UI genéricos sin lógica de negocio: botones, modales, inputs. Son los building blocks, no las features.

shared/composables/ es para lógica reutilizable sin estado de negocio: useDebounce, useLocalStorage, useIntersectionObserver.

shared/utils/ es para funciones puras: formatDate, slugify, validateEmail.

¿Y el estado compartido? Si Auth y Orders necesitan datos del usuario, Auth es dueño del estado. Orders importa useCurrentUser desde @/modules/Auth. Si con el tiempo más módulos necesitan el mismo dato, puedes mover ese composable a shared/. Pero no lo hagas preventivamente. Empieza con el módulo dueño y mueve solo cuando hay evidencia real de que es cross-cutting.

Cuándo sí y cuándo no

Este enfoque funciona mejor en proyectos medianos a grandes, con lógica de negocio real y equipos de varios devs. Si tu app tiene 5+ features que crecen de forma independiente, Screaming Architecture te va a ordenar la vida.

Para MVPs, landing pages o CRUDs simples, quizás es excesivo. No necesitas una estructura de módulos para 3 vistas y un formulario.

Si tu proyecto tiene potencial de crecer, adoptarlo temprano te ahorra la migración dolorosa después. Y si ya tienes un proyecto grande con estructura genérica, puedes migrar de forma progresiva, un módulo a la vez, empezando por la feature que más crece.

La próxima vez que arranques un proyecto, piensa en cómo se verá la estructura cuando el equipo crezca. Si tus carpetas solo dicen components/ y utils/, ya sabes por dónde empezar.

Nuevos posts en tu inbox

Escribo sobre desarrollo de software, herramientas que voy probando y cosas que aprendo construyendo. Cuando publico algo nuevo, te llega al email. Sin más.

Sin spam. Cancela cuando quieras.