Xây Dựng Ứng Dụng Đa Nền Tảng với Deno 2.0: CLI Tools, HTTP Server và Tích Hợp npm

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Bắt Đầu Nhanh: Chạy Được Trong 5 Phút

Nếu bạn đã nghe về Deno mà chưa thử, thì Deno 2.0 chính là phiên bản khiến mọi thứ trở nên rõ ràng. Mình bắt đầu dùng khi cần chạy script TypeScript mà không muốn qua bước build. Điều bất ngờ: toàn bộ chu trình cài-và-chạy mất chưa đến ba phút, và mình không cần chạm vào tsconfig.

Cài Deno bằng một lệnh duy nhất — không cần npm, không cần node_modules:

# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh

# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex

# Hoặc dùng package manager
brew install deno          # macOS Homebrew
winget install DenoLand.Deno  # Windows

Kiểm tra cài đặt:

deno --version
# deno 2.0.x (release, ...)

Giờ chạy file TypeScript đầu tiên — không cần compiler, không cần file cấu hình:

// hello.ts
const name: string = "Deno 2.0";
console.log(`Xin chào từ ${name}!`);
deno run hello.ts
# Xin chào từ Deno 2.0!

Đơn giản vậy thôi. TypeScript chạy trực tiếp. So sánh với cách thiết lập Node + TypeScript thông thường: npm init, cài ts-node hoặc tsx, viết tsconfig.json, rồi mới chạy được file. Deno bỏ qua tất cả những bước đó.

Đi Sâu Hơn: Xây Dựng CLI Tool và HTTP Server

Xây Dựng CLI Tool

Viết script CLI là lúc Deno thực sự tỏa sáng. Đồng nghiệp của bạn không cần cài Node, npm hay bất kỳ runtime nào — bạn compile ra một file binary duy nhất rồi giao đi. Chỉ điều đó thôi đã giải quyết được rất nhiều vấn đề kiểu “chạy được trên máy mình nhưng không chạy được trên máy người khác”.

Dưới đây là CLI tool đọc file và đếm tần suất từ:

// wordcount.ts
const args = Deno.args;

if (args.length === 0) {
  console.error("Cách dùng: deno run --allow-read wordcount.ts <tên-file>");
  Deno.exit(1);
}

const filename = args[0];
const text = await Deno.readTextFile(filename);

const words = text.toLowerCase().match(/\b\w+\b/g) ?? [];
const freq: Record<string, number> = {};

for (const word of words) {
  freq[word] = (freq[word] ?? 0) + 1;
}

const sorted = Object.entries(freq)
  .sort((a, b) => b[1] - a[1])
  .slice(0, 10);

console.log("Top 10 từ xuất hiện nhiều nhất:");
for (const [word, count] of sorted) {
  console.log(`  ${word}: ${count}`);
}
deno run --allow-read wordcount.ts README.md

Flag --allow-read là có chủ đích. Mô hình bảo mật của Deno rất rõ ràng: script không thể truy cập file, mạng hay biến môi trường trừ khi bạn cấp quyền. Ban đầu có thể thấy hơi rườm rà. Nhưng khi bạn đang chạy một script từ bên thứ ba trong môi trường production và muốn chắc chắn nó không thể gọi về nhà hay sửa filesystem của bạn, bạn sẽ thấy rất hữu ích khi có danh sách cụ thể những gì nó được phép làm.

Compile Ra File Binary Duy Nhất

Khi CLI tool chạy ổn, hãy compile thành file thực thi độc lập. Output thường nặng 60–80 MB cho một tool đơn giản — đó là do Deno runtime được đóng gói kèm theo.

# Compile cho nền tảng hiện tại
deno compile --allow-read wordcount.ts -o wordcount

# Cross-compile cho các nền tảng khác
deno compile --target x86_64-pc-windows-msvc --allow-read wordcount.ts -o wordcount.exe
deno compile --target x86_64-apple-darwin --allow-read wordcount.ts -o wordcount-mac
deno compile --target x86_64-unknown-linux-gnu --allow-read wordcount.ts -o wordcount-linux

Đặt file binary đó lên bất kỳ máy nào — không cần cài runtime.

Xây Dựng HTTP Server

Deno 2.0 tích hợp sẵn HTTP server sẵn sàng cho production. Với nhiều trường hợp sử dụng, bạn không cần Express hay Fastify nữa:

// server.ts
const PORT = 8000;

const handler = (req: Request): Response => {
  const url = new URL(req.url);

  if (url.pathname === "/") {
    return new Response("Chào mừng đến với Deno server của tôi!", {
      headers: { "Content-Type": "text/plain" },
    });
  }

  if (url.pathname === "/health") {
    return Response.json({ status: "ok", timestamp: new Date().toISOString() });
  }

  if (url.pathname === "/echo" && req.method === "POST") {
    const body = req.body;
    return new Response(body, {
      headers: { "Content-Type": req.headers.get("Content-Type") ?? "text/plain" },
    });
  }

  return new Response("Không tìm thấy", { status: 404 });
};

console.log(`Server đang chạy tại http://localhost:${PORT}`);
Deno.serve({ port: PORT }, handler);
deno run --allow-net server.ts

# Kiểm tra
curl http://localhost:8000/health
# {"status":"ok","timestamp":"2026-05-21T10:00:00.000Z"}

Handler sử dụng các đối tượng RequestResponse chuẩn — chính xác là những Web API mà trình duyệt của bạn cung cấp. Nếu bạn đã từng viết code với Fetch API, điều này sẽ cảm thấy quen thuộc ngay lập tức.

Nâng Cao: Tích Hợp Gói npm

Dùng Gói npm Trực Tiếp

Deno 2.0 xem npm là công dân hạng nhất. Bạn có thể import gói bằng specifier npm:, không cần thư mục node_modules:

// Dùng gói npm với specifier npm:
import express from "npm:express@4";
import chalk from "npm:chalk@5";

const app = express();

app.get("/", (req, res) => {
  console.log(chalk.green("Nhận được request!"));
  res.json({ message: "Xin chào từ Deno + Express!" });
});

app.listen(3000, () => {
  console.log(chalk.blue("Server đang chạy trên cổng 3000"));
});
deno run --allow-net --allow-read --allow-env app.ts

Thiết Lập File Cấu Hình deno.json

Các dự án thực tế sẽ hưởng lợi từ file deno.json — nó tập trung các alias import và task script, tương tự package.json:

{
  "imports": {
    "@std/fs": "jsr:@std/fs@1",
    "@std/path": "jsr:@std/path@1",
    "zod": "npm:zod@3",
    "chalk": "npm:chalk@5"
  },
  "tasks": {
    "start": "deno run --allow-net --allow-read server.ts",
    "dev": "deno run --watch --allow-net --allow-read server.ts",
    "build": "deno compile --allow-net --allow-read server.ts -o myserver",
    "test": "deno test --allow-read"
  },
  "compilerOptions": {
    "strict": true
  }
}

Chạy task giống như dùng npm scripts:

deno task dev    # phát triển với hot reload
deno task build  # compile ra binary
deno task test   # chạy test

Ví Dụ Thực Tế: API Có Validation với Zod

Dưới đây là server hoàn chỉnh hơn sử dụng Zod để validate đầu vào — gần với những gì bạn thực sự triển khai:

// api.ts
import { z } from "zod";

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().min(18),
});

const handler = async (req: Request): Promise<Response> => {
  if (req.method === "POST" && new URL(req.url).pathname === "/users") {
    try {
      const body = await req.json();
      const user = UserSchema.parse(body);
      // Trong ứng dụng thực tế: lưu vào database ở đây
      return Response.json({ success: true, user }, { status: 201 });
    } catch (err) {
      if (err instanceof z.ZodError) {
        return Response.json(
          { success: false, errors: err.errors },
          { status: 400 }
        );
      }
      return Response.json({ success: false, message: "JSON không hợp lệ" }, { status: 400 });
    }
  }

  return new Response("Không tìm thấy", { status: 404 });
};

Deno.serve({ port: 8000 }, handler);
# Request hợp lệ
curl -X POST http://localhost:8000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "[email protected]", "age": 25}'

# Request không hợp lệ — Zod bắt lỗi
curl -X POST http://localhost:8000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "", "email": "not-an-email", "age": 15}'

Mẹo Thực Tế

Dùng –watch Khi Phát Triển

Flag --watch tự động restart script khi file thay đổi. Không cần nodemon, không cần cài thêm gì:

deno run --watch --allow-net server.ts

Hiểu Mô Hình Phân Quyền Từ Sớm

Trong giai đoạn đầu phát triển, dùng --allow-all để không phải đoán mò từng flag. Sau đó hãy giới hạn lại trước khi triển khai:

# Phát triển: mở toàn bộ quyền
deno run --allow-all server.ts

# Production: chỉ cấp quyền cần thiết
deno run --allow-net=:8000 --allow-read=./data server.ts

Testing Tích Hợp Sẵn

Deno tích hợp sẵn test runner. Không cần Jest, Mocha hay file cấu hình:

// wordcount_test.ts
import { assertEquals } from "jsr:@std/assert";

Deno.test("đếm từ chính xác", () => {
  const text = "hello world hello";
  const words = text.match(/\b\w+\b/g) ?? [];
  const freq: Record<string, number> = {};
  for (const word of words) freq[word] = (freq[word] ?? 0) + 1;
  assertEquals(freq["hello"], 2);
  assertEquals(freq["world"], 1);
});
deno test wordcount_test.ts

Cache Dependencies Để Dùng Offline

Deno tải dependencies lần đầu chạy và cache lại ở local. Với CI pipeline hoặc môi trường hạn chế mạng, hãy cache trước toàn bộ:

# Cài và cache tất cả dependencies từ deno.json
deno install

# Hoặc cache một file entry cụ thể với lockfile
deno cache --lock=deno.lock server.ts

Triển Khai Lên Production

File binary đã compile chạy được trên bất kỳ Linux server nào — máy đích không cần cài Deno:

# Build
deno compile --target x86_64-unknown-linux-gnu \
  --allow-net --allow-read \
  server.ts -o myserver

# Triển khai
scp myserver user@your-server:/opt/myapp/
ssh user@your-server '/opt/myapp/myserver'

Ngoài ra, bạn có thể push lên GitHub và để Deno Deploy lo phần còn lại — không cần cấu hình server, không cần Docker, chỉ cần một URL trực tiếp.

Nếu bạn đến từ Node.js, hãy tính khoảng một tuần để cảm thấy thoải mái hoàn toàn. Hệ thống phân quyền rõ ràng ban đầu có vẻ phiền phức. Nhưng sau vài ngày nó sẽ trở thành phản xạ tự nhiên, và bạn sẽ bắt đầu trân trọng việc mỗi script bạn xem qua đều có khai báo rõ ràng về những gì nó có thể truy cập. Bắt đầu nhỏ — xây một CLI tool nội bộ. Chỉ một dự án đó thôi sẽ dạy bạn 80% những gì cần biết về cách Deno vận hành.

Share: