Saltar al contenido

Caso de estudio: arquitectar una app grande de punta a punta

Poner todo junto: el plano completo de una aplicación grande con contenido público, catálogo, panel privado y API. Cómo se reparten colecciones, actions, auth, i18n y modos de render entre cuatro zonas, cómo el middleware compone los flujos transversales, y cómo el adapter de edge cierra el círculo con un deploy coherente donde cada pieza del track ocupa su lugar.

El plano: cuatro zonas, un proyecto

Una app grande no es homogénea: es un archipiélago de zonas con naturalezas distintas conviviendo en un solo build. Cuatro bastan para casi todo.

El reparto en el sistema de ficheros hace visible esta anatomía, y cada carpeta declara su modo de render por defecto:

Texto
src/  content/          # colecciones: blog, docs (Content Layer, tipadas con Zod)  pages/    [locale]/       # zona publica estatica, i18n por routing    tienda/         # catalogo: estatico mas server islands de precio y stock    panel/          # app SSR privada (prerender false)    api/            # endpoints: webhooks, movil, integraciones  actions/          # mutaciones del panel, validadas y con accept form  middleware.ts     # la secuencia transversal: log, seguridad, auth, i18n

Por qué cuatro zonas y no una

La tentación del principiante es tratar todo el sitio con un solo modo de render y una sola estrategia de datos, y esa uniformidad es justo lo que arruina un producto grande: o bien vuelve dinámico el contenido que debía ser estático —y pagas cómputo por cada artículo—, o bien intenta hornear lo que dependía del usuario —y no puede—. Separar en zonas no es burocracia: es reconocer que un blog y un panel no comparten naturaleza y no deben compartir régimen. Cada zona tiene su modo de render, su fuente de datos y su presupuesto de JavaScript, y la coherencia del conjunto nace precisamente de respetar esas diferencias en lugar de aplanarlas.

Los flujos transversales: auth, i18n y datos

Lo que es cierto para muchas rutas no vive en ninguna página: vive en el middleware, compuesto como una secuencia ordenada donde el orden codifica las dependencias. El registro envuelve todo, la seguridad blinda cada respuesta, la autenticación resuelve la sesión en locals antes de que ninguna ruta privada la necesite, y el idioma se lee de la URL una vez para todo el árbol.

TypeScript
// src/middleware.tsimport { sequence } from 'astro:middleware';import { logging, seguridad, auth, i18n } from './middleware/index';// El orden es la arquitectura: registrar, blindar, autenticar, traducirexport const onRequest = sequence(logging, seguridad, auth, i18n);

Los datos siguen la misma lógica de reparto que el render: el contenido editorial entra por el Content Layer —colecciones tipadas, cacheadas incrementalmente—, mientras que el dato de aplicación —usuarios, pedidos, sesiones— vive en la base de la plataforma, alcanzable desde el SSR a través de los bindings del adapter. La misma app lee un artículo de una colección y un pedido de una base sin que ninguna de las dos fuentes contamine a la otra.

ASTRO
---// Una pagina del panel: dato de app desde el binding, sesion desde localsexport const prerender = false;const { usuario } = Astro.locals;const { results } = await Astro.locals.runtime.env.DB  .prepare('SELECT * FROM pedidos WHERE usuario = ?')  .bind(usuario.id)  .all();---<h1>Tus pedidos</h1><ul>{results.map((p) => <li>{p.referencia}</li>)}</ul>

Deploy al edge: coherencia entre render y runtime

La elección del adapter debe rimar con el diseño del render, no contradecirlo. Para este producto, un adapter de edge como el de Cloudflare cierra el círculo con elegancia: la zona pública estática se sirve desde el CDN global, el SSR del panel corre en Workers a milisegundos del usuario, las sesiones viven en KV, el dato de aplicación en D1, y los assets en R2.

JS
// astro.config.mjsimport { defineConfig } from 'astro/config';import cloudflare from '@astrojs/cloudflare';export default defineConfig({  adapter: cloudflare(),  i18n: { defaultLocale: 'es', locales: ['es', 'en'] },});

El build partido en dos

Un solo build produce dos artefactos complementarios, y el adapter sabe repartir cada petición entre ellos. La parte client son ficheros terminados que la plataforma sirve sin computar nada; la parte server es el manejador que ejecuta las rutas vivas cerca del usuario. Esta división es la que permite que estático y dinámico convivan sin estorbarse, cada uno servido por el mecanismo que le corresponde.

Esta coherencia no es estética: es económica y de latencia. El contenido, que domina en volumen, no cuesta cómputo porque es estático y cacheado; la aplicación, que es minoría, corre cerca del usuario y ligera; el dato está donde el código lo alcanza rápido. Cambiar de proveedor sería, como aprendiste, mudar una importación y una clave, porque toda la especificidad vive comprimida en el adapter y el resto de tu código habla el lenguaje universal de la plataforma web.