Wrangler và Miniflare dev loop: từ init tới deploy trong 30 phút

Dev loop thực tế của Workers: wrangler init, dev local với Miniflare, vitest, D1 migration, secret, triển khai 300+ PoP trong 30 giây. Vòng đời từ file trống đến production.

· 6 phút đọc · Read in English
Vòng dev loop Worker với Wrangler + Miniflare: wrangler init → wrangler dev local trên workerd với binding giả lập, test vitest, D1 migration, quản lý secret và deploy ra 300+ PoP trong 30 giây

TL;DR

Dev loop của Workers về cơ bản là:

  1. wrangler dev chạy Miniflare local (cùng môi trường chạy workerd với production), hot reload, bindings mô phỏng.
  2. vitest với @cloudflare/vitest-pool-workers chạy unit test trong isolate thật, không mock.
  3. wrangler deploy upload 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 dev cho 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

Dev loop Workers: viết code trong editor, wrangler dev chạy Miniflare local với môi trường chạy workerd, hot reload, test với vitest và vitest-pool-workers, wrangler deploy đẩy lên production, CI GitHub Actions chạy test và smoke test tự động.

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" Worker cho 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 COLUMN hoặ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 put lại thủ công.
  • Secret được inject lúc triển khai. Đổi secret xong phải wrangler deploy lạ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 ProfileAPI TokensCreate 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.vars trong .gitignore.
  • Secret production qua wrangler secret put, không hardcode.
  • CI dùng scoped token, không Global API Key.
  • vitest-pool-workers cho 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: true trong 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.


Tham khảo