---
id: programming-nodejs
title: "Node.js cho CLI nội bộ"
shortTitle: "Node.js"
track: programming
trackTitle: "Programming cho DevOps"
order: 2
level: core
status: published
estimatedMinutes: 40
prerequisites:
  - programming-python
next: programming-go
reviewedAgainst: 2026-10-12
sources:
  - title: "Node.js v24 child_process"
    url: https://nodejs.org/docs/latest-v24.x/api/child_process.html
  - title: "Node.js v24 fetch"
    url: https://nodejs.org/docs/latest-v24.x/api/globals.html
  - title: "process.exitCode"
    url: https://nodejs.org/api/process.html#processexitcode
---

# Node.js cho CLI nội bộ

Lab dùng Node 24, cùng mốc với bài GitHub Actions. File `.mjs` chạy được mà không cần `package.json`.

## Để làm gì

Nếu bạn đã quen JavaScript, bạn có thể viết một CLI ngắn vừa gọi HTTP API vừa chạy lệnh hệ thống mà không cần học thêm ngôn ngữ. Bài này không cài dependency nào: `fetch` và `child_process` đều có sẵn trong Node.

## Gồm những gì

- `fetch` global, có từ Node 18, docs v24 mô tả implementation dựa trên Undici.
- `execFile` trong `node:child_process`: chạy executable trực tiếp, không mở shell.
- `spawn` với `stdio: "inherit"` khi cần thấy log của process con ngay lúc nó in.
- `process.exitCode` thay cho `process.exit()`.

Nếu script chủ yếu xử lý file và text thì Python vẫn hợp hơn. Nếu cần một binary chạy không kèm runtime thì chọn Go. Bài này không dùng Express, cũng không dùng axios.

## Dùng khi nào

Tình huống điển hình: team fullstack đã có Node trên máy CI, và cần một tool đọc JSON, gọi API nội bộ rồi in kết quả. Viết cùng ngôn ngữ với app thì người trực bug lúc nửa đêm vẫn đọc được tool.

Đừng kéo framework vào một file 40 dòng. Đừng truyền input người dùng vào `exec` hoặc vào `execFile` khi bật option `shell`. Docs child_process nói rõ: shell bật lên thì metacharacter trong input có thể chạy lệnh khác.

## Kiến thức cốt lõi

`fetch(url)` trả `Response`. `response.ok` là true khi status từ 200 đến 299. `response.json()` parse body, vì API vận hành thường trả JSON. Lưu ý: status 404 vẫn là một response hợp lệ nên `fetch` không ném exception. Bạn phải tự kiểm tra `response.ok` hoặc `response.status`.

`execFile(file, args)` spawn đúng file đó. Argument là mảng và không đi qua shell, cùng lý do bài Python dùng list cho `subprocess.run`. Bản promisify trả `{ stdout, stderr }`. Output bị giữ trong buffer đến khi process con thoát, nên đừng dùng cho lệnh in log rất dài.

`spawn(file, args, { stdio: "inherit" })` nối stdin, stdout, stderr của process con vào process hiện tại, nên log hiện dần ra terminal. Muốn biết lệnh thành công hay không thì nghe event `close` và đọc `code`.

`process.exit(1)` cắt process ngay, nên stdout có thể chưa kịp flush. Nếu bạn gán `process.exitCode = 1` rồi return, Node sẽ thoát với mã đó sau khi xử lý xong hàng đợi I/O. Docs process mô tả field này.

## Lệnh và code mẫu

`check.mjs`:

```javascript
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

const url = process.argv[2] ?? "https://example.com";
const response = await fetch(url);
if (!response.ok) {
  console.error(`${url} returned ${response.status}`);
  process.exitCode = 1;
} else {
  console.log(`${url} ${response.status}`);
}

const { stdout } = await execFileAsync("node", ["--version"]);
console.log(`node ${stdout.trim()}`);
```

Xem log của một lệnh dài, không buffer hết:

```javascript
import { spawn } from "node:child_process";

const child = spawn("npm", ["ci"], { stdio: "inherit" });
child.on("close", (code) => {
  process.exitCode = code ?? 1;
});
```

`npm ci` chỉ là ví dụ. Trong lab chưa có `package.json` thì đừng chạy đoạn này.

## Thực hành

1. `node --version` phải là 24.
2. Lưu `check.mjs` trong `$HOME/devops-lab/programming`.
3. `node check.mjs` và `echo $?`. example.com trả 200, exit code 0.
4. `node check.mjs https://example.com/does-not-exist`. example.com trả 404 cho path lạ. Exit code phải là 1.
5. Tạm bỏ đoạn `if (!response.ok)` rồi chạy lại URL 404. Bạn sẽ thấy status in ra nhưng exit code vẫn là 0. Xong thì trả lại đoạn kiểm tra.

## Kết quả mong đợi

- URL sống: một dòng status 200, một dòng version Node, exit code 0.
- URL 404: dòng lỗi trên stderr, exit code 1, không có thêm stack trace vì đây không phải exception.
- Không có `node_modules` mới.

## Checklist

- [ ] Dùng `fetch` có sẵn, không cài HTTP client cho một request.
- [ ] Kiểm tra `response.ok` hoặc status, không chỉ chờ `fetch` không ném lỗi.
- [ ] Chạy executable bằng `execFile` hoặc `spawn` với argument dạng mảng.
- [ ] Dùng `stdio: "inherit"` khi cần stream log.
- [ ] Gán `process.exitCode` thay vì gọi `process.exit` giữa chừng.

## Lỗi thường gặp

**Coi `fetch` reject là HTTP lỗi.** `fetch` reject khi mạng đứt hoặc DNS fail. HTTP 500 vẫn resolve. Không check `response.ok` thì tool báo thành công trong lúc API đang lỗi.

**`exec("git status " + branch)`.** Chuỗi này đi qua shell, nên nếu tên branch có `;` thì phần sau dấu đó chạy thành lệnh riêng. Dùng `execFile("git", ["status", branch])`.

**`process.exit(1)` ngay sau `console.log` trong script lớn.** Nếu còn handle đang ghi, dòng log cuối có thể mất. Gán `process.exitCode` rồi để function trả về.

**Dựng project Express chỉ để probe một URL.** Bạn tốn thêm thời gian cài và có thêm một server không ai cần. Một file `.mjs` là đủ.

## Nguồn

1. [child_process](https://nodejs.org/docs/latest-v24.x/api/child_process.html) — Node.js v24, `execFile` không qua shell, `spawn` và `stdio`.
2. [Global objects — fetch](https://nodejs.org/docs/latest-v24.x/api/globals.html) — `fetch` global.
3. [process.exitCode](https://nodejs.org/api/process.html#processexitcode) — mã thoát sau khi process kết thúc bình thường.

## Tiếp theo

[Go cho CLI một binary](./03-go.md)
