Mantenibilidad: escalar el código y el equipo
Un proyecto grande lo mantiene un equipo, y lo que aguanta el paso del tiempo son las convenciones que una máquina verifica: TypeScript en su modo más estricto con astro check, lint y formato que imponen una gramática común, commits convencionales y git hooks que cierran el paso a lo que no cumple, y documentación viva que reparte el conocimiento para que el proyecto no dependa de quien lo escribió.
El código se escribe una vez y se lee mil, y a escala esas mil lecturas las hacen personas distintas de quien lo escribió. La mantenibilidad no es una virtud estética del código: es la propiedad que decide si un equipo puede seguir cambiándolo con confianza dentro de dos años o si cada modificación se vuelve una apuesta. Y la lección más dura de los proyectos grandes es que las convenciones que dependen de la buena voluntad no se cumplen: bajo presión, el humano olvida el formato, se salta el tipo, escribe el commit de cualquier manera. Lo único que aguanta es la convención que una máquina verifica y que cierra el paso cuando no se cumple. Este capítulo cierra el nivel montando esa red de seguridad —tipos, lint, commits, hooks, documentación— que permite que un proyecto crezca en líneas y en personas sin perder el control.
- Activar el TypeScript más estricto y verificarlo con
astro checkyastro sync. - Imponer una gramática común de código con lint y formato automáticos.
- Convertir commits convencionales y git hooks en guardianes que la máquina aplica.
- Documentar decisiones y repartir propiedad para escalar el equipo, no solo el código.
TypeScript estricto y la verdad verificada
La primera red de seguridad es el tipo. Astro trae configuraciones de TypeScript listas para extender, y a escala la elección correcta no es la intermedia sino la más severa: astro/tsconfigs/strictest. Cada regla que activa —no permitir any implícito, exigir manejar null, prohibir accesos sin comprobar— es un error que dejas de descubrir en producción para descubrirlo mientras escribes. En un proyecto pequeño el modo estricto se siente como fricción; en uno grande, con gente que no escribió el código tocándolo, es lo que evita que un cambio inocente rompa algo tres módulos más allá.
// tsconfig.json
{
"extends": "astro/tsconfigs/strictest",
"compilerOptions": {
"verbatimModuleSyntax": true
}
}
El editor comprueba tipos mientras escribes, pero eso no basta: hace falta un comando que verifique el proyecto entero, incluidos los ficheros .astro que un tsc normal no entiende. Ese comando es astro check, y su lugar natural es la integración continua, como una puerta que no deja pasar código que no tipa. Junto a él, astro sync regenera los tipos que Astro deriva de tu contenido y tu configuración —los esquemas de las colecciones, los módulos virtuales—, de modo que el autocompletado y la comprobación conocen la forma exacta de tus datos. Correr astro sync antes de astro check en la CI garantiza que se verifica contra tipos frescos, no contra una foto vieja.
Lint, formato y una gramática común
Cuando ocho personas escriben en el mismo repositorio, cada una trae sus costumbres: comillas, sangrías, orden de imports. Discutir eso en cada revisión es desgaste puro. La cura es delegarlo en herramientas que impongan una sola gramática y la apliquen solas. El formato lo fija un formateador —Prettier con prettier-plugin-astro, o Biome como alternativa integrada— que reescribe el código a una forma canónica sin que nadie opine. El lint lo pone un linter —ESLint con eslint-plugin-astro— que va más allá del estilo y caza patrones peligrosos: una variable sin usar, una promesa sin esperar, un acceso que puede fallar.
// package.json
{
"scripts": {
"check": "astro sync && astro check",
"lint": "eslint .",
"format": "prettier --write .",
"test": "vitest run"
}
}
La distinción vale la pena tenerla clara: el formateador se ocupa de cómo se ve el código —algo sin opinión moral, mecánico, que nunca debería debatirse— y el linter se ocupa de qué hace el código —reglas con criterio, que pueden atrapar errores reales—. Separarlos evita que peleen y deja que cada uno haga bien su parte. Con ambos configurados, las revisiones de código dejan de gastarse en comas y sangrías y se dedican a lo único que un humano aporta y una máquina no: si el diseño es acertado.
El mayor ahorro de estas herramientas no es la calidad, es el fin de una clase entera de discusiones. Cuando el formato lo decide una máquina, deja de ser una opinión que defender y pasa a ser un hecho que se ejecuta: nadie gana ni pierde el debate de las comillas porque no hay debate. Esa neutralidad libera la energía del equipo para las decisiones que sí importan y que ninguna herramienta puede tomar. La regla cultural que lo hace funcionar es simple e innegociable: la configuración se acuerda una vez, se versiona en el repositorio, y a partir de ahí nadie la discute en una revisión; si algo molesta, se cambia la configuración para todos, no se hace una excepción para uno.
Convenciones que la máquina impone
Tener los comandos no basta si alguien puede saltárselos. El eslabón que los vuelve obligatorios son los git hooks: ganchos que se disparan en momentos del flujo de git y pueden abortarlo. Un gestor como Lefthook —o Husky— los declara en un fichero versionado, de modo que todo el equipo comparte los mismos guardianes sin instalarlos a mano. En el pre-commit corres formato y lint solo sobre lo que cambió; en el commit-msg, validas que el mensaje siga una convención.
# lefthook.yml
pre-commit:
parallel: true
commands:
format: { run: prettier --check {staged_files} }
lint: { run: eslint {staged_files} }
commit-msg:
commands:
conventional: { run: commitlint --edit {1} }
Esa convención de mensajes son los commits convencionales: un prefijo que declara la naturaleza del cambio, un ámbito opcional y una descripción. No es burocracia; es metadato legible por máquina. De él se derivan solos el registro de cambios y el número de versión, y en un monorepo esa automatización la remata una herramienta como Changesets, que agrupa los cambios de cada paquete, calcula qué versión le toca según la semántica de sus commits y publica solo lo que cambió.
feat(checkout): permite pagar con un solo clic
fix(ui): el foco del boton respeta prefers-reduced-motion
chore(deps): sube astro a la 7.1
flowchart LR DEV[cambio local] --> HOOK[git hook pre-commit] HOOK --> FMT[formato y lint] FMT --> MSG[mensaje convencional] MSG --> CI[ci sync check test build] CI --> MERGE[merge a la rama principal] style HOOK fill:#f9e2af,color:#11111b style CI fill:#89b4fa,color:#11111b style MERGE fill:#a6e3a1,color:#11111b
El hook es la primera línea, rápida y local, que atrapa lo trivial antes de que salga de tu máquina; la CI es la línea de fondo, que vuelve a correrlo todo —porque un hook se puede saltar— y añade lo que tarda demasiado para el commit: la comprobación completa de tipos, los tests, el build de todas las apps.
# .github/workflows/ci.yml
name: ci
on: [push, pull_request]
jobs:
calidad:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
- run: pnpm run check # astro sync mas astro check
- run: pnpm run test
- run: turbo run build # respeta el grafo y cachea
Solo lo que pasa las dos líneas llega a la rama principal. Esa doble puerta es lo que mantiene sana una base de código que muchas manos tocan a diario, y en un monorepo la última orden delega en Turborepo para construir únicamente lo que cambió.
Los tests de la CI no son un bloque uniforme: forman una pirámide. En la base, ancha y veloz, las pruebas unitarias con Vitest sobre la lógica pura de src/lib y de las features. En medio, las pruebas de componente con la Container API de Astro, que renderiza un .astro a una cadena de forma aislada, sin arrancar el sitio entero, para comprobar que produce el HTML esperado. En la cima, estrecha y lenta, los flujos críticos de extremo a extremo con Playwright sobre el sitio ya construido. Muchas pruebas rápidas abajo, poquísimas lentas arriba: esa proporción es lo que da confianza sin frenar la iteración.
Documentar y escalar el equipo
Al final, un proyecto grande no lo mantiene un código: lo mantiene un equipo, y un equipo escala solo si el conocimiento no vive en una cabeza. La documentación que aguanta no es un manual que nadie actualiza, sino piezas vivas y ubicadas: un README por paquete que dice qué hace y cómo se usa; registros de decisión —los ADR— que capturan por qué se eligió algo, para que quien llegue no repita el debate ni deshaga la razón; y una ruta de muestrario que renderiza cada primitiva del design system en sus variantes, sirviendo a la vez de catálogo y de prueba visual.
El reparto de propiedad se declara también en el repositorio. Un fichero CODEOWNERS mapea zonas del árbol a las personas o equipos responsables, de modo que un cambio en el paquete de UI pide revisión a quien lo cuida y uno en checkout, a quien lo conoce. Es la organización por dominio del primer capítulo elevada a las personas: cada feature tiene dueño, y la estructura del código y la del equipo se reflejan la una en la otra.
# .github/CODEOWNERS
/packages/ui/ @equipo-design-system
/apps/web/ @equipo-web
/src/features/checkout/ @equipo-pagos
Tipos estrictos
strictest mas astro check y astro sync en la ci; los errores se descubren al escribir, no en produccion.
Formato y lint
Un formateador para como se ve y un linter para que hace; la gramatica se acuerda una vez y se ejecuta.
Hooks y commits
Git hooks que abortan lo que no cumple y commits convencionales que la maquina lee para versionar.
Docs y propiedad
Readme por paquete, adr para el porque, codeowners que reparte la responsabilidad por dominio.
La conclusión de todo este nivel, y quizá de toda la ingeniería a escala, es una idea incómoda sobre la naturaleza humana: las convenciones que dependen de que la gente se acuerde de cumplirlas no se cumplen. No porque el equipo sea negligente, sino porque es humano, y lo humano opera bajo presión, con prisa, con la cabeza en el problema de negocio y no en el orden de los imports. Un proyecto pequeño sobrevive a esa falibilidad porque cabe en una cabeza y una persona lo cuida entero; un proyecto grande, tocado por muchas manos a lo largo de años, no tiene esa suerte. Su única defensa es sacar la disciplina del terreno de la voluntad y meterla en el de la máquina, convirtiendo cada convención frágil —“formatea así”, “tipa esto”, “escribe el commit de esta forma”— en una comprobación automática que no olvida, no se cansa y no hace excepciones por amistad ni por urgencia. Ahí está el cambio de mentalidad que define la madurez de un equipo: dejar de pensar en la calidad como un esfuerzo que cada uno pone y empezar a pensarla como una propiedad que el sistema garantiza. El linter no es un juez que te riñe: es un compañero incansable que te libera de tener que acordarte. El tipo estricto no es una camisa de fuerza: es un colaborador que verifica lo que tú ya no puedes abarcar. El git hook no desconfía de ti: te salva de tu propio despiste a las siete de la tarde de un viernes. Y la documentación viva no es papeleo: es el equipo negándose a que su conocimiento muera con la persona que lo tuvo. Todo ello comparte una misma forma —codificar el criterio una vez, para que se aplique siempre, sin depender de que nadie lo recuerde— y esa forma es, en el fondo, la misma que recorre el nivel entero: hacer explícito y verificable lo que de otro modo quedaría implícito y frágil. La estructura por dominio hacía verificable la dirección de las dependencias; el monorepo, el grafo entre proyectos; los esquemas, la forma del contenido; los slots, los límites de composición; y ahora los tipos, el lint y los hooks hacen verificable la disciplina del equipo. Un proyecto grande no se mantiene sano porque tenga buena gente —la tiene—, sino porque ha convertido su buena voluntad en garantías que no dependen de ella. Cuando dejes de esperar que el equipo recuerde y empieces a diseñar sistemas que no exijan recordar, habrás aprendido lo único que de verdad escala: no el código, sino la confianza con la que se cambia.
- Extiende
astro/tsconfigs/strictesten tutsconfig.jsony añade a la CI un paso que corraastro sync && astro check; arregla el primer error que aflore. - Configura un formateador y un linter con sus plugins de Astro, versiona su configuración y añade scripts de
formatylintalpackage.json. - Instala git hooks que en
pre-commitformateen y linten lo cambiado, y encommit-msgvaliden un commit convencional; intenta un commit que no cumpla y observa cómo se rechaza. - Escribe un
READMEpara uno de tus paquetes y unCODEOWNERSque asigne cada zona del árbol a un dueño; verifica que un cambio pide revisión a quien corresponde.