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
- Nắm vững TypeScript Generics, Utility Types, Type Guards và Monorepo với npm workspaces
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 <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
{
"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 <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);
});
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 <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)
#!/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
{
"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
- 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
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
// ❌ 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
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
// 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
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/
{
"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"
}
}
// Đâ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'>>
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".
- Lấy 1 interface User có 8+ fields trong project bạn đang làm
- Tạo:
UserResponse(Omit password),UpdateUserDto(Partial + Omit id),UserPreview(Pick 3 fields) - Hỏi Copilot: "Tôi có interface User. Tạo đầy đủ utility types cho CRUD operations theo REST API conventions"
- Setup monorepo đơn giản:
npm init -w packages/shared -w packages/backend - Di chuyển interface User vào shared package và import từ backend
🧠 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?
2. ESM sử dụng cú pháp nào?
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?
4. npm là gì?
5. Monorepo là gì?