Saltar al contenido

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.

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.

ASTRO
---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.

TypeScript
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.

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.

JS
// astro.config.mjsimport { 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.

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.

TypeScript
// src/env.d.tsdeclare 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.