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.
Un proyecto Astro de cinco páginas se organiza solo: todo cabe en src/components y nadie se pierde. Pero el mismo árbol, escalado a doscientas rutas y ocho personas tocándolo a la vez, se convierte en un pantano donde nadie sabe qué importa a qué ni dónde vive la verdad de cada dominio. La diferencia entre un proyecto que envejece bien y uno que colapsa bajo su propio peso casi nunca es el framework: es cómo está dispuesto el código. Este nivel trata de esa disposición, y empieza por la decisión más silenciosa y más determinante de todas —cómo repartes src/—, porque es la que, sin hacer ruido, decide si dentro de un año querrás mantener el proyecto o reescribirlo.
- Cambiar la organización por tipo de fichero por una organización por dominio.
- Mantener
src/pagesfina y trasladar la lógica asrc/features. - Separar primitivas de UI, componentes de dominio y utilidades sin UI.
- Fijar alias, barrels y fronteras de import que contengan el crecimiento.
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 mdx
Ese á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.
Organizar por tipo de fichero no es un error de principiante: es el punto de partida correcto para un proyecto pequeño, y forzar src/features en una landing de tres páginas es sobreingeniería. El problema aparece con la escala. Cuando una carpeta components acumula sesenta ficheros sin relación entre sí, la agrupación por tipo ha dejado de ayudar y empieza a estorbar. La señal para migrar es sencilla: cuando entender una funcionalidad exige abrir tres carpetas distintas, tu proyecto ya pide dominios.
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.
flowchart TD PG[pages orquestan rutas] --> FE[features logica de dominio] FE --> UI[components primitivas de ui] FE --> LIB[lib clientes y utilidades] UI --> TOK[tokens y estilos base] LIB --> TOK style PG fill:#89b4fa,color:#11111b style FE fill:#f9e2af,color:#11111b style TOK fill:#a6e3a1,color:#11111b
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.
La ventaja práctica de la organización por dominio se cobra en el día a día: dentro de src/features/checkout puedes poner el componente junto a su test, su estilo, su esquema de Zod y su cliente de datos, todo a un palmo. Cuando lo relacionado vive junto, cambiar una funcionalidad es abrir una carpeta y no rastrear el árbol; y borrarla es borrar esa carpeta, sin dejar huérfanos dispersos por medio proyecto. La co-locación es la recompensa cotidiana de haber agrupado por lo que cambia junto.
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.
Un index.ts que reexporta módulos crea una dependencia hacia todo lo que agrupa, y sembrar barrels en cada subcarpeta puede inflar lo que Vite ha de analizar y enturbiar el tree-shaking. La regla sana es reservarlos para las fronteras que de verdad quieres publicar —la raíz de cada feature, la de tu librería de UI— y no para cada rincón interno. Dentro de una feature, importa directo del fichero; hacia fuera, importa del barrel. Frontera pública sí, barrel por costumbre no.
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 maquina
import 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.
Junto a las fronteras conviene fijar convenciones de nombres y no discutirlas más: los componentes en PascalCase —TarjetaProducto.astro—, las utilidades y ficheros de lógica en kebab-case —cliente-datos.ts—, y las carpetas de feature con el sustantivo del dominio en singular —checkout, no checkouts—. Lo que importa no es cuál de las dos capitalizaciones eliges, sino que el proyecto entero elija la misma: una convención mediocre aplicada sin excepción vale más que la convención perfecta aplicada a medias, porque su valor está en la predecibilidad, no en la elegancia.
Por dominio
src/features reune cada funcionalidad completa. Abrir una carpeta muestra la historia entera, sin saltar por el arbol.
Pages finas
Una pagina coordina: pide datos, elige layout, compone. Si hace logica de negocio, esa logica va a una feature.
Capas transversales
components para primitivas de ui, lib para logica sin interfaz, layouts para marcos. Nadie de dominio se los apropia.
Alias y barrels
Un prefijo hacia src acaba con los imports relativos, y el barrel de cada feature fija su fachada publica.
Es tentador creer que estructurar un proyecto consiste en elegir buenos nombres de carpeta, pero eso es la superficie de una idea mucho más honda: una arquitectura no es un árbol de directorios, es un grafo de dependencias, y las carpetas son solo la forma más visible de dibujarlo. Lo que de verdad determina si un sistema envejece bien no es dónde vive cada fichero, sino quién puede depender de quién, y en qué dirección. Toda la disciplina de este capítulo —pages que solo miran hacia abajo, features que se ignoran entre sí, primitivas que no saben de dominio, reglas de import que la máquina vigila— apunta a una única propiedad: que el grafo de dependencias sea acíclico y fluya en un solo sentido, de lo concreto y volátil hacia lo general y estable. Esa propiedad es lo que hace posible el razonamiento local. En un grafo dirigido y limpio puedes coger una feature, entenderla mirando solo lo que importa —sus dependencias hacia abajo— y modificarla sabiendo que nada por encima de ella, que no controlas, se colará por una puerta trasera. En un grafo enredado, en cambio, cada cambio es una apuesta: tocas el carrito y se rompe el buscador porque, sin que nadie lo decidiera, uno acabó dependiendo del otro. La razón profunda de que la organización por dominio escale mejor que la organización por tipo no es estética ni cuestión de gusto: es que alinea la estructura del código con la estructura del cambio. El software no cambia por tipos —rara vez tocas todos los componentes a la vez—, cambia por funcionalidades: llega una petición sobre el checkout y quieres que todo lo del checkout esté junto y aislado. Una arquitectura que agrupa lo que cambia junto y separa lo que cambia por razones distintas está, en el fondo, apostando a favor de su propio futuro. Cuando dejes de ver src/ como un cajón que ordenar y empieces a verlo como un grafo que gobernar, habrás cruzado la línea que separa acomodar ficheros de diseñar sistemas.
- Toma un proyecto Astro con la típica
src/componentsinflada y elige una funcionalidad concreta: agrupa todas sus piezas en una carpeta bajosrc/features. - Añade un barrel
index.tsa esa feature que reexporte solo lo que las páginas necesitan; deja lo demás como detalle interno. - Declara un alias
@/*haciasrcentsconfig.jsony sustituye los imports relativos más profundos por su versión con alias. - Adelgaza una página que tuviera lógica de negocio: mueve esa lógica a la feature y deja la página coordinando en una docena de líneas.