Introdução
Um Cloudflare Worker comum pode responder a muitas solicitações, mas uma solicitação não pode presumir que a próxima chegará à mesma instância JavaScript em execução. Esse design sem estado é excelente para trabalhos independentes. Ele se torna inconveniente quando várias solicitações precisam concordar sobre um valor que muda, como o número de pessoas aguardando em uma fila de suporte.
Um Durable Object fornece à aplicação uma unidade de coordenação endereçável. Neste laboratório, cada nome de contador seleciona um objeto diferente. As solicitações para support chegam repetidamente ao mesmo contador lógico, enquanto as solicitações para billing chegam a outro contador, com estado separado. A Cloudflare pode mover ou reiniciar o runtime subjacente; a identidade estável do objeto e o estado armazenado no SQLite continuam sendo o contrato da aplicação.
Você conectará quatro conceitos:
- Uma classe define o que um objeto de contador pode fazer.
- Um namespace é a coleção de objetos baseados nessa classe.
- Um binding dá ao Worker de entrada acesso ao namespace.
getByName()transforma o mesmo nome validado na mesma referência de objeto, e um método RPC chama o código desse objeto.
Você criará a aplicação, comprovará localmente o encaminhamento baseado em nomes, fará o deploy na sua própria conta de aprendizado da Cloudflare, relacionará as evidências do terminal ao Dashboard e removerá o namespace da classe e o Worker ao terminar.
Antes de iniciar este curso, conclua Conectar o LabEx à sua conta da Cloudflare. Esse laboratório ensina a usar o terminal da VM do LabEx, a autorização de dispositivo do Wrangler, a confirmação da conta e a configuração do ID da conta. Você já deve saber como um Worker JavaScript simples processa uma solicitação HTTP. Não é necessário ter conhecimento prévio sobre Durable Objects.
A documentação oficial atualmente disponibiliza Durable Objects com armazenamento SQLite no Workers Free. Este laboratório cria um namespace de classe descartável, alguns objetos pequenos e apenas solicitações limitadas. Ele não exige o Workers Paid. A configuração instala o Node.js 22.22.0 e o Wrangler 4.132.0 localmente no projeto, em /home/labex/project/named-counters; ela não faz login, não cria estado na nuvem, não faz deploy de código nem conclui a implementação do aluno.
Autorizar a VM e nomear a aplicação
Nesta etapa, você conectará esta VM nova do LabEx à sua conta de aprendizado da Cloudflare e criará uma configuração exclusiva para a aplicação. Estar conectado ao Dashboard em um navegador não autoriza automaticamente os comandos dentro de uma VM nova.
Entre no projeto preparado e confirme a versão fixada do Wrangler:
cd /home/labex/project/named-counters
npx wrangler --version
O resultado esperado é 4.132.0. Inicie o fluxo de autorização por dispositivo do Wrangler:
npx wrangler login --device --browser=false
O Wrangler exibirá uma URL e um código curto de dispositivo. Abra a URL no navegador, informe o código, confirme que a conta selecionada é sua conta de aprendizado dedicada e verifique as permissões solicitadas antes de autorizar. O acesso em segundo plano pode aparecer porque o Wrangler precisa continuar funcionando depois que você voltar ao terminal. Nunca envie uma senha ou um token pelo terminal.
Depois que o navegador informar que a autorização foi concluída, volte ao terminal e aguarde o Wrangler terminar. Solicite as informações estruturadas da conta:
npx wrangler whoami --json
Confirme loggedIn: true e identifique a conta pretendida, mesmo quando apenas uma conta aparecer. O nome da conta é a verificação humana; o ID é um valor de configuração estável que não precisa ser exibido no terminal.
Salve o resultado estruturado, exiba apenas o nome da conta, que não é sensível, e selecione o ID correspondente 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 a saída de um comando em uma variável do shell. Primeiro, jq exibe apenas o nome da conta para confirmação; depois, seleciona de forma privada o ID associado. test -n só tem sucesso quando o valor selecionado não está vazio. Se sua conta de aprendizado dedicada tiver outro nome de exibição, substitua LabEx Learning na expressão de seleção depois de confirmar esse nome.
Gere um nome exclusivo para o Worker. openssl rand -hex 6 produz 12 caracteres hexadecimais aleatórios, e $(...) os insere na variável do shell:
RUN="labex-c10-o01-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"
Crie wrangler.jsonc. Um arquivo de configuração informa ao Wrangler qual código deve ser implantado e quais recursos da Cloudflare o runtime deve anexar. O marcador JSON sem aspas permite que $RUN e $ACCOUNT_ID sejam expandidos, enquanto a barra invertida mantém a chave $schema literal.
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
Esse arquivo descreve a aplicação, mas ainda não cria nada na Cloudflare. observability mantém os logs de solicitações e da aplicação para uma verificação posterior no Dashboard. Os campos de Durable Objects serão usados na próxima etapa.
Conectar um namespace, um binding e uma classe
Nesta etapa, você lerá a configuração de Durable Objects como um mapa que mostra como uma solicitação chega a um objeto com estado e, em seguida, gerará tipos de runtime que expõem o binding ao seu código.
Uma classe de Durable Object é o blueprint JavaScript de um objeto. A classe Counter que você escreverá mais adiante define operações como incrementar e ler um valor.
Um namespace é a coleção de todos os objetos baseados nessa classe. Um namespace pode conter support, billing e muitos outros contadores nomeados. O namespace não significa que esses contadores compartilham um único valor; cada identidade estável de objeto possui armazenamento separado.
Um binding é o nome usado pelo Worker de entrada para acessar esse namespace. Essa configuração associa o nome COUNTERS à classe Counter. Portanto, seu código usará env.COUNTERS.
A entrada exports declara o estado atual do ciclo de vida da classe. Ela informa à Cloudflare para criar Counter com o backend de armazenamento SQLite no primeiro deploy. SQLite é o backend recomendado para classes novas e está disponível no Workers Free. A pequena tabela deste laboratório armazena apenas um número inteiro dentro de cada objeto.
Gere uma descrição de tipos a partir da configuração:
npx wrangler types
Procure COUNTERS no arquivo gerado:
grep -n 'COUNTERS' worker-configuration.d.ts
A linha será semelhante a:
COUNTERS: DurableObjectNamespace<import("./src/index").Counter>;
O texto gerado ao redor pode mudar, mas três fatos são importantes: o binding se chama COUNTERS, é um DurableObjectNamespace e aponta para a classe Counter exportada. Gere os tipos novamente sempre que um binding mudar, para evitar que a configuração e o código se afastem silenciosamente.
Criar o contador nomeado
Nesta etapa, você implementará a classe Counter e o Worker de entrada que encaminha um nome de URL validado para um objeto.
Todo Durable Object tem armazenamento privado. O construtor cria uma tabela de uma linha chamada counter_state e insere o valor inicial somente quando a linha ainda não existe. blockConcurrencyWhile() atrasa as solicitações ao objeto até que essa inicialização curta seja concluída. Ele é apropriado para configurar o esquema; não deve envolver todas as solicitações nem operações de rede externas.
Os métodos públicos increment() e getCount() são métodos RPC. RPC, abreviação de remote procedure call, permite que o Worker chame um método em um stub de Durable Object como se fosse um objeto JavaScript assíncrono. A Cloudflare encaminha a chamada ao objeto selecionado.
Crie o ponto de entrada do 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
A linha de encaminhamento getByName(name) é o limite da identidade. O mesmo texto validado seleciona deterministicamente o mesmo objeto lógico; um texto diferente seleciona outro objeto. O stub é apenas uma referência. O objeto é criado sob demanda quando uma chamada RPC realmente chega a ele.
Execute os testes determinísticos fornecidos. Eles usam um pequeno fixture de namespace, portanto não fazem solicitações à nuvem:
NODE_NO_WARNINGS=1 node --experimental-loader ./test/cloudflare-loader.mjs --test test/worker.test.mjs
O loader pequeno fornece apenas uma implementação local substituta para a classe base cloudflare:workers, permitindo que o Node importe o módulo; o fixture do namespace ainda controla todas as chamadas testadas, e nenhuma API da Cloudflare é contatada. Espere três testes aprovados. Depois, peça ao Wrangler para compilar o Worker sem fazer o deploy:
npx wrangler deploy --dry-run
Os testes comprovam o contrato de encaminhamento HTTP, e a execução simulada comprova que o Wrangler consegue empacotar a classe real de Durable Object. Nenhuma das duas ações cria um namespace remoto.
Comprovar nomes estáveis localmente
Nesta etapa, você executará a aplicação no runtime local de Workers e usará dois nomes para observar a regra de encaminhamento antes de criar um recurso na nuvem.
Inicie o Wrangler na porta 8787 em segundo plano. > salva os logs, 2>&1 combina os erros com a saída normal e & devolve o prompt do terminal. $! é o ID do processo do comando que acabou de ser iniciado.
npx wrangler dev --port 8787 > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid
Aguarde a rota de health. O loop tenta uma vez por segundo e para assim que o 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
Espere {"status":"ok"}. Incremente o contador support duas vezes:
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
As respostas mostrarão support passando de 1 para 2:
{
"name": "support",
"count": 2
}
Agora incremente billing uma vez:
curl --silent --request POST http://127.0.0.1:8787/counters/billing | jq
A contagem será 1, e não 3. Um namespace é uma coleção, enquanto cada nome seleciona um objeto isolado dentro dessa coleção.
Leia os dois objetos sem alterá-los:
curl --silent http://127.0.0.1:8787/counters/support | jq
curl --silent http://127.0.0.1:8787/counters/billing | jq
As contagens continuarão sendo 2 e 1. Por fim, comprove que uma entrada inválida é rejeitada antes que getByName() possa selecionar um objeto:
curl --silent --request POST --write-out '\nHTTP %{http_code}\n' \
http://127.0.0.1:8787/counters/Not_Allowed
Espere {"error":"invalid_counter_name"} e HTTP 400. O sublinhado e as letras maiúsculas estão fora da regra de nomenclatura documentada.
Fazer o deploy e inspecionar o namespace
Nesta etapa, você interromperá o runtime local, fará o deploy da mesma aplicação na Cloudflare e relacionará o comportamento da API ao namespace, ao binding, às métricas e aos logs visíveis no Dashboard.
Interrompa apenas o processo de desenvolvimento cujo ID você salvou:
kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true
Faça o deploy do Worker e da classe Counter declarada, com armazenamento SQLite:
npx wrangler deploy
O Wrangler exibirá uma URL pública workers.dev e o resultado da reconciliação da classe. Salve a URL exata, substituindo o valor de exemplo:
WORKER_URL="https://YOUR_WORKER_URL"
A rota de borda pode levar alguns instantes para ficar pronta. Consulte apenas a rota de health, que não acessa um Durable Object:
for attempt in $(seq 1 30); do
if curl --silent --fail "$WORKER_URL/health"; then
break
fi
sleep 2
done
Crie duas solicitações para support e uma 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
Leia os valores:
curl --silent "$WORKER_URL/counters/support" | jq
curl --silent "$WORKER_URL/counters/billing" | jq
A aplicação remota deve mostrar o mesmo contrato de identidade do runtime local: support vale 2, enquanto billing vale 1.
Abra Workers & Pages no Cloudflare Dashboard. Seu Worker com nome exclusivo aparecerá na lista de aplicações. O nome do Worker, os timestamps e os totais de uso da conta na captura de tela a seguir são exemplos da execução testada; procure o nome labex-c10-o01-... gerado no seu próprio terminal.

Abra o Cloudflare Dashboard e acesse Workers & Pages → Overview → seu Worker labex-c10-o01-... → Settings → Bindings. Encontre o binding de Durable Object chamado COUNTERS e sua classe Counter. O Worker conhece o nome do binding; a Cloudflare o conecta ao namespace declarado pela exportação da classe.
O diagrama de binding deve mostrar o Worker conectado a um Durable Object por meio de COUNTERS. Os nomes específicos do Worker e do namespace nesta captura de tela são exemplos; o nome do binding e o relacionamento são as partes importantes.

Em seguida, abra Durable Objects na navegação da Developer Platform. Selecione o namespace pertencente ao seu Worker descartável. Confirme que ele usa armazenamento SQLite e que a classe é Counter. Um namespace é a coleção no nível da classe; os nomes support e billing identificam objetos dentro dela.
A visão geral do namespace mostra Storage: SQL. O nome e o ID do namespace pertencem à execução descartável testada, portanto seus valores serão diferentes.

Abra a visão Metrics do namespace. As solicitações recentes podem levar algum tempo para aparecer, portanto um gráfico temporariamente vazio não é conclusivo. Não gere um loop grande de solicitações para forçar a exibição de um gráfico.
A captura de tela de exemplo do namespace ainda informa zero invocações recentes, embora as solicitações do runtime tenham sido concluídas. Isso mostra por que as métricas atrasadas do Dashboard são um contexto complementar, e não a verificação funcional autoritativa.
Volte ao Worker e abra Observability → Logs. Encontre um evento recente counter_incremented ou counter_read. O log estruturado contém o nome sintético do contador e a contagem, mas nenhum identificador de conta ou credencial. Relacione-o a uma das solicitações limitadas feitas acima.
Expanda um evento correspondente. Na execução testada, um nome gerado pelo verificador terminou com a contagem 2, enquanto o gráfico de eventos informou solicitações bem-sucedidas e zero erros. Seu nome sintético e seus totais serão diferentes.

Valores do Dashboard, como nomes de Workers, IDs de objetos, timestamps e contagens de solicitações, são específicos da sua execução. As verificações de CLI/API/runtime continuam sendo as evidências autoritativas; as visões do Dashboard ensinam onde esses mesmos relacionamentos podem ser observados.
Remover o namespace e sair da sessão
Nesta etapa, você retirará deliberadamente a classe Counter, excluirá o namespace e os dados armazenados, removerá o Worker e, em seguida, revogará a sessão do Wrangler nesta VM.
Excluir apenas o script do Worker não declara claramente que os dados armazenados do Durable Object devem desaparecer. O ciclo de vida de exports usa um tombstone deleted: uma entrada de configuração de curta duração que informa à Cloudflare para excluir permanentemente um namespace de classe. Essa operação não possui Lixeira, portanto confirme que a classe e o nome do Worker pertencem a este laboratório.
Crie um ponto de entrada mínimo de limpeza, sem a exportação Counter:
cat > src/cleanup.js <<'JS'
export default {
fetch() {
return Response.json({ status: "cleanup" }, { status: 410 });
}
};
JS
Crie uma configuração de limpeza. Ela mantém o mesmo nome do Worker e a mesma conta, remove o binding e marca apenas Counter como excluída:
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
Faça o deploy do tombstone:
npx wrangler deploy --config wrangler.cleanup.jsonc
Leia atentamente a saída de reconciliação do Wrangler. Ela deve informar que Counter foi excluída. Isso remove permanentemente o namespace da classe e os pequenos valores armazenados por support, billing e pelo verificador independente.
Agora exclua o Worker de limpeza sem estado que restou:
npx wrangler delete --config wrangler.cleanup.jsonc
Confirme apenas a aplicação exata labex-c10-o01-.... No Dashboard, verifique se o Worker exato não está mais presente e se o namespace pertencente a ele também não aparece. Métricas ou logs históricos podem permanecer temporariamente e não são recursos ativos.
Execute a verificação de exclusão autenticada antes de remover a autorização:
python3 .labex/verify.py deleted
Somente depois que o comando exibir PASS: deleted, saia da sessão:
npx wrangler logout
npx wrangler whoami --json
A saída final deve informar explicitamente loggedIn: false. Um erro de rede não comprova que você saiu da sessão.
Resumo
Você criou e operou sua primeira aplicação com Durable Objects. Aprendeu que uma classe define o comportamento de um objeto, um namespace agrupa objetos dessa classe, um binding expõe o namespace a um Worker e getByName() seleciona deterministicamente um objeto lógico. Os métodos RPC alteraram e leram o estado armazenado no SQLite, nomes repetidos compartilharam uma contagem, nomes diferentes permaneceram isolados e nomes inválidos foram rejeitados antes da seleção do objeto.
Você também relacionou o comportamento do runtime ao Cloudflare Dashboard e usou um tombstone declarativo de classe para remover o namespace e seus dados antes de excluir o Worker e sair da sessão. O próximo laboratório ampliará esse modelo de identidade tratando o SQLite como um log de atividades e demonstrando por que o armazenamento persistente é diferente do estado temporário mantido na memória.



