Vibe Code
Giáo trình/Phần 3 — App có dự liệu

Workers — backend trong một file

Mã chạy phía máy chủ, không cần máy chủ. Dựng Worker đầu tiên, phục vụ cả web tĩnh lẫn API, và hiểu giới hạn thật của nó.

Cấp 340 phút làm

Đây là bài mở ra Cấp 3. Từ đây app của bạn có phía máy chủ: lưu dữ liệu chung, đăng nhập thật, giữ khóa bí mật, gửi email.

Workers là gì, nói cho người không chuyên

Pages trả file có sẵn. Workers chạy mã của bạn mỗi khi có người gọi tới, trên máy chủ Cloudflare gần người dùng nhất.

Người dùng ở Thanh Hóa gọi https://app.cua-ban.com/api/don-hang
        │
        ▼
Cloudflare nhận tại điểm mạng gần nhất (Hà Nội / Singapore)
        │
        ▼
Mã của bạn chạy NGAY TẠI ĐÓ (vài milli-giây)
        │
        ▼
Trả kết quả

Không có máy chủ nào của bạn phải bật 24/7. Không có hóa đơn VPS. Không ai phải vá hệ điều hành. Không có chuyện "máy chủ đầy ổ cứng lúc 2 giờ sáng".

Cái giá của việc đó — và bạn phải biết trước:

Workers không có Nghĩa là
Hệ thống file để ghi Không fs.writeFile. Lưu dữ liệu phải dùng D1/KV/R2
Tiến trình chạy liên tục Mỗi request là một lượt chạy riêng, xong là hết
Bộ nhớ giữ giữa các lượt Biến toàn cục không đáng tin (xem bẫy ở dưới)
Toàn bộ thư viện Node.js Phần lớn chạy được, một số thì không

Mô hình đúng trong đầu: một hàm, nhận một request, trả một response, và quên hết.

Worker đầu tiên

cd C:\NVAI\Claude
npm create cloudflare@latest app-don-hang

Chọn: Hello World example → Worker only → TypeScript hay JavaScript đều được (giáo trình dùng JavaScript cho dễ đọc) → Git: Yes → Deploy: No (ta deploy tay để hiểu).

Mở src/index.js, nó đại khái là:

export default {
  async fetch(request, env, ctx) {
    return new Response("Xin chao!");
  },
};

Ba tham số, học một lần dùng mãi:

  • request — ai gọi, gọi đường nào, mang gì theo.
  • env — bí mật và các "binding": cơ sở dữ liệu, kho tệp, biến cấu hình.
  • ctx — điều khiển vòng đời (ctx.waitUntil — xem cảnh báo ở bài Cron).

Chạy thử tại máy:

npx wrangler dev

Mở http://localhost:8787. Sửa chữ trong file, lưu — nó tự nạp lại.

Đưa lên mạng:

npx wrangler deploy

Ra link kiểu app-don-hang.ten-ban.workers.dev. Đó là backend của bạn, đang chạy thật, miễn phí.

Định tuyến: một Worker, nhiều đường

Một Worker thường phục vụ cả web lẫn API. Mẫu định tuyến tối giản, dùng được cho app thật:

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const p = url.pathname;

    // ---- API ----
    if (p.startsWith("/api/")) {
      try {
        if (p === "/api/don-hang" && request.method === "GET")  return await dsDonHang(request, env);
        if (p === "/api/don-hang" && request.method === "POST") return await themDonHang(request, env);
        return json({ loi: "Khong co duong nay" }, 404);
      } catch (e) {
        // Không bao giờ để lỗi lọt ra thành trang lỗi trắng của Cloudflare
        console.log("Loi API:", p, e.message);
        return json({ loi: "Loi he thong" }, 500);
      }
    }

    // ---- Trang web tĩnh ----
    return env.ASSETS.fetch(request);
  },
};

function json(obj, status = 200) {
  return new Response(JSON.stringify(obj), {
    status,
    headers: { "content-type": "application/json; charset=utf-8" },
  });
}

Bốn điểm trong 25 dòng này, đều là kinh nghiệm trả giá:

  1. try/catch bọc cả nhánh API. Không có nó, một lỗi nhỏ trả về trang lỗi của Cloudflare và người dùng thấy màn hình lạ, còn bạn không biết lỗi gì.
  2. Trả lỗi dạng JSON có cấu trúc, để phía trình duyệt hiện được thông báo tử tế.
  3. charset=utf-8 trong header. Thiếu là chữ tiếng Việt vỡ ở một số trình duyệt.
  4. Thông báo lỗi trả cho người dùng không tiết lộ chi tiết nội bộ — chi tiết đi vào console.log, bạn xem bằng wrangler tail.

Phục vụ web tĩnh từ Worker

Đây là cách dựng app thật: một Worker vừa trả HTML vừa có API. Trong wrangler.toml:

name = "app-don-hang"
main = "src/index.js"
compatibility_date = "2026-01-01"

[assets]
directory = "./public"
binding = "ASSETS"

Đặt index.html vào public/. Giờ https://app.../ trả trang web, https://app.../api/... chạy mã của bạn. Một tên miền, một project, không có chuyện CORS.

Static Assets phục vụ thẳng từ biên — mã Worker KHÔNG chạy

Đây là bẫy đã mất nhiều giờ. Khi một đường dẫn khớp với file trong public/, Cloudflare trả file đó ngay tại biên, không gọi hàm fetch của bạn.

Hậu quả thực tế: - Đặt header (cache, bảo mật) trong mã Worker là vô nghĩa với các file tĩnh. Phải đặt trong public/_headers. - Không chèn thêm được gì vào HTML bằng mã Worker theo cách thông thường. - Nếu public/ tình cờ có file tên trùng một đường API, API của bạn bị file đó nuốt — rất khó đoán.

Luật: file tĩnh thì cấu hình bằng _headers; logic thì để dưới tiền tố /api/ và đừng để public/ có thứ gì tên giống đường API.

Xem log: wrangler tail

Đây là "Console" của phía máy chủ. Mở một cửa sổ riêng, để nó chạy trong lúc bạn bấm thử app:

npx wrangler tail --format=pretty

Mỗi request hiện một dòng, kèm mọi console.log và lỗi. Không có công cụ này thì phía máy chủ là một cái hộp đen.

`wallTime` trong log tính cả việc chạy nền

Nhìn log thấy một request wallTime: 23000ms thì rất dễ tưởng app chậm thảm họa. Thực tế, wallTime cộng cả thời gian của việc chạy nền ctx.waitUntil — người dùng đã nhận kết quả từ giây đầu, phần còn lại là việc hậu kỳ.

Muốn biết người dùng chờ bao lâu thì đo từ phía người dùng: tab Network của trình duyệt, hoặc curl -w "%{time_total}".

Biến toàn cục và nhiều phiên bản chạy song song

Mã này trông đúng và là một cái bẫy kinh điển:

let soLuot = 0;                      // SAI nếu dùng để đếm thật

export default {
  async fetch(request, env) {
    soLuot++;
    return new Response("Luot thu " + soLuot);
  },
};

Cloudflare chạy mã của bạn trong nhiều bản độc lập, ở nhiều điểm mạng, và dọn chúng đi khi rảnh. Mỗi bản có soLuot riêng, bắt đầu từ 0. Con số bạn thấy là vô nghĩa.

Hệ quả nghiêm trọng hơn con số sai: mọi cơ chế "chỉ chạy mỗi 10 phút một lần" dựa trên biến toàn cục đều vỡ. Lúc tải cao, Cloudflare bung thêm bản mới, mỗi bản tưởng mình là lần đầu → việc định kỳ chạy dày hơn hàng chục lần. Chuyện này đã gây treo app thật.

Luật: cần nhớ gì giữa các lượt thì lưu vào D1 / KV / Durable Object, đừng tin biến trong bộ nhớ.

Vì sao Workers thắng VPS với người làm một mìnhchuyên gia insight & case thực tế

Tôi đã vận hành cả hai. So sánh thật, không phải quảng cáo:

VPS 5 USD/tháng Workers gói free
Tiền/tháng ~130 nghìn 0
Việc phải làm hằng tháng vá bảo mật, xem ổ cứng, gia hạn chứng chỉ không
Hỏng lúc 2 giờ sáng bạn dậy xử lý Cloudflare xử lý
Người dùng ở Thanh Hóa ~200ms (máy ở Singapore) ~20ms (điểm trong nước)
Việc chạy quá 30 giây được không — phải chia nhỏ
Ghi file lên ổ đĩa được không

Hai dòng cuối là cái giá thật, và nó buộc bạn thiết kế khác đi. Nhưng với 90% app doanh nghiệp nhỏ — nhập liệu, xem báo cáo, duyệt, thông báo — bạn không bao giờ chạm tới hai giới hạn đó.

Điều tôi đánh giá cao nhất lại là thứ không có trong bảng: không có cuộc gọi lúc nửa đêm. Với người làm một mình, đó là yếu tố quyết định có trụ được lâu dài hay không.

Giới hạn gói miễn phí — con số cần biết

Hạng mục Gói free Ý nghĩa thực tế
Số request 100.000/ngày App nội bộ 200 người dùng thì rất khó chạm
Thời gian CPU mỗi request 10 ms (free) Đây mới là giới hạn hay chạm, không phải số request
Thời gian chờ (I/O) không tính vào CPU Chờ cơ sở dữ liệu, chờ API ngoài thì không tốn CPU
Cỡ mã sau nén 3 MB (free) Thừa cho app một file

Điểm hay bị hiểu sai: thời gian chờ không tính là CPU. Một request chờ D1 trả về 2 giây vẫn có thể chỉ dùng 3 ms CPU. Cái tốn CPU là tính toán: vòng lặp lớn, xử lý ảnh, mã hóa.

Hạn mức và giá đổi theo thời gian — kiểm lại tại developers.cloudflare.com/workers/platform/limits trước khi dựa vào con số nào.

Đề dựng Worker đầu tiên cho việc thật của bạnchuyên gia thực thi
Dựng một Cloudflare Worker trong thư mục này:

- Phục vụ web tĩnh từ ./public qua binding ASSETS.
- API dưới tiền tố /api/, chỉ 2 đường để bắt đầu:
  GET  /api/don-hang       → trả danh sách (tạm thời trả mảng gán cứng)
  POST /api/don-hang       → nhận {ten, sdt, mon, soLuong}, kiểm dữ liệu,
                             tạm thời chỉ console.log rồi trả {ok:true}
- Bọc try/catch cho cả nhánh API, lỗi trả JSON {loi:"..."}, không để lộ
  chi tiết nội bộ cho người dùng.
- Mọi response JSON có charset=utf-8.
- Chưa dùng cơ sở dữ liệu, chưa đăng nhập — sẽ thêm ở bước sau.
- wrangler.toml đầy đủ.

Xong rồi chạy `npx wrangler dev` và tự kiểm bằng curl cả hai đường,
báo cho tôi kết quả thật.

Câu cuối lại là câu quan trọng nhất: bắt nó tự chứng minh. "Tôi đã tạo xong" không có giá trị; curl trả về {"ok":true} thì có.

"Serverless" là cái tên tệ nhất ngành nàyngười dùng bình thường góp ý

Tôi mất một tuần hiểu sai vì cái tên. "Không máy chủ" — vậy mã chạy ở đâu? Trên không khí?

Dịch cho người như tôi: vẫn có máy chủ, nhưng không phải máy của bạn và bạn không phải nghĩ về nó. Như đi xe khách thay vì mua xe: vẫn có xe, nhưng bạn không đổi dầu.

Thứ hai làm tôi vướng: "mỗi request là một lượt chạy riêng, xong là quên". Tôi cứ muốn "lưu tạm vào biến cho nhanh". Phải tự nhắc mình: muốn nhớ thì phải ghi ra ngoài — và bài sau là chỗ ghi.

Chốt review bài nàychuyên gia review
  • Đủ: mô hình Workers, ba tham số, định tuyến có xử lý lỗi, phục vụ tĩnh, wrangler tail, bẫy biến toàn cục, bẫy static assets, hạn mức.
  • Chưa làm: chưa lưu được gì — bài sau (D1).
  • Rủi ro còn lại: app hiện tại chưa có xác thực, mọi đường /api/ ai cũng gọi được. Đừng đưa dữ liệu thật vào trước khi học Đăng nhập.
Bài tập chốt
  1. Dựng Worker theo đề ở trên, deploy, gọi thử bằng curl cả GET và POST.
  2. Mở wrangler tail trong cửa sổ thứ hai, bấm thử app, xem log chạy.
  3. Cố ý gây lỗi: trong một đường API, gọi một biến không tồn tại. Kiểm rằng người dùng nhận {"loi":"Loi he thong"} chứ không phải trang lỗi trắng, và chi tiết lỗi hiện trong wrangler tail. Đây là phép thử rằng lớp try/catch của bạn thật sự hoạt động.