Introduction
Un Worker Cloudflare classique peut répondre à de nombreuses requêtes, mais une requête ne peut pas supposer que la suivante sera traitée par la même instance JavaScript en cours d’exécution. Cette conception sans état convient parfaitement aux tâches indépendantes. Elle devient moins pratique lorsque plusieurs requêtes doivent partager une valeur qui évolue, par exemple le nombre de personnes en attente dans une file d’assistance.
Un Durable Object fournit à l’application une unité de coordination adressable. Dans ce lab, chaque nom de compteur sélectionne un objet différent. Les requêtes destinées à support atteignent toujours le même compteur logique, tandis que celles destinées à billing atteignent un autre compteur, avec un état séparé. Cloudflare peut déplacer ou redémarrer l’environnement d’exécution sous-jacent ; l’identité stable de l’objet et son état stocké dans SQLite restent le contrat de l’application.
Vous allez relier quatre concepts :
- Une classe définit les opérations qu’un objet compteur peut effectuer.
- Un espace de noms regroupe les objets associés à cette classe.
- Un binding donne au Worker d’entrée accès à cet espace de noms.
getByName()transforme le même nom validé en la même référence d’objet, et une méthode RPC appelle le code de cet objet.
Vous allez créer l’application, démontrer localement l’acheminement fondé sur le nom, la déployer sur votre propre compte Cloudflare d’apprentissage, relier les résultats du terminal au Dashboard, puis supprimer l’espace de noms de la classe et le Worker à la fin.
Avant de commencer ce cours, terminez Connect LabEx to Your Cloudflare Account. Vous y apprendrez à utiliser le terminal de la VM LabEx, l’autorisation de l’appareil Wrangler, la confirmation du compte et la configuration de l’ID du compte. Vous devez déjà savoir comment un petit Worker JavaScript traite une requête HTTP. Aucune connaissance préalable des Durable Objects n’est requise.
La documentation officielle rend actuellement les Durable Objects adossés à SQLite disponibles avec Workers Free. Ce lab crée un espace de noms de classe temporaire, quelques objets très petits et uniquement des requêtes limitées. Workers Paid n’est pas nécessaire. La configuration installe Node.js 22.22.0 et Wrangler 4.132.0 au niveau du projet dans /home/labex/project/named-counters ; elle ne se connecte pas, ne crée pas de ressources cloud, ne déploie pas de code et ne réalise pas l’implémentation demandée à l’apprenant.
Autoriser la VM et nommer l’application
Dans cette étape, vous allez connecter cette nouvelle VM LabEx à votre compte Cloudflare d’apprentissage et créer une configuration d’application unique. Le fait d’être connecté au Dashboard dans un navigateur n’autorise pas automatiquement les commandes exécutées dans une nouvelle VM.
Accédez au projet préparé et vérifiez la version épinglée de Wrangler :
cd /home/labex/project/named-counters
npx wrangler --version
La version attendue est 4.132.0. Lancez le processus d’autorisation de l’appareil de Wrangler :
npx wrangler login --device --browser=false
Wrangler affiche une URL et un court code d’appareil. Ouvrez l’URL dans le navigateur, saisissez le code, vérifiez que le compte sélectionné est bien votre compte d’apprentissage dédié et examinez les autorisations demandées avant d’autoriser l’accès. Un accès en arrière-plan peut apparaître, car Wrangler doit continuer à fonctionner après votre retour au terminal. N’envoyez jamais de mot de passe ni de jeton par le terminal.
Lorsque le navigateur indique que l’opération a réussi, revenez au terminal et attendez que Wrangler termine. Demandez les informations structurées du compte :
npx wrangler whoami --json
Vérifiez que loggedIn: true apparaît, puis identifiez le compte voulu, même si un seul compte est affiché. Le nom du compte sert à la vérification visuelle ; l’ID est une valeur de configuration stable qui n’a pas besoin d’être affichée dans le terminal.
Enregistrez le résultat structuré, affichez uniquement le nom du compte, qui n’est pas sensible, puis sélectionnez l’ID correspondant à 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"
$(...) capture la sortie d’une commande dans une variable shell. jq affiche d’abord uniquement le nom du compte pour confirmation, puis sélectionne de manière privée l’ID associé. test -n réussit uniquement lorsque la valeur sélectionnée n’est pas vide. Si votre compte d’apprentissage dédié porte un autre nom d’affichage, remplacez LabEx Learning dans l’expression de sélection après avoir confirmé ce nom.
Générez un nom de Worker unique. openssl rand -hex 6 produit 12 caractères hexadécimaux aléatoires, et $(...) les insère dans la variable shell :
RUN="labex-c10-o01-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"
Créez wrangler.jsonc. Un fichier de configuration indique à Wrangler quel code déployer et quelles fonctionnalités Cloudflare l’environnement d’exécution doit associer. Le marqueur JSON non placé entre guillemets permet l’expansion de $RUN et $ACCOUNT_ID, tandis que l’antislash conserve la clé $schema telle quelle.
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
Ce fichier décrit l’application, mais ne crée encore rien dans Cloudflare. observability conserve les journaux des requêtes et de l’application pour une vérification ultérieure dans le Dashboard. Les champs Durable Object prendront leur importance à l’étape suivante.
Relier un espace de noms, un binding et une classe
Dans cette étape, vous allez lire la configuration Durable Objects comme une carte indiquant comment une requête atteint un objet avec état, puis générer les types d’exécution qui exposent le binding à votre code.
Une classe Durable Object est le modèle JavaScript d’un objet. La classe Counter, que vous écrirez plus tard, définit des opérations telles que l’incrémentation et la lecture d’une valeur.
Un espace de noms est l’ensemble de tous les objets associés à cette classe. Un même espace de noms peut contenir support, billing et de nombreux autres compteurs nommés. Cela ne signifie pas que ces compteurs partagent une seule valeur : chaque identité d’objet stable possède son propre stockage.
Un binding est le nom utilisé par le Worker d’entrée pour accéder à cet espace de noms. Cette configuration associe le nom COUNTERS à la classe Counter. Votre code utilisera donc env.COUNTERS.
L’entrée exports déclare l’état actuel du cycle de vie de la classe. Elle indique à Cloudflare de créer Counter avec le backend de stockage SQLite lors du premier déploiement. SQLite est le backend recommandé pour les nouvelles classes et est disponible avec Workers Free. La petite table utilisée dans ce lab ne stocke qu’un seul entier dans chaque objet.
Générez une description des types à partir de la configuration :
npx wrangler types
Recherchez COUNTERS dans le fichier généré :
grep -n 'COUNTERS' worker-configuration.d.ts
La ligne ressemblera à ceci :
COUNTERS: DurableObjectNamespace<import("./src/index").Counter>;
Le texte généré autour de cette ligne peut varier, mais trois faits sont importants : le binding s’appelle COUNTERS, il est de type DurableObjectNamespace et il pointe vers la classe exportée Counter. Régénérez les types chaque fois qu’un binding est modifié afin d’éviter toute divergence silencieuse entre la configuration et le code.
Créer le compteur nommé
Dans cette étape, vous allez implémenter la classe Counter et le Worker d’entrée qui achemine un nom d’URL validé vers un objet.
Chaque Durable Object possède un stockage privé. Le constructeur crée une table à une ligne nommée counter_state et insère la valeur initiale uniquement si la ligne n’existe pas déjà. blockConcurrencyWhile() retarde les requêtes destinées à l’objet jusqu’à la fin de cette courte initialisation. Cette fonction convient à la configuration du schéma ; elle ne doit pas entourer chaque requête ni les opérations réseau externes.
Les méthodes publiques increment() et getCount() sont des méthodes RPC. RPC signifie remote procedure call, c’est-à-dire appel de procédure distante. Il permet au Worker d’appeler une méthode sur un stub Durable Object comme s’il s’agissait d’un objet JavaScript asynchrone. Cloudflare transmet l’appel à l’objet sélectionné.
Créez le point d’entrée du 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 ligne de routage getByName(name) constitue la frontière d’identité. Le même texte validé sélectionne de manière déterministe le même objet logique ; un texte différent en sélectionne un autre. Le stub n’est qu’une référence. L’objet est créé à la demande lorsqu’un appel RPC l’atteint effectivement.
Exécutez les tests déterministes fournis. Ils utilisent un petit jeu de données local pour l’espace de noms et n’envoient donc aucune requête cloud :
NODE_NO_WARNINGS=1 node --experimental-loader ./test/cloudflare-loader.mjs --test test/worker.test.mjs
Le petit loader fournit uniquement une implémentation locale de remplacement pour la classe de base cloudflare:workers, afin que Node puisse importer le module ; le jeu de données local de l’espace de noms contrôle toujours chaque appel testé et aucune API Cloudflare n’est contactée. Attendez-vous à trois tests réussis. Demandez ensuite à Wrangler de construire le Worker sans le déployer :
npx wrangler deploy --dry-run
Les tests vérifient le contrat d’acheminement HTTP, tandis que l’exécution à blanc vérifie que Wrangler peut regrouper la véritable classe Durable Object. Aucune de ces actions ne crée un espace de noms distant.
Vérifier localement la stabilité des noms
Dans cette étape, vous allez exécuter l’application dans l’environnement d’exécution local de Workers et utiliser deux noms pour observer la règle d’acheminement avant de créer une ressource cloud.
Démarrez Wrangler en arrière-plan sur le port 8787. > enregistre les journaux, 2>&1 combine les erreurs avec la sortie ordinaire et & rend immédiatement l’invite du terminal. $! correspond à l’ID du processus qui vient d’être lancé.
npx wrangler dev --port 8787 > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid
Attendez que la route de vérification d’état réponde. La boucle effectue une tentative par seconde et s’arrête dès que le Worker répond :
for attempt in $(seq 1 30); do
if curl --silent --fail http://127.0.0.1:8787/health; then
break
fi
sleep 1
done
Vous devez obtenir {"status":"ok"}. Incrémentez deux fois le compteur 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
Les réponses montrent que support passe de 1 à 2 :
{
"name": "support",
"count": 2
}
Incrémentez maintenant une fois billing :
curl --silent --request POST http://127.0.0.1:8787/counters/billing | jq
Sa valeur est 1, et non 3. Un espace de noms est un ensemble, tandis que chaque nom sélectionne un objet isolé à l’intérieur de cet ensemble.
Lisez les deux objets sans les modifier :
curl --silent http://127.0.0.1:8787/counters/support | jq
curl --silent http://127.0.0.1:8787/counters/billing | jq
Les compteurs restent à 2 et 1. Enfin, vérifiez que l’entrée non valide est rejetée avant que getByName() puisse sélectionner un objet :
curl --silent --request POST --write-out '\nHTTP %{http_code}\n' \
http://127.0.0.1:8787/counters/Not_Allowed
Vous devez obtenir {"error":"invalid_counter_name"} et le code HTTP 400. Le caractère de soulignement et les lettres majuscules ne respectent pas la règle de nommage documentée.
Déployer et inspecter l’espace de noms
Dans cette étape, vous allez arrêter l’environnement d’exécution local, déployer la même application sur Cloudflare et relier le comportement de l’API à l’espace de noms, au binding, aux métriques et aux journaux visibles dans le Dashboard.
Arrêtez uniquement le processus de développement dont vous avez enregistré l’ID :
kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true
Déployez le Worker et sa classe Counter déclarée, adossée à SQLite :
npx wrangler deploy
Wrangler affiche une URL publique workers.dev et le résultat de la synchronisation de la classe. Enregistrez l’URL exacte en remplaçant la valeur d’exemple :
WORKER_URL="https://YOUR_WORKER_URL"
La route edge peut mettre un court instant à devenir disponible. Interrogez uniquement la route de vérification d’état, qui n’utilise aucun Durable Object :
for attempt in $(seq 1 30); do
if curl --silent --fail "$WORKER_URL/health"; then
break
fi
sleep 2
done
Créez deux requêtes pour support et une pour 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
Lisez les valeurs :
curl --silent "$WORKER_URL/counters/support" | jq
curl --silent "$WORKER_URL/counters/billing" | jq
L’application distante doit respecter le même contrat d’identité que l’environnement local : support vaut 2, tandis que billing vaut 1.
Ouvrez Workers & Pages dans le Dashboard Cloudflare. Votre Worker au nom unique apparaît dans la liste des applications. Le nom du Worker, les horodatages et les totaux d’utilisation du compte dans la capture suivante sont des exemples issus de l’exécution testée ; recherchez dans votre terminal le nom labex-c10-o01-... qui a été généré pour votre propre exécution.

Ouvrez le Dashboard Cloudflare, puis accédez à Workers & Pages → Overview → votre Worker labex-c10-o01-... → Settings → Bindings. Recherchez le binding Durable Object nommé COUNTERS et sa classe Counter. Le Worker connaît le nom du binding ; Cloudflare le relie à l’espace de noms déclaré par l’export de la classe.
Le diagramme des bindings doit montrer le Worker connecté à un Durable Object via COUNTERS. Les noms du Worker et de l’espace de noms propres à l’exécution dans cette capture sont des exemples ; le nom du binding et la relation sont les éléments importants.

Ouvrez ensuite Durable Objects depuis la navigation Developer Platform. Sélectionnez l’espace de noms appartenant à votre Worker temporaire. Vérifiez qu’il utilise le stockage SQLite et que la classe est Counter. Un espace de noms est la collection au niveau de la classe ; les noms support et billing identifient les objets qu’il contient.
La vue d’ensemble de l’espace de noms affiche Storage: SQL. Son nom et son ID correspondent à l’exécution temporaire testée ; vos valeurs seront donc différentes.

Ouvrez la vue Metrics de l’espace de noms. Les requêtes récentes peuvent mettre du temps à apparaître ; un graphique momentanément vide ne permet pas de conclure. Ne générez pas une grande boucle de requêtes pour forcer l’affichage d’un graphique.
La capture d’exemple de l’espace de noms indique encore zéro invocation récente, même si les requêtes d’exécution ont réussi. Cela montre pourquoi les métriques différées du Dashboard servent de contexte complémentaire et non de vérification fonctionnelle faisant foi.
Revenez au Worker et ouvrez Observability → Logs. Recherchez un événement récent counter_incremented ou counter_read. Le journal structuré contient le nom synthétique du compteur et sa valeur, mais aucun identifiant de compte ni identifiant d’authentification. Faites-le correspondre à l’une des requêtes limitées exécutées précédemment.
Développez un événement correspondant. Lors de l’exécution testée, un nom généré pour la vérification atteignait la valeur 2, tandis que le graphique des événements indiquait des requêtes réussies et zéro erreur. Votre nom synthétique et vos totaux seront différents.

Les valeurs du Dashboard, comme les noms des Workers, les ID des objets, les horodatages et le nombre de requêtes, dépendent de votre exécution. Les vérifications effectuées par la CLI, l’API et l’environnement d’exécution restent les preuves faisant foi ; les vues du Dashboard vous montrent où ces mêmes relations sont visibles.
Supprimer l’espace de noms et se déconnecter
Dans cette étape, vous allez retirer délibérément la classe Counter, supprimer son espace de noms et les données stockées, supprimer le Worker, puis révoquer la session Wrangler de cette VM.
Supprimer uniquement le script d’un Worker ne signifie pas clairement que les données Durable Object stockées doivent disparaître. Le cycle de vie exports utilise un marqueur de suppression : une entrée de configuration de courte durée qui indique à Cloudflare de supprimer définitivement l’espace de noms d’une classe. Cette opération ne dispose d’aucune corbeille ; vérifiez donc que la classe et le nom du Worker correspondent bien à ce lab.
Créez un point d’entrée minimal de nettoyage, sans export Counter :
cat > src/cleanup.js <<'JS'
export default {
fetch() {
return Response.json({ status: "cleanup" }, { status: 410 });
}
};
JS
Créez une configuration de nettoyage. Elle conserve le même nom de Worker et le même compte, supprime le binding et marque uniquement Counter comme supprimée :
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
Déployez le marqueur de suppression :
npx wrangler deploy --config wrangler.cleanup.jsonc
Lisez attentivement la sortie de synchronisation de Wrangler. Elle doit indiquer que Counter a été supprimée. Cette opération supprime définitivement l’espace de noms de la classe et les petites valeurs stockées par support, billing et le vérificateur indépendant.
Supprimez maintenant le Worker de nettoyage sans état restant :
npx wrangler delete --config wrangler.cleanup.jsonc
Vérifiez qu’il s’agit exactement de l’application labex-c10-o01-.... Dans le Dashboard, vérifiez que le Worker exact est absent et que l’espace de noms qui lui appartenait n’apparaît plus. Les anciennes métriques ou les anciens journaux peuvent rester temporairement visibles ; ce ne sont pas des ressources actives.
Exécutez la vérification authentifiée de suppression avant de retirer l’autorisation :
python3 .labex/verify.py deleted
Attendez que la commande affiche PASS: deleted, puis déconnectez-vous :
npx wrangler logout
npx wrangler whoami --json
La sortie finale doit indiquer explicitement loggedIn: false. Une erreur réseau ne prouve pas que la déconnexion a réussi.
Résumé
Vous avez créé et utilisé votre première application Durable Objects. Vous avez appris qu’une classe définit le comportement d’un objet, qu’un espace de noms regroupe les objets de cette classe, qu’un binding expose l’espace de noms à un Worker et que getByName() sélectionne de manière déterministe un objet logique. Les méthodes RPC ont modifié et lu un état stocké dans SQLite ; les noms répétés ont partagé un compteur, les noms différents sont restés isolés et les noms non valides ont été rejetés avant la sélection d’un objet.
Vous avez également relié le comportement de l’environnement d’exécution au Dashboard Cloudflare, puis utilisé un marqueur de suppression déclaratif de classe pour supprimer l’espace de noms et ses données avant de supprimer le Worker et de vous déconnecter. Le prochain lab s’appuiera sur ce modèle d’identité en utilisant SQLite comme journal d’activité et en montrant pourquoi le stockage durable diffère d’un état temporaire conservé en mémoire.



