Tool nội bộ một file — playbook cho builder thực chiến

Tool nội bộ một file — playbook cho builder thực chiến

Playbook giúp builder quyết định khi nào tool nội bộ chỉ cần một file HTML duy nhất, và cách dựng từ prompt đến deploy trong một buổi chiều.

Bao nhiêu tool nội bộ trong team bạn đang nằm chết vì không ai dám mở codebase ra sửa?

Mình thấy pattern này lặp đi lặp lại ở các team Việt Nam: cần một cái converter, một cái formatter, hoặc một dashboard nhỏ — rồi ai đó dựng lên bằng Next.js, thêm database, deploy lên Vercel, setup CI/CD. Ba tháng sau, dependency lỗi thời, không ai maintain, tool chết. Trong khi đó, Simon Willison đã dùng một cách tiếp cận ngược lại để dựng hơn 150 tool — mỗi tool là một file HTML duy nhất chứa cả JavaScript và CSS, phần lớn viết bằng LLM. Ông gọi chúng là single-file HTML tool.

Đây không phải hack hay prototype tạm. Đây là một pattern triển khai hợp lệ cho một lớp bài toán cụ thể — và builder cần framework rõ ràng để phân biệt khi nào nó phù hợp.

Khi nào single-file là đủ?

Không phải tool nào cũng nên nhét vào một file. Bảng phân loại nhanh:

| Tiêu chí | Single-file HTML | Full-stack app |
|---|---|---|
| Xử lý dữ liệu phía client (convert, format, preview) | ✅ | Overkill |
| Cần auth, phân quyền, multi-user | ❌ | ✅ |
| State nhỏ, localStorage đủ giữ | ✅ | Không cần |
| Logic nghiệp vụ phức tạp, cần test suite | ❌ | ✅ |
| Cần ship trong ngày | ✅ | Khó |
| Team dưới 10 người dùng | ✅ | Tùy |

Nói thẳng ra: nếu tool không cần server, single-file là lựa chọn hợp lý hơn là dựng cả project.

Bốn nhịp: từ prompt đến URL

Nhịp 1 — Viết prompt như viết brief

Prompt cho LLM cần rõ ba thứ: input là gì, output là gì, và constraint nào không được vi phạm.

Build a single HTML file tool that:
- Takes JSON input in a textarea
- Validates and pretty-prints with 2-space indent
- Shows errors inline with line numbers
- No external dependencies, no React, no build step
- Dark theme, responsive

Luôn ghi rõ "no React" và "single HTML file". Nếu không, LLM hay đề xuất JSX — mà JSX (cú pháp mở rộng của JavaScript dùng trong React) cần build step (bước biên dịch), phá vỡ toàn bộ lợi thế của pattern này.

Nhịp 2 — Paste, mở, test

Copy output từ LLM, lưu thành file .html, mở trực tiếp bằng trình duyệt. Không cần npm install, không cần dev server. Feedback loop gần như tức thì — sửa prompt, nhận file mới, refresh trình duyệt.

Nhịp 3 — Thêm persistence nếu cần

Nếu tool cần nhớ dữ liệu giữa các lần dùng — API key, preferences, lịch sử — dùng localStorage (bộ nhớ cục bộ của trình duyệt):

localStorage.setItem('api_key', value);  // Lưu
const key = localStorage.getItem('api_key');  // Đọc

Lưu ý: localStorage nằm trên trình duyệt nên dữ liệu không đồng bộ giữa các máy. Nếu cần sync — bạn đã vượt khỏi scope single-file.

Nhịp 4 — Host tĩnh

Push file lên GitHub Pages, Cloudflare Pages, hoặc bất kỳ static hosting nào. Không cần container, không cần CI/CD pipeline (quy trình tự động build-test-deploy). Một file HTML = một URL. Nếu cần thêm library — ví dụ PapaParse cho CSV parsing — load từ CDN (mạng phân phối nội dung) thay vì cài local.

Hai kịch bản thực tế

Kịch bản 1 — Team QA so sánh JSON response

Giả sử team QA 4 người cần compare response giữa staging và production mỗi ngày. Trước đó họ dùng diff online, paste qua paste lại, mất context liên tục. Bạn viết một prompt, LLM trả về file json-diff.html — highlight khác biệt, hỗ trợ copy kết quả, chạy offline hoàn toàn. Deploy lên GitHub Pages trong 10 phút. Tool đó giờ được dùng hàng ngày mà chưa ai cần "maintain" gì.

Kịch bản 2 — Data engineer preview CSV trước khi load

Thay vì mở Excel (chậm với file lớn) hoặc viết script Python mỗi lần, bạn dựng csv-preview.html — drag & drop file, render bảng với filter và sort, tất cả chạy trong trình duyệt bằng JavaScript thuần. Nếu file nặng, thêm PapaParse từ CDN. Toàn bộ tool vẫn nằm trong một file duy nhất.

Ba cái bẫy và cách né

Bẫy 1: "Thêm React cho chuyên nghiệp"

Đây là lỗi phổ biến nhất. Bạn bắt đầu với một file chạy tốt, rồi ai đó suggest dùng React "cho dễ maintain". Nhưng React cần JSX, JSX cần build step, build step cần Node.js, Node.js cần package.json — và bạn vừa biến tool 20 phút thành project 2 ngày. Trong võ thuật, đai trắng cứ muốn thêm chiêu mới, trong khi cao thủ thắng bằng một thế đơn giản đã luyện kỹ.

Bẫy 2: Nhét secret vào source

Single-file chạy hoàn toàn phía client. Nếu bạn hardcode API key trong file rồi push lên GitHub public — key lộ ngay. Giải pháp: dùng localStorage để người dùng tự nhập key khi mở tool lần đầu, hoặc chỉ host trên mạng nội bộ.

Bẫy 3: Scope creep — feature bò dần

Tool ban đầu format JSON, rồi có người xin thêm API call, rồi auth, rồi dashboard. Scope creep (hiện tượng yêu cầu phình dần) giết tool nhanh hơn bug. Đặt ngưỡng rõ: nếu feature mới cần server-side logic, đó là tín hiệu tách tool ra thành project riêng.

Xây dojo tool cho team

Gợi ý cụ thể:

  1. Chiều nay: chọn một tác vụ lặp đi lặp lại trong team — convert, preview, validate — viết prompt, dựng tool đầu tiên
  2. Tuần này: tạo repo team-tools/ trên GitHub, mỗi tool là một file .html, ai cần thì thêm
  3. Tháng này: review tool nào được dùng thật — nếu nó cần mở rộng vượt khỏi client-side, lúc đó mới lên full-stack

Pattern này không thay thế full-stack. Nhưng đối với hàng chục tác vụ nhỏ mà team nào cũng có, một file HTML viết bằng LLM trong 20 phút đáng giá hơn một project mà không ai maintain.

Plot twist: tool tốt nhất không phải tool phức tạp nhất — mà là tool mọi người thực sự mở lên dùng mỗi ngày.

---

Bụi Wire — nghiện đọc release notes lúc 2 giờ sáng

Nguồn tham khảo