📘 Phần 2 · Lập Trình Tools · Chương 5

JavaScript / Node.js Tools với AI

Xây dựng CLI tools mạnh mẽ, automation scripts và utility packages bằng JavaScript/Node.js — với sức mạnh của AI trong VS Code.

6 giờ học
📝 3 bài học
🎯 2 dự án mini
📊 Mức độ: Trung cấp

🎯 Mục tiêu học tập

  • Hiểu Node.js module system và npm ecosystem
  • Xây dựng CLI tool với Commander.js và Chalk
  • Làm việc với file system, processes, và streams trong Node.js
  • Tạo automation script chạy được bằng lệnh npm
1

Bài 5.1 — Node.js Fundamentals

Event Loop
Cơ chế Node.js xử lý I/O bất đồng bộ trên single thread. Callbacks, Promises, async/await đều chạy qua event loop — không block nhau.
npm (Node Package Manager)
Package manager mặc định của Node.js. npm install, npm run script. Có hơn 2 triệu packages. Dependencies lưu trong node_modules/.
CommonJS vs ESM
CommonJS: require() / module.exports — cũ, vẫn phổ biến. ESM: import / export — chuẩn mới. Dùng "type": "module" trong package.json để bật ESM.
Buffer / Stream
Buffer: vùng nhớ tạm xử lý binary data. Stream: xử lý data theo từng chunk (không load toàn bộ vào RAM). Dùng khi xử lý file lớn.

Built-in Modules quan trọng nhất

ModuleCông dụngAPI thường dùng
fsFile system operationsreadFile, writeFile, mkdir, readdir, stat, watch
pathXử lý đường dẫn filejoin, resolve, dirname, basename, extname, parse
osThông tin hệ điều hànhhomedir, platform, cpus, totalmem, tmpdir
httpHTTP server/client thôcreateServer, request, get (Express wrap cái này)
cryptoMã hóa, hashcreateHash, randomBytes, createHmac
eventsEvent emitter patternEventEmitter, on, emit, once, removeListener
child_processChạy command ngoàiexec, execSync, spawn

Node.js là gì và tại sao dùng cho tooling?

Node.js cho phép chạy JavaScript bên ngoài browser. Lý do Node.js phổ biến cho tooling:

  • JavaScript là ngôn ngữ developer biết nhiều nhất
  • npm có hơn 2 triệu packages — nhiều nhất thế giới
  • Async I/O nhanh — phù hợp file manipulation và network calls
  • Cùng ngôn ngữ với frontend — không cần học thêm

npm / package.json

bash
# Tạo project mới
mkdir my-node-tool
cd my-node-tool

# Khởi tạo package.json (trả lời các câu hỏi, hoặc -y để dùng default)
npm init -y

# Cài packages
npm install commander chalk ora

# Cài dev dependencies (chỉ dùng khi development)
npm install --save-dev nodemon jest

# Chạy script
node src/index.js

Module System — CommonJS vs ES Modules

javascript — Hai cách import/export
// CommonJS (require/module.exports) — cách cũ, vẫn phổ biến
const fs = require('fs');
const path = require('path');
module.exports = { myFunction };

// ES Modules (import/export) — cách mới, dùng trong package.json: "type": "module"
import { readFile } from 'fs/promises';
import path from 'path';
export { myFunction };

// Trong giáo trình này, ta dùng CommonJS để tương thích rộng nhất

Async/Await — Pattern chuẩn cho Node.js

javascript
const fs = require('fs').promises;
const path = require('path');

// ✅ Cách đúng: async/await với try/catch
async function readJsonFile(filePath) {
  try {
    const absolutePath = path.resolve(filePath);
    const content = await fs.readFile(absolutePath, 'utf-8');
    return JSON.parse(content);
  } catch (error) {
    if (error.code === 'ENOENT') {
      throw new Error(`File không tồn tại: ${filePath}`);
    }
    if (error instanceof SyntaxError) {
      throw new Error(`File không phải JSON hợp lệ: ${filePath}`);
    }
    throw error;
  }
}

// Sử dụng
async function main() {
  const data = await readJsonFile('./config.json');
  console.log('Dữ liệu:', data);
}

main().catch(console.error);

2

Bài 5.2 — CLI Tool với Commander.js

Commander.js là thư viện CLI phổ biến nhất cho Node.js — tương đương Click trong Python. Chalk giúp tô màu output.

bash
npm install commander chalk ora inquirer

Dự án: JSON Toolkit CLI

Tool thao tác với file JSON: validate, format, query, merge.

javascript — src/index.js
#!/usr/bin/env node
/**
 * JSON Toolkit CLI
 * Xây dựng với Node.js + Commander.js + AI
 */
const { Command } = require('commander');
const chalk = require('chalk');
const fs = require('fs').promises;
const path = require('path');

const program = new Command();

program
  .name('json-toolkit')
  .description('🛠 Công cụ thao tác JSON từ command line')
  .version('1.0.0');

// ---- COMMAND: validate ----
program
  .command('validate ')
  .description('Kiểm tra file JSON có hợp lệ không')
  .action(async (file) => {
    try {
      const content = await fs.readFile(file, 'utf-8');
      JSON.parse(content); // Throws nếu không hợp lệ
      console.log(chalk.green(`✅ File hợp lệ: ${file}`));
      const lines = content.split('\n').length;
      console.log(chalk.dim(`   ${lines} dòng, ${content.length} ký tự`));
    } catch (err) {
      if (err.code === 'ENOENT') {
        console.error(chalk.red(`❌ Không tìm thấy file: ${file}`));
      } else {
        console.error(chalk.red(`❌ JSON không hợp lệ: ${err.message}`));
      }
      process.exit(1);
    }
  });

// ---- COMMAND: format ----
program
  .command('format ')
  .description('Format (pretty-print) file JSON')
  .option('-i, --indent ', 'Số spaces indent', '2')
  .option('-o, --output ', 'File output (mặc định: ghi đè file gốc)')
  .action(async (file, options) => {
    try {
      const content = await fs.readFile(file, 'utf-8');
      const parsed = JSON.parse(content);
      const formatted = JSON.stringify(parsed, null, parseInt(options.indent));
      
      const outputFile = options.output || file;
      await fs.writeFile(outputFile, formatted + '\n', 'utf-8');
      
      console.log(chalk.green(`✅ Đã format: ${outputFile}`));
    } catch (err) {
      console.error(chalk.red(`❌ Lỗi: ${err.message}`));
      process.exit(1);
    }
  });

// ---- COMMAND: get ----
program
  .command('get  ')
  .description('Lấy giá trị theo key path (vd: user.name, items[0].id)')
  .action(async (file, keyPath) => {
    try {
      const content = await fs.readFile(file, 'utf-8');
      const data = JSON.parse(content);
      
      // Traverse key path: "user.address.city" → data.user.address.city
      const value = keyPath.split('.').reduce((obj, key) => {
        // Handle array notation: items[0]
        const match = key.match(/^(.+)\[(\d+)\]$/);
        if (match) return obj?.[match[1]]?.[parseInt(match[2])];
        return obj?.[key];
      }, data);
      
      if (value === undefined) {
        console.error(chalk.yellow(`⚠️  Key không tồn tại: ${keyPath}`));
        process.exit(1);
      }
      
      // Nếu là object, pretty print
      if (typeof value === 'object') {
        console.log(JSON.stringify(value, null, 2));
      } else {
        console.log(chalk.cyan(String(value)));
      }
    } catch (err) {
      console.error(chalk.red(`❌ Lỗi: ${err.message}`));
      process.exit(1);
    }
  });

// ---- COMMAND: merge ----
program
  .command('merge   [output]')
  .description('Merge 2 file JSON thành một')
  .option('--deep', 'Deep merge (mặc định: shallow merge)')
  .action(async (file1, file2, output, options) => {
    try {
      const [data1, data2] = await Promise.all([
        fs.readFile(file1, 'utf-8').then(JSON.parse),
        fs.readFile(file2, 'utf-8').then(JSON.parse),
      ]);
      
      const merged = options.deep
        ? deepMerge(data1, data2)
        : { ...data1, ...data2 };
      
      const result = JSON.stringify(merged, null, 2);
      
      if (output) {
        await fs.writeFile(output, result + '\n', 'utf-8');
        console.log(chalk.green(`✅ Đã merge vào: ${output}`));
      } else {
        console.log(result);
      }
    } catch (err) {
      console.error(chalk.red(`❌ Lỗi: ${err.message}`));
      process.exit(1);
    }
  });

function deepMerge(target, source) {
  const result = { ...target };
  for (const key of Object.keys(source)) {
    if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
      result[key] = deepMerge(target[key] || {}, source[key]);
    } else {
      result[key] = source[key];
    }
  }
  return result;
}

program.parse();

Thêm vào package.json để chạy dễ hơn

json — package.json
{
  "name": "json-toolkit",
  "version": "1.0.0",
  "description": "CLI tool thao tác JSON",
  "main": "src/index.js",
  "bin": {
    "jsontk": "./src/index.js"
  },
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js",
    "test": "jest"
  },
  "dependencies": {
    "chalk": "^4.1.2",
    "commander": "^11.0.0"
  }
}
bash — Chạy thử
node src/index.js --help
node src/index.js validate data.json
node src/index.js format data.json --indent 4
node src/index.js get data.json "user.name"
node src/index.js merge a.json b.json merged.json

3

Bài 5.3 — Automation Scripts với Node.js

Script tự động hóa: Project Setup Generator

Script này tự động tạo cấu trúc folder và files boilerplate cho project mới — tiết kiệm 15-30 phút setup mỗi lần.

bash
npm install inquirer chalk fs-extra
javascript — create-project.js
#!/usr/bin/env node
/**
 * Project Setup Generator
 * Tạo cấu trúc project mới tương tác
 */
const inquirer = require('inquirer');
const chalk    = require('chalk');
const fse      = require('fs-extra');
const path     = require('path');
const { execSync } = require('child_process');

// Templates cho từng loại project
const PROJECT_TEMPLATES = {
  'node-api': {
    folders: ['src', 'src/routes', 'src/middleware', 'src/models', 'tests'],
    files: {
      'src/index.js': `const express = require('express');\nconst app = express();\nconst PORT = process.env.PORT || 3000;\n\napp.use(express.json());\n\napp.get('/health', (req, res) => res.json({ status: 'ok' }));\n\napp.listen(PORT, () => console.log(\`Server running on port \${PORT}\`));\n`,
      '.env.example': 'PORT=3000\nNODE_ENV=development\nDB_URL=\n',
      '.gitignore': 'node_modules/\n.env\ndist/\n',
    },
    dependencies: ['express', 'dotenv'],
    devDependencies: ['nodemon', 'jest'],
  },
  'react-app': {
    folders: ['src', 'src/components', 'src/pages', 'src/hooks', 'public'],
    files: {
      'src/App.jsx': `function App() {\n  return 
Hello World
;\n}\nexport default App;\n`, '.gitignore': 'node_modules/\ndist/\n.env\n', }, dependencies: ['react', 'react-dom'], devDependencies: ['vite', '@vitejs/plugin-react'], }, 'python-tool': { folders: ['src', 'tests'], files: { 'src/__init__.py': '', 'src/main.py': 'import click\n\n@click.group()\ndef cli():\n """My Tool"""\n pass\n\nif __name__ == "__main__":\n cli()\n', 'requirements.txt': 'click>=8.0\nrich>=13.0\n', '.gitignore': 'venv/\n__pycache__/\n*.pyc\n.env\n', }, postSetup: 'python -m venv venv', }, }; async function main() { console.log(chalk.bold.cyan('\n🚀 Project Setup Generator\n')); const answers = await inquirer.prompt([ { type: 'input', name: 'projectName', message: 'Tên project:', validate: (val) => /^[a-z0-9-_]+$/.test(val) || 'Tên chỉ gồm chữ thường, số, - và _', }, { type: 'list', name: 'template', message: 'Loại project:', choices: [ { name: '🟢 Node.js API (Express)', value: 'node-api' }, { name: '⚛️ React App (Vite)', value: 'react-app' }, { name: '🐍 Python Tool (Click)', value: 'python-tool' }, ], }, { type: 'confirm', name: 'initGit', message: 'Khởi tạo Git repository?', default: true, }, { type: 'confirm', name: 'installDeps', message: 'Cài đặt dependencies ngay?', default: true, }, ]); const { projectName, template, initGit, installDeps } = answers; const projectDir = path.resolve(projectName); const tmpl = PROJECT_TEMPLATES[template]; console.log(chalk.dim(`\nTạo project tại: ${projectDir}\n`)); // Kiểm tra folder chưa tồn tại if (await fse.pathExists(projectDir)) { console.error(chalk.red(`❌ Folder '${projectName}' đã tồn tại!`)); process.exit(1); } // Tạo folders for (const folder of tmpl.folders) { await fse.ensureDir(path.join(projectDir, folder)); console.log(chalk.green(` ✓ Tạo folder: ${folder}/`)); } // Tạo files for (const [filePath, content] of Object.entries(tmpl.files)) { await fse.outputFile(path.join(projectDir, filePath), content); console.log(chalk.green(` ✓ Tạo file: ${filePath}`)); } // Tạo package.json cho Node.js templates if (['node-api', 'react-app'].includes(template)) { const packageJson = { name: projectName, version: '1.0.0', scripts: template === 'node-api' ? { start: 'node src/index.js', dev: 'nodemon src/index.js', test: 'jest' } : { dev: 'vite', build: 'vite build', preview: 'vite preview' }, dependencies: {}, devDependencies: {}, }; await fse.outputJson(path.join(projectDir, 'package.json'), packageJson, { spaces: 2 }); console.log(chalk.green(` ✓ Tạo file: package.json`)); } // README await fse.outputFile( path.join(projectDir, 'README.md'), `# ${projectName}\n\nProject được tạo bởi Project Setup Generator.\n\n## Getting Started\n\nXem hướng dẫn chi tiết...\n` ); console.log(chalk.green(` ✓ Tạo file: README.md`)); // Git init if (initGit) { try { execSync('git init', { cwd: projectDir, stdio: 'ignore' }); console.log(chalk.green('\n ✓ Git repository khởi tạo')); } catch { console.log(chalk.yellow('\n ⚠️ Không thể khởi tạo Git (git chưa được cài?)')); } } // Install dependencies if (installDeps && ['node-api', 'react-app'].includes(template)) { console.log(chalk.dim('\nCài đặt dependencies...')); const deps = tmpl.dependencies.join(' '); const devDeps = tmpl.devDependencies.join(' '); execSync(`npm install ${deps}`, { cwd: projectDir, stdio: 'inherit' }); execSync(`npm install --save-dev ${devDeps}`, { cwd: projectDir, stdio: 'inherit' }); } console.log(chalk.bold.green(`\n✅ Project '${projectName}' đã được tạo!\n`)); console.log(chalk.cyan(`Tiếp theo:\n cd ${projectName}\n code .\n`)); } main().catch((err) => { console.error(chalk.red(`Lỗi: ${err.message}`)); process.exit(1); });
bash — Chạy
node create-project.js
# Hoặc thêm vào bin trong package.json để chạy như: npx create-my-project
💡
AI Prompt để mở rộng script này

Thử hỏi Copilot: "Thêm template cho 'Next.js App' vào PROJECT_TEMPLATES, bao gồm cấu trúc folder chuẩn Next.js 14, tailwind.config.js và tsconfig.json"


4

Bài 5.4 — Dự Án Thực Hành: Xây Dựng JSON Config Manager CLI

Chúng ta sẽ xây dựng một CLI tool Node.js thực tế — jconfig — quản lý nhiều file config JSON cho dự án, hỗ trợ get/set/delete giá trị, compare giữa các env, và validate schema. Đây là loại tool mà developer dùng hàng ngày khi quản lý config cho dev/staging/production.

🎯
Kết quả cuối bài

Tool jconfig với 5 lệnh: get, set, delete, list, diff. Hỗ trợ dot-notation (database.host), màu sắc terminal với chalk, spinner với ora.

Bước 1 — Setup Dự Án Node.js

bash — Khởi tạo project
# Tạo thư mục và vào đó
mkdir jconfig-tool
cd jconfig-tool

# Khởi tạo package.json với -y để dùng default
npm init -y

# Cài dependencies chính
npm install commander chalk@4 ora@5

# Cài dev dependencies
npm install --save-dev nodemon

# Xem package.json vừa tạo
cat package.json
⚠️
Lưu ý phiên bản chalk và ora

Giáo trình dùng chalk@4ora@5 (CommonJS). Phiên bản 5+ của chalk và ora chỉ hỗ trợ ESM. Nếu muốn dùng phiên bản mới nhất, thêm "type": "module" vào package.json.

bash — Tạo cấu trúc file
# Tạo các file và thư mục cần thiết
mkdir src configs
# Windows:
ni src/index.js, src/commands.js, src/utils.js
# macOS/Linux:
touch src/index.js src/commands.js src/utils.js

# Tạo file config mẫu để test
echo '{"database":{"host":"localhost","port":5432},"app":{"name":"MyApp","debug":true}}' > configs/dev.json
echo '{"database":{"host":"db.prod.example.com","port":5432},"app":{"name":"MyApp","debug":false}}' > configs/prod.json

Cấu trúc project sau khi setup:

Cấu trúc thư mục
jconfig-tool/
├── src/
│   ├── index.js        ← Entry point, định nghĩa CLI commands
│   ├── commands.js     ← Logic xử lý từng command
│   └── utils.js        ← Helper functions (đọc/ghi JSON, dot-notation)
├── configs/
│   ├── dev.json        ← Config file mẫu dev
│   └── prod.json       ← Config file mẫu production
├── package.json
└── README.md

Bước 2 — Viết utils.js (Hàm Tiện Ích)

javascript — src/utils.js
// src/utils.js — Các hàm tiện ích dùng chung
const fs = require('fs');
const path = require('path');

/**
 * Đọc file JSON, trả về object. Throw error nếu file không tồn tại hoặc sai format.
 */
function readJSON(filePath) {
  const absolute = path.resolve(filePath);
  if (!fs.existsSync(absolute)) {
    throw new Error(`File không tồn tại: ${absolute}`);
  }
  try {
    const raw = fs.readFileSync(absolute, 'utf-8');
    return JSON.parse(raw);
  } catch (err) {
    throw new Error(`Lỗi đọc JSON từ ${absolute}: ${err.message}`);
  }
}

/**
 * Ghi object vào file JSON (format đẹp với indent 2 spaces)
 */
function writeJSON(filePath, data) {
  const absolute = path.resolve(filePath);
  fs.writeFileSync(absolute, JSON.stringify(data, null, 2) + '\n', 'utf-8');
}

/**
 * Lấy giá trị từ object theo dot-notation: "database.host" → obj.database.host
 */
function getByPath(obj, dotPath) {
  return dotPath.split('.').reduce((current, key) => {
    if (current === undefined || current === null) return undefined;
    return current[key];
  }, obj);
}

/**
 * Set giá trị vào object theo dot-notation, tạo nested object nếu chưa có
 */
function setByPath(obj, dotPath, value) {
  const keys = dotPath.split('.');
  const lastKey = keys.pop();
  const target = keys.reduce((current, key) => {
    if (!current[key] || typeof current[key] !== 'object') {
      current[key] = {};
    }
    return current[key];
  }, obj);
  target[lastKey] = value;
  return obj;
}

/**
 * Xóa key từ object theo dot-notation
 */
function deleteByPath(obj, dotPath) {
  const keys = dotPath.split('.');
  const lastKey = keys.pop();
  const target = getByPath(obj, keys.join('.')) || obj;
  if (target && lastKey in target) {
    delete target[lastKey];
    return true;
  }
  return false;
}

/**
 * Flatten object thành danh sách "key.nested.path": value
 */
function flattenObject(obj, prefix = '') {
  const result = {};
  for (const [key, value] of Object.entries(obj)) {
    const fullKey = prefix ? `${prefix}.${key}` : key;
    if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
      Object.assign(result, flattenObject(value, fullKey));
    } else {
      result[fullKey] = value;
    }
  }
  return result;
}

/**
 * Auto-convert string value sang đúng kiểu dữ liệu
 * "true" → true, "42" → 42, "null" → null, còn lại giữ nguyên string
 */
function parseValue(str) {
  if (str === 'true') return true;
  if (str === 'false') return false;
  if (str === 'null') return null;
  if (!isNaN(str) && str.trim() !== '') return Number(str);
  return str;
}

module.exports = { readJSON, writeJSON, getByPath, setByPath, deleteByPath, flattenObject, parseValue };

Bước 3 — Viết commands.js (Logic Từng Lệnh)

javascript — src/commands.js
// src/commands.js — Xử lý logic từng CLI command
const chalk = require('chalk');
const { readJSON, writeJSON, getByPath, setByPath, deleteByPath, flattenObject, parseValue } = require('./utils');

/**
 * jconfig get  
 * Lấy giá trị theo dot-notation path
 */
function cmdGet(file, key) {
  try {
    const data = readJSON(file);
    const value = getByPath(data, key);

    if (value === undefined) {
      console.error(chalk.red(`✗ Key "${key}" không tồn tại trong ${file}`));
      process.exitCode = 1;
      return;
    }

    // In đẹp tùy theo kiểu dữ liệu
    if (typeof value === 'object') {
      console.log(chalk.cyan(JSON.stringify(value, null, 2)));
    } else if (typeof value === 'boolean') {
      console.log(value ? chalk.green(value) : chalk.red(value));
    } else {
      console.log(chalk.yellow(value));
    }
  } catch (err) {
    console.error(chalk.red('✗ ' + err.message));
    process.exitCode = 1;
  }
}

/**
 * jconfig set   
 * Set giá trị, tự động parse kiểu dữ liệu
 */
function cmdSet(file, key, rawValue) {
  try {
    const data = readJSON(file);
    const value = parseValue(rawValue);
    setByPath(data, key, value);
    writeJSON(file, data);
    console.log(chalk.green(`✓ Đã set ${chalk.bold(key)} = ${chalk.yellow(JSON.stringify(value))} trong ${file}`));
  } catch (err) {
    console.error(chalk.red('✗ ' + err.message));
    process.exitCode = 1;
  }
}

/**
 * jconfig delete  
 */
function cmdDelete(file, key) {
  try {
    const data = readJSON(file);
    const deleted = deleteByPath(data, key);
    if (!deleted) {
      console.error(chalk.yellow(`⚠ Key "${key}" không tồn tại, không có gì để xóa.`));
      return;
    }
    writeJSON(file, data);
    console.log(chalk.green(`✓ Đã xóa key "${chalk.bold(key)}" khỏi ${file}`));
  } catch (err) {
    console.error(chalk.red('✗ ' + err.message));
    process.exitCode = 1;
  }
}

/**
 * jconfig list 
 * Hiển thị toàn bộ key-value dưới dạng bảng phẳng
 */
function cmdList(file, options) {
  try {
    const data = readJSON(file);

    if (options.raw) {
      // In JSON thô
      console.log(JSON.stringify(data, null, 2));
      return;
    }

    const flat = flattenObject(data);
    const entries = Object.entries(flat);

    if (entries.length === 0) {
      console.log(chalk.yellow('Config file trống.'));
      return;
    }

    console.log(chalk.bold(`\n📄 ${file} — ${entries.length} keys:\n`));
    // Tìm độ dài key dài nhất để căn chỉnh
    const maxKeyLen = Math.max(...entries.map(([k]) => k.length));

    for (const [key, val] of entries) {
      const paddedKey = key.padEnd(maxKeyLen);
      let valueStr;
      if (typeof val === 'boolean') valueStr = val ? chalk.green(val) : chalk.red(val);
      else if (typeof val === 'number') valueStr = chalk.cyan(val);
      else if (val === null) valueStr = chalk.dim('null');
      else valueStr = chalk.yellow(`"${val}"`);
      console.log(`  ${chalk.blue(paddedKey)}  ${valueStr}`);
    }
    console.log();
  } catch (err) {
    console.error(chalk.red('✗ ' + err.message));
    process.exitCode = 1;
  }
}

/**
 * jconfig diff  
 * So sánh 2 config file và hiển thị sự khác biệt
 */
function cmdDiff(file1, file2) {
  try {
    const data1 = flattenObject(readJSON(file1));
    const data2 = flattenObject(readJSON(file2));
    const allKeys = new Set([...Object.keys(data1), ...Object.keys(data2)]);

    let diffCount = 0;
    console.log(chalk.bold(`\n🔍 So sánh: ${file1}  ↔  ${file2}\n`));

    for (const key of [...allKeys].sort()) {
      const v1 = data1[key];
      const v2 = data2[key];

      if (JSON.stringify(v1) === JSON.stringify(v2)) continue; // Giống nhau, bỏ qua

      diffCount++;
      console.log(chalk.bold(key));
      if (v1 !== undefined) console.log(`  ${chalk.red('−')} ${file1}: ${chalk.red(JSON.stringify(v1))}`);
      if (v2 !== undefined) console.log(`  ${chalk.green('+')} ${file2}: ${chalk.green(JSON.stringify(v2))}`);
      console.log();
    }

    if (diffCount === 0) {
      console.log(chalk.green('✓ Hai file config giống hệt nhau.'));
    } else {
      console.log(chalk.yellow(`Tổng: ${diffCount} điểm khác biệt.`));
    }
  } catch (err) {
    console.error(chalk.red('✗ ' + err.message));
    process.exitCode = 1;
  }
}

module.exports = { cmdGet, cmdSet, cmdDelete, cmdList, cmdDiff };

Bước 4 — Viết index.js (Entry Point)

javascript — src/index.js
#!/usr/bin/env node
// src/index.js — Entry point của jconfig CLI
'use strict';

const { Command } = require('commander');
const { cmdGet, cmdSet, cmdDelete, cmdList, cmdDiff } = require('./commands');

const program = new Command();

program
  .name('jconfig')
  .description('🔧 JSON Config Manager — Quản lý file config dự án dễ dàng')
  .version('1.0.0');

// jconfig get  
program
  .command('get  ')
  .description('Lấy giá trị của một key (hỗ trợ dot-notation)')
  .action((file, key) => cmdGet(file, key));

// jconfig set   
program
  .command('set   ')
  .description('Set giá trị cho một key (tự động detect kiểu: string/number/boolean)')
  .action((file, key, value) => cmdSet(file, key, value));

// jconfig delete  
program
  .command('delete  ')
  .alias('del')
  .description('Xóa một key khỏi config')
  .action((file, key) => cmdDelete(file, key));

// jconfig list 
program
  .command('list ')
  .alias('ls')
  .description('Liệt kê tất cả key-value trong config')
  .option('-r, --raw', 'In JSON thô thay vì bảng đẹp')
  .action((file, options) => cmdList(file, options));

// jconfig diff  
program
  .command('diff  ')
  .description('So sánh sự khác biệt giữa 2 file config')
  .action((f1, f2) => cmdDiff(f1, f2));

program.parse(process.argv);

Bước 5 — Cấu Hình package.json và Chạy Tool

json — package.json (thêm vào)
{
  "name": "jconfig-tool",
  "version": "1.0.0",
  "description": "JSON Config Manager CLI",
  "main": "src/index.js",
  "bin": {
    "jconfig": "./src/index.js"
  },
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  },
  "dependencies": {
    "chalk": "^4.1.2",
    "commander": "^11.0.0",
    "ora": "^5.4.1"
  },
  "devDependencies": {
    "nodemon": "^3.0.0"
  }
}
bash — Test tất cả lệnh của tool
# Chạy trực tiếp bằng node
node src/index.js --help

# Lấy giá trị
node src/index.js get configs/dev.json database.host
# Kết quả: localhost

node src/index.js get configs/dev.json app
# Kết quả: { "name": "MyApp", "debug": true }

# Set giá trị
node src/index.js set configs/dev.json database.port 5433
node src/index.js set configs/dev.json app.debug false
node src/index.js set configs/dev.json cache.ttl 300

# Liệt kê tất cả keys
node src/index.js list configs/dev.json

# Xóa key
node src/index.js delete configs/dev.json cache.ttl

# So sánh dev vs prod
node src/index.js diff configs/dev.json configs/prod.json

# Cài globally để dùng lệnh ngắn gọn
npm link

# Sau khi npm link, có thể dùng:
jconfig list configs/dev.json
jconfig diff configs/dev.json configs/prod.json

Kết Quả Mong Đợi Khi Chạy

Ví dụ output khi chạy jconfig list
📄 configs/dev.json — 3 keys:

  app.debug     true          ← màu xanh (boolean true)
  app.name      "MyApp"       ← màu vàng (string)
  database.host "localhost"   ← màu vàng
  database.port 5432          ← màu cyan (number)
🎉
Tool Node.js hoàn chỉnh!

Bạn đã xây dựng xong một CLI tool Node.js thực sự với 5 commands, dot-notation navigation, màu sắc terminal, và diff comparison. Đây là nền tảng để bạn tự build thêm các tool phức tạp hơn.

💡 Mẹo từ ThanhDoIT
  • Luôn dùng async/await thay vì callback hoặc .then().catch() chain khi viết code Node.js hiện đại — dễ đọc, dễ debug hơn nhiều.
  • Khi publish npm package, đặt "engines": {"node": ">=18"} trong package.json để tránh lỗi ở môi trường cũ.
  • Dùng process.exitCode = 1 thay vì process.exit(1) trong CLI — cho phép cleanup code (finally blocks) chạy xong trước khi exit.
  • AI thường generate code mà không handle edge cases của CLI (stdin piped, no TTY). Luôn hỏi AI: "Có edge cases nào tôi cần handle không?"

5

Bài 5.5 — Express.js API & TypeScript với AI

Node.js không chỉ cho CLI tools — nó là nền tảng của hầu hết các backend web hiện đại. Trong bài này bạn sẽ học cách dùng AI để xây dựng nhanh một REST API Express + TypeScript từ scratch.

🚀
Express.js Essentials
  • Routing: GET, POST, PUT, DELETE
  • Middleware: logging, cors, body-parser
  • Error handling centralized
  • Environment config với dotenv
📦
TypeScript Benefits
  • Type safety: bắt lỗi trước khi chạy
  • IntelliSense tốt hơn trong VS Code
  • AI generate code chính xác hơn với types
  • Refactor an toàn hơn

Khởi Tạo Express + TypeScript Project

1
Init Project
bash
mkdir my-api && cd my-api
npm init -y
npm install express cors dotenv
npm install -D typescript @types/express @types/node ts-node nodemon
npx tsc --init
2
Cấu hình tsconfig.json
json — tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  }
}
3
package.json scripts
json
{
  "scripts": {
    "dev": "nodemon --exec ts-node src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
4
src/index.ts — Server cơ bản
typescript — src/index.ts
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import userRoutes from './routes/users';

dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(express.json());

// Routes
app.get('/health', (_req: Request, res: Response) => {
  res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
app.use('/api/users', userRoutes);

// Global error handler
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal Server Error' });
});

app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));
Tôi đang xây dựng REST API với Express + TypeScript. Hãy generate cho tôi:

1. Interface TypeScript cho User model: { id, email, name, role, createdAt }
2. CRUD routes đầy đủ cho /api/users (GET list, GET by id, POST, PUT, DELETE)
3. Middleware validateUser kiểm tra request body khi POST/PUT
4. Error handling đúng chuẩn với typed errors
5. Thêm pagination cho GET /api/users (page, limit query params)

Tất cả phải có TypeScript types đầy đủ, không dùng any.
TypeScript types trong prompt giúp Copilot generate code chính xác và an toàn hơn nhiều so với plain JavaScript.

npm Scripts Nâng Cao — Package.json Tricks

json — package.json scripts nâng cao
{
  "scripts": {
    "dev": "nodemon --exec ts-node src/index.ts",
    "build": "tsc --noEmit && tsc",
    "start": "node dist/index.js",
    "test": "jest --coverage",
    "test:watch": "jest --watch",
    "lint": "eslint src/**/*.ts",
    "lint:fix": "eslint src/**/*.ts --fix",
    "format": "prettier --write src/**/*.ts",
    "clean": "rimraf dist",
    "prebuild": "npm run clean && npm run lint",
    "prepare": "husky install",
    "db:migrate": "prisma migrate dev",
    "db:studio": "prisma studio",
    "typecheck": "tsc --noEmit"
  }
}
💡 npm Scripts = Task Runner Miễn Phí
  • pre/post hooks: prebuild tự chạy trước build, postinstall tự chạy sau npm install
  • Dùng npm-run-all để chạy scripts song song: "dev": "run-p dev:server dev:client"
  • Scripts có thể reference scripts khác: clean + lint trong prebuild
  • Hỏi Copilot: "Tối ưu package.json scripts cho TypeScript + Express + Prisma project"
🎯 Thực Hành: Build REST API Hoàn Chỉnh
  1. Khởi tạo Express + TypeScript project với cấu trúc ở trên
  2. Hỏi Copilot Chat: "Generate CRUD API cho Product với interface TypeScript"
  3. Thêm validation middleware cho POST /api/products
  4. Test API bằng REST Client file (api.http) trong VS Code
  5. Thêm error handling: trả về lỗi 404 khi không tìm thấy product
  6. Viết npm script test:api để auto-test endpoints cơ bản
✅ Kết Quả Mong Đợi
  • Express + TypeScript project chạy npm run dev → hot reload
  • REST API với CRUD hoàn chỉnh, type-safe
  • package.json scripts tối ưu cho development workflow
  • Biết cách dùng Copilot để generate boilerplate code nhanh
🎯 Bài Tập Tổng Kết Chương 5 — Node.js Toolkit
  1. Chạy thành công JSON Toolkit CLI với cả 4 commands (read, write, merge, validate)
  2. Thêm command minify vào JSON Toolkit — hỏi Copilot để generate
  3. Viết script file-watcher.js dùng fs.watch để log khi file trong folder thay đổi
  4. Khởi tạo Express + TypeScript project, generate CRUD API cho 1 resource
  5. Thử thách: Viết CLI tool env-validator đọc file .env.example và kiểm tra xem .env có đủ tất cả variables không
⚠️ 5 Cạm Bẫy Phổ Biến Với Node.js & TypeScript
  • Quên xử lý lỗi async: Không bắt try/catch trong async function → unhandled rejection crash server. Mọi async route trong Express đều cần try/catch hoặc wrapper.
  • Blocking the event loop: Dùng fs.readFileSync, JSON.parse file lớn, tính toán nặng trong route handler → server đơ cho mọi request. Dùng fs.promises và stream.
  • TypeScript any khắp nơi: Mục đích của TypeScript là type safety. Dùng any = tắt type checking. Dùng unknown hoặc define types đủng.
  • Commit node_modules vào Git: Folder này có hàng nghìn files. Luôn thêm vào .gitignore. Ai clone repo chỉ cần chạy npm install.
  • Không dùng environment variables: Hard-code API keys, port, database URLs trong code → security risk và không switch được giữa dev/production. Dùng .env + dotenv.
  • npm ecosystem: npm init, npm install, package.json scripts với pre/post hooks
  • Commander.js = Click cho Node.js — định nghĩa commands, options, arguments
  • Chalk tô màu output, Inquirer.js tạo interactive prompts
  • fs.promises (async) vs fs (sync) — luôn dùng async trong Node.js production code
  • Express + TypeScript: cấu trúc MVC, middleware, error handling, typed routes
  • TypeScript giúp AI generate code chính xác hơn — luôn define interfaces trước
Zalo: 0898 619 966 Z Gọi: 0898 619 966