Enrutar solicitudes a contadores con nombre

CloudflareBeginner
Practicar Ahora

Introducción

Un Cloudflare Worker normal puede responder a muchas solicitudes, pero una solicitud no puede suponer que la siguiente llegará a la misma instancia de JavaScript en ejecución. Este diseño sin estado es excelente para tareas independientes. Sin embargo, resulta poco práctico cuando varias solicitudes deben coincidir en un valor cambiante, como el número de personas que esperan en una cola de soporte.

Un Durable Object proporciona a la aplicación una unidad de coordinación direccionable. En este laboratorio, cada nombre de contador selecciona un objeto diferente. Las solicitudes para support llegan repetidamente al mismo contador lógico, mientras que las solicitudes para billing llegan a otro contador con un estado independiente. Cloudflare puede mover o reiniciar el entorno de ejecución subyacente; la identidad estable del objeto y el estado respaldado por SQLite siguen siendo parte del contrato de la aplicación.

Conectará cuatro conceptos:

  1. Una clase define lo que puede hacer un objeto contador.
  2. Un namespace es la colección de objetos respaldados por esa clase.
  3. Un binding permite que el Worker de entrada acceda al namespace.
  4. getByName() convierte el mismo nombre validado en la misma referencia de objeto, y un método RPC llama al código de ese objeto.

Construirá la aplicación, demostrará el enrutamiento basado en nombres de forma local, la desplegará en su propia cuenta de aprendizaje de Cloudflare, relacionará las evidencias del terminal con el Dashboard y eliminará tanto el namespace de la clase como el Worker al finalizar.

Antes de comenzar este curso, complete Conectar LabEx a su cuenta de Cloudflare. Allí aprenderá a usar el terminal de la VM de LabEx, la autorización del dispositivo de Wrangler, la confirmación de la cuenta y la configuración del ID de cuenta. Ya debe saber cómo un Worker pequeño de JavaScript gestiona una solicitud HTTP. No se presupone experiencia previa con Durable Objects.

La documentación oficial indica actualmente que los Durable Objects respaldados por SQLite están disponibles en Workers Free. Este laboratorio crea un namespace de clase desechable, unos pocos objetos pequeños y solo solicitudes acotadas. No requiere Workers Paid. La configuración instala Node.js 22.22.0 y Wrangler 4.132.0 localmente en el proyecto /home/labex/project/named-counters; no inicia sesión, no crea recursos en la nube, no despliega código ni completa la implementación del estudiante.

Autorizar la VM y asignar un nombre a la aplicación

En este paso, conectará esta VM nueva de LabEx a su cuenta de aprendizaje de Cloudflare y creará una configuración de aplicación única. Haber iniciado sesión en el Dashboard desde un navegador no autoriza automáticamente los comandos dentro de una VM nueva.

Entre en el proyecto preparado y confirme la versión fijada de Wrangler:

cd /home/labex/project/named-counters
npx wrangler --version

Debe aparecer 4.132.0. Inicie el flujo de autorización del dispositivo de Wrangler:

npx wrangler login --device --browser=false

Wrangler muestra una URL y un código de dispositivo corto. Abra la URL en el navegador, introduzca el código, confirme que la cuenta seleccionada es su cuenta de aprendizaje dedicada y revise los permisos solicitados antes de autorizar. Puede aparecer el acceso en segundo plano porque Wrangler debe continuar funcionando después de que usted vuelva al terminal. Nunca envíe una contraseña ni un token a través del terminal.

Cuando el navegador indique que la autorización se completó correctamente, vuelva al terminal y espere a que Wrangler termine. Solicite la información estructurada de la cuenta:

npx wrangler whoami --json

Confirme loggedIn: true y, a continuación, identifique la cuenta prevista, incluso si solo aparece una cuenta. El nombre de la cuenta sirve para la comprobación humana; el ID es un valor de configuración estable que no es necesario mostrar en el terminal.

Guarde el resultado estructurado, muestre únicamente el nombre de la cuenta que no es confidencial y seleccione el ID correspondiente a LabEx Learning:

WHOAMI="$(npx wrangler whoami --json)"
printf '%s\n' "$WHOAMI" | jq '{loggedIn, authType, accounts: [.accounts[] | {name}]}'
ACCOUNT_ID="$(printf '%s\n' "$WHOAMI" | jq -r '.accounts[] | select(.name == "LabEx Learning") | .id')"
test -n "$ACCOUNT_ID"

$(...) captura la salida de un comando en una variable de shell. jq muestra primero solo el nombre de la cuenta para confirmarlo y, después, selecciona de forma privada el ID asociado. test -n solo tiene éxito cuando el valor seleccionado no está vacío. Si su cuenta de aprendizaje dedicada tiene otro nombre visible, sustituya LabEx Learning en la expresión de selección después de confirmar ese nombre.

Genere un nombre único para el Worker. openssl rand -hex 6 produce 12 caracteres hexadecimales aleatorios y $(...) los inserta en la variable de shell:

RUN="labex-c10-o01-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"

Cree wrangler.jsonc. Un archivo de configuración indica a Wrangler qué código debe desplegar y qué capacidades de Cloudflare debe asociar al entorno de ejecución. El marcador JSON sin comillas permite expandir $RUN y $ACCOUNT_ID, mientras que la barra invertida mantiene literal la clave $schema.

cat > wrangler.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "$ACCOUNT_ID",
  "main": "src/index.js",
  "compatibility_date": "2026-09-18",
  "workers_dev": true,
  "preview_urls": false,
  "observability": {
    "enabled": true,
    "head_sampling_rate": 1
  },
  "durable_objects": {
    "bindings": [
      { "name": "COUNTERS", "class_name": "Counter" }
    ]
  },
  "exports": {
    "Counter": { "type": "durable-object", "storage": "sqlite" }
  }
}
JSON

Este archivo describe la aplicación, pero todavía no crea nada en Cloudflare. observability conserva los registros de solicitudes y de la aplicación para una comprobación posterior en el Dashboard. Los campos de Durable Object serán relevantes en el siguiente paso.

Conectar un namespace, un binding y una clase

En este paso, leerá la configuración de Durable Objects como un mapa que muestra cómo una solicitud llega a un objeto con estado y, después, generará los tipos del entorno de ejecución que exponen el binding a su código.

Una clase de Durable Object es la plantilla de JavaScript para un objeto. La clase Counter que escribirá más adelante definirá operaciones como incrementar y leer un valor.

Un namespace es la colección de todos los objetos respaldados por esa clase. Un namespace puede contener support, billing y muchos otros contadores con nombre. El namespace no significa que esos contadores compartan un único valor; cada identidad estable de objeto tiene su propio almacenamiento independiente.

Un binding es el nombre que utiliza el Worker de entrada para acceder a ese namespace. Esta configuración vincula el nombre COUNTERS con la clase Counter. Por lo tanto, su código utilizará env.COUNTERS.

La entrada exports declara el estado actual del ciclo de vida de la clase. Indica a Cloudflare que cree Counter con el backend de almacenamiento SQLite en el primer despliegue. SQLite es el backend recomendado para las clases nuevas y está disponible en Workers Free. La pequeña tabla de este laboratorio almacena un solo entero dentro de cada objeto.

Genere una descripción de tipos a partir de la configuración:

npx wrangler types

Busque COUNTERS en el archivo generado:

grep -n 'COUNTERS' worker-configuration.d.ts

La línea será similar a esta:

COUNTERS: DurableObjectNamespace<import("./src/index").Counter>;

El texto generado que rodea la línea puede cambiar, pero importan tres hechos: el binding se llama COUNTERS, es un DurableObjectNamespace y apunta a la clase Counter exportada. Vuelva a generar los tipos cada vez que cambie un binding para evitar que la configuración y el código se separen silenciosamente.

Crear el contador con nombre

En este paso, implementará la clase Counter y el Worker de entrada que dirige un nombre de URL validado a un objeto.

Cada Durable Object tiene almacenamiento privado. El constructor crea una tabla de una sola fila llamada counter_state e inserta el valor inicial únicamente si la fila todavía no existe. blockConcurrencyWhile() retrasa las solicitudes al objeto hasta que finaliza esta breve inicialización. Es adecuado para configurar el esquema; no debe envolver todas las solicitudes ni operaciones de red externas.

Los métodos públicos increment() y getCount() son métodos RPC. RPC, abreviatura de llamada a procedimiento remoto, permite que el Worker llame a un método de un stub de Durable Object como si fuera un objeto de JavaScript asíncrono. Cloudflare transporta la llamada hasta el objeto seleccionado.

Cree el punto de entrada del Worker:

cat > src/index.js <<'JS'
import { DurableObject } from "cloudflare:workers";

export class Counter extends DurableObject {
  constructor(ctx, env) {
    super(ctx, env);
    ctx.blockConcurrencyWhile(async () => {
      this.ctx.storage.sql.exec(`
        CREATE TABLE IF NOT EXISTS counter_state (
          key INTEGER PRIMARY KEY CHECK (key = 1),
          value INTEGER NOT NULL
        )
      `);
      this.ctx.storage.sql.exec(
        "INSERT OR IGNORE INTO counter_state (key, value) VALUES (1, 0)"
      );
    });
  }

  increment() {
    return this.ctx.storage.sql
      .exec("UPDATE counter_state SET value = value + 1 WHERE key = 1 RETURNING value")
      .one().value;
  }

  getCount() {
    return this.ctx.storage.sql
      .exec("SELECT value FROM counter_state WHERE key = 1")
      .one().value;
  }
}

function json(data, status = 200) {
  return Response.json(data, { status });
}

function counterName(pathname) {
  const match = pathname.match(/^\/counters\/([^/]+)$/);
  if (!match) return { error: "not_found", status: 404 };

  let name;
  try {
    name = decodeURIComponent(match[1]);
  } catch {
    return { error: "invalid_counter_name", status: 400 };
  }

  if (!/^[a-z][a-z0-9-]{0,31}$/.test(name)) {
    return { error: "invalid_counter_name", status: 400 };
  }
  return { name };
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (request.method === "GET" && url.pathname === "/health") {
      return json({ status: "ok" });
    }

    const parsed = counterName(url.pathname);
    if (parsed.error) return json({ error: parsed.error }, parsed.status);
    if (request.method !== "GET" && request.method !== "POST") {
      return json({ error: "method_not_allowed" }, 405);
    }

    const name = parsed.name;
    const stub = env.COUNTERS.getByName(name);
    const count = request.method === "POST"
      ? await stub.increment()
      : await stub.getCount();

    console.log(JSON.stringify({
      event: request.method === "POST" ? "counter_incremented" : "counter_read",
      name,
      count
    }));
    return json({ name, count });
  }
};
JS

La línea de enrutamiento getByName(name) es el límite de identidad. El mismo texto validado selecciona de forma determinista el mismo objeto lógico; un texto diferente selecciona otro objeto. El stub es solo una referencia. El objeto se crea de forma diferida cuando una llamada RPC llega realmente a él.

Ejecute las pruebas deterministas proporcionadas. Utilizan un pequeño fixture de namespace, por lo que no realizan ninguna solicitud en la nube:

NODE_NO_WARNINGS=1 node --experimental-loader ./test/cloudflare-loader.mjs --test test/worker.test.mjs

El loader pequeño proporciona únicamente una sustitución local para la clase base cloudflare:workers, de modo que Node pueda importar el módulo; el fixture del namespace sigue controlando todas las llamadas probadas y no se contacta con ninguna API de Cloudflare. Deben pasar tres pruebas. Después, pida a Wrangler que compile el Worker sin desplegarlo:

npx wrangler deploy --dry-run

Las pruebas demuestran el contrato de enrutamiento HTTP y la ejecución en seco demuestra que Wrangler puede empaquetar la clase real de Durable Object. Ninguna de las dos acciones crea un namespace remoto.

Demostrar la estabilidad de los nombres localmente

En este paso, ejecutará la aplicación en el entorno local de Workers y utilizará dos nombres para observar la regla de enrutamiento antes de crear un recurso en la nube.

Inicie Wrangler en segundo plano en el puerto 8787. > guarda los registros, 2>&1 combina los errores con la salida normal y & devuelve el prompt del terminal. $! es el ID del proceso del comando que acaba de iniciarse.

npx wrangler dev --port 8787 > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid

Espere a que esté disponible la ruta de estado. El bucle lo intenta una vez por segundo y se detiene en cuanto el Worker responde:

for attempt in $(seq 1 30); do
  if curl --silent --fail http://127.0.0.1:8787/health; then
    break
  fi
  sleep 1
done

Debe aparecer {"status":"ok"}. Incremente dos veces el contador support:

curl --silent --request POST http://127.0.0.1:8787/counters/support | jq
curl --silent --request POST http://127.0.0.1:8787/counters/support | jq

Las respuestas muestran que support pasa de 1 a 2:

{
  "name": "support",
  "count": 2
}

Ahora incremente una vez billing:

curl --silent --request POST http://127.0.0.1:8787/counters/billing | jq

Su valor es 1, no 3. Un namespace es una colección, mientras que cada nombre selecciona un objeto aislado dentro de esa colección.

Lea ambos objetos sin modificarlos:

curl --silent http://127.0.0.1:8787/counters/support | jq
curl --silent http://127.0.0.1:8787/counters/billing | jq

Los valores siguen siendo 2 y 1. Por último, demuestre que la entrada no válida se rechaza antes de que getByName() pueda seleccionar un objeto:

curl --silent --request POST --write-out '\nHTTP %{http_code}\n' \
  http://127.0.0.1:8787/counters/Not_Allowed

Debe aparecer {"error":"invalid_counter_name"} y el código HTTP 400. El guion bajo y las letras mayúsculas no cumplen la regla de nombres documentada.

Desplegar e inspeccionar el namespace

En este paso, detendrá el entorno local, desplegará la misma aplicación en Cloudflare y relacionará el comportamiento de la API con el namespace, el binding, las métricas y los registros visibles en el Dashboard.

Detenga únicamente el proceso de desarrollo cuyo ID guardó:

kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true

Despliegue el Worker y su clase Counter declarada y respaldada por SQLite:

npx wrangler deploy

Wrangler muestra una URL pública workers.dev y el resultado de conciliación de la clase. Guarde la URL exacta sustituyendo el valor de ejemplo:

WORKER_URL="https://YOUR_WORKER_URL"

La ruta del edge puede tardar un poco en estar disponible. Consulte únicamente la ruta de estado, que no accede a un Durable Object:

for attempt in $(seq 1 30); do
  if curl --silent --fail "$WORKER_URL/health"; then
    break
  fi
  sleep 2
done

Realice dos solicitudes para support y una para billing:

curl --silent --request POST "$WORKER_URL/counters/support" | jq
curl --silent --request POST "$WORKER_URL/counters/support" | jq
curl --silent --request POST "$WORKER_URL/counters/billing" | jq

Lea los valores:

curl --silent "$WORKER_URL/counters/support" | jq
curl --silent "$WORKER_URL/counters/billing" | jq

La aplicación remota debe mostrar el mismo contrato de identidad que el entorno local: support tiene el valor 2, mientras que billing tiene el valor 1.

Abra Workers & Pages en el Dashboard de Cloudflare. Su Worker con nombre único aparece en la lista de aplicaciones. El nombre del Worker, las marcas de tiempo y los totales de uso de toda la cuenta de la siguiente captura son ejemplos de la ejecución probada; busque el nombre labex-c10-o01-... generado en su propio terminal.

El Worker del laboratorio desplegado en la lista de aplicaciones de Workers y Pages

Abra el Dashboard de Cloudflare y vaya a Workers & Pages → Overview → su Worker labex-c10-o01-... → Settings → Bindings. Busque el binding de Durable Object llamado COUNTERS y su clase Counter. El Worker conoce el nombre del binding; Cloudflare lo conecta con el namespace declarado por la exportación de la clase.

El diagrama de bindings debe mostrar el Worker conectado a un Durable Object mediante COUNTERS. Los nombres específicos del Worker y del namespace de esta captura corresponden a una ejecución de ejemplo; el nombre del binding y la relación son las partes importantes.

El binding de Durable Object COUNTERS conectado al Worker

A continuación, abra Durable Objects desde la navegación de Developer Platform. Seleccione el namespace perteneciente a su Worker desechable. Confirme que utiliza almacenamiento SQLite y que la clase es Counter. Un namespace es la colección a nivel de clase; los nombres support y billing identifican objetos dentro de ella.

La vista general del namespace muestra Storage: SQL. El nombre y el ID del namespace pertenecen a la ejecución desechable probada, por lo que sus valores serán diferentes.

La vista general del namespace Counter con almacenamiento SQL

Abra la vista Metrics del namespace. Las solicitudes recientes pueden tardar en aparecer, por lo que un gráfico temporalmente vacío no es concluyente. No genere un bucle grande de solicitudes para forzar la aparición de un gráfico.

La captura de ejemplo del namespace todavía muestra cero invocaciones recientes, aunque las solicitudes del entorno de ejecución se completaron correctamente. Esto demuestra por qué las métricas retrasadas del Dashboard sirven como contexto complementario y no como comprobación funcional autorizada.

Vuelva al Worker y abra Observability → Logs. Busque un evento reciente counter_incremented o counter_read. El registro estructurado contiene el nombre sintético del contador y el valor, pero no contiene ningún identificador de cuenta ni credencial. Relaciónelo con una de las solicitudes acotadas anteriores.

Expanda un evento coincidente. En la ejecución probada, un nombre generado por el verificador terminó con el valor 2, mientras que el gráfico de eventos indicó solicitudes correctas y cero errores. Su nombre sintético y sus totales serán diferentes.

Un evento counter_read estructurado con su nombre y valor

Los valores del Dashboard, como los nombres de los Workers, los ID de los objetos, las marcas de tiempo y el número de solicitudes, dependen de su ejecución. Las comprobaciones realizadas mediante CLI, API y el entorno de ejecución siguen siendo la evidencia autorizada; las vistas del Dashboard le muestran dónde aparecen esas mismas relaciones.

Eliminar el namespace y cerrar la sesión

En este paso, retirará deliberadamente la clase Counter, eliminará su namespace y los datos almacenados, eliminará el Worker y, finalmente, revocará la sesión de Wrangler de esta VM.

Eliminar únicamente el script del Worker no declara claramente que los datos almacenados del Durable Object deban desaparecer. El ciclo de vida de exports utiliza una tombstone deleted: una entrada de configuración de corta duración que indica a Cloudflare que elimine permanentemente un namespace de clase. Esta operación no tiene papelera, así que confirme que la clase y el nombre del Worker pertenecen a este laboratorio.

Cree un punto de entrada mínimo de limpieza sin ninguna exportación Counter:

cat > src/cleanup.js <<'JS'
export default {
  fetch() {
    return Response.json({ status: "cleanup" }, { status: 410 });
  }
};
JS

Cree una configuración de limpieza. Mantiene el mismo nombre de Worker y la misma cuenta, elimina el binding y marca únicamente Counter como eliminado:

ACCOUNT_ID="$(node -e 'console.log(JSON.parse(require("fs").readFileSync("wrangler.jsonc", "utf8")).account_id)')"
cat > wrangler.cleanup.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "$ACCOUNT_ID",
  "main": "src/cleanup.js",
  "compatibility_date": "2026-09-18",
  "workers_dev": true,
  "preview_urls": false,
  "exports": {
    "Counter": { "type": "durable-object", "state": "deleted" }
  }
}
JSON

Despliegue la tombstone:

npx wrangler deploy --config wrangler.cleanup.jsonc

Lea con atención la salida de conciliación de Wrangler. Debe indicar que Counter se eliminó. Esto elimina permanentemente el namespace de la clase y los pequeños valores almacenados por support, billing y el verificador independiente.

Ahora elimine el Worker de limpieza sin estado que queda:

npx wrangler delete --config wrangler.cleanup.jsonc

Confirme únicamente la aplicación exacta labex-c10-o01-.... En el Dashboard, compruebe que el Worker exacto ya no está y que el namespace que le pertenecía tampoco aparece. Las métricas o los registros históricos pueden permanecer temporalmente y no son recursos activos.

Ejecute la comprobación autenticada de eliminación antes de retirar la autorización:

python3 .labex/verify.py deleted

Solo después de que muestre PASS: deleted, cierre la sesión:

npx wrangler logout
npx wrangler whoami --json

La salida final debe indicar explícitamente loggedIn: false. Un error de red no demuestra que la sesión se haya cerrado.

Resumen

Construyó y utilizó su primera aplicación con Durable Objects. Aprendió que una clase define el comportamiento de un objeto, que un namespace agrupa los objetos de esa clase, que un binding expone el namespace a un Worker y que getByName() selecciona de forma determinista un objeto lógico. Los métodos RPC modificaron y leyeron el estado respaldado por SQLite, los nombres repetidos compartieron un contador, los nombres diferentes permanecieron aislados y los nombres no válidos se rechazaron antes de seleccionar un objeto.

También relacionó el comportamiento del entorno de ejecución con el Dashboard de Cloudflare y utilizó una tombstone declarativa de clase para eliminar el namespace y sus datos antes de eliminar el Worker y cerrar la sesión. El siguiente laboratorio ampliará este modelo de identidad utilizando SQLite como registro de actividad y demostrando por qué el almacenamiento persistente es diferente del estado temporal en memoria.