La estructura de un proyecto Astro serio
Cómo pasa un proyecto Astro de juguete a sistema mantenible: organizar src por dominio y no por tipo, mantener src/pages fina y src/features gruesa, separar primitivas de UI de componentes de dominio, y fijar alias, barrels y fronteras de import que impidan que el código se enrede a medida que crece el equipo.
Del árbol por tipo al árbol por dominio
La estructura por defecto reparte el código según qué es cada fichero: todos los componentes en src/components, todos los layouts en src/layouts, todas las utilidades en src/lib. Funciona mientras el proyecto es pequeño, porque hay pocas piezas y la relación entre ellas cabe en la cabeza. Pero esa disposición esconde una trampa: agrupa por naturaleza técnica y dispersa por dominio. El buscador, el carrito y el panel de cuenta quedan repartidos por tres carpetas distintas, y entender una sola funcionalidad obliga a saltar entre components, lib y pages reconstruyendo a mano qué pieza pertenece a qué historia.
La organización por dominio invierte el criterio. En lugar de preguntar qué es cada fichero, pregunta de qué trata, y agrupa por eso. Nace así src/features, donde cada subcarpeta es una funcionalidad completa —checkout, search, account— que reúne dentro sus propios componentes, su lógica, sus tipos y sus datos. Abrir src/features/checkout te muestra el carrito entero de un vistazo, sin peregrinar por el árbol. Es lo que se ha llamado arquitectura que grita: al mirar la raíz de src, lo primero que ves no es que usas Astro, sino de qué va tu producto.
src/├─ pages/ rutas: orquestan y componen (fina)├─ features/ dominios autocontenidos (gruesa)│ ├─ checkout/│ │ ├─ components/ ui propia del carrito│ │ ├─ lib/ logica: precios y pagos│ │ ├─ schema.ts validacion con zod│ │ └─ index.ts fachada publica (barrel)│ └─ search/├─ components/ primitivas de ui sin dominio├─ layouts/ marcos de pagina├─ lib/ clientes y utilidades globales└─ content/ colecciones md y mdxEse árbol condensa la tesis del capítulo: la mayor parte del volumen vive en features, cada una cerrada sobre sí misma, mientras que las carpetas transversales —components, lib, layouts— quedan delgadas porque solo albergan lo que de verdad sirve a todos. Si con el tiempo una de ellas engorda más que tus features, es señal de que hay dominio escondido ahí dentro pidiendo salir.
src, capa por capa: pages finas, features gruesas
Un proyecto serio se lee como una pila de capas con una regla de dependencia clara: cada capa puede apoyarse en las de debajo, nunca en las de encima. Arriba, src/pages orquesta rutas y casi nada más: recibe la URL, pide datos a una feature, elige un layout y compone. Una página de doscientas líneas con lógica de negocio incrustada es una señal de alarma; lo que debería tener es una docena de líneas que delegan. La página no hace, la página coordina.
Debajo vive src/features, la capa gruesa donde reside la lógica de cada dominio: los componentes específicos, las funciones que hablan con la base de datos o la API, los esquemas de validación, los tipos. Una feature es autocontenida —importa primitivas y utilidades, pero no conoce las páginas que la usan ni a sus features hermanas—. Esa ignorancia deliberada es lo que la hace movible: puedes extraerla, probarla o borrarla sin desatar un efecto dominó por todo el proyecto.
Más abajo están las capas transversales, las que sirven a todos por igual. src/components guarda las primitivas de UI sin dominio —el botón, la tarjeta, el campo de texto—, piezas que no saben nada de checkout ni de cuentas y por eso las usa cualquiera. src/lib aloja la lógica sin interfaz: clientes de datos, helpers, adaptadores, tipos compartidos. Y src/layouts pone los marcos de página. La prueba de que una pieza pertenece a estas capas y no a una feature es que ninguna funcionalidad concreta se la apropia: si el botón supiera de precios, ya no sería una primitiva.
La flecha siempre apunta hacia abajo, y esa dirección única es toda la disciplina. Cuando una primitiva de UI empieza a importar de una feature, la pila se ha roto: has creado un ciclo en el que lo general depende de lo particular, y con él llega el acoplamiento que hace que tocar el carrito rompa el botón. Vigilar la dirección de las flechas es, en la práctica, vigilar la salud entera de la arquitectura.
Alias, barrels y fronteras de import
Con varias capas y dominios, los imports relativos se vuelven ilegibles: un ../../../lib/db no dice de dónde viene ni sobrevive a mover un fichero. La cura son los alias de ruta, declarados una vez en tsconfig.json y entendidos por Astro y Vite sin plugins. Un prefijo @/ o ~/ apuntando a src convierte aquel jeroglífico en un @/lib/db estable, que se lee igual desde cualquier profundidad y no se rompe al reorganizar.
// tsconfig.json{ "extends": "astro/tsconfigs/strict", "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } }}El segundo instrumento es el barrel: un index.ts en la raíz de cada feature que reexporta lo que esa feature ofrece al resto del proyecto. Convierte la carpeta en un módulo con una fachada pública —importas de @/features/checkout, no de sus entrañas— y con ello ganas una frontera: lo que no está en el barrel es privado, detalle interno que puedes cambiar sin avisar a nadie. Esa distinción entre superficie pública y tripa privada es lo que permite refactorizar una feature entera sin miedo, siempre que su fachada siga honrando el mismo contrato.
Con alias y barrels aparece la tercera pieza, la más valiosa y la más invisible: la posibilidad de imponer reglas de import que la máquina verifique. Una regla que prohíba que src/components importe de src/features, o que dos features se importen entre sí, convierte la dirección de las flechas de la que hablábamos en algo que el linter comprueba en cada commit.
// eslint.config.js — la frontera la vigila la maquinaimport boundaries from 'eslint-plugin-boundaries';export default [{ plugins: { boundaries }, rules: { 'boundaries/element-types': ['error', { default: 'disallow', rules: [ { from: 'pages', allow: ['features', 'components', 'layouts'] }, { from: 'features', allow: ['components', 'lib'] }, { from: 'components', allow: ['components'] }, ], }], },}];Cada regla es una capa de la pila declarada como ley: las páginas pueden apoyarse en features, componentes y layouts; las features solo en primitivas y utilidades; una primitiva, únicamente en otras primitivas. Todo lo demás queda prohibido por defecto. Es la diferencia entre una arquitectura que existe en un diagrama y una que existe de verdad: la segunda tiene un guardián automático que la defiende cuando el equipo, con prisa, olvida el diagrama.