[{"data":1,"prerenderedAt":973},["ShallowReactive",2],{"blog-screaming-architecture-la-clave-para-un-frontend-escalable-es":3},{"post":4,"surround":539,"translatedPost":545},{"id":5,"title":6,"author":7,"body":8,"date":523,"dateModified":524,"description":525,"extension":526,"img":527,"meta":528,"navigation":345,"path":529,"published":345,"readingTime":376,"seo":530,"sitemap":531,"slug":527,"stem":532,"tags":533,"union":537,"__hash__":538},"blog_es\u002Fes\u002Fblog\u002Fscreaming-architecture-la-clave-para-un-frontend-escalable.md","Screaming Architecture: la clave para un frontend escalable","Sergio Azócar",{"type":9,"value":10,"toc":513},"minimark",[11,16,35,45,48,51,55,58,61,65,106,110,113,119,137,141,144,150,156,162,168,175,264,271,275,287,290,296,402,412,418,424,428,431,438,444,459,474,487,491,494,497,500,509],[12,13,15],"h2",{"id":14},"el-patrón-que-vi-en-todos-lados","El patrón que vi en todos lados",[17,18,19,20,24,25,24,28,24,31,34],"p",{},"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: ",[21,22,23],"code",{},"components\u002Fauth\u002F",", ",[21,26,27],{},"services\u002Fauth\u002F",[21,29,30],{},"store\u002Fauth\u002F",[21,32,33],{},"views\u002Fauth\u002F",". El nombre de la feature aparecía en cada carpeta técnica, pero nunca en un solo lugar.",[36,37,43],"pre",{"className":38,"code":40,"language":41,"meta":42},[39],"language-text","src\u002F\n├── components\u002F\n│   ├── auth\u002F          # LoginButton.vue, RegisterForm.vue\n│   ├── products\u002F      # ProductCard.vue, ProductList.vue\n│   └── orders\u002F        # OrderSummary.vue\n├── views\u002F\n│   ├── auth\u002F          # LoginView.vue, RegisterView.vue\n│   ├── products\u002F      # ProductsView.vue\n│   └── orders\u002F        # OrdersView.vue\n├── services\u002F\n│   ├── auth.service.ts\n│   ├── products.service.ts\n│   └── orders.service.ts\n├── store\u002F\n│   ├── auth.store.ts\n│   ├── products.store.ts\n│   └── orders.store.ts\n└── utils\u002F\n","text","",[21,44,40],{"__ignoreMap":42},[17,46,47],{},"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.",[17,49,50],{},"El problema no es la falta de organización. Es que la organización grita \"tecnología\" en vez de gritar \"negocio\".",[12,52,54],{"id":53},"la-analogía-que-lo-explica","La analogía que lo explica",[17,56,57],{},"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.",[17,59,60],{},"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.",[12,62,64],{"id":63},"qué-ganas-con-este-enfoque","Qué ganas con este enfoque",[66,67,68,76,82,88,94,100],"ul",{},[69,70,71,75],"li",{},[72,73,74],"strong",{},"Entendimiento instantáneo",": abres el proyecto y de un vistazo sabes qué hace el negocio.",[69,77,78,81],{},[72,79,80],{},"Onboarding veloz",": nuevos devs entienden la estructura del proyecto en minutos, no días.",[69,83,84,87],{},[72,85,86],{},"Cambios localizados",": modificar una feature no te obliga a tocar 4 carpetas. Todo está en un lugar.",[69,89,90,93],{},[72,91,92],{},"Escalabilidad real",": agregar una nueva feature es crear una carpeta, no insertar archivos en 6 lugares distintos.",[69,95,96,99],{},[72,97,98],{},"Testing más fácil",": cada módulo es una unidad aislada que se puede testear independiente.",[69,101,102,105],{},[72,103,104],{},"Refactorizaciones seguras",": mover o eliminar una feature entera es mover o eliminar una carpeta.",[12,107,109],{"id":108},"aplicando-screaming-architecture","Aplicando Screaming Architecture",[17,111,112],{},"La idea es simple: agrupar por features o dominios de negocio. Cada carpeta contiene todo lo necesario para esa funcionalidad.",[36,114,117],{"className":115,"code":116,"language":41,"meta":42},[39],"src\u002F\n├── modules\u002F\n│   ├── Auth\u002F\n│   │   ├── components\u002F   # LoginButton.vue, RegisterForm.vue\n│   │   ├── views\u002F        # LoginView.vue, RegisterView.vue\n│   │   ├── routes\u002F       # Rutas de este módulo\n│   │   ├── store\u002F        # auth.store.ts\n│   │   └── services\u002F     # auth.service.ts\n│   ├── Products\u002F\n│   │   ├── components\u002F   # ProductCard.vue, ProductList.vue\n│   │   ├── views\u002F        # ProductsView.vue, ProductDetailView.vue\n│   │   ├── store\u002F        # products.store.ts\n│   │   └── services\u002F     # products.service.ts\n│   └── Orders\u002F\n│       ├── components\u002F\n│       ├── views\u002F\n│       ├── store\u002F\n│       └── services\u002F\n├── shared\u002F\n│   ├── ui\u002F\n│   │   ├── components\u002F   # BaseButton.vue, ModalBase.vue\n│   │   └── composables\u002F  # useModal.ts, useDarkMode.ts\n│   ├── composables\u002F      # useDebounce.ts, useLocalStorage.ts\n│   ├── utils\u002F            # formatDate.ts, validateEmail.ts\n│   └── assets\u002F           # Imágenes globales, estilos base\n├── layouts\u002F              # DefaultLayout.vue, AuthLayout.vue\n├── router\u002F               # Router general\n├── app.vue\n└── main.ts\n",[21,118,116],{"__ignoreMap":42},[17,120,121,122,125,126,129,130,125,133,136],{},"No todos los módulos necesitan la misma estructura interna. Auth puede tener ",[21,123,124],{},"store\u002F"," y ",[21,127,128],{},"services\u002F",", pero un módulo de Landing puede ser solo ",[21,131,132],{},"components\u002F",[21,134,135],{},"views\u002F",". Cada módulo define lo que necesita.",[12,138,140],{"id":139},"de-la-teoría-a-la-práctica-migrando-una-feature","De la teoría a la práctica: migrando una feature",[17,142,143],{},"Tomemos el ejemplo de Auth en la estructura desagregada y veamos cómo queda la migración:",[17,145,146,149],{},[72,147,148],{},"Antes"," (4 carpetas):",[36,151,154],{"className":152,"code":153,"language":41,"meta":42},[39],"src\u002Fcomponents\u002Fauth\u002FLoginButton.vue\nsrc\u002Fcomponents\u002Fauth\u002FRegisterForm.vue\nsrc\u002Fviews\u002Fauth\u002FLoginView.vue\nsrc\u002Fviews\u002Fauth\u002FRegisterView.vue\nsrc\u002Fservices\u002Fauth.service.ts\nsrc\u002Fstore\u002Fauth.store.ts\n",[21,155,153],{"__ignoreMap":42},[17,157,158,161],{},[72,159,160],{},"Después"," (1 carpeta):",[36,163,166],{"className":164,"code":165,"language":41,"meta":42},[39],"src\u002Fmodules\u002FAuth\u002Fcomponents\u002FLoginButton.vue\nsrc\u002Fmodules\u002FAuth\u002Fcomponents\u002FRegisterForm.vue\nsrc\u002Fmodules\u002FAuth\u002Fviews\u002FLoginView.vue\nsrc\u002Fmodules\u002FAuth\u002Fviews\u002FRegisterView.vue\nsrc\u002Fmodules\u002FAuth\u002Fservices\u002Fauth.service.ts\nsrc\u002Fmodules\u002FAuth\u002Fstore\u002Fauth.store.ts\n",[21,167,165],{"__ignoreMap":42},[17,169,170,171,174],{},"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 ",[21,172,173],{},"index.ts"," que haga de API pública del módulo:",[36,176,180],{"className":177,"code":178,"language":179,"meta":42,"style":42},"language-ts shiki shiki-themes material-theme-ocean","\u002F\u002F src\u002Fmodules\u002FAuth\u002Findex.ts\nexport { useAuthStore } from '.\u002Fstore\u002Fauth.store'\nexport { useCurrentUser } from '.\u002Fcomposables\u002FuseCurrentUser'\nexport { authGuard } from '.\u002Froutes\u002Fguards'\n","ts",[21,181,182,191,222,243],{"__ignoreMap":42},[183,184,187],"span",{"class":185,"line":186},"line",1,[183,188,190],{"class":189},"sC9rS","\u002F\u002F src\u002Fmodules\u002FAuth\u002Findex.ts\n",[183,192,194,198,202,206,209,212,215,219],{"class":185,"line":193},2,[183,195,197],{"class":196},"s6cf3","export",[183,199,201],{"class":200},"sAklC"," {",[183,203,205],{"class":204},"s0W1g"," useAuthStore",[183,207,208],{"class":200}," }",[183,210,211],{"class":196}," from",[183,213,214],{"class":200}," '",[183,216,218],{"class":217},"sfyAc",".\u002Fstore\u002Fauth.store",[183,220,221],{"class":200},"'\n",[183,223,225,227,229,232,234,236,238,241],{"class":185,"line":224},3,[183,226,197],{"class":196},[183,228,201],{"class":200},[183,230,231],{"class":204}," useCurrentUser",[183,233,208],{"class":200},[183,235,211],{"class":196},[183,237,214],{"class":200},[183,239,240],{"class":217},".\u002Fcomposables\u002FuseCurrentUser",[183,242,221],{"class":200},[183,244,246,248,250,253,255,257,259,262],{"class":185,"line":245},4,[183,247,197],{"class":196},[183,249,201],{"class":200},[183,251,252],{"class":204}," authGuard",[183,254,208],{"class":200},[183,256,211],{"class":196},[183,258,214],{"class":200},[183,260,261],{"class":217},".\u002Froutes\u002Fguards",[183,263,221],{"class":200},[17,265,266,267,270],{},"Así el resto de la app importa desde ",[21,268,269],{},"@\u002Fmodules\u002FAuth",", no desde las carpetas internas. Si refactorizas la estructura interna del módulo, los imports externos no se rompen.",[12,272,274],{"id":273},"convivencia-con-frameworks","Convivencia con frameworks",[17,276,277,278,24,281,24,284,286],{},"Si usas Nuxt, Next o cualquier framework con convenciones de carpetas (",[21,279,280],{},"pages\u002F",[21,282,283],{},"composables\u002F",[21,285,132],{},"), la pregunta obvia es: ¿cómo convive esto con Screaming Architecture?",[17,288,289],{},"Las convenciones del framework se encargan del routing y el auto-import. Screaming Architecture se encarga de la lógica de negocio.",[17,291,292,293,295],{},"En Nuxt, por ejemplo, ",[21,294,280],{}," define las rutas, pero la página puede ser un wrapper liviano que importa el módulo real:",[36,297,301],{"className":298,"code":299,"language":300,"meta":42,"style":42},"language-vue shiki shiki-themes material-theme-ocean","\u003C!-- pages\u002Fauth\u002Flogin.vue -->\n\u003Ctemplate>\n  \u003CLoginView \u002F>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport LoginView from '~\u002Fmodules\u002FAuth\u002Fviews\u002FLoginView.vue'\n\u003C\u002Fscript>\n","vue",[21,302,303,308,320,331,340,347,374,393],{"__ignoreMap":42},[183,304,305],{"class":185,"line":186},[183,306,307],{"class":189},"\u003C!-- pages\u002Fauth\u002Flogin.vue -->\n",[183,309,310,313,317],{"class":185,"line":193},[183,311,312],{"class":200},"\u003C",[183,314,316],{"class":315},"s-wAU","template",[183,318,319],{"class":200},">\n",[183,321,322,325,328],{"class":185,"line":224},[183,323,324],{"class":200},"  \u003C",[183,326,327],{"class":315},"LoginView",[183,329,330],{"class":200}," \u002F>\n",[183,332,333,336,338],{"class":185,"line":245},[183,334,335],{"class":200},"\u003C\u002F",[183,337,316],{"class":315},[183,339,319],{"class":200},[183,341,343],{"class":185,"line":342},5,[183,344,346],{"emptyLinePlaceholder":345},true,"\n",[183,348,350,352,355,359,362,365,368,370,372],{"class":185,"line":349},6,[183,351,312],{"class":200},[183,353,354],{"class":315},"script",[183,356,358],{"class":357},"sJ14y"," setup",[183,360,361],{"class":357}," lang",[183,363,364],{"class":200},"=",[183,366,367],{"class":200},"\"",[183,369,179],{"class":217},[183,371,367],{"class":200},[183,373,319],{"class":200},[183,375,377,380,383,386,388,391],{"class":185,"line":376},7,[183,378,379],{"class":196},"import",[183,381,382],{"class":204}," LoginView ",[183,384,385],{"class":196},"from",[183,387,214],{"class":200},[183,389,390],{"class":217},"~\u002Fmodules\u002FAuth\u002Fviews\u002FLoginView.vue",[183,392,221],{"class":200},[183,394,396,398,400],{"class":185,"line":395},8,[183,397,335],{"class":200},[183,399,354],{"class":315},[183,401,319],{"class":200},[17,403,404,405,408,409,411],{},"Nuxt se encarga del routing (",[21,406,407],{},"\u002Fauth\u002Flogin","), y tu módulo Auth se encarga de la lógica. ",[21,410,280],{}," queda liviana, solo como punto de entrada.",[17,413,414,415,417],{},"Lo mismo aplica para ",[21,416,283],{},". 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:",[36,419,422],{"className":420,"code":421,"language":41,"meta":42},[39],"composables\u002F              # Auto-imported por Nuxt (globales)\n├── useTheme.ts\n└── useBreakpoint.ts\nmodules\u002FAuth\u002Fcomposables\u002F # Específicos de Auth (import manual)\n├── useCurrentUser.ts\n└── useAuthRedirect.ts\n",[21,423,421],{"__ignoreMap":42},[12,425,427],{"id":426},"cross-cutting-concerns-qué-va-en-shared","Cross-cutting concerns: qué va en shared",[17,429,430],{},"La pregunta más común cuando aplicas esto: ¿qué pasa cuando dos módulos necesitan lo mismo?",[17,432,433,434,437],{},"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 ",[21,435,436],{},"shared\u002F",".",[17,439,440,443],{},[21,441,442],{},"shared\u002Fui\u002F"," es para componentes de UI genéricos sin lógica de negocio: botones, modales, inputs. Son los building blocks, no las features.",[17,445,446,449,450,24,453,24,456,437],{},[21,447,448],{},"shared\u002Fcomposables\u002F"," es para lógica reutilizable sin estado de negocio: ",[21,451,452],{},"useDebounce",[21,454,455],{},"useLocalStorage",[21,457,458],{},"useIntersectionObserver",[17,460,461,464,465,24,468,24,471,437],{},[21,462,463],{},"shared\u002Futils\u002F"," es para funciones puras: ",[21,466,467],{},"formatDate",[21,469,470],{},"slugify",[21,472,473],{},"validateEmail",[17,475,476,477,480,481,483,484,486],{},"¿Y el estado compartido? Si Auth y Orders necesitan datos del usuario, Auth es dueño del estado. Orders importa ",[21,478,479],{},"useCurrentUser"," desde ",[21,482,269],{},". Si con el tiempo más módulos necesitan el mismo dato, puedes mover ese composable a ",[21,485,436],{},". Pero no lo hagas preventivamente. Empieza con el módulo dueño y mueve solo cuando hay evidencia real de que es cross-cutting.",[12,488,490],{"id":489},"cuándo-sí-y-cuándo-no","Cuándo sí y cuándo no",[17,492,493],{},"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.",[17,495,496],{},"Para MVPs, landing pages o CRUDs simples, quizás es excesivo. No necesitas una estructura de módulos para 3 vistas y un formulario.",[17,498,499],{},"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.",[17,501,502,503,125,505,508],{},"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 ",[21,504,132],{},[21,506,507],{},"utils\u002F",", ya sabes por dónde empezar.",[510,511,512],"style",{},"html pre.shiki code .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}html pre.shiki code .s6cf3, html code.shiki .s6cf3{--shiki-default:#89DDFF;--shiki-default-font-style:italic}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s-wAU, html code.shiki .s-wAU{--shiki-default:#F07178}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}",{"title":42,"searchDepth":193,"depth":224,"links":514},[515,516,517,518,519,520,521,522],{"id":14,"depth":193,"text":15},{"id":53,"depth":193,"text":54},{"id":63,"depth":193,"text":64},{"id":108,"depth":193,"text":109},{"id":139,"depth":193,"text":140},{"id":273,"depth":193,"text":274},{"id":426,"depth":193,"text":427},{"id":489,"depth":193,"text":490},"2025-06-11","2026-04-13","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.","md",null,{},"\u002Fes\u002Fblog\u002Fscreaming-architecture-la-clave-para-un-frontend-escalable",{"title":6,"description":525},{"loc":529},"es\u002Fblog\u002Fscreaming-architecture-la-clave-para-un-frontend-escalable",[534,535,300,536],"architecture","frontend","nuxt","screaming-architecture-the-key-to-scalable-frontend","-uiKRO7Zh7WEinDFEoFj5f0CEyeobYVOdKdgQISG2P8",[540,527],{"title":541,"path":542,"stem":543,"description":544,"children":-1},"oxlint-tailwindcss: el plugin de linting que Tailwind v4 necesitaba","\u002Fes\u002Fblog\u002Foxlint-tailwindcss-el-plugin-de-linting-que-tailwindcss-necesitaba","es\u002Fblog\u002Foxlint-tailwindcss-el-plugin-de-linting-que-tailwindcss-necesitaba","El plugin de linting más completo para Tailwind CSS, nativo para oxlint. 22 reglas con autofix, zero-config y diseñado desde cero para Tailwind CSS v4.",{"id":546,"title":547,"author":7,"body":548,"date":523,"dateModified":524,"description":965,"extension":526,"img":527,"meta":966,"navigation":345,"path":967,"published":345,"readingTime":376,"seo":968,"sitemap":969,"slug":527,"stem":970,"tags":971,"union":537,"__hash__":972},"blog_en\u002Fen\u002Fblog\u002Fscreaming-architecture-the-key-to-scalable-frontend.md","Screaming Architecture: the key to a scalable frontend",{"type":9,"value":549,"toc":955},[550,554,566,571,574,577,581,584,587,591,629,633,636,642,656,660,663,669,674,680,685,691,753,759,763,773,776,782,860,869,875,881,885,888,893,898,909,920,932,936,939,942,945,953],[12,551,553],{"id":552},"the-pattern-i-saw-everywhere","The pattern I saw everywhere",[17,555,556,557,24,559,24,561,24,563,565],{},"Across multiple companies where I worked, I kept running into the same pattern. Teams knew that organizing by features was important, the intention was there. But in practice, the feature ended up scattered across the entire project: ",[21,558,23],{},[21,560,27],{},[21,562,30],{},[21,564,33],{},". The feature name appeared in every technical folder, but never in a single place.",[36,567,569],{"className":568,"code":40,"language":41,"meta":42},[39],[21,570,40],{"__ignoreMap":42},[17,572,573],{},"At first glance it looks organized. But when you need to change something in Auth, you touch 4 folders. When a new dev joins, they have to mentally reconstruct which files belong to which feature. And as the app grows, each technical folder becomes a junk drawer with 30+ files.",[17,575,576],{},"The problem isn't a lack of organization. It's that the organization screams \"technology\" instead of screaming \"business.\"",[12,578,580],{"id":579},"the-analogy-that-explains-it-all","The analogy that explains it all",[17,582,583],{},"Imagine a house blueprint. You don't see a \"bricks\" section, a \"cement\" section, and a \"windows\" section. You see \"kitchen,\" \"bedroom,\" \"bathroom.\" The blueprints scream the purpose of each space, not the materials it's built with.",[17,585,586],{},"Screaming Architecture seeks the same for your code. Your top-level folder structure should scream the features of your application, not the technologies you use.",[12,588,590],{"id":589},"what-you-gain-with-this-approach","What you gain with this approach",[66,592,593,599,605,611,617,623],{},[69,594,595,598],{},[72,596,597],{},"Instant understanding",": you open the project and at a glance you know what the business does.",[69,600,601,604],{},[72,602,603],{},"Fast onboarding",": new devs understand the project structure in minutes, not days.",[69,606,607,610],{},[72,608,609],{},"Localized changes",": modifying a feature doesn't force you to touch 4 folders. Everything is in one place.",[69,612,613,616],{},[72,614,615],{},"Real scalability",": adding a new feature means creating a folder, not inserting files in 6 different places.",[69,618,619,622],{},[72,620,621],{},"Easier testing",": each module is an isolated unit you can test independently.",[69,624,625,628],{},[72,626,627],{},"Safe refactoring",": moving or deleting an entire feature means moving or deleting a folder.",[12,630,632],{"id":631},"applying-screaming-architecture","Applying Screaming Architecture",[17,634,635],{},"The idea is simple: group by features or business domains. Each folder contains everything needed for that functionality.",[36,637,640],{"className":638,"code":639,"language":41,"meta":42},[39],"src\u002F\n├── modules\u002F\n│   ├── Auth\u002F\n│   │   ├── components\u002F   # LoginButton.vue, RegisterForm.vue\n│   │   ├── views\u002F        # LoginView.vue, RegisterView.vue\n│   │   ├── routes\u002F       # Routes for this module\n│   │   ├── store\u002F        # auth.store.ts\n│   │   └── services\u002F     # auth.service.ts\n│   ├── Products\u002F\n│   │   ├── components\u002F   # ProductCard.vue, ProductList.vue\n│   │   ├── views\u002F        # ProductsView.vue, ProductDetailView.vue\n│   │   ├── store\u002F        # products.store.ts\n│   │   └── services\u002F     # products.service.ts\n│   └── Orders\u002F\n│       ├── components\u002F\n│       ├── views\u002F\n│       ├── store\u002F\n│       └── services\u002F\n├── shared\u002F\n│   ├── ui\u002F\n│   │   ├── components\u002F   # BaseButton.vue, ModalBase.vue\n│   │   └── composables\u002F  # useModal.ts, useDarkMode.ts\n│   ├── composables\u002F      # useDebounce.ts, useLocalStorage.ts\n│   ├── utils\u002F            # formatDate.ts, validateEmail.ts\n│   └── assets\u002F           # Global images, base styles\n├── layouts\u002F              # DefaultLayout.vue, AuthLayout.vue\n├── router\u002F               # General router\n├── app.vue\n└── main.ts\n",[21,641,639],{"__ignoreMap":42},[17,643,644,645,647,648,650,651,647,653,655],{},"Not every module needs the same internal structure. Auth may have ",[21,646,124],{}," and ",[21,649,128],{},", but a Landing module might just be ",[21,652,132],{},[21,654,135],{},". Each module defines what it needs.",[12,657,659],{"id":658},"from-theory-to-practice-migrating-a-feature","From theory to practice: migrating a feature",[17,661,662],{},"Let's take the Auth example from the disaggregated structure and see what the migration looks like:",[17,664,665,668],{},[72,666,667],{},"Before"," (4 folders):",[36,670,672],{"className":671,"code":153,"language":41,"meta":42},[39],[21,673,153],{"__ignoreMap":42},[17,675,676,679],{},[72,677,678],{},"After"," (1 folder):",[36,681,683],{"className":682,"code":165,"language":41,"meta":42},[39],[21,684,165],{"__ignoreMap":42},[17,686,687,688,690],{},"The imports change, the code doesn't. If your Auth module needs to expose something to the rest of the app (a guard, a composable, the user state), you can create an ",[21,689,173],{}," that serves as the module's public API:",[36,692,693],{"className":177,"code":178,"language":179,"meta":42,"style":42},[21,694,695,699,717,735],{"__ignoreMap":42},[183,696,697],{"class":185,"line":186},[183,698,190],{"class":189},[183,700,701,703,705,707,709,711,713,715],{"class":185,"line":193},[183,702,197],{"class":196},[183,704,201],{"class":200},[183,706,205],{"class":204},[183,708,208],{"class":200},[183,710,211],{"class":196},[183,712,214],{"class":200},[183,714,218],{"class":217},[183,716,221],{"class":200},[183,718,719,721,723,725,727,729,731,733],{"class":185,"line":224},[183,720,197],{"class":196},[183,722,201],{"class":200},[183,724,231],{"class":204},[183,726,208],{"class":200},[183,728,211],{"class":196},[183,730,214],{"class":200},[183,732,240],{"class":217},[183,734,221],{"class":200},[183,736,737,739,741,743,745,747,749,751],{"class":185,"line":245},[183,738,197],{"class":196},[183,740,201],{"class":200},[183,742,252],{"class":204},[183,744,208],{"class":200},[183,746,211],{"class":196},[183,748,214],{"class":200},[183,750,261],{"class":217},[183,752,221],{"class":200},[17,754,755,756,758],{},"The rest of the app imports from ",[21,757,269],{},", not from internal folders. If you refactor the module's internal structure, external imports don't break.",[12,760,762],{"id":761},"framework-coexistence","Framework coexistence",[17,764,765,766,24,768,24,770,772],{},"If you use Nuxt, Next, or any framework with folder conventions (",[21,767,280],{},[21,769,283],{},[21,771,132],{},"), the obvious question is: how does this coexist with Screaming Architecture?",[17,774,775],{},"Framework conventions handle routing and auto-imports. Screaming Architecture handles business logic.",[17,777,778,779,781],{},"In Nuxt, for example, ",[21,780,280],{}," defines routes, but the page can be a thin wrapper that imports the actual module:",[36,783,784],{"className":298,"code":299,"language":300,"meta":42,"style":42},[21,785,786,790,798,806,814,818,838,852],{"__ignoreMap":42},[183,787,788],{"class":185,"line":186},[183,789,307],{"class":189},[183,791,792,794,796],{"class":185,"line":193},[183,793,312],{"class":200},[183,795,316],{"class":315},[183,797,319],{"class":200},[183,799,800,802,804],{"class":185,"line":224},[183,801,324],{"class":200},[183,803,327],{"class":315},[183,805,330],{"class":200},[183,807,808,810,812],{"class":185,"line":245},[183,809,335],{"class":200},[183,811,316],{"class":315},[183,813,319],{"class":200},[183,815,816],{"class":185,"line":342},[183,817,346],{"emptyLinePlaceholder":345},[183,819,820,822,824,826,828,830,832,834,836],{"class":185,"line":349},[183,821,312],{"class":200},[183,823,354],{"class":315},[183,825,358],{"class":357},[183,827,361],{"class":357},[183,829,364],{"class":200},[183,831,367],{"class":200},[183,833,179],{"class":217},[183,835,367],{"class":200},[183,837,319],{"class":200},[183,839,840,842,844,846,848,850],{"class":185,"line":376},[183,841,379],{"class":196},[183,843,382],{"class":204},[183,845,385],{"class":196},[183,847,214],{"class":200},[183,849,390],{"class":217},[183,851,221],{"class":200},[183,853,854,856,858],{"class":185,"line":395},[183,855,335],{"class":200},[183,857,354],{"class":315},[183,859,319],{"class":200},[17,861,862,863,865,866,868],{},"Nuxt handles routing (",[21,864,407],{},"), and your Auth module handles the logic. ",[21,867,280],{}," stays thin, just an entry point.",[17,870,871,872,874],{},"The same applies to ",[21,873,283],{},". Global composables (auto-imported by Nuxt) go in the framework's conventional folder. Feature-specific composables go inside the module:",[36,876,879],{"className":877,"code":878,"language":41,"meta":42},[39],"composables\u002F              # Auto-imported by Nuxt (global)\n├── useTheme.ts\n└── useBreakpoint.ts\nmodules\u002FAuth\u002Fcomposables\u002F # Auth-specific (manual import)\n├── useCurrentUser.ts\n└── useAuthRedirect.ts\n",[21,880,878],{"__ignoreMap":42},[12,882,884],{"id":883},"cross-cutting-concerns-what-goes-in-shared","Cross-cutting concerns: what goes in shared",[17,886,887],{},"The most common question when applying this: what happens when two modules need the same thing?",[17,889,890,891,437],{},"The rule is simple. If something is specific to a feature, it goes in its module. If two or more features use it, it goes in ",[21,892,436],{},[17,894,895,897],{},[21,896,442],{}," is for generic UI components with no business logic: buttons, modals, inputs. They're the building blocks, not the features.",[17,899,900,902,903,24,905,24,907,437],{},[21,901,448],{}," is for reusable logic without business state: ",[21,904,452],{},[21,906,455],{},[21,908,458],{},[17,910,911,913,914,24,916,24,918,437],{},[21,912,463],{}," is for pure functions: ",[21,915,467],{},[21,917,470],{},[21,919,473],{},[17,921,922,923,925,926,928,929,931],{},"What about shared state? If Auth and Orders both need user data, Auth owns the state. Orders imports ",[21,924,479],{}," from ",[21,927,269],{},". If over time more modules need the same data, you can move that composable to ",[21,930,436],{},". But don't do it preemptively. Start with the owning module and only move when there's real evidence it's cross-cutting.",[12,933,935],{"id":934},"when-to-use-it-and-when-not-to","When to use it and when not to",[17,937,938],{},"This approach works best for medium to large projects with real business logic and multi-dev teams. If your app has 5+ features that grow independently, Screaming Architecture will bring order to your life.",[17,940,941],{},"For MVPs, landing pages, or simple CRUDs, it might be overkill. You don't need a module structure for 3 views and a form.",[17,943,944],{},"If your project has the potential to grow, adopting it early saves you the painful migration later. And if you already have a large project with a generic structure, you can migrate progressively, one module at a time, starting with the feature that grows the most.",[17,946,947,948,647,950,952],{},"Next time you start a project, think about what the structure will look like when the team grows. If your folders only say ",[21,949,132],{},[21,951,507],{},", you know where to start.",[510,954,512],{},{"title":42,"searchDepth":193,"depth":224,"links":956},[957,958,959,960,961,962,963,964],{"id":552,"depth":193,"text":553},{"id":579,"depth":193,"text":580},{"id":589,"depth":193,"text":590},{"id":631,"depth":193,"text":632},{"id":658,"depth":193,"text":659},{"id":761,"depth":193,"text":762},{"id":883,"depth":193,"text":884},{"id":934,"depth":193,"text":935},"Your frontend project should scream what it does, not what it is built with. Concrete migration examples, framework coexistence with Nuxt, and shared state handling.",{},"\u002Fen\u002Fblog\u002Fscreaming-architecture-the-key-to-scalable-frontend",{"title":547,"description":965},{"loc":967},"en\u002Fblog\u002Fscreaming-architecture-the-key-to-scalable-frontend",[534,535,300,536],"XUt7tDpWWDSC_awv1-uP2jYfdyuo9GHsd__h8kII6ds",1777609841481]