Bỏ qua

Hono — Khung Xương Lõi Của Ứng Dụng (Web Framework) 🦖

Tại Sao Chúng Ta Lại "Bơ" Express.js?

Nếu bạn đã từng học Node.js, chắc chắn bạn biết đến Express. Nó giống như "anh cả" trong làng làm web vậy. Nhưng ngặt nỗi, Express không thể chạy trên Cloudflare Workers được. Vì sao thế?

  • Express được sinh ra dành riêng cho Node.js, nó xài những "món đồ" đặc quyền của Node (như http.IncomingMessage hay http.ServerResponse). Workers thì lại không có mấy món đó.
  • Cách Express xử lý công việc không hợp với triết lý "nhanh, gọn, lẹ" của V8 Isolate trên Workers.

Hono xuất hiện như một vị cứu tinh! Nó được thiết kế dựa trên Tiêu chuẩn Web Quốc tế (Request/Response) — nghĩa là nó cực kỳ linh hoạt. Bạn mang nó lên Cloudflare Workers, Pages, Deno, Bun, hay thậm chí quay lại chạy trên Node.js nó đều "cân" được hết.


Bộ Khung Cơ Bản Trông Như Thế Nào?

import { Hono } from 'hono';
import { cors } from 'hono/cors';
import type { Env } from './types';

// Tạo app mới. Bindings là túi đồ nghề (chứa DB, bí mật...), Variables là bộ nhớ tạm
const app = new Hono<{ Bindings: Env; Variables: Variables }>();

// Chốt chặn toàn cục (Middleware) — Ai vào nhà cũng phải qua bước kiểm tra này
app.use('*', cors({
  origin: ['[https://quythuchi.pages.dev](https://quythuchi.pages.dev)', 'http://localhost:5173'], // Chỉ cho phép 2 nhà này gọi cửa
  allowHeaders: ['Authorization', 'Content-Type'],
  allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
}));

// Một tuyến đường (route) siêu đơn giản để kiểm tra xem server còn sống không
app.get('/health', (c) => c.json({ status: 'ok' }));

// Tuyến đường có tham số truyền vào (VD: /groups/123)
app.get('/groups/:groupId', async (c) => {
  const groupId = c.req.param('groupId');  // Lấy số 123 ra xài
  const group = await c.env.DB.prepare('SELECT * FROM groups WHERE id = ?')
    .bind(groupId)
    .first();

  if (!group) return c.json({ error: 'Tìm không thấy bạn ơi' }, 404);
  return c.json(group);
});

// Gói ghém lại và xuất xưởng cho Workers dùng
export default app;

Chiếc Túi Thần Kỳ c (Context Object) 🎒

Trong Hono, chữ c (viết tắt của context) là người bạn đồng hành vạn năng. Nó chứa mọi thứ bạn cần để xử lý một yêu cầu của khách hàng:

app.post('/auth/login', async (c) => {
  // 1. Nhận đồ khách gửi (Request)
  const body = await c.req.json();           // Đọc dữ liệu khách gửi lên (JSON)
  const token = c.req.header('Authorization'); // Lấy cái thẻ chứng minh từ tiêu đề
  const groupId = c.req.param('groupId');    // Lấy ID trên đường link
  const page = c.req.query('page');          // Lấy tham số kiểu ?page=2

  // 2. Lôi đồ nghề ra xài (Env bindings)
  const user = await c.env.DB.prepare('...').first(); // Chọc vào Database
  const secret = c.env.JWT_SECRET;                    // Lấy chìa khóa bí mật

  // 3. Ghi chép vào sổ tay tạm thời (Variables)
  c.set('jwtPayload', payload);              // Cất thông tin vào sổ
  const payload = c.get('jwtPayload');       // Mở sổ ra lấy thông tin xài tiếp

  // 4. Trả lời khách (Response)
  return c.json({ success: true });          // Báo thành công (mặc định là mã 200)
  return c.json({ error: 'Không thấy' }, 404); // Báo lỗi 404
  return c.text('Hello');                    // Trả về chữ bình thường
  return c.redirect('[https://example.com](https://example.com)');  // Đuổi khách sang trang web khác
});

Chuỗi Trạm Gác (Middleware Chain) 👮

Middleware nôm na là những "anh bảo vệ" đứng trước cửa phòng làm việc (route handler). Nhiệm vụ của các anh là xét giấy tờ, kiểm tra an ninh, ghi chép sổ sách trước khi cho khách gặp bạn.

// Định nghĩa một anh bảo vệ chuyên kiểm tra thẻ (JWT)
const jwtMiddleware = async (c: any, next: () => Promise<void>) => {
  const authHeader = c.req.header('Authorization');

  // Khách không đeo thẻ? Đuổi ra!
  if (!authHeader?.startsWith('Bearer ')) {
    return c.json({ error: 'Chưa đăng nhập nha!' }, 401);
  }

  // Khách có thẻ thì soi xem thẻ thật hay giả
  const token = authHeader.slice(7);
  const payload = await verifyJwt(token, c.env.JWT_SECRET);
  if (!payload) return c.json({ error: 'Thẻ hết hạn hoặc bị làm giả' }, 401);

  // Thẻ xịn! Ghi tên khách vào sổ tay (context) để lát vào trong người ta đỡ phải hỏi lại
  c.set('jwtPayload', payload);

  // Mời khách bước tiếp vào trong (gọi hàm next)
  await next();
};

Mô hình dòng chảy qua các trạm gác trong dự án này:

Khách tới gõ cửa
  → Gặp bảo vệ vòng ngoài (CORS) - Hỏi xem đến từ đâu
  → Gặp bảo vệ vòng trong (JWT) - Soi thẻ nhân viên
  → Gặp quản lý nhóm (Group Access) - Kiểm tra xem có đúng là thành viên nhóm không
  → Vào gặp người xử lý (Route handler)
  → Cầm kết quả đi ra (Response)

Cách phái bảo vệ đi canh gác

// Cách 1: Gắn thẳng bảo vệ vào trước cửa 1 phòng cụ thể
app.get('/protected', jwtMiddleware, (c) => {
  const { userId } = c.get('jwtPayload'); // Khách đã qua cửa là chắc chắn an toàn
  return c.json({ userId });
});

// Cách 2: Gắn bảo vệ canh nguyên cả một khu vực (vd: khu /groups)
app.use('/groups/*', jwtMiddleware);
app.get('/groups', listGroupsHandler);
app.post('/groups', createGroupHandler);

Chia Nhỏ Bản Đồ Đường Đi (Sub-routing) 🗺️

Đừng nhét tất cả code vào một file, nó sẽ biến thành một đống rác khổng lồ. Hãy chia nhỏ nó ra theo từng chức năng:

// File: routes/transactions.ts (Chuyên lo chuyện thu chi)
import { Hono } from 'hono';

export const transactions = new Hono<{ Bindings: Env; Variables: Variables }>();

transactions.get('/', async (c) => {
  // Thực chất là đường dẫn: GET /groups/:groupId/transactions
  const { results } = await c.env.DB.prepare(
    'SELECT * FROM transactions WHERE group_id = ? ORDER BY date DESC'
  ).bind(c.req.param('groupId')).all();
  return c.json(results);
});

transactions.post('/', async (c) => {
  // Thực chất là: POST /groups/:groupId/transactions
  const body = await c.req.json();
  // ... xử lý lưu vào DB
});
// File: index.ts — Nơi lắp ráp các mảnh bản đồ lại với nhau
import { transactions } from './routes/transactions';

// Bấm nút gắn toàn bộ khu vực "thu chi" vào sau "nhóm"
app.route('/groups/:groupId/transactions', transactions);
// Thế là xong, gọn gàng và sạch sẽ!

Trạm Gác "Quản Lý Nhóm" — Một Ví Dụ Thực Tế 🕵️‍♂️

Đây là anh bảo vệ kỹ tính nhất trong dự án của chúng ta. Anh này không chỉ coi thẻ, mà còn phải mở sổ cái (Database) ra dò xem khách có đúng là người trong nhóm không:

const groupAccessMiddleware = async (c: any, next: () => Promise<void>) => {
  // 1. Phải vượt qua trạm kiểm tra thẻ (JWT) trước đã
  await jwtMiddleware(c, async () => {
    const groupId = c.req.param('groupId');
    const { userId } = c.get('jwtPayload') as JwtPayload;

    // 2. Chạy vào kho hồ sơ (Database) xem người này có trong nhóm không
    const member = await c.env.DB.prepare(
      `SELECT id, role FROM group_members WHERE group_id = ? AND user_id = ?`
    ).bind(groupId, userId).first() as { id: string; role: string } | null;

    // 3. Không có tên trong sổ? Trục xuất!
    if (!member) {
      return c.json({ error: 'Bạn không phải thành viên của nhóm này' }, 403);
    }

    // 4. Có tên! Ghi chú lại chức vụ (Admin hay Member) để lát nữa xử lý tiếp
    c.set('groupMember', { id: member.id, role: member.role });

    // Mời đi tiếp
    await next();
  });
};

Tổng Kết: Bảng So Sánh Tại Sao Lại Chọn Hono? 🏆

Tính Năng Hono 🦖 Express 🚂 Fastify 🚀
Chạy được trên Workers ✅ Mượt mà ❌ Thua ❌ Bó tay
Xài Tiêu Chuẩn Web ✅ Có ❌ Không ❌ Không
Hỗ trợ TypeScript ✅ Từ trong trứng nước Tạm tạm ✅ Tốt
Độ nặng của thư viện Siêu nhẹ (~14KB) Khá nặng (~200KB) Nặng đô (~300KB)
Tốc độ xử lý Nhanh như chớp Trung bình Rất nhanh
Cộng đồng / Hệ sinh thái Đang lớn nhanh như thổi Lão làng, khổng lồ Lớn

Nói tóm lại, nếu bạn chơi hệ Cloudflare Workers, Hono chính là "chân ái"!