Skip to content
youhoc
  • Pages
    • Home
    • Modern App Guidelines
    • Node.js
      • Installing & Exploring
      • Loading Modules
      • npm - Get Command Input
      • Express.js
        • Express Web Server
        • Template Engine & MVC
        • Authentication
        • Authentication Trong REST API Với JWT
        • File Upload with Multer, Express.js
        • Server-Side Validation Với Express-Validator
      • Sequelize
        • Sequelize Transactions: Đảm Bảo Tính Toàn Vẹn Dữ Liệu
        • 7 loại Data Types phổ biến Trong Sequelize
        • Phân Trang (Pagination) Trong Express.js Với Sequelize/MySQL
      • Hướng dẫn Cơ bản về Rest API
      • Node-cron Simple to Complex Setup with PM2
      • Hono
        • Hono Response
        • Error Handling
    • Cloudflare
      • Minimal Cloudflare Worker + Hono + Drizzle ORM (part 1)
      • icon picker
        Minimal Cloudflare Worker + Hono + Drizzle ORM (part 2)
    • htmx
      • HTMx Form: Request, Response, Swap
    • Linux
      • Day 1: Linux Distributions & Navigation
      • Day 2: User Management
      • Day 3: File Permission & Ownership
      • Day 4: Package Management
      • Day 5: Services Management
    • Javascript
      • JS The Weird Part
        • Execution Context
        • Types & Operators
        • Objects & Functions
        • Error Handling & Strict Mode
        • Typescript, ES6, Tra
      • Modern JS
        • JS in the Browser
        • Data Storage JSON
        • Modern JS
        • Advanced Objects & Methods
        • Webpack & Babel
        • Async
      • jQuery
        • In-depth Analysis of jQuery
      • React-ready JS
        • Arrow Function
        • Template Literals
        • Logical AND, OR, Ternary, Nullish Operators
        • Destructuring & Rest Operator
        • Array Method
        • Immutability and Spread Operator
        • Promises, Async/Await, Callback
    • Typescript
      • TypeScript cơ bản (phần 1)
      • TypeScript cơ bản (phần 2)
      • require vs import
    • ReactJS
      • React from Andrew
        • Summary from Next
        • 1. Basics
        • 2. React Components
        • 3. Webpack
        • 4. Styling with SCSS
        • 5. React Router
        • 6. React Hook
      • Modern React From The Beginning
        • Intro to JSX
        • Vite Build Tools
        • Basic Component Creation
        • Component State
        • Props & Component Composition
        • useState with Inputs & Form Submission
        • useEffect, useRef & Local Storage
        • Async / Await and Http Request in React
        • React Router: Declarative Mode
        • ContextAPI
        • React Router: Framework Mode
          • File-routing & HTML Layouts
          • Server-side Data Query
          • Links & Navigation
          • Loaders
    • PHP
      • gruntJS
      • composer
      • MySQL
      • Thiết lập Cloudflare Turnstile chống spam trong PHP
    • Docker
      • Container Basics
      • Container Networking
      • Container Image
      • Container Volume & Persistent Data
      • Dockerfile
      • Docker Compose
      • Docker Registry
    • Payload CMS

Minimal Cloudflare Worker + Hono + Drizzle ORM (part 2)

Một dự án Cloudflare Worker Serverless tối giản, tương đương với Node.js, Express.js và Sequelize.
Sau khi đã chạy được Worker và thiết lập được MVC tối giản, nhu cầu tiếp theo gần như luôn là đưa dữ liệu ra khỏi file tĩnh.
Nếu đang quen với Express, phản xạ thường là: cài Sequelize, tạo file kết nối database, viết model, thêm migration, rồi để app server giữ connection đó trong suốt vòng đời process.
Nhưng với Hono chạy trên Cloudflare Workers, tư duy này cần đổi một chút.
Bạn vẫn có schema, query method, migration và type, nhưng cách “gắn” database vào app không giống Express. Không có một Node server chạy mãi để bạn giữ connection global theo kiểu quen thuộc. Thay vào đó, database binding được Cloudflare inject vào runtime, rồi Hono đọc nó qua c.env.
Bài viết chính là đoạn chuyển mình đó:
thêm D1 binding và schema/query đầu tiên với Drizzle
định nghĩa schema bằng TypeScript
sinh migration SQL từ schema
tạo typed query method
đưa database binding vào Hono app bằng type rõ ràng
chuẩn hóa quy trình migration và Wrangler
Trong bài này, mình sẽ hướng dẫn cách cài đặt và thiết lập Drizzle ORM cho một Hono Worker rất nhỏ, đồng thời luôn so sánh với cách nghĩ quen thuộc trong Express để bạn dễ map hơn.

Trước hết, cần cài những package nào?

Để cài đặt Drizzle, package.json cần được thêm các package và scripts sau:
Ý nghĩa của từng phần:
drizzle-orm: thư viện ORM dùng trong code ứng dụng
drizzle-kit: công cụ CLI generate migration từ schema (giống Sequelize CLI)
db:generate: sinh SQL migration từ schema TypeScript
db:types: generate type cho Worker bindings
So với Express:
Express thường thêm driver như pg, mysql2, better-sqlite3, hoặc adapter riêng của ORM
Với Cloudflare D1, bạn không tự mở connection bằng URL database như trong Node
Thay vì cấu hình host, port, username, password, bạn cấu hình binding trong wrangler.toml
Đây là khác biệt đầu tiên rất quan trọng: trong Express, app “chủ động đi kết nối database”. Trong Workers, runtime “đưa database binding vào app”.

Bước 1: khai báo D1 binding trong Wrangler

Mở rộng wrangler.toml như sau:
Ở đây:
binding = "DB" là tên mà app sẽ dùng trong code, ví dụ c.env.DB
database_name là tên database trên Cloudflare
database_idpreview_database_id dùng để Wrangler biết phải nối vào D1 nào
So với Express, phần này tương đương với việc bạn tạo DATABASE_URL hoặc một object config database. Nhưng thay vì:
thì trong Hono Worker bạn sẽ làm việc với:
Điều này khiến code chạy gần với runtime của Cloudflare hơn, nhưng cũng buộc bạn phải nghĩ theo hướng binding thay vì connection string.

Bước 2: generate type cho binding

Sau khi có D1 binding, sẽ có thêm 2 lớp type. Tại sao cần 2 lớp type?
Hãy hiểu theo cách này: Wrangler và Hono là 2 thế giới khác nhau, chúng không tự biết về nhau.

Type do Wrangler generate

worker-configuration.d.ts là file Wrangler tự sinh ra để TypeScript biết rằng môi trường runtime của Cloudflare (c.env) có tồn tại binding tên DB:
Đây là type được sinh ra từ lệnh:
File này nói với TypeScript toàn cục: "Ở tầng Cloudflare Worker, có một thứ tên DB."
Nhưng Hono không đọc file này. Hono là một framework độc lập — nó không quan tâm đến Cloudflare hay Wrangler.

Type app tự định nghĩa

Hono dùng generic type để biết app của bạn có những gì.
src/types/env.ts
Type này nhìn hơi nhỏ, nhưng rất quan trọng. Nó là cầu nối để Hono hiểu Bindings của app.
Hiểu đơn giản như sau:
worker-configuration.d.ts → nguồn (Cloudflare định nghĩa D1Database là gì)
AppBindings → bạn dùng D1Database đó để khai báo
Hono<{ Bindings: AppBindings }> → Hono đọc và gán vào c.env
Khi bạn truyền AppBindings vào đây, Hono mới hiểu rằng: "Trong c.env sẽ có DB kiểu D1Database."
Nếu không có bước này, khi bạn gõ c.env.DB, TypeScript sẽ báo lỗi vì Hono không biết c.env có gì bên trong.

Lưu ý: Type do Wrangler thực tế sẽ tạo ra

Khi chạy npm run db:types không chỉ sinh type cho D1 binding — nó sinh type cho toàn bộ môi trường Cloudflare Worker của bạn.
Nhìn vào file thực tế, phần binding của bạn chỉ có vài dòng đầu:
Phần còn lại — hàng nghìn dòng bên dưới — là runtime types của toàn bộ môi trường Cloudflare Workers. Đây là các khai báo cho mọi thứ như DOMException, WebSocket, ReadableStream, D1Database, R2Bucket, KVNamespace, tất cả các AI model types, v.v.
Wrangler sinh ra file này không phải chỉ để mô tả binding của bạn — mà để thay thế hoàn toàn TypeScript lib mặc định (vốn được thiết kế cho trình duyệt, không phải Workers). Không có file này, TypeScript sẽ không biết D1Database là gì, fetch trong Workers hoạt động ra sao, hay ExecutionContext là cái gì.
Nói cách khác:
Phần bạn quan tâm chỉ là phần đầu. Phần còn lại là “nền tảng” mà Cloudflare cần inject để TypeScript hiểu đúng môi trường Workers thay vì môi trường browser.

Bước 3: tạo schema Drizzle

Phần trung tâm nhất của commit 4 là src/db/schema.ts:
Schema này khai báo bảng sitemap_entries với 3 cột:
id: khóa chính
url: bắt buộc và unique
lastmod: bắt buộc
Nếu bạn quen Express + Sequelize, ý tưởng không lạ:
bạn mô tả shape của bảng
ORM dùng shape đó để hỗ trợ query và type
Nhưng có một khác biệt nhẹ:
với Drizzle, schema trông rất gần với SQL thật
bạn không viết class model theo phong cách Active Record: nghĩa là bạn không tạo một class Model có sẵn các method như findAll() hay save() như nhiều ORM quen thuộc trong Express
bạn không gọi kiểu SitemapEntry.findAll()
Thay vào đó, bạn định nghĩa schema riêng, rồi viết query function riêng. Đây là phong cách rất hợp với project nhỏ cần sự rõ ràng.

Ghi chú về phong cách Active Record

Có nghĩa là với Drizzle trong project này, chúng ta không tạo một class kiểu SitemapEntry rồi gắn method trực tiếp lên class đó như:
Đó là kiểu gần với Active Record: model vừa đại diện cho dữ liệu, vừa tự chứa luôn logic truy vấn/ghi dữ liệu.
Còn trong repo này, mình đang làm theo kiểu khác:
schema.ts chỉ mô tả cấu trúc bảng
queries/sitemapQuery.ts chứa các hàm query như getSitemapEntries()
Tức là “model” không phải một class biết tự findAll() hay save().
Logic query được tách ra thành các function riêng.
Nếu so với Express:
Sequelize thường làm bạn nghĩ theo hướng gần Active Record hơn
Drizzle trong repo này gần kiểu query builder / data mapper hơn, tức là tách schema và query ra riêng

Bước 4: tạo database client cho D1

Thêm src/db/client.ts:
Ý nghĩa rất đơn giản:
getDb là một hàm
hàm này nhận vào dbBinding
Phần : AppBindings['DB'] chỉ là type annotation của TypeScript, để nói rằng: tham số dbBinding phải có cùng kiểu với DB trong AppBindings
dbBinding chính là c.env.DB từ Cloudflare Worker.
rồi truyền dbBinding cho drizzle(...)
đưa D1 cho Drizzle xử lý để mình không phải làm việc trực tiếp với raw D1 API nữa
kết quả trả về là một Drizzle database client để mình query tiếp

Bước 5: tạo query method thay cho model kiểu Express

Commit 4 xóa src/models/sitemapModel.ts và thay bằng src/queries/sitemapQuery.ts:
Đây là chỗ nên dừng lại một chút, vì nó thể hiện rõ triết lý khác với Express.
Trong Express, nhiều người quen với:
model class có method tĩnh như findAll
service gọi model
controller gọi service
Ở repo này, tác giả chọn:
db/schema.ts để mô tả bảng
queries/ để chứa các method đọc/ghi cụ thể
controller gọi trực tiếp query function
Cách này có vài lợi ích:
ít abstraction hơn
dễ đọc hơn cho người mới học TypeScript
query nằm gần schema nhưng không trộn vào controller
Nếu so với Express theo phong cách Mongoose hoặc Sequelize, đây là sự khác biệt khá lớn. Bạn không thao tác với “model instance” nữa, mà thao tác với “query function + schema object”.
Đó là lý do tại sao tên folder cũng thay đổi
Sequelize: config, model
Drizzle ORM: schema (có client.ts, sitemapSchema.ts), queries

Bước 6: cập nhật Hono app để hiểu Bindings

Khi đã có D1 binding, src/index.ts, src/routes/siteRoutes.ts và controller đều cần type hóa theo AppBindings.
src/index.ts:
src/routes/siteRoutes.ts:
src/controllers/siteController.ts:
và ở route sitemap:
Đây là khác biệt rất rõ với Express:
Express hay truyền req, res
nếu cần database, bạn thường import thẳng db từ module khác
hoặc gắn thêm property vào req
Còn ở Hono Worker:
c.env.DB là nguồn dữ liệu runtime
type của c.env được gắn trực tiếp vào app qua Bindings
Nói ngắn gọn:
Express quen với “import db trực tiếp”
Hono Worker quen với “lấy binding từ context”

Bước 7: cấu hình Drizzle Kit để generate migration

Thêm file drizzle.config.ts để cấu hình Drizzle CLI

File này nói cho Drizzle biết:
schema nằm ở đâu
migration SQL sẽ được ghi ra thư mục nào
database dialect là sqlite vì D1 dựa trên SQLite
So với Express, phần này không khác nhiều về ý tưởng. Dù là Express hay Hono, bạn vẫn cần một nơi để ORM biết schema và nơi output migration. Khác biệt nằm ở runtime dùng migration đó như thế nào.

Chuẩn hóa migration bằng wrangler d1 migrations apply

Cập nhật package.json scripts mới:
Khác biệt là gì?
migrations apply là flow có tracking migration, biết file nào đã apply rồi
nó phù hợp để hướng dẫn ngay từ đầu, vì người đọc sẽ đi theo đúng quy trình schema evolution của D1
execute --file chỉ nên giữ cho seed hoặc các tác vụ SQL chủ đích, không nên dùng để dạy migration chuẩn
Wrangler cũng được cấu hình thêm trong wrangler.toml:
Nghĩa là D1 sẽ dùng bảng d1_migrations để ghi nhận migration nào đã được chạy.

Quy trình db migration chuẩn

1. chạy npm run db:generate để để sinh ra migration file
2. viết migration schma SQL
3. dùng wrangler d1 migrations apply để apply migration
4. tạo seed file
Cấu hình để 2 file ở đâu?
migration để ở migrations/
seed để ở seeds/
5. seed dữ liệu mẫu sau khi schema đã sẵn sàng

Cách khác quản lý db client: Controller tạo db, rồi truyền db vào query

Controller:
Cách này hợp nếu bạn muốn:
tách rõ “khởi tạo db” và “chạy query”
dễ reuse nhiều query trong cùng một request
dễ test hơn vì query function không phụ thuộc trực tiếp vào binding
Mình khuyên dùng cách 2 nếu project sẽ lớn dần. Nó rõ trách nhiệm hơn:
controller lấy dependency từ runtime
query function chỉ làm query
Nếu project vẫn rất nhỏ và bạn ưu tiên ít code, cách 1 vẫn ổn.
Nếu muốn, mình có thể sửa luôn bài viết để dùng cách 2, vì nó cũng dễ giải thích hơn khi so sánh với Express.

Vậy toàn bộ flow cấu hình Drizzle cho project này là gì?

Nếu bạn muốn dựng lại theo đúng tinh thần commit 4 và 5, thứ tự nên là:

1. Cài package

2. Tạo D1 database

Sau đó copy database_id thật vào wrangler.toml.

3. Khai báo binding trong wrangler.toml

4. Generate Worker types

5. Tạo app binding type

6. Tạo schema

7. Tạo Drizzle config

8. Tạo DB client

9. Tạo query method

10. Generate và apply migration

11. Seed dữ liệu

12. Gọi query từ controller

Đây là flow tối thiểu nhưng đã khá sạch và bền để đi tiếp.
Nếu muốn viết package.json theo đúng chuẩn ngay từ đầu, phần scripts nên ở dạng này:

Những khác biệt quan trọng với Express cần luôn nhớ

Đây là phần mình nghĩ đáng nhớ nhất nếu bạn đang chuyển từ Express sang Hono Worker:
Express thường tự tạo database connection từ env vars; Hono Worker nhận database binding từ c.env.
Express hay import db như một singleton cấp ứng dụng; Hono Worker trong repo này tạo Drizzle client từ binding ngay lúc cần dùng.
Express hay dùng model class hoặc service layer dày hơn; repo này dùng schema + query function, mỏng và dễ đọc hơn.
Express migration thường do Prisma, Knex, Sequelize CLI, TypeORM quản lý; Hono + D1 ở đây dùng drizzle-kit để generate và wrangler d1 migrations apply để track/apply.
Express hay dựa nhiều vào process.env; Hono trên Workers cần type hóa Bindings để TypeScript hiểu c.env.DB.
Đừng nhét query SQL trực tiếp vào controller nếu muốn code dễ mở rộng; cứ giữ nó trong queries/. Nếu app lớn hơn, bạn có thể thêm write methods như createSitemapEntry, updateSitemapEntry, nhưng vẫn nên giữ schema, client và query method tách biệt.
Nếu ghi nhớ 5 điểm này, bạn sẽ đỡ bị “mang nguyên tư duy Express” sang Workers rồi thấy mọi thứ hơi lệch.

Kết luận

Phiên bản mới youhono thực ra làm một việc rất rõ ràng: biến một Hono Worker đang đọc JSON thành một app có database thực sự, có schema bằng TypeScript, có query typed, có migration, có seed, và có quy trình local/remote sạch hơn.
Nếu so với Express, khái niệm cốt lõi không thay đổi nhiều. Bạn vẫn có ORM, schema, method và migration. Thứ thay đổi là cách runtime cung cấp database cho app, và cách Hono type hóa binding qua c.env.
Đó là nền tảng rất tốt để đi tiếp sang các bước sau như viết method ghi dữ liệu, thêm validation, thêm auth, hoặc mở rộng schema sang nhiều bảng hơn. Nếu trước đây bạn quen Express, hãy xem đây là cùng một bài toán ORM, nhưng được giải theo phong cách Cloudflare Workers: gọn hơn, sát runtime hơn, và dựa nhiều hơn vào bindings thay vì connection server truyền thống.


Want to print your doc?
This is not the way.
Try clicking the ··· in the right corner or using a keyboard shortcut (
CtrlP
) instead.