Cloudflare Workers Cloudflare Workers Cloudflare Workers · Lab 3/7 Lab 3/7 Lab 3/7 · 15 phút · 15 min · 15 នាទី

03

Lưu bền bằng KV Persistent Storage with KV រក្សាទុកជាប់ដោយ KV

Đổi store trong bộ nhớ sang Workers KV để bookmark còn sau restart và deploy. Replace the in-memory bookmark store with Workers KV so bookmarks persist across restarts and deployments. ប្តូរ store ក្នុងអង្គចងចាំទៅ Workers KV ដើម្បីឲ្យ bookmark នៅក្រោយ restart និង deploy។

Nội dung bước (lệnh, code) giữ nguyên tiếng Anh từ nguồn chính thức. Step body (commands, code) stays in English from the official source. ខ្លឹមសារជំហាន (ពាក្យបញ្ជា និង code) រក្សាភាសាអង់គ្លេសពីប្រភពផ្លូវការ។ labs.cloudflare.dev ↗

Cần trước Prerequisites តម្រូវការជាមុន

  • Đã xong bước 2 Completed Step 2 បានបញ្ចប់ជំហាន 2
  • bookmark-api đã có endpoint CRUD bookmark-api project with CRUD endpoints bookmark-api មាន endpoint CRUD

Bạn sẽ làm được Learning objectives គោលបំណងសិក្សា

  • Nối resource ngoài vào Worker bằng binding Connect an external resource to a Worker using bindings ភ្ជាប់ resource ខាងក្រៅទៅ Worker ដោយ binding
  • Đổi tầng data in-memory sang KV mà không đổi contract API Swap an in-memory data layer for persistent KV storage without changing the API contract ប្តូរស្រទាប់ data in-memory ទៅ KV ដោយមិនផ្លាស់ប្តូរ contract API
  • Hiểu eventual consistency và khi nào KV là lựa chọn đúng Reason about eventual consistency and when KV is the right storage choice យល់ eventual consistency និងពេលណា KV ជាជម្រើសត្រឹមត្រូវ

Bước 1: Tạo namespace KV Step 1: Create a KV Namespace ជំហាន 1: បង្កើត namespace KV

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
A KV namespace called BOOKMARKS bound to your Worker.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
KV provides globally distributed, persistent key-value storage that survives Worker restarts and deployments.
bash
npx wrangler kv namespace create "BOOKMARKS"

When prompted “Would you like Wrangler to add it on your behalf?”, type Y. This adds the KV binding to your wrangler.jsonc.

When prompted “Would you like Wrangler to add it on your behalf?”, type Y. This adds the KV binding to your wrangler.jsonc.

When prompted “Would you like Wrangler to add it on your behalf?”, type Y. This adds the KV binding to your wrangler.jsonc.

Then regenerate your types so TypeScript knows about the binding:

Then regenerate your types so TypeScript knows about the binding:

Then regenerate your types so TypeScript knows about the binding:

bash
npx wrangler types

Bước 2: Đổi handler sang KV Step 2: Update the Handlers to Use KV ជំហាន 2: ប្តូរ handler ទៅ KV

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
All four CRUD handlers using KV instead of the in-memory Map.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
This is the core migration. The route structure stays the same; only the data access layer changes.

Replace the entire src/index.ts with the following. The key changes from Step 2 are marked with comments:

Replace the entire src/index.ts with the following. The key changes from Step 2 are marked with comments:

Replace the entire src/index.ts with the following. The key changes from Step 2 are marked with comments:

typescript

interface Bookmark {
  id: string;
  url: string;
  title: string;
  createdAt: string;
}

// REMOVED: const bookmarks: Map<string, Bookmark> = new Map();
// Bookmarks are now stored in KV via env.BOOKMARKS

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const path = url.pathname;
    const method = request.method;

    // CHANGED: all handlers now receive env for KV access
    if (path === '/bookmarks' && method === 'GET') {
      return listBookmarks(env);
    }

    if (path === '/bookmarks' && method === 'POST') {
      return createBookmark(request, env);
    }

    const match = path.match(/^\/bookmarks\/([a-zA-Z0-9_-]+)$/);
    if (match && method === 'GET') {
      return getBookmark(match[1], env);
    }

    if (match && method === 'DELETE') {
      return deleteBookmark(match[1], env);
    }

    if (path === '/') {
      return Response.json({
        name: 'Bookmark API',
        version: '2.0.0',
        storage: 'Workers KV',
        endpoints: ['GET /bookmarks', 'POST /bookmarks', 'GET /bookmarks/:id', 'DELETE /bookmarks/:id']
      });
    }

    return Response.json({ error: 'Not Found' }, { status: 404 });
  },
};

// CHANGED: list from KV using .list() + individual .get() calls
// NOTE: This fetches each value individually (N+1 pattern). Fine for small
// datasets, but for better performance at scale, store titles as KV metadata
// so you can list without fetching each value (see the challenge below).
async function listBookmarks(env: Env): Promise<Response> {
  const keys = await env.BOOKMARKS.list();
  const all: Bookmark[] = [];

  for (const key of keys.keys) {
    const value = await env.BOOKMARKS.get<Bookmark>(key.name, 'json');
    if (value) all.push(value);
  }

  return Response.json({ bookmarks: all, count: all.length });
}

// CHANGED: store in KV with .put()
async function createBookmark(request: Request, env: Env): Promise<Response> {
  let body: { url?: string; title?: string };

  try {
    body = await request.json() as { url?: string; title?: string };
  } catch {
    return Response.json(
      { error: 'Invalid JSON in request body' },
      { status: 400 }
    );
  }

  if (!body.url || !body.title) {
    return Response.json(
      { error: 'Missing required fields: url, title' },
      { status: 400 }
    );
  }

  const id = crypto.randomUUID().slice(0, 8);
  const bookmark: Bookmark = {
    id,
    url: body.url,
    title: body.title,
    createdAt: new Date().toISOString(),
  };

  // Store as JSON in KV, keyed by ID
  await env.BOOKMARKS.put(id, JSON.stringify(bookmark));

  return Response.json(bookmark, { status: 201 });
}

// CHANGED: retrieve from KV with .get()
async function getBookmark(id: string, env: Env): Promise<Response> {
  const bookmark = await env.BOOKMARKS.get<Bookmark>(id, 'json');
  if (!bookmark) {
    return Response.json({ error: 'Bookmark not found' }, { status: 404 });
  }
  return Response.json(bookmark);
}

// CHANGED: delete from KV with .delete()
async function deleteBookmark(id: string, env: Env): Promise<Response> {
  const existing = await env.BOOKMARKS.get(id);
  if (!existing) {
    return Response.json({ error: 'Bookmark not found' }, { status: 404 });
  }
  await env.BOOKMARKS.delete(id);
  return Response.json({ message: 'Bookmark deleted' });
}

Bước 3: Test lưu bền Step 3: Test Persistent Storage ជំហាន 3: Test ការរក្សាទុកជាប់

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
Bookmarks that survive a Worker restart.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
This confirms KV is working. The same tests from Step 2 should pass, but now data persists.

Tạo và kiểm tra persistence Create and verify persistence បង្កើត និងពិនិត្យ persistence

Create a bookmark:

Create a bookmark:

Create a bookmark:

bash
curl -X POST http://localhost:8787/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"url":"https://developers.cloudflare.com/kv/","title":"KV Docs"}'

List bookmarks:

List bookmarks:

List bookmarks:

bash
curl -s http://localhost:8787/bookmarks | jq

Now stop the dev server (Ctrl+C) and restart it (npm run dev). Then list again:

Now stop the dev server (Ctrl+C) and restart it (npm run dev). Then list again:

Now stop the dev server (Ctrl+C) and restart it (npm run dev). Then list again:

bash
curl -s http://localhost:8787/bookmarks | jq

If the bookmark is still there, KV is working.

If the bookmark is still there, KV is working.

If the bookmark is still there, KV is working.

Test mọi thao tác Test all operations Test ប្រតិបត្តិការទាំងអស់

Create a bookmark:

Create a bookmark:

Create a bookmark:

bash
curl -X POST http://localhost:8787/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"url":"https://developers.cloudflare.com/workers-ai/","title":"Workers AI Docs"}'

Get by ID (use the id from the create response):

Get by ID (use the id from the create response):

Get by ID (use the id from the create response):

bash
curl -s http://localhost:8787/bookmarks/REPLACE_ID | jq

Delete:

Delete:

Delete:

bash
curl -X DELETE http://localhost:8787/bookmarks/REPLACE_ID

Verify deletion (should return 404):

Verify deletion (should return 404):

Verify deletion (should return 404):

bash
curl http://localhost:8787/bookmarks/REPLACE_ID