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.
🎯 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
Bài 5.1 — Node.js Fundamentals
npm install, npm run script. Có hơn 2 triệu packages. Dependencies lưu trong node_modules/.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.Built-in Modules quan trọng nhất
| Module | Công dụng | API thường dùng |
|---|---|---|
fs | File system operations | readFile, writeFile, mkdir, readdir, stat, watch |
path | Xử lý đường dẫn file | join, resolve, dirname, basename, extname, parse |
os | Thông tin hệ điều hành | homedir, platform, cpus, totalmem, tmpdir |
http | HTTP server/client thô | createServer, request, get (Express wrap cái này) |
crypto | Mã hóa, hash | createHash, randomBytes, createHmac |
events | Event emitter pattern | EventEmitter, on, emit, once, removeListener |
child_process | Chạy command ngoài | exec, 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
# 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
// 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
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);
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.
npm install commander chalk ora inquirer
Dự án: JSON Toolkit CLI
Tool thao tác với file JSON: validate, format, query, merge.
#!/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
{
"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"
}
}
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
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.
npm install inquirer chalk fs-extra
#!/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);
});
node create-project.js
# Hoặc thêm vào bin trong package.json để chạy như: npx create-my-project
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"
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.
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
# 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
Giáo trình dùng chalk@4 và ora@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.
# 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:
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)
// 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)
// 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)
#!/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
{
"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"
}
}
# 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
📄 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)
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.
- 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 = 1thay 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?"
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.
- Routing: GET, POST, PUT, DELETE
- Middleware: logging, cors, body-parser
- Error handling centralized
- Environment config với dotenv
- 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
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
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true
}
}
{
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
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.
npm Scripts Nâng Cao — Package.json Tricks
{
"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"
}
}
- pre/post hooks:
prebuildtự chạy trướcbuild,postinstalltự chạy saunpm 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"
- Khởi tạo Express + TypeScript project với cấu trúc ở trên
- Hỏi Copilot Chat: "Generate CRUD API cho Product với interface TypeScript"
- Thêm validation middleware cho POST /api/products
- Test API bằng REST Client file (
api.http) trong VS Code - Thêm error handling: trả về lỗi 404 khi không tìm thấy product
- Viết npm script
test:apiđể auto-test endpoints cơ bản
- 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
- Chạy thành công JSON Toolkit CLI với cả 4 commands (
read,write,merge,validate) - Thêm command
minifyvào JSON Toolkit — hỏi Copilot để generate - Viết script
file-watcher.jsdùngfs.watchđể log khi file trong folder thay đổi - Khởi tạo Express + TypeScript project, generate CRUD API cho 1 resource
- Thử thách: Viết CLI tool
env-validatorđọc file.env.examplevà kiểm tra xem.envcó đủ tất cả variables không
- Quên xử lý lỗi async: Không bắt
try/catchtrongasyncfunction → unhandled rejection crash server. Mọi async route trong Express đều cầntry/catchhoặc wrapper. - Blocking the event loop: Dùng
fs.readFileSync,JSON.parsefile lớn, tính toán nặng trong route handler → server đơ cho mọi request. Dùngfs.promisesvà stream. - TypeScript
anykhắp nơi: Mục đích của TypeScript là type safety. Dùngany= tắt type checking. Dùngunknownhoặ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ạynpm 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 scriptsvớ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