wandres.dev
ADAPTERS · Node, Cloudflare, Vercel

@astrojs/node: standalone y middleware

El adapter para servir Astro en un servidor Node que tú controlas. Los dos modos que ofrece: standalone, que construye un servidor completo listo para arrancar con node, y middleware, que produce un handler que injertas en un servidor Express o Fastify propio. Configurar HOST y PORT, empaquetar en un contenedor, inyectar locals, y situar el proceso detrás de un proxy inverso y varios núcleos.

⏱ 15 min

@astrojs/node es el adapter para ejecutar Astro sobre un servidor Node que gobiernas tú. A diferencia de las plataformas serverless, aquí no hay ningún proveedor entre tu proceso y la máquina: posees el servidor, el puerto y el ciclo de vida. El adapter ofrece dos modos que responden a dos filosofías. En standalone construye un servidor completo, autosuficiente, que arrancas con node y que sirve tanto los estáticos como las rutas dinámicas. En middleware produce un manejador que injertas en un servidor Express, Fastify o Connect que ya tenías: Astro pasa a ser una pieza más de tu cadena, y tú sigues al mando de todo lo demás.

🎯 Al terminar esta lección sabrás
  • Instalar @astrojs/node y elegir entre standalone y middleware.
  • Arrancar el servidor standalone, gobernar HOST y PORT, y empaquetarlo.
  • Montar el handler del modo middleware en un servidor Express propio.
  • Situar el proceso Node detrás de un proxy inverso y varios núcleos.

standalone: un servidor completo listo para correr

El modo standalone es el más directo: le pides a Astro que construya un servidor web entero, con su bucle de escucha ya incluido. El build deja un punto de entrada ejecutable, y arrancarlo es una sola orden.

import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
});
npm run build
# el build produce dist/server/entry.mjs
node ./dist/server/entry.mjs
# el servidor escucha y sirve estaticos mas rutas dinamicas

Ese servidor no depende de nada externo: sirve por sí mismo los assets del cliente y ejecuta las rutas bajo demanda. Su comportamiento se ajusta por variables de entorno —HOST para la interfaz de escucha y PORT para el puerto (8080 por defecto)—, de modo que el mismo artefacto se despliega en distintos entornos sin recompilar. Esa portabilidad lo hace el candidato natural para un contenedor.

# Dockerfile para el modo standalone
FROM node:lts-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
ENV HOST=0.0.0.0
ENV PORT=8080
EXPOSE 8080
CMD ["node", "./dist/server/entry.mjs"]
💡
El modo natural para contenedores y VPS

standalone brilla cuando controlas la máquina: un contenedor Docker, un VPS, un servidor propio. Copias el dist, defines PORT, ejecutas node ./dist/server/entry.mjs y tienes un proceso de larga vida atendiendo peticiones. Es la ruta más parecida al despliegue clásico de una aplicación Node, sin intermediarios ni formatos de función que aprender.

middleware: Astro como una pieza de tu servidor

A veces no quieres que Astro sea el servidor, sino que viva dentro de uno tuyo —porque ya tienes rutas de API, autenticación, sockets o lógica que no es de Astro—. Para eso está el modo middleware: el build no arranca nada, sino que exporta un handler que tú montas donde quieras.

import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'middleware' }),
});
// servidor propio con Express
import express from 'express';
import { handler as ssrHandler } from './dist/server/entry.mjs';

const app = express();
app.use(express.static('dist/client/')); // sirve tu los estaticos
app.use(ssrHandler);                     // y delega el resto en Astro
app.listen(8080);

Aquí tú posees la cadena: registras tus rutas antes o después de Astro, decides quién sirve los estáticos y cómo se ordenan las capas. El handler acepta incluso un cuarto argumento para inyectar locals, de forma que tu servidor puede pasar datos —un usuario autenticado, un identificador de petición— al contexto que verán tus páginas.

app.use((req, res, next) => {
  const locals = { usuario: req.session?.usuario };
  ssrHandler(req, res, next, locals); // los locals llegan a Astro.locals
});
💡
Astro bajo un subcamino de tu servidor

Si tu servidor Express expone Astro bajo un prefijo —por ejemplo /app— monta el handler en esa ruta y declara el mismo base en astro.config. Así los enlaces internos y los assets se resuelven contra el prefijo correcto, y Astro convive con el resto de tu API sin pisarse las URLs. Es el detalle que distingue un injerto limpio de uno que funciona solo a medias.

📝
standalone y middleware sirven a dos dueños distintos

La elección no es de rendimiento sino de propiedad. En standalone, Astro es el dueño del servidor y tú le pones ajustes. En middleware, tú eres el dueño y Astro es un invitado en tu cadena. Si tu proyecto es fundamentalmente un sitio Astro, standalone te ahorra trabajo; si es una aplicación Node que además renderiza páginas con Astro, middleware respeta esa jerarquía.

📦

standalone

Astro es el servidor. Arrancas el proceso y sirve todo. Ideal para contenedores y VPS sin más piezas.

🧩

middleware

Astro es un handler dentro de tu Express o Fastify. Tú posees la cadena y el ciclo de vida completo.

🗂️

Los estáticos

En standalone los sirve Astro; en middleware los sirves tú con express.static desde dist client.

🔁

El proceso vivo

En ambos, un proceso de larga vida con memoria entre peticiones: pool de conexiones, caché, sockets.

Detrás de un proxy inverso y varios núcleos

Un proceso Node en producción rara vez se expone desnudo a internet. Lo habitual es ponerle delante un proxy inverso —nginx, Caddy— que termina el TLS, comprime respuestas, cachea estáticos y reparte carga. El proceso de Astro escucha en un puerto local, y el proxy es la cara pública que atiende el mundo.

Node ejecuta un único hilo por proceso, así que para aprovechar varios núcleos se lanzan varias instancias —con un gestor como PM2, con systemd, o con el cluster del propio Node— y el proxy balancea entre ellas. Ese patrón, un servidor de aplicación replicado tras un balanceador, es el mismo que rige cualquier backend Node clásico, y hereda sus mismas responsabilidades: vigilar la salud de cada proceso, reiniciarlo si cae y drenar conexiones al desplegar una versión nueva.

flowchart LR
C[cliente] --> PX[proxy inverso nginx o caddy]
PX --> N1[proceso node uno]
PX --> N2[proceso node dos]
N1 --> APP[servidor de Astro]
N2 --> APP
style PX fill:#89b4fa,color:#11111b
style APP fill:#a6e3a1,color:#11111b
Elegir Node es elegir un proceso con memoria, y con ello un mundo de posibilidades y de deberes

Correr Astro sobre @astrojs/node no es solo una opción técnica de despliegue: es abrazar un modelo de cómputo distinto al del serverless, con virtudes y obligaciones que conviene ver enteras. Un proceso Node de larga vida es una entidad con estado y con memoria: puede sostener un pool de conexiones a la base de datos abierto entre peticiones, cachear en memoria lo caro de calcular, mantener sockets vivos, precalentar lo que haga falta una sola vez al arrancar. Nada de eso se paga en cada petición, porque el proceso no nace y muere con ella —vive—. Esa persistencia es un poder enorme frente al modelo efímero de las funciones, donde cada invocación empieza casi de cero. Pero la misma persistencia trae sus deberes: un proceso que vive acumula fugas de memoria si las hay, hay que reiniciarlo, vigilarlo, replicarlo para tolerar fallos, escalarlo a mano cuando la carga sube, parchear la máquina que lo aloja. El serverless te libera de todo ese trabajo indiferenciado a cambio de quitarte la memoria entre peticiones; Node te devuelve la memoria a cambio de devolverte la operación. Ninguno es superior en abstracto: son dos puntos opuestos de un mismo eje —cuánto estado guardas entre peticiones y cuánta operación asumes a cambio—. Entender @astrojs/node a fondo es entender que, al elegirlo, no estás solo escogiendo dónde corre tu sitio, sino qué clase de recurso computacional quieres que sea: un proceso vivo que recuerda y que exige cuidados, o una función que olvida y que casi se cuida sola. La madurez consiste en elegir ese eje por la naturaleza de tu carga, no por costumbre.

⚔️ Sirve Astro en un Node que controlas
  1. Configura node({ mode: 'standalone' }), construye y arranca con node ./dist/server/entry.mjs; cambia el PORT con una variable de entorno.
  2. Empaqueta ese servidor en un contenedor con un Dockerfile mínimo y comprueba que responde en el puerto expuesto.
  3. Cambia a node({ mode: 'middleware' }), monta el handler en un Express mínimo, sirve tú los estáticos con express.static e inyecta un locals que leas con Astro.locals.
  4. Esboza un despliegue con un proxy inverso delante y dos procesos Node detrás, y razona qué gana cada pieza de esa arquitectura.