📘 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.

7 giờ học
📝 6 bài học
🎯 2 dự án + TypeScript nâng cao
📊 Mức độ: Trung cấp
⚡ 1. Node.js Fundamentals
Event Loop, npm, ESM/CJS
⌨️ 2. CLI với Commander.js
Build & publish npm package
📂 3. Automation Scripts
fs, glob, cross-platform tools
⚙️ 4. JSON Config Manager
Dự án CLI hoàn chỉnh
🚀 5. Express + TypeScript
REST API chuẩn production
🔷 6. TypeScript Nâng Cao
Generics, Utility Types, Monorepo

🎯 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
  • Nắm vững TypeScript Generics, Utility Types, Type Guards và Monorepo với npm workspaces
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 <file>')
  .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 <file>')
  .description('Format (pretty-print) file JSON')
  .option('-i, --indent <n>', 'Số spaces indent', '2')
  .option('-o, --output <file>', '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 <file> <key>')
  .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 <file1> <file2> [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 <div>Hello World</div>;\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 <file> <key>
 * 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 <file> <key> <value>
 * 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 <file> <key>
 */
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 <file>
 * 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 <file1> <file2>
 * 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 <file> <key>
program
  .command('get <file> <key>')
  .description('Lấy giá trị của một key (hỗ trợ dot-notation)')
  .action((file, key) => cmdGet(file, key));

// jconfig set <file> <key> <value>
program
  .command('set <file> <key> <value>')
  .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 <file> <key>
program
  .command('delete <file> <key>')
  .alias('del')
  .description('Xóa một key khỏi config')
  .action((file, key) => cmdDelete(file, key));

// jconfig list <file>
program
  .command('list <file>')
  .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 <file1> <file2>
program
  .command('diff <file1> <file2>')
  .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
  • TypeScript nâng cao: Generic types, utility types (Partial/Pick/Omit), type guards
  • Monorepo với npm workspaces: chia sẻ types giữa frontend và backend

6

Bài 5.6 — TypeScript Nâng Cao: Generics, Utility Types & Patterns

TypeScript không chỉ là JavaScript với types cơ bản. Những tính năng nâng cao của TypeScript là sự khác biệt giữa junior và senior developer. Hiểu Generics và Utility Types giúp bạn viết code tái sử dụng được, type-safe hoàn toàn, và dễ maintain hơn.

1. Generics — Types Tham Số Hóa

typescript — Generics từ cơ bản đến nâng cao
// ❌ Không có Generic — phải duplicate code
function getFirstNumber(arr: number[]): number | undefined { return arr[0] }
function getFirstString(arr: string[]): string | undefined { return arr[0] }

// ✅ Generic — 1 function, hoạt động với mọi type
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0]
}

const num = getFirst([1, 2, 3])    // TypeScript tự suy ra: T = number
const str = getFirst(['a', 'b'])   // T = string
const obj = getFirst([{id: 1}])    // T = {id: number}

// Generic với constraints — T phải có property id
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id)
}

// Generic với multiple type params
function zip<A, B>(a: A[], b: B[]): [A, B][] {
  return a.map((item, i) => [item, b[i]])
}
const pairs = zip([1, 2], ['a', 'b'])  // [number, string][]

// Generic Class — Repository pattern type-safe
class Repository<T extends { id: string }> {
  private items: Map<string, T> = new Map()
  
  save(item: T): void { this.items.set(item.id, item) }
  findById(id: string): T | undefined { return this.items.get(id) }
  findAll(): T[] { return Array.from(this.items.values()) }
  delete(id: string): boolean { return this.items.delete(id) }
}

interface User { id: string; name: string; email: string }
interface Product { id: string; name: string; price: number }

const userRepo = new Repository<User>()
const productRepo = new Repository<Product>()
userRepo.save({ id: '1', name: 'Alice', email: 'alice@example.com' })

2. Utility Types — TypeScript Built-in Magic

typescript — Utility types thực tế
interface User {
  id: string
  name: string
  email: string
  password: string
  role: 'admin' | 'user' | 'moderator'
  createdAt: Date
  updatedAt: Date
}

// Partial<T> — tất cả fields đều Optional (dùng cho PATCH/update)
type UpdateUserDto = Partial<User>
// Kết quả: { id?: string; name?: string; email?: string; ... }

// Required<T> — tất cả fields đều bắt buộc
type StrictUser = Required<User>

// Pick<T, K> — chỉ lấy một số fields
type UserPublicProfile = Pick<User, 'id' | 'name' | 'role'>
// Kết quả: { id: string; name: string; role: 'admin' | 'user' | 'moderator' }

// Omit<T, K> — bỏ một số fields (dùng cho response — tránh trả password)
type UserResponse = Omit<User, 'password'>
// Kết quả: User nhưng không có password field

// Record<K, V> — tạo object type với keys và values cụ thể
type UsersByRole = Record<User['role'], User[]>
// Kết quả: { admin: User[]; user: User[]; moderator: User[] }

type ApiStatus = Record<string, 'loading' | 'success' | 'error'>

// Readonly<T> — tất cả fields immutable
type FrozenConfig = Readonly<{
  apiUrl: string
  timeout: number
}>

// ReturnType<T> — lấy kiểu return của function
async function fetchUser(id: string) {
  return { id, name: 'Alice', role: 'user' as const }
}
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>
// Kết quả: { id: string; name: string; role: "user" }

// Parameters<T> — lấy kiểu params của function
type FetchUserParams = Parameters<typeof fetchUser>
// Kết quả: [id: string]

3. Type Guards & Discriminated Unions

typescript — Type Guards & phân biệt union types
// Discriminated Union — pattern mạnh nhất trong TypeScript
type ApiResponse<T> =
  | { status: 'success'; data: T; timestamp: number }
  | { status: 'error'; message: string; code: number }
  | { status: 'loading' }

function handleResponse<T>(response: ApiResponse<T>): string {
  switch (response.status) {
    case 'success':
      return `Data: ${JSON.stringify(response.data)}`
      // TypeScript biết: response.data có type T
    case 'error':
      return `Error ${response.code}: ${response.message}`
      // TypeScript biết: response.message và response.code tồn tại
    case 'loading':
      return 'Loading...'
      // TypeScript biết: chỉ có status property
  }
  // TypeScript đảm bảo tất cả cases được handle — không cần default!
}

// Custom Type Guard — hàm narrowing type
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'email' in obj &&
    typeof (obj as User).email === 'string'
  )
}

// Dùng khi nhận data từ API (unknown type)
async function processApiData(data: unknown) {
  if (isUser(data)) {
    console.log(data.email)  // TypeScript biết data là User ở đây
  }
}

// satisfies operator (TypeScript 4.9+) — kiểm tra type mà không mất inference
const config = {
  host: 'localhost',
  port: 3000,
  debug: true,
} satisfies Record<string, string | number | boolean>

config.port  // Type: number (giữ được, không bị widened thành string|number|boolean)

4. Monorepo với npm Workspaces — Chia Sẻ Types

Cấu trúc monorepo fullstack với shared types
my-fullstack-app/
├── package.json          ← Root workspace config
├── packages/
│   ├── shared/           ← Package chia sẻ (types, utils, constants)
│   │   ├── package.json
│   │   └── src/
│   │       ├── types.ts  ← Interfaces dùng chung
│   │       └── index.ts
│   │
│   ├── backend/          ← Express API
│   │   ├── package.json
│   │   └── src/
│   │
│   └── frontend/         ← React/Next.js
│       ├── package.json
│       └── src/
package.json — Root workspace config
{
  "name": "my-fullstack-app",
  "private": true,
  "workspaces": ["packages/*"],
  "scripts": {
    "dev": "concurrently \"npm run dev -w backend\" \"npm run dev -w frontend\"",
    "build": "npm run build -w shared && npm run build -w backend && npm run build -w frontend",
    "test": "npm run test --workspaces"
  },
  "devDependencies": {
    "concurrently": "^8.0.0"
  }
}
typescript — packages/shared/src/types.ts — Types dùng chung
// Đây là "contract" giữa backend và frontend
// Cả 2 import từ @my-app/shared — không bao giờ out of sync

export interface User {
  id: string
  name: string
  email: string
  role: 'admin' | 'user'
  createdAt: string  // ISO string để serialize qua JSON
}

export interface ApiResponse<T = void> {
  success: boolean
  data?: T
  message?: string
  errors?: Record<string, string[]>
}

export interface PaginatedResponse<T> {
  data: T[]
  total: number
  page: number
  limit: number
  totalPages: number
}

export type CreateUserDto = Omit<User, 'id' | 'createdAt'> & { password: string }
export type UpdateUserDto = Partial<Omit<User, 'id' | 'createdAt'>>
🎯
TypeScript + Copilot = Siêu Năng Suất

Khi bạn define types/interfaces rõ ràng trước, Copilot generate code chính xác hơn 80%. AI đọc types của bạn như "specification" và tự biết cần implement gì. Đây là lý do senior developer luôn "types first".

🛠 Thực Hành: Refactor Với Utility Types
  1. Lấy 1 interface User có 8+ fields trong project bạn đang làm
  2. Tạo: UserResponse (Omit password), UpdateUserDto (Partial + Omit id), UserPreview (Pick 3 fields)
  3. Hỏi Copilot: "Tôi có interface User. Tạo đầy đủ utility types cho CRUD operations theo REST API conventions"
  4. Setup monorepo đơn giản: npm init -w packages/shared -w packages/backend
  5. Di chuyển interface User vào shared package và import từ backend
🗺 Sơ Đồ — Bộ Công Cụ Node.js Với AI
flowchart LR A["Node.js"] --> B["Commander CLI"] A --> C["Automation Scripts"] A --> D["Express + TypeScript"] D --> E["API có kiểu an toàn"]

🧠 Kiểm Tra Kiến Thức Chương 5

Trả lời 5 câu để củng cố. Đạt ≥ 80% sẽ tự động đánh dấu hoàn thành chương.

1. Thư viện nào dùng để xây CLI cho Node.js?

Commander.js là thư viện phổ biến để xây dựng CLI trong Node.js.

2. ESM sử dụng cú pháp nào?

ESM dùng import/export; CommonJS dùng require/module.exports.

3. Điểm khác biệt cốt lõi của TypeScript so với JavaScript?

TypeScript thêm kiểu tĩnh cho JavaScript, giúp bắt lỗi sớm và code an toàn hơn.

4. npm là gì?

npm (Node Package Manager) quản lý và cài đặt thư viện cho Node.js.

5. Monorepo là gì?

Monorepo là một repository chứa nhiều dự án/package (vd: frontend + backend chung repo).
Zalo: 0898 619 966 Z Gọi: 0898 619 966