wandres.dev
SESIONES Y ESTADO · Astro.session y cookies

La Sessions API de Astro: estado del lado del servidor

El problema que resuelve una sesión frente a un HTTP sin memoria: guardar el estado en el servidor y entregar al navegador solo un identificador opaco. Qué es Astro.session, cómo se lee y escribe con get, set, regenerate y destroy, la asimetría entre un get asíncrono y un set síncrono, y por qué la Sessions API solo funciona con un adapter que habilite el render bajo demanda y un driver que persista los datos.

⏱ 15 min

HTTP no recuerda. Cada petición aterriza huérfana, sin memoria de la anterior, y esa amnesia es un rasgo de diseño del protocolo, no un defecto. Pero una aplicación real necesita hilar: saber que esta visita es la misma persona que inició sesión hace tres pantallas. Una cookie podría cargar el dato, pero es pequeña, pública y manipulable. La sesión invierte el reparto: guarda el estado en el servidor y entrega al navegador únicamente un identificador opaco. Astro.session es la API oficial de Astro para ese patrón —estable desde la 5.7 y, en Astro 7, la forma canónica de sostener estado por usuario sin arrastrar un framework al cliente—.

🎯 Al terminar esta lección sabrás
  • Entender qué resuelve una sesión frente a un HTTP sin estado.
  • Leer y escribir estado con Astro.session en páginas y context.session fuera de ellas.
  • Distinguir el get asíncrono del set síncrono y por qué difieren.
  • Reconocer los dos requisitos de la Sessions API: un adapter y un driver.

HTTP no recuerda: el problema que la sesión resuelve

El protocolo web trata cada petición como un suceso aislado. El servidor responde y olvida; la siguiente petición del mismo navegador llega sin ninguna marca que la ligue a la anterior. Esa ausencia de estado es lo que hace a HTTP escalable —cualquier máquina puede atender cualquier petición—, pero deja un vacío: ¿cómo sabe la aplicación que quien pide el carrito es quien acaba de añadir un producto?

La respuesta clásica es una cookie: un dato que el navegador reenvía en cada petición. El error tentador es meter el estado entero en ella —el carrito, el usuario, sus permisos—. Una cookie viaja al cliente, así que es visible para quien abra las herramientas del navegador, manipulable por quien quiera falsearla, y está limitada a unos cuatro kilobytes. Guardar ahí datos sensibles o voluminosos es pedir problemas. La sesión resuelve el dilema con una indirección: en la cookie va solo un identificador —un token aleatorio e ininteligible—, y el dato real vive en un almacén del servidor, indexado por ese id. El navegador presenta el token; el servidor lo cambia por los datos.

Astro.session: get, set, regenerate, destroy

Dentro de una página o componente .astro, la sesión se alcanza por el objeto global Astro. Fuera —en endpoints, middleware y actions— llega dentro del contexto, como context.session. La API es la misma en ambos sitios y se reduce a un puñado de métodos deliberadamente pequeños.

---
export const prerender = false; // innecesario si output es 'server'
const carrito = await Astro.session?.get('carrito');
---
<a href="/checkout">🛒 {carrito?.length ?? 0} artículos</a>

La escritura es igual de directa, pero esconde una asimetría que conviene interiorizar. Astro.session.get es asíncrono —devuelve una promesa— porque puede tener que ir al almacén de fondo, leer los bytes y deserializarlos. Astro.session.set, en cambio, es síncrono: escribe en el objeto de sesión que vive en memoria durante la petición y devuelve al instante; el volcado al backend ocurre una sola vez, al final de la petición. De ahí que se espere el get con await y el set no lo necesite.

import type { APIContext } from 'astro';

export async function POST({ session, request }: APIContext) {
  const carrito = (await session?.get('carrito')) ?? [];
  const item = await request.json();
  carrito.push(item);
  session?.set('carrito', carrito); // sincrono; se persiste al cerrar la peticion
  return Response.json({ carrito });
}

Los otros dos métodos gobiernan el ciclo de vida del identificador. Astro.session.regenerate cambia el id de sesión conservando los datos —la defensa contra la fijación de sesión, que veremos a fondo—. Astro.session.destroy aniquila la sesión: borra la cookie y el objeto del almacén, el gesto exacto del cierre de sesión. Hay además un Astro.session.load para cargar una sesión por id cuando gestionas el token por tu cuenta y no por cookie.

Un matiz que ahorra sustos: leer, modificar y volver a escribir una clave de sesión no es una operación atómica. Si dos peticiones del mismo usuario cargan el carrito a la vez, le añaden un artículo y lo guardan, la segunda escritura puede pisar a la primera y perder un ítem. Para estado que se actualiza en paralelo, evita el ciclo leer-antes-de-escribir cuando puedas —o serializa esas escrituras— en lugar de confiar en que nunca coincidan dos peticiones.

ℹ️
Un get que espera, un set que no

La firma lo dice todo: get es (key) => Promise<any> y set es (key, value, options?) => void. No es un capricho. El set solo toca una estructura en memoria y agenda el guardado para el cierre de la petición, así que puede ser instantáneo; el get puede necesitar una ida y vuelta al disco, a Redis o a KV, y por eso devuelve una promesa. Olvidar el await en un get es el tropiezo número uno: recibirías la promesa en vez del dato, y una comprobación como if (carrito) daría verdadero aunque no haya nada dentro.

Los dos requisitos: un adapter y un driver

La Sessions API no funciona en el vacío. Necesita dos piezas que ya conoces a medias. La primera es un adapter: las sesiones solo tienen sentido en rutas renderizadas bajo demanda, porque una página horneada en el build no tiene una petición viva a la que asociar un usuario. Sin adapter no hay render en servidor, y sin render en servidor Astro.session es undefined. La segunda es un driver de almacenamiento: el lugar físico donde se guardan los datos indexados por id. Astro apoya el almacenamiento en unstorage, y ofrece un catálogo de drivers —memoria, filesystem, KV, Redis— que estudiaremos en la próxima lección.

// astro.config.mjs
import { defineConfig, sessionDrivers } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  adapter: node({ mode: 'standalone' }),
  session: {
    driver: sessionDrivers.redis({ url: process.env.REDIS_URL }),
  },
});

Hay un atajo cómodo: los adapters de Node, Cloudflare y Netlify configuran un driver por defecto por ti —filesystem, Workers KV y Blobs respectivamente—, así que en esos destinos las sesiones funcionan casi sin tocar la configuración. Los demás adapters exigen que nombres un driver a mano. En todos los casos, la cookie que Astro planta guarda solo el identificador; su forma por defecto es { name: "astro-session", sameSite: "lax", httpOnly: true, secure: true }, unas opciones que ya delatan una intención de seguridad.

📝
Las sesiones no viven en el edge middleware

Un límite que ahorra depuración: la Sessions API no está soportada en middleware de edge. Si despliegas el middleware en el edge de tu plataforma, context.session no estará disponible allí, aunque sí lo esté en tus páginas y endpoints renderizados bajo demanda. La razón es de arquitectura —el edge corre antes y más lejos que tu almacén de sesión—, y la salida es tratar la sesión en el middleware normal o en las propias rutas, no en la capa de edge.

⚠️
Astro.session es undefined en rutas prerenderizadas

Si una página se prerenderiza —el comportamiento por defecto en output: 'static'—, no hay petición de servidor a la que colgar una sesión, y Astro.session vale undefined. Por eso el ejemplo usa Astro.session?.get con encadenamiento opcional. Cuando de verdad necesites sesión en una ruta, márcala bajo demanda con export const prerender = false (o pon el proyecto entero en output: 'server'). Un undefined silencioso aquí suele ser una ruta que creías dinámica y seguía siendo estática.

Tipar la sesión con App.SessionData

Por defecto la sesión no tiene forma declarada: Astro.session.get devuelve any, sin autocompletado ni red de seguridad del compilador. Igual que App.Locals tipa el maletín del middleware, existe App.SessionData para tipar el almacén de sesión. Se declara una sola vez en src/env.d.ts, y Astro la aplica en páginas, endpoints, middleware y actions por igual.

// src/env.d.ts
declare namespace App {
  interface SessionData {
    carrito: string[];
    userId: string;
  }
}

Con esa declaración, await Astro.session?.get('carrito') se estrecha a string[] | undefined, y un Astro.session?.set('userId', 42) se rechaza en compilación porque un número no encaja con la forma prometida. Bajo el capó, Astro serializa y deserializa los valores con devalue —la misma biblioteca que usan las content collections y las actions—, de modo que no te limitas a cadenas y números: puedes guardar Date, Map, Set, URL, arrays y objetos planos, y recuperarlos con su tipo intacto, sin el aplanamiento que impondría un JSON.stringify a secas.

📝
El tipo es una promesa de compilación, no una migración

App.SessionData existe solo para el compilador: no valida en runtime ni transforma lo que ya está guardado. Si cambias la forma de un campo cuando hay usuarios con sesiones vivas, el dato viejo permanecerá en el almacén con su estructura antigua y tu código lo leerá creyendo el tipo nuevo. Trata los cambios de forma de la sesión como lo que de verdad son —migraciones de datos— y contempla el caso de las sesiones que aún llevan el formato anterior, o púrgalas al desplegar.

flowchart LR
B[navegador con cookie de id] --> S[servidor lee el identificador]
S --> DR[el driver busca por id]
DR --> D[datos de la sesion en el almacen]
D --> R[respuesta personalizada]
style S fill:#89b4fa,color:#11111b
style D fill:#a6e3a1,color:#11111b
🔑

get asincrono

await Astro.session?.get(clave) lee del almacen y deserializa. Siempre con await.

✏️

set sincrono

Astro.session?.set(clave, valor) escribe en memoria y persiste al cerrar la peticion.

♻️

regenerate y destroy

regenerate cambia el id conservando datos; destroy borra cookie y almacen al salir.

🧱

adapter y driver

Sin render bajo demanda y sin almacen configurado, session es undefined.

La cookie no guarda el dato: guarda una llave

El corazón conceptual de una sesión es una indirección tan simple que es fácil pasarla por alto y tan profunda que reorganiza toda tu manera de pensar el estado en la web. Un principiante, ante la amnesia de HTTP, mete el dato en la cookie: guarda userId=42 y lo lee en la siguiente petición. Funciona, y es una trampa. Esa cookie viaja al cliente, y todo lo que viaja al cliente es visible y manipulable: cualquiera puede cambiar el 42 por un 43 y hacerse pasar por otro usuario, porque el servidor está confiando en un dato que no controla. La sesión rompe esa confianza mal puesta con un movimiento elegante: en la cookie no va el hecho, va una referencia al hecho. Un token aleatorio, largo e imposible de adivinar, que por sí mismo no significa nada —no dice quién eres, no afirma ningún permiso—, solo sirve como llave para abrir un cajón que vive del lado del servidor, donde el dato real reposa fuera del alcance del cliente. Manipular el token no sirve de nada: cambiar un carácter no te lleva al cajón de otro, te lleva a un cajón inexistente. Esta es la misma idea que sostiene los tokens al portador, las claves de API y las capacidades en los sistemas operativos: separar la identidad de la autoridad, entregar al mundo exterior algo que se puede presentar pero no interpretar ni falsear. Cuando entiendes que la cookie de sesión es una llave y no un contenido, dejas de preguntarte qué guardo en la cookie —la respuesta es casi siempre “nada, salvo la llave”— y empiezas a preguntarte dónde vive el estado y quién puede tocarlo. El servidor recuerda; el cliente solo lleva el número del guardarropa. Toda la seguridad del patrón brota de esa asimetría: el que sabe menos es el que no es de fiar.

⚔️ Da memoria a una ruta bajo demanda
  1. Instala un adapter (por ejemplo npx astro add node) y marca una página con export const prerender = false.
  2. Escribe un contador de visitas: lee await Astro.session?.get('visitas'), súmale uno y guárdalo con Astro.session?.set.
  3. Recarga varias veces y observa que el número crece; borra la cookie del navegador y comprueba que el contador vuelve a empezar.
  4. Elimina el await del get a propósito y razona por qué el contador se rompe: qué recibes cuando no esperas la promesa.