Triển khai API với Wrangler CLI, D1 và KV trên Cloudflare
Hướng dẫn từng bước xây dựng, tối ưu cache và deploy REST API serverless với Cloudflare Workers, Wrangler CLI, D1 SQL Database và KV Cache.
Trước đây, để xây dựng một hệ thống API phân tán toàn cầu, chúng ta thường phải thiết lập cụm server đa vùng (multi-region), cấu hình load balancer, quản lý các bản sao cơ sở dữ liệu (read-replicas) và đối mặt với độ trễ mạng lớn.
Với nền tảng Cloudflare Edge và công cụ Wrangler CLI, bạn hoàn toàn có thể xây dựng, kiểm thử cục bộ và triển khai một REST API chuẩn production kết hợp cơ sở dữ liệu quan hệ (D1) cùng bộ nhớ đệm tốc độ cao (KV) chỉ trong vài phút — vận hành tức thì trên hơn 300 trung tâm dữ liệu toàn cầu mà không lo cold start.
1. Mô hình hoạt động (ELI5: Giải thích đơn giản)#
Hãy tưởng tượng hệ thống như một nhà hàng cao cấp:
- Cloudflare Worker (Người phục vụ / Đầu bếp): Tiếp nhận yêu cầu từ thực khách, xử lý logic và phục vụ món ăn ngay tại bàn ở chi nhánh gần khách nhất.
- Cloudflare KV (Quầy đồ ăn nhanh / Bộ nhớ đệm): Lưu sẵn các món ăn phổ biến vừa chế biến. Khi khách gọi món quen thuộc, người phục vụ lấy ngay lập tức mà không cần vào bếp (đọc siêu nhanh, độ trễ < 1ms).
- Cloudflare D1 (Kho nguyên liệu & Sổ công thức / Cơ sở dữ liệu SQL): Cơ sở dữ liệu SQLite serverless lưu trữ bền vững tất cả dữ liệu gốc và thực hiện các truy vấn SQL chính xác, toàn vẹn.
graph TD
Client["📱 Khách hàng (Client Request)"] -->|HTTP GET/POST| Worker["⚡ Cloudflare Worker (Edge API)"]
Worker -->|1. Kiểm tra Cache| KV["⚡ Cloudflare KV (Đọc Cache siêu nhanh)"]
KV -.->|Cache Hit - Trả lời ngay| Worker
KV -.->|Cache Miss| D1
Worker -->|2. Truy vấn/Ghi dữ liệu| D1["🗄️ Cloudflare D1 (Serverless SQLite)"]
D1 -->|3. Cập nhật Cache| KV
Worker -->|HTTP JSON Response| Client
2. Khởi tạo Dự án với Wrangler CLI#
Đảm bảo bạn đã cài đặt Node.js ↗ hoặc Bun ↗ và có tài khoản Cloudflare miễn phí.
Bước 1: Khởi tạo Project Worker#
Sử dụng bộ công cụ C3 (create-cloudflare) hoặc khởi tạo trực tiếp với TypeScript:
# Khởi tạo project TypeScript
npm create cloudflare@latest edge-api -- --type=hello-world-typescript --ts --git --deploy=false
cd edge-apibashCài đặt Wrangler CLI mới nhất vào dependencies nếu chưa có:
npm install -D wranglerbashĐăng nhập tài khoản Cloudflare qua trình duyệt:
npx wrangler loginbash3. Khởi tạo Cơ sở Dữ liệu Cloudflare D1 (SQL)#
Cloudflare D1 là cơ sở dữ liệu quan hệ serverless chạy trên nền tảng SQLite được phân tán toàn cầu.
Bước 1: Tạo Database D1#
Chạy lệnh sau để tạo một database mang tên ecommerce-db:
npx wrangler d1 create ecommerce-dbbashWrangler sẽ trả về đoạn cấu hình tương tự như sau:
[[d1_databases]]
binding = "DB"
database_name = "ecommerce-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"textBước 2: Tạo Schema và Chạy Migration#
Tạo tệp schema.sql trong thư mục gốc của dự án:
DROP TABLE IF EXISTS products;
CREATE TABLE products (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO products (id, name, price) VALUES
('prod_1', 'Bàn phím cơ không dây', 129.99),
('prod_2', 'Màn hình Gaming Ultra-Wide', 499.99),
('prod_3', 'Bàn nâng hạ Ergonomic', 349.50);sqlChạy schema trên môi trường cục bộ (Local Development):
npx wrangler d1 execute ecommerce-db --local --file=./schema.sqlbashChạy schema trên cơ sở dữ liệu đám mây Production:
npx wrangler d1 execute ecommerce-db --remote --file=./schema.sqlbash4. Khởi tạo Bộ nhớ đệm Cloudflare KV#
Cloudflare KV là kho lưu trữ Key-Value toàn cầu, tối ưu hóa cho các thao tác đọc tần suất cao với độ trễ cực thấp.
Bước 1: Tạo KV Namespace#
Tạo 2 namespace cho môi trường Production và Preview:
# Tạo namespace Production
npx wrangler kv namespace create CACHE_KV
# Tạo namespace Preview (cho testing)
npx wrangler kv namespace create CACHE_KV --previewbashWrangler sẽ xuất ra các ID tương ứng:
[[kv_namespaces]]
binding = "CACHE_KV"
id = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
preview_id = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"text5. Cấu hình tệp wrangler.toml#
Thêm cấu hình binding của D1 và KV vào tệp wrangler.toml:
name = "edge-api"
main = "src/index.ts"
compatibility_date = "2026-08-01"
# Binding D1 Database
[[d1_databases]]
binding = "DB"
database_name = "ecommerce-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Binding KV Cache
[[kv_namespaces]]
binding = "CACHE_KV"
id = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
preview_id = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"tomlTự động sinh TypeScript Types cho các bindings:
npx wrangler typesbash6. Viết mã nguồn API theo mô hình Cache-Aside#
Mở tệp src/index.ts và triển khai chiến lược Cache-Aside Pattern:
- Tiếp nhận request
GET /api/products/:id, kiểm tra dữ liệu trong KV Cache trước. - Nếu có (Cache HIT): Trả về dữ liệu ngay lập tức.
- Nếu không có (Cache MISS): Truy vấn D1 Database, lưu kết quả vào KV với thời gian sống (TTL), sau đó trả về.
- Khi có request
POST /api/products: Thêm bản ghi mới vào D1 và cập nhật cache trong KV.
export interface Env {
DB: D1Database;
CACHE_KV: KVNamespace;
}
interface Product {
id: string;
name: string;
price: number;
created_at?: string;
}
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;
// Route: GET /api/products/:id
if (method === 'GET' && path.startsWith('/api/products/')) {
const id = path.replace('/api/products/', '');
const cacheKey = `product:${id}`;
// 1. Đọc từ KV Cache
const cached = await env.CACHE_KV.get(cacheKey, 'json');
if (cached) {
return Response.json(
{ source: 'kv-cache', data: cached },
{ headers: { 'X-Cache-Status': 'HIT', 'Content-Type': 'application/json' } }
);
}
// 2. Cache Miss: Truy vấn từ D1 Database
const product = await env.DB.prepare('SELECT * FROM products WHERE id = ?')
.bind(id)
.first<Product>();
if (!product) {
return Response.json({ error: 'Không tìm thấy sản phẩm' }, { status: 404 });
}
// 3. Lưu vào KV Cache với TTL 60 giây ở background
ctx.waitUntil(
env.CACHE_KV.put(cacheKey, JSON.stringify(product), {
expirationTtl: 60,
})
);
return Response.json(
{ source: 'd1-database', data: product },
{ headers: { 'X-Cache-Status': 'MISS', 'Content-Type': 'application/json' } }
);
}
// Route: POST /api/products
if (method === 'POST' && path === '/api/products') {
const body = (await request.json()) as Partial<Product>;
if (!body.name || !body.price) {
return Response.json({ error: 'Thiếu tên hoặc giá sản phẩm' }, { status: 400 });
}
const id = `prod_${Date.now()}`;
await env.DB.prepare('INSERT INTO products (id, name, price) VALUES (?, ?, ?)')
.bind(id, body.name, body.price)
.run();
const newProduct: Product = { id, name: body.name, price: body.price };
// Cập nhật KV Cache
await env.CACHE_KV.put(`product:${id}`, JSON.stringify(newProduct), {
expirationTtl: 300,
});
return Response.json({ success: true, product: newProduct }, { status: 201 });
}
return new Response('Edge API đang hoạt động ổn định!', { status: 200 });
},
};typescript[!TIP] Sử dụng
ctx.waitUntil(...)khi cập nhật KV Cache để API phản hồi ngay lập tức cho client mà không bị chặn lại bởi tiến trình ghi cache ngầm.
7. Chạy thử Cục bộ và Triển khai lên Production#
Chạy Local Development#
Chạy server giả lập D1 và KV trên máy cá nhân:
npx wrangler devbashKiểm tra API bằng lệnh curl:
# Lần gọi đầu tiên (MISS - đọc từ D1)
curl http://localhost:8787/api/products/prod_1
# Lần gọi thứ hai (HIT - đọc từ KV Cache)
curl http://localhost:8787/api/products/prod_1bashTriển khai lên Mạng Toàn Cầu (Production)#
Deploy mã nguồn lên toàn bộ mạng lưới Edge của Cloudflare chỉ bằng 1 câu lệnh:
npx wrangler deploybashHệ thống sẽ cung cấp ngay một đường dẫn trực tiếp dạng https://edge-api.<subdomain>.workers.dev hoạt động tại hơn 300 thành phố trên thế giới!
8. Nếu viết bằng Rust và Axum thì khác biệt thế nào?#
Nếu bạn yêu thích sự an toàn tuyệt đối về bộ nhớ và hiệu năng tối đa của Rust, Cloudflare Workers hoàn toàn hỗ trợ biên dịch Rust sang WebAssembly (wasm32-unknown-unknown) và tích hợp mượt mà với framework Axum ↗ thông qua worker-rs ↗.
So sánh Kiến trúc: TypeScript vs Rust (Axum)#
graph LR
subgraph TS["TypeScript Worker"]
direction TB
TS_Code["src/index.ts"] --> V8["Engine V8 Isolate<br/>(Native JS bindings)"]
V8 --> DB_TS["env.DB / env.CACHE_KV"]
end
subgraph RS["Rust + Axum Worker"]
direction TB
RS_Code["src/lib.rs (Axum Router)"] --> WASM["Module WebAssembly<br/>(wasm32-unknown-unknown)"]
WASM --> WRS["Cầu nối FFI worker-rs"]
WRS --> DB_RS["env.d1('DB') / env.kv('CACHE_KV')"]
end
| Tiêu chí | TypeScript | Rust (Axum + worker-rs) |
|---|---|---|
| Môi trường thực thi | JavaScript chạy trực tiếp trên V8 Isolate | WebAssembly (.wasm) được biên dịch qua worker-build |
| Mô hình định tuyến (Routing) | Hàm fetch(request, env, ctx) tiêu chuẩn | axum::Router mạnh mẽ với các extractor (State, Path, Json) |
| Truy cập Binding (D1, KV) | Đối tượng có sẵn: env.DB, env.CACHE_KV | Thông qua worker::Env FFI: env.d1("DB")?, env.kv("CACHE_KV")? |
| Hiệu năng & An toàn | Phát triển nhanh, kiểm tra kiểu ở build time | An toàn bộ nhớ tuyệt đối, zero-cost abstractions, mã máy WASM |
Cấu hình wrangler.toml | main = "src/index.ts" | main = "build/worker/shim.mjs" kèm lệnh build: [build] command = "worker-build --release" |
Code mẫu Rust Axum kết hợp D1 và KV#
Trong Rust, ta khởi tạo axum::Router ngay trong hàm bắt sự kiện fetch của worker, chia sẻ worker::Env thông qua Axum State:
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tower_service::Service;
use worker::*;
#[derive(Clone)]
struct AppState {
env: Arc<worker::Env>,
}
#[derive(Serialize, Deserialize)]
struct Product {
id: String,
name: String,
price: f64,
}
// GET /api/products/:id với Cache-Aside (KV) & truy vấn D1
async fn get_product(
Path(id): Path<String>,
State(state): State<AppState>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let kv = state.env.kv("CACHE_KV").map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let cache_key = format!("product:{}", id);
// 1. Kiểm tra trong KV Cache
if let Ok(Some(cached_json)) = kv.get(&cache_key).text().await {
return Ok((
StatusCode::OK,
[("X-Cache-Status", "HIT"), ("Content-Type", "application/json")],
cached_json,
));
}
// 2. Cache Miss: Truy vấn D1 Database
let d1 = state.env.d1("DB").map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let statement = d1.prepare("SELECT id, name, price FROM products WHERE id = ?1").bind(&[&id.into()])
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let product = statement.first::<Product>(None).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
.ok_or_else(|| (StatusCode::NOT_FOUND, "Không tìm thấy sản phẩm".to_string()))?;
let json_str = serde_json::to_string(&product)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// 3. Ghi vào KV Cache với TTL 60 giây
let _ = kv.put(&cache_key, &json_str).unwrap().expiration_ttl(60).execute().await;
Ok((
StatusCode::OK,
[("X-Cache-Status", "MISS"), ("Content-Type", "application/json")],
json_str,
))
}
#[event(fetch)]
async fn fetch(req: HttpRequest, env: Env, _ctx: Context) -> Result<axum::http::Response<axum::body::Body>> {
let state = AppState { env: Arc::new(env) };
let mut router = Router::new()
.route("/api/products/:id", get(get_product))
.with_state(state);
let response = router.call(req).await.unwrap();
Ok(response)
}rust9. Bảng Tổng hợp Lệnh Wrangler Thường Dùng#
| Lệnh Wrangler | Công dụng |
|---|---|
wrangler login | Đăng nhập tài khoản Cloudflare vào CLI |
wrangler dev | Khởi động môi trường giả lập local với D1 & KV |
wrangler d1 create <name> | Khởi tạo cơ sở dữ liệu D1 mới |
wrangler d1 execute <name> --file=./schema.sql | Chạy file SQL tạo bảng dữ liệu (--local hoặc --remote) |
wrangler kv namespace create <name> | Tạo kho lưu trữ KV mới |
wrangler types | Tự động sinh kiểu dữ liệu TypeScript cho Env |
wrangler deploy | Build và deploy Worker lên Production toàn cầu |
wrangler tail | Xem log trực tiếp thời gian thực từ Production |