Bước 1: Tạo database D1 Step 1: Create the D1 Database ជំហាន 1: បង្កើត database D1
- Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
- A D1 database bound to your Worker alongside the existing KV namespace.
- Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
- D1 gives you SQL queries, joins, and filtering that KV cannot provide.
npx wrangler d1 create bookmark-db When prompted “Would you like Wrangler to add it on your behalf?”, type Y. Wrangler will then ask for a binding name. Enter DB so it matches the env.DB calls used throughout this workshop.
When prompted “Would you like Wrangler to add it on your behalf?”, type Y. Wrangler will then ask for a binding name. Enter DB so it matches the env.DB calls used throughout this workshop.
When prompted “Would you like Wrangler to add it on your behalf?”, type Y. Wrangler will then ask for a binding name. Enter DB so it matches the env.DB calls used throughout this workshop.
Regenerate types:
Regenerate types:
Regenerate types:
npx wrangler types Bước 2: Định nghĩa schema Step 2: Define the Schema ជំហាន 2: កំណត់ schema
- Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
- A bookmarks table with columns for URL, title, tags, and timestamps.
- Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
- A well-designed schema is the foundation of your relational data layer.
Create schema.sql in your project root:
Create schema.sql in your project root:
Create schema.sql in your project root:
CREATE TABLE IF NOT EXISTS bookmarks (
id TEXT PRIMARY KEY,
url TEXT NOT NULL,
title TEXT NOT NULL,
tags TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); Apply it to the local database. Make sure the dev server is stopped before running this:
Apply it to the local database. Make sure the dev server is stopped before running this:
Apply it to the local database. Make sure the dev server is stopped before running this:
npx wrangler d1 execute bookmark-db --local --file=./schema.sql Bước 3: Cập nhật Worker Step 3: Update the Worker ជំហាន 3: ធ្វើបច្ចុប្បន្នភាព Worker
- Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
- Handlers that use D1 as the source of truth and KV as a read cache.
- Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
- This demonstrates a real-world caching pattern: write to D1, cache in KV, read from KV first.
Replace src/index.ts with the following. Key changes from Step 3 are marked:
Replace src/index.ts with the following. Key changes from Step 3 are marked:
Replace src/index.ts with the following. Key changes from Step 3 are marked:
interface Bookmark {
id: string;
url: string;
title: string;
tags: string; // NEW: comma-separated tags
created_at: string; // NEW: from D1 DATETIME
}
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;
if (path === '/bookmarks' && method === 'GET') {
return listBookmarks(env, url);
}
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: '3.0.0',
storage: 'D1 + KV cache',
endpoints: [
'GET /bookmarks',
'GET /bookmarks?tag=docs',
'POST /bookmarks',
'GET /bookmarks/:id',
'DELETE /bookmarks/:id'
]
});
}
return Response.json({ error: 'Not Found' }, { status: 404 });
},
};
// CHANGED: query D1, support filtering by tag
async function listBookmarks(env: Env, url: URL): Promise<Response> {
const tag = url.searchParams.get('tag');
let results: Bookmark[];
if (tag) {
// Filter by tag using LIKE (tags is comma-separated)
const { results: rows } = await env.DB.prepare(
`SELECT * FROM bookmarks WHERE tags LIKE ? ORDER BY created_at DESC`
).bind(`%${tag}%`).all<Bookmark>();
results = rows;
} else {
const { results: rows } = await env.DB.prepare(
`SELECT * FROM bookmarks ORDER BY created_at DESC`
).all<Bookmark>();
results = rows;
}
return Response.json({ bookmarks: results, count: results.length });
}
// CHANGED: write to D1, then cache in KV
// NOTE on SQL safety: every user-provided value is passed through .bind(),
// which uses parameterized queries. This prevents SQL injection, even when
// using LIKE with wildcards like %${tag}%. Never concatenate user input
// directly into SQL strings.
async function createBookmark(request: Request, env: Env): Promise<Response> {
let body: { url?: string; title?: string; tags?: string };
try {
body = await request.json() as { url?: string; title?: string; tags?: 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 tags = body.tags || '';
// Write to D1 (source of truth)
const result = await env.DB.prepare(
`INSERT INTO bookmarks (id, url, title, tags)
VALUES (?, ?, ?, ?)
RETURNING *`
).bind(id, body.url, body.title, tags).first<Bookmark>();
if (!result) {
return Response.json({ error: 'Failed to create bookmark' }, { status: 500 });
}
// Cache in KV for fast reads
await env.BOOKMARKS.put(id, JSON.stringify(result), { expirationTtl: 3600 });
return Response.json(result, { status: 201 });
}
// CHANGED: check KV cache first, fall back to D1
async function getBookmark(id: string, env: Env): Promise<Response> {
// Try KV cache first
const cached = await env.BOOKMARKS.get<Bookmark>(id, 'json');
if (cached) {
return Response.json({ ...cached, _cached: true });
}
// Fall back to D1
const bookmark = await env.DB.prepare(
'SELECT * FROM bookmarks WHERE id = ?'
).bind(id).first<Bookmark>();
if (!bookmark) {
return Response.json({ error: 'Bookmark not found' }, { status: 404 });
}
// Populate cache for next time
await env.BOOKMARKS.put(id, JSON.stringify(bookmark), { expirationTtl: 3600 });
return Response.json(bookmark);
}
// CHANGED: delete from D1 and invalidate KV cache
async function deleteBookmark(id: string, env: Env): Promise<Response> {
const existing = await env.DB.prepare(
'SELECT id FROM bookmarks WHERE id = ?'
).bind(id).first();
if (!existing) {
return Response.json({ error: 'Bookmark not found' }, { status: 404 });
}
// Delete from D1
await env.DB.prepare('DELETE FROM bookmarks WHERE id = ?').bind(id).run();
// Invalidate KV cache
await env.BOOKMARKS.delete(id);
return Response.json({ message: 'Bookmark deleted' });
} Bước 4: Test D1 + cache KV Step 4: Test D1 + KV Caching ជំហាន 4: Test D1 + cache KV
- Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
- Bookmarks stored in D1 with KV caching verified.
- Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
- Testing confirms the migration is complete and both storage layers work together.
Tạo bookmark có tag Create bookmarks with tags បង្កើត bookmark មាន tag
curl -X POST http://localhost:8787/bookmarks \
-H "Content-Type: application/json" \
-d '{"url":"https://developers.cloudflare.com/workers/","title":"Workers Docs","tags":"docs,cloudflare,workers"}' curl -X POST http://localhost:8787/bookmarks \
-H "Content-Type: application/json" \
-d '{"url":"https://developers.cloudflare.com/d1/","title":"D1 Docs","tags":"docs,cloudflare,database"}' curl -X POST http://localhost:8787/bookmarks \
-H "Content-Type: application/json" \
-d '{"url":"https://github.com/cloudflare/workers-sdk","title":"Workers SDK","tags":"github,cloudflare,tools"}' Lọc theo tag Filter by tag ច្រោះតាម tag
All bookmarks tagged “docs”:
All bookmarks tagged “docs”:
All bookmarks tagged “docs”:
curl -s "http://localhost:8787/bookmarks?tag=docs" | jq All bookmarks tagged “cloudflare”:
All bookmarks tagged “cloudflare”:
All bookmarks tagged “cloudflare”:
curl -s "http://localhost:8787/bookmarks?tag=cloudflare" | jq Kiểm tra cache Verify caching ពិនិត្យ cache
Get a bookmark by ID (first request comes from D1, no _cached field):
Get a bookmark by ID (first request comes from D1, no _cached field):
Get a bookmark by ID (first request comes from D1, no _cached field):
curl -s http://localhost:8787/bookmarks/REPLACE_ID | jq Get the same bookmark again (second request comes from KV cache, should include "_cached": true):
Get the same bookmark again (second request comes from KV cache, should include "_cached": true):
Get the same bookmark again (second request comes from KV cache, should include "_cached": true):
curl -s http://localhost:8787/bookmarks/REPLACE_ID | jq Kiểm tra persistence Verify persistence ពិនិត្យ persistence
Stop and restart the dev server. List bookmarks. They should still be there (D1 is persistent).
Stop and restart the dev server. List bookmarks. They should still be there (D1 is persistent).
Stop and restart the dev server. List bookmarks. They should still be there (D1 is persistent).