TL;DR
Dev loop của Workers về cơ bản là:
wrangler devchạy Miniflare local (cùng môi trường chạyworkerdvới production), hot reload, bindings mô phỏng.vitestvới@cloudflare/vitest-pool-workerschạy unit test trong isolate thật, không mock.wrangler deployupload Worker lên 300+ PoP trong ~30 giây.
Luận điểm chính:
Workers không có “staging environment” theo nghĩa EC2 truyền thống. Nhưng bạn có 3 environment thay thế: Miniflare local (nhanh, mô phỏng),
wrangler dev --remote(code local + bindings production), và production với rollback trong 10 giây. Biết dùng đúng cái giảm lỗi và tiết kiệm chi phí.
Bài này đi hết một vòng dev từ npm create cloudflare@latest tới CI auto-triển khai, kèm gotcha thực tế.
Dành cho ai
- Dev đã đọc Part 1-3 và sắp build Worker đầu tiên.
- Người đang bực mình vì dev loop chậm, test tốn thời gian, hoặc triển khai sai.
- Ai quen với quy trình Lambda (SAM, Serverless Framework) và muốn biết cái tương đương của Cloudflare.
Nên biết trước: Node.js + npm, git, khái niệm CI/CD cơ bản.
Sau bài này bạn sẽ:
- Thiết lập project Workers từ đầu.
- Chọn đúng chế độ
wrangler devcho từng tình huống. - Viết unit test chạy trong isolate với Miniflare.
- Thiết lập GitHub Actions triển khai với scoped token.
Bài này không nói về gì
- Thiết lập riêng cho framework (Hono, Remix, SvelteKit): Part 9, 11.
- CI/CD chi tiết (matrix deploy, preview environment): Part 12.
- Pattern testing nâng cao (contract test, integration test với API bên ngoài): Part 12.
Dev loop tổng quan
2 chu kỳ chính:
Chu kỳ ngắn (phút): sửa code → Miniflare reload → reload tab trình duyệt. Nhanh nhất, dùng 80% thời gian.
Chu kỳ trung bình (~1 phút): commit → push → CI chạy test → CI triển khai → smoke test. Dùng mỗi lần hoàn thành một tính năng.
Không có “chu kỳ dài” kiểu container build 10 phút. Workers không có Docker image, không có AMI, không có container registry.
Bắt đầu: từ file trống
Scaffold project
npm create cloudflare@latest my-worker
Wizard hỏi:
- Template? Chọn
"Hello World" Workercho TypeScript tối giản. - TypeScript? Yes.
- Use git? Yes.
- Deploy now? No (để xem file trước).
Cấu trúc file:
my-worker/
├── src/
│ └── index.ts # Handler chính
├── test/
│ └── index.test.ts # Vitest setup
├── wrangler.jsonc # Config + bindings
├── package.json
├── tsconfig.json
└── vitest.config.ts
src/index.ts có template:
export default {
async fetch(request, env, ctx): Promise<Response> {
return new Response("Hello World!");
},
} satisfies ExportedHandler<Env>;
wrangler.jsonc: config center
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-01",
"compatibility_flags": ["nodejs_compat"],
// Bindings: thêm khi cần
"vars": {
"ENVIRONMENT": "production"
},
// "kv_namespaces": [{ "binding": "KV", "id": "..." }],
// "d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "..." }],
// "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "my-bucket" }]
}
3 field bắt buộc:
name: tên Worker trên Cloudflare (duy nhất mỗi account).main: entry point.compatibility_date: khoá hành vi môi trường chạy. Không bao giờ đổi sau khi triển khai (hành vi có thể breaking change).
compatibility_flags: nodejs_compat bật API kiểu Node (Buffer, process). Cần cho hầu hết thư viện npm.
wrangler dev: 3 mode
Chế độ 1: Miniflare local (mặc định)
wrangler dev
Chạy Miniflare, local server ở http://localhost:8787. Hot reload khi file thay đổi.
Bindings được mô phỏng:
- KV: lưu trong file JSON
.wrangler/state/v3/kv/. - D1: SQLite file local
.wrangler/state/v3/d1/. - R2: FS local
.wrangler/state/v3/r2/. - Queues: in-memory queue.
- Workers AI: KHÔNG mô phỏng (gọi remote thật).
- Vectorize: KHÔNG mô phỏng (gọi remote thật).
Ưu: nhanh (< 1s reload), không tốn chi phí Cloudflare, test offline được.
Nhược: bindings không hoàn toàn giống production. KV eventual consistency không mô phỏng. D1 không mô phỏng replication lag.
Chế độ 2: --remote (bindings thật)
wrangler dev --remote
Code chạy local trên máy dev, nhưng bindings dùng tài nguyên production thật (D1 production database, KV production namespace, …).
Khi nào dùng:
- Test tích hợp với dữ liệu production.
- Debug hành vi khác giữa Miniflare local và production.
- Test Workers AI / Vectorize (không mô phỏng được).
Cẩn thận: wrangler dev --remote ghi vào dữ liệu production thật. Nếu INSERT INTO users, dữ liệu vào DB production.
Chế độ 3: Preview environment (staging lite)
{
"env": {
"staging": {
"name": "my-worker-staging",
"kv_namespaces": [{ "binding": "KV", "id": "staging-kv-id" }],
"d1_databases": [{ "binding": "DB", "database_name": "my-db-staging", "database_id": "..." }]
}
}
}
Triển khai staging:
wrangler deploy --env staging
Chạy ở my-worker-staging.<subdomain>.workers.dev, bindings riêng. Đây là cách làm “staging” trên Cloudflare.
Testing với vitest + vitest-pool-workers
Pattern chuẩn cho unit test Worker.
Cài đặt
npm install -D vitest @cloudflare/vitest-pool-workers
vitest.config.ts
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: "./wrangler.jsonc" },
},
},
},
});
Test đầu tiên
// test/index.test.ts
import { SELF, env } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("Worker", () => {
it("returns Hello World", async () => {
const response = await SELF.fetch("https://example.com");
expect(await response.text()).toBe("Hello World!");
expect(response.status).toBe(200);
});
it("has D1 binding available", async () => {
await env.DB.prepare("CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)").run();
await env.DB.prepare("INSERT INTO users VALUES (?, ?)").bind("u1", "Alice").run();
const row = await env.DB.prepare("SELECT * FROM users WHERE id = ?").bind("u1").first();
expect(row).toEqual({ id: "u1", name: "Alice" });
});
});
Điều đặc biệt: test chạy trong isolate thật với Miniflare. env.DB là D1 thật (chế độ local), không mock. SELF.fetch() gọi vào handler thật.
Khác hẳn Jest/vitest thông thường với jest.mock(). Ở đây không cần mock vì môi trường chạy đã thật.
Chạy test
npm test # vitest run (chế độ CI)
npm run test:watch # chế độ watch
npm test -- --coverage # báo cáo coverage
Test chạy ~50ms mỗi suite, vì isolate cold start rẻ.
D1 migration workflow
Nếu Worker dùng D1, migration là phần không tránh được.
Tạo file migration
wrangler d1 migrations create my-db add-users-table
Tạo file migrations/0001_add-users-table.sql:
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX idx_users_email ON users(email);
Apply local
wrangler d1 migrations apply my-db --local
Chạy migration vào file D1 SQLite local (.wrangler/state/v3/d1/).
Apply remote (production)
wrangler d1 migrations apply my-db --remote
Luôn test local trước. Migration production không tự rollback — phải viết migration đảo chiều thủ công.
Gotcha
- D1 không hỗ trợ
ALTER TABLEđầy đủ. Không thểDROP COLUMNhoặc đổi kiểu. Giải pháp: tạo bảng mới, copy dữ liệu, drop bảng cũ, rename. - Migration chạy một file/một lần. Không tự rollback nếu fail ở giữa.
- Với dữ liệu production lớn, test migration trên D1 staging trước (preview environment).
Quản lý secret
Local
.dev.vars ở thư mục gốc project (gitignored):
API_KEY=dev-key-123
RESEND_API_KEY=re_dev_...
Wrangler tự nạp vào env khi chạy wrangler dev.
Production
wrangler secret put API_KEY
# nhập giá trị khi được hỏi
Secret được mã hoá trong Cloudflare, expose vào Worker như binding chuỗi:
async fetch(request, env) {
const key = env.API_KEY; // string, không phải getter
}
List / delete
wrangler secret list
wrangler secret delete API_KEY
Gotcha
- Secret không hiện trong
wrangler.jsonc. Không hardcode. - Không có tính năng “xoay” tự động; phải
putlại thủ công. - Secret được inject lúc triển khai. Đổi secret xong phải
wrangler deploylại hoặc restart Worker.
Triển khai: 30 giây lên 300+ PoP
Lần triển khai đầu tiên
wrangler deploy
Hỏi xác thực lần đầu (OAuth trên trình duyệt). Sau đó:
Total Upload: 3.41 KiB / gzip: 1.22 KiB
Worker Startup Time: 5 ms
Uploaded my-worker (1.23 sec)
Deployed my-worker triggers (4.56 sec)
https://my-worker.<subdomain>.workers.dev
Worker live sau ~6 giây, propagate 300+ PoP sau ~30 giây nữa.
Triển khai lại
Chỉ wrangler deploy lại. Không có bước build riêng, không container registry, không orchestration.
Kết nối custom domain
Trong wrangler.jsonc:
{
"routes": [
{ "pattern": "my-app.com/*", "zone_name": "my-app.com" }
]
}
Zone phải thuộc Cloudflare DNS của account. Triển khai xong, traffic my-app.com/* đi qua Worker.
Rollback
Nếu bản triển khai bị lỗi:
wrangler rollback
Hoặc chỉ version cụ thể:
wrangler rollback --version-id <id>
Rollback trong ~10 giây. Đây là một trong những điểm mạnh Workers so với Lambda (Lambda cần triển khai lại từ artifact cũ).
CI/CD pattern
Blog này dùng pattern này cho GitHub Actions.
.github/workflows/deploy.yml
name: Deploy to Cloudflare Workers
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
- run: npm run build
- name: Deploy
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Smoke test
run: scripts/smoke.sh
Scoped API token
KHÔNG dùng Global API Key trong CI. Tạo scoped token ở dashboard:
- Cloudflare dashboard → My Profile → API Tokens → Create Token.
- Quyền:
- Account → Workers Scripts → Edit.
- Account → Account Settings → Read.
- Account → D1 → Edit (nếu dùng).
- Account → Workers KV Storage → Edit (nếu dùng).
- Account → Workers R2 Storage → Edit (nếu dùng).
- Zone → Workers Routes → Edit (nếu dùng custom domain).
Lưu token vào repo secret CLOUDFLARE_API_TOKEN.
Smoke test
scripts/smoke.sh sau khi triển khai kiểm tra site thật:
#!/usr/bin/env bash
set -euo pipefail
SITE="https://my-app.workers.dev"
# 1. Home page 200
[[ "$(curl -sI "$SITE/" -o /dev/null -w '%{http_code}')" = "200" ]] || { echo "Home not 200"; exit 1; }
# 2. 404 page actually 404
[[ "$(curl -sI "$SITE/nonexistent" -o /dev/null -w '%{http_code}')" = "404" ]] || { echo "404 page broken"; exit 1; }
# 3. API returns JSON
curl -s "$SITE/api/health" | jq -e '.ok == true' > /dev/null || { echo "API health fail"; exit 1; }
# 4. HSTS header set
curl -sI "$SITE/" | grep -qi 'strict-transport-security' || { echo "HSTS missing"; exit 1; }
echo "✓ Smoke test passed"
Bất kỳ bước nào fail cũng kill CI run. Blog này có 19 assertion, chạy ~8 giây.
Gotchas
① wrangler dev vs wrangler dev --remote lẫn lộn
Đừng quen dùng --remote rồi quên tắt. Một dòng DELETE FROM users WHERE ... không WHERE id = ? sẽ xoá hết dữ liệu production. Miniflare local là mặc định có lý do.
② Secret không nạp vào test
@cloudflare/vitest-pool-workers không tự nạp .dev.vars. Phải đặt rõ ràng trong config:
defineWorkersConfig({
test: {
poolOptions: {
workers: {
miniflare: {
bindings: { API_KEY: "test-key" },
},
},
},
},
});
③ Khoá compatibility date
compatibility_date là snapshot của hành vi môi trường chạy. Đổi có thể breaking. Nếu muốn tận dụng tính năng mới, tạo env riêng:
{
"compatibility_date": "2026-01-01", // production an toàn
"env": {
"preview": {
"compatibility_date": "2026-05-01" // thử tính năng mới
}
}
}
④ Bước build với framework
Nếu dùng Astro/Remix, phải build trước khi triển khai:
{
"build": {
"command": "npm run build",
"cwd": ".",
"watch_paths": ["src/**"]
}
}
Hoặc chạy build thủ công trong CI trước wrangler deploy.
⑤ Biến env vs binding
vars trong wrangler.jsonc là chuỗi thường, hiện trong dashboard. Không dùng cho secret. Secret dùng wrangler secret put.
{
"vars": {
"ENVIRONMENT": "production",
"SITE_ORIGIN": "https://cloudsecop.net"
}
}
Production checklist
-
compatibility_dateđã đặt và không đổi tuỳ tiện. -
.dev.varstrong.gitignore. - Secret production qua
wrangler secret put, không hardcode. - CI dùng scoped token, không Global API Key.
-
vitest-pool-workerscho unit test, không Jest với mock. - Smoke test sau khi triển khai kiểm tra ít nhất: home 200, 404 đúng, 1 API endpoint, security headers.
- Test D1 migration local trước khi apply remote.
-
concurrency: cancel-in-progress: truetrong GitHub Actions tránh triển khai chồng chéo.
Kết
Dev loop Workers nhanh hơn phần lớn nền tảng khác: chu kỳ ngắn < 1 giây (Miniflare reload), chu kỳ triển khai ~30 giây (upload + propagate). Không container, không orchestration, không staging environment cồng kềnh.
Điểm cần nhớ: dùng đúng chế độ wrangler dev (local vs —remote), test trong isolate thật qua vitest-pool-workers, CI dùng scoped token, rollback sẵn sàng.
Từ Part 5 bắt đầu đi sâu từng storage primitive. Bắt đầu với KV: pattern cache, feature flag, session, và khi nào KV thua D1.