Vertical Slice Architecture trên Cloudflare Workers — tại sao không phải NestJS?
Vertical Slice Architecture trên Cloudflare Workers — tại sao không phải NestJS?
Khi bắt đầu xây dựng edu-ai-platform — một nền tảng học từ vựng/ngôn ngữ chạy hoàn toàn trên Cloudflare — câu hỏi đầu tiên tôi phải trả lời không phải là "dùng framework nào", mà là: môi trường runtime này có đặc điểm gì, và architecture nào phù hợp với nó nhất?
Sau một thời gian làm việc với NestJS ở các dự án trước, lựa chọn của tôi là Hono + Vertical Slice Architecture. Bài viết này giải thích tại sao.
Vấn đề với NestJS trên Cloudflare Workers
NestJS là một framework tuyệt vời. Tôi đã dùng nó cho edu-manager — chạy trên Node.js + PostgreSQL + AWS — và nó rất phù hợp ở đó.
Nhưng Cloudflare Workers không phải Node.js.
Workers chạy trên V8 Isolates, một runtime cực kỳ giới hạn:
- Không có
fs, không cónet, không cóprocess - Cold start tính bằng milliseconds (không phải seconds) — mỗi byte thêm vào bundle đều có chi phí
- NestJS với Dependency Injection, Decorators, Reflection API... kéo theo hàng trăm KB overhead
reflect-metadata— thứ NestJS phụ thuộc nặng — không hoạt động trong Workers environment
Về mặt kỹ thuật, có thể workaround một số thứ, nhưng bạn đang đi ngược lại với thiết kế của cả hai hệ thống. Đó là dấu hiệu rõ ràng: NestJS không phải công cụ cho bài toán này.
Tại sao Hono?
Hono được thiết kế từ đầu cho Edge Runtime:
- Bundle size ~15KB (so với NestJS ~300KB+)
- Hỗ trợ native: Cloudflare Workers, Deno, Bun, Node.js
- TypeScript-first, API tường minh, không magic
- Middleware pattern quen thuộc với bất kỳ ai đã dùng Express/Koa
// Hono trên Workers — đơn giản và thẳng thắn
import { Hono } from 'hono'
const app = new Hono<{ Bindings: CloudflareBindings }>()
app.get('/api/health', (c) => c.json({ status: 'ok' }))
export default app
Không có decorator, không có IoC container, không có bootstrapping ceremony. Chỉ là code.
Vertical Slice Architecture là gì?
Thay vì tổ chức code theo layer (controllers/, services/, repositories/), Vertical Slice Architecture tổ chức theo feature (tính năng).
Cấu trúc layer truyền thống (Layered Architecture):
src/
controllers/
vocabulary.controller.ts
lesson.controller.ts
services/
vocabulary.service.ts
lesson.service.ts
repositories/
vocabulary.repository.ts
lesson.repository.ts
Khi bạn cần thêm một tính năng mới, bạn phải chạm vào ít nhất 3 file ở 3 folder khác nhau. Với dự án lớn, điều này tạo ra coupling ẩn giữa các tính năng không liên quan.
Vertical Slice (theo feature):
src/
features/
vocabulary/
add-vocabulary.ts # handler + logic + types trong 1 file
get-vocabulary-list.ts
update-vocabulary.ts
lesson/
create-lesson.ts
get-lesson-progress.ts
shared/
db.ts
auth.ts
Mỗi "slice" là một feature hoàn chỉnh theo chiều dọc: từ HTTP handler → business logic → data access, tất cả trong cùng một chỗ.
Vertical Slice phù hợp với Workers như thế nào?
Đây là điểm thú vị: Workers có một đặc tính gọi là isolation per request. Mỗi request chạy trong isolate riêng, không có shared memory giữa các request (trừ khi bạn dùng Durable Objects).
Điều này có nghĩa là:
- Không có singleton service tồn tại xuyên suốt lifecycle của app
- Không cần DI container để quản lý dependency lifecycle
- Mỗi request thực sự là stateless by default
Vertical Slice khớp hoàn hảo với mô hình này. Thay vì một service class được inject vào nhiều nơi, mỗi feature tự chứa logic của nó:
// features/vocabulary/add-vocabulary.ts
import { Context } from 'hono'
import { z } from 'zod'
const schema = z.object({
word: z.string().min(1),
meaning: z.string().min(1),
tags: z.array(z.string()).optional(),
})
export async function addVocabulary(c: Context) {
const body = await c.req.json()
const parsed = schema.safeParse(body)
if (!parsed.success) {
return c.json({ error: parsed.error.flatten() }, 400)
}
const db = c.env.DB // Cloudflare D1
const userId = c.get('userId') // từ auth middleware
await db.prepare(
'INSERT INTO vocabulary (user_id, word, meaning) VALUES (?, ?, ?)'
).bind(userId, parsed.data.word, parsed.data.meaning).run()
return c.json({ success: true }, 201)
}
Không có service layer, không có repository pattern, không có abstraction không cần thiết. Logic nằm ngay đây, dễ đọc và dễ test.
Trade-offs cần nhìn thẳng vào
Không có architecture nào hoàn hảo. Với Vertical Slice trên Workers:
Ưu điểm:
- Code locality cao — mọi thứ liên quan đến một feature nằm cùng chỗ
- Dễ onboard developer mới vào một feature cụ thể
- Không có coupling ẩn giữa các features
- Phù hợp với mô hình stateless của Workers
Nhược điểm:
- Dễ bị code duplication nếu không có shared layer được thiết kế cẩn thận
- Khi business logic phức tạp, một "slice" có thể phình to
- Cần discipline để quyết định cái gì vào
shared/, cái gì ở trong feature
Nguyên tắc tôi đang dùng: chỉ extract vào shared/ khi đã duplicate ít nhất 3 lần. Premature abstraction trong VSA đau hơn duplication ngắn hạn.
Kết
Nếu bạn đang build trên Cloudflare Workers, câu hỏi không phải là "NestJS hay Express?" mà là "runtime này cần gì?".
- Workers cần bundle nhỏ → Hono
- Workers cần stateless, isolated execution → Vertical Slice phù hợp hơn Layered Architecture
- Workers có D1, KV, Durable Objects, R2 — mỗi primitive phục vụ một mục đích cụ thể, và VSA giúp bạn dùng đúng primitive cho đúng feature mà không cần centralize tất cả vào một "database layer"
Architecture tốt nhất là architecture phù hợp với constraints của môi trường bạn đang chạy, không phải architecture bạn quen dùng nhất.
Bài viết này là một phần trong series về việc xây dựng edu-ai-platform — nền tảng học từ vựng Cloudflare-native với Hono, React + Vite, D1, KV, Durable Objects và Workers AI.