HARRY-13 — GĐ1 — TypeScript & toolchain cho Backend (học trước GĐ2)
GĐ1 — TypeScript & toolchain cho Backend (học trước GĐ2)
Study note cho FE engineer (mạnh JS/TS) chuyển sang Backend. Mỗi concept: định nghĩa → tại sao quan trọng → cơ chế → ví dụ → pitfall. Đây là giai đoạn "ngày 0". Ở FE, Vite/Next đã lo hết: transpile, bundle, watch, alias, env, source map. Ở BE không có ai lo hộ — bạn tự dựng toolchain, và nếu dựng sai, mọi lỗi sau này (import không chạy, alias vỡ khi build,
__dirnameundefined) đều bắt nguồn từ đây chứ không phải từ code backend.
1. Vì sao backend phải tự dựng toolchain
Định nghĩa. Toolchain = tập công cụ biến source TypeScript thành process Node chạy được: type checker, transpiler, watcher (dev), bundler/emitter (build), linter, formatter.
Tại sao quan trọng. Ở FE bạn viết import Foo from '@/components/Foo' và nó chạy — vì Vite/webpack resolve alias, transpile TS, hot-reload, inject env, tạo source map. Node không làm gì trong số đó. Node nhận một file JS và chạy. Mọi thứ ở giữa là việc của bạn.
Cơ chế. Có 2 pha tách biệt mà FE thường gộp làm một:
- Type checking —
tsc --noEmit. Chỉ kiểm tra kiểu, không sinh file. Chậm, chạy ở CI + IDE. - Transpile/emit — biến TS → JS.
tsc, hoặc esbuild/swc (nhanh gấp 10–50 lần vì không type-check, chỉ xoá type).
Đây là lý do tsx/esbuild chạy được cả file TS đang có lỗi type: chúng chỉ xoá type, không kiểm tra. Type an toàn là do tsc --noEmit ở CI đảm bảo, không phải do runtime.
Pitfall. Tin rằng "build pass = type đúng". Nếu build bằng esbuild/swc/tsup mà CI không có bước tsc --noEmit riêng, bạn đã tắt TypeScript mà không biết — code có lỗi type vẫn deploy lên prod bình thường.
2. tsconfig.json cho Node — từng dòng, và vì sao
Định nghĩa. File cấu hình compiler TypeScript: nhắm runtime nào, sinh module kiểu gì, chặt tới đâu.
Tại sao quan trọng. tsconfig của FE ("lib": ["DOM"], "module": "ESNext", "noEmit": true) sao chép sang BE là sai ngay. DOM cho phép bạn gọi document và localStorage mà không báo lỗi — cho tới khi chạy trên Node và crash.
Ví dụ — tsconfig baseline cho Node 22+ / TS 5.x:
{
"compilerOptions": {
// --- Nhắm runtime ---
"target": "ES2023", // Node 22 hỗ trợ; đừng để ES5 (downlevel vô ích, code xấu, chậm)
"lib": ["ES2023"], // KHÔNG có "DOM" — không có document/window/localStorage ở BE
"types": ["node"], // chỉ nạp @types/node, chặn type global rác lọt vào
// --- Module system (mục 3) ---
"module": "NodeNext", // Node quyết định ESM/CJS theo package.json + đuôi file
"moduleResolution": "NodeNext", // resolve đúng như Node thật, kể cả "exports" field
// --- Output ---
"rootDir": "src",
"outDir": "dist",
"sourceMap": true, // stack trace prod trỏ về .ts thay vì .js đã transpile
"declaration": false, // true chỉ khi publish package/dùng monorepo
// --- Strictness (mục 10) ---
"strict": true,
"noUncheckedIndexedAccess": true, // arr[0] có kiểu T | undefined — ép bạn check
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
// --- Interop & tốc độ ---
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true, // macOS không phân biệt hoa/thường, Linux CÓ
"skipLibCheck": true, // bỏ type-check node_modules — nhanh hơn nhiều, an toàn
"incremental": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Cơ chế của 2 dòng hay bị bỏ qua.
noUncheckedIndexedAccess: mặc định TS nóiusers[999]có kiểuUser— nói dối. Bật lên thànhUser | undefined. Đây là nguồnCannot read property 'x' of undefinedphổ biến nhất ở BE khi xử lý mảng từ DB.forceConsistentCasingInFileNames: bạn dev trên macOS (case-insensitive),import './UserService'trong khi file tênuserService.tsvẫn chạy. Deploy lên Docker/Linux (case-sensitive) →MODULE_NOT_FOUND. Lỗi "chạy máy tôi ok" kinh điển số 1.
Pitfall. "skipLibCheck": false với dự án lớn khiến tsc chậm gấp nhiều lần vì phải check toàn bộ .d.ts trong node_modules — bao gồm cả những package có type lỗi mà bạn không sửa được. Gần như luôn để true.
3. ESM vs CommonJS — cuộc chiến thật sự của Node
Định nghĩa. Node có hai module system:
- CommonJS (CJS) —
require()/module.exports. Đồng bộ, cũ, mặc định lịch sử. - ES Modules (ESM) —
import/export. Bất đồng bộ, chuẩn, tương lai.
Tại sao quan trọng. Ở FE bạn viết import và bundler xử lý hết — bạn chưa từng phải biết file cuối cùng là ESM hay CJS. Ở BE, Node phân biệt thật, và hai bên không trộn tự do được. Đây là nguồn lỗi số 1 của FE mới sang BE.
Cơ chế — Node quyết định file là ESM hay CJS thế nào:
| Điều kiện | Kết quả |
|---|---|
package.json có "type": "module" | .js = ESM |
Không có "type" (hoặc "type": "commonjs") | .js = CJS |
Đuôi .mjs | luôn ESM (bất kể type) |
Đuôi .cjs | luôn CJS |
.ts → .mts / .cts | tương ứng ESM / CJS |
Luật trộn:
- ESM import được CJS:
import express from 'express'→ OK (chỉ default import; named import chỉ hoạt động nhờ Node phân tích tĩnh, không phải lúc nào cũng được). - CJS KHÔNG
require()được ESM đồng bộ ở Node cũ →ERR_REQUIRE_ESM. (Node 22.12+ / 23+ cho phéprequire()một ESM không có top-level await — nhưng đừng dựa vào nó khi hỗ trợ nhiều version.)
Ví dụ — khác biệt cụ thể phải nhớ:
// === ESM ===
import { readFile } from 'node:fs/promises';
import path from 'node:path';
// __dirname và __filename KHÔNG TỒN TẠI trong ESM
const __dirname = import.meta.dirname; // Node 20.11+ / 21.2+
// bản cũ hơn: path.dirname(fileURLToPath(import.meta.url))
// import PHẢI có đuôi file khi trỏ file nội bộ
import { userService } from './user.service.js'; // .js — KHÔNG phải .ts (xem pitfall)
// top-level await hoạt động
const config = await loadConfig();
// === CJS ===
const { readFile } = require('node:fs/promises');
const __dirname_is_free = __dirname; // có sẵn
const userService = require('./user.service'); // không cần đuôi
// top-level await KHÔNG dùng đượcPitfall — hai cái đau nhất:
Viết
.tsnhưng import phải ghi.js. Vớimodule: NodeNext+ ESM, bạn có fileuser.service.tsnhưng phải viếtimport ... from './user.service.js'. Trông vô lý nhưng đúng: TS không đổi đường dẫn khi emit, nên đường dẫn phải là đường dẫn sau khi build. Viết.ts→ build ra JS rồi crashMODULE_NOT_FOUNDở runtime, màtsckhông hề báo lỗi lúc build.Package ESM-only. Nhiều package hiện đại (
chalkv5+,node-fetchv3+,nanoidv4+) bỏ CJS hoàn toàn. Project CJSrequire('chalk')→ nổ. Cách xử: dùngawait import('chalk')(dynamic import chạy được từ CJS), hoặc pin version cũ, hoặc chuyển hẳn project sang ESM.
Khuyến nghị cho học viên. Chọn ESM ("type": "module") cho project mới và đi tới cùng — đó là hướng của hệ sinh thái. Ngoại lệ: NestJS mặc định CJS (vì decorator + emitDecoratorMetadata + reflect-metadata), cứ để nguyên CJS khi làm GĐ6, đừng cố ép sang ESM.
4. Path alias — và bẫy "chạy dev ok, build xong vỡ"
Định nghĩa. Alias = ánh xạ đường dẫn ngắn (@/services/user) sang đường dẫn thật (src/services/user).
Tại sao quan trọng. Không có alias, module lồng sâu sinh ra import ... from '../../../../config/env.js' — di chuyển file là vỡ hàng loạt.
Cơ chế — và cái bẫy. tsconfig.paths chỉ dạy TypeScript hiểu alias để type-check. Nó không ghi lại đường dẫn khi emit. File JS sinh ra vẫn chứa nguyên chuỗi "@/services/user" — và Node không biết @ là gì → crash lúc chạy.
Ví dụ — 3 cách xử lý, chọn 1:
// tsconfig.json — chỉ là bước 1, chưa đủ
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }// CÁCH 1 (khuyên dùng cho ESM, không cần tool): Node subpath imports
// package.json — Node hiểu native, không cần plugin gì
{
"imports": { "#config/*": "./dist/config/*", "#services/*": "./dist/services/*" }
}
// dùng: import { env } from '#config/env.js' // chú ý tiền tố # là bắt buộc// CÁCH 2: bundle lúc build — alias biến mất vì mọi thứ gộp thành 1 file
// tsup/esbuild tự resolve tsconfig.paths (xem mục 6)# CÁCH 3: rewrite sau khi tsc emit (dự án đã lỡ dùng @/ khắp nơi)
npm i -D tsc-alias
tsc && tsc-alias # tsc-alias sửa lại đường dẫn trong dist/Pitfall. Dev bằng tsx (tự đọc tsconfig.paths → chạy ngon), rồi build bằng tsc thuần và deploy → prod crash Cannot find module '@/config/env' ngay lúc boot. Lỗi này chỉ xuất hiện ở prod, không bao giờ thấy lúc dev. Luôn chạy thử npm run build && node dist/main.js ở local trước khi tin toolchain của mình.
5. Chạy dev — watch mode
Định nghĩa. Chạy TS trực tiếp, tự restart khi đổi file.
Cơ chế & lựa chọn:
| Cách | Lệnh | Ghi chú |
|---|---|---|
| tsx (khuyên dùng) | tsx watch src/main.ts | esbuild bên dưới, rất nhanh, hiểu tsconfig.paths, chạy cả ESM/CJS |
| Node native | node --watch --experimental-strip-types src/main.ts | Node 22.6+ (Node 23.6+ bỏ cờ). Chỉ xoá type, không hỗ trợ enum/namespace/decorator |
ts-node + nodemon | nodemon --exec ts-node src/main.ts | Cũ, chậm hơn (type-check mỗi lần restart), còn gặp nhiều ở dự án legacy |
| NestJS | nest start --watch | Nest lo sẵn, đừng tự chế |
Ví dụ scripts:
{
"scripts": {
"dev": "tsx watch --env-file=.env src/main.ts",
"typecheck": "tsc --noEmit"
}
}Pitfall. tsx không type-check → bạn code cả buổi thấy "chạy ngon", tới lúc CI chạy tsc --noEmit thì đỏ 40 lỗi. Cách chống: mở IDE có TS server (VSCode làm sẵn) + chạy npm run typecheck trước mỗi commit (hoặc gắn vào pre-commit hook).
6. Build cho production
Định nghĩa. Sinh JS chạy được, tối ưu cho môi trường prod.
Cơ chế & lựa chọn:
| Công cụ | Type-check? | Tốc độ | Khi nào |
|---|---|---|---|
tsc | ✅ có | Chậm | Mặc định an toàn. NestJS dùng cái này |
tsup (esbuild) | ❌ | Rất nhanh | App/lib thường; bundle được → giảm cold start serverless |
swc | ❌ | Rất nhanh | Thay tsc trong Nest/Jest khi dự án lớn |
Ví dụ — cấu hình tsup:
// tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/main.ts'],
format: ['esm'],
target: 'node22',
sourcemap: true, // BẮT BUỘC: không có thì stack trace prod vô nghĩa
clean: true,
// KHÔNG bundle dependencies: native module (bcrypt, sharp) sẽ vỡ nếu bundle
skipNodeModulesBundle: true,
});{
"scripts": {
"build": "npm run typecheck && tsup", // type-check TÁCH RIÊNG vì tsup không làm
"start": "node dist/main.js"
}
}Pitfall. Bundle luôn node_modules để "gọn hơn" → vỡ với native addon (bcrypt, sharp, better-sqlite3 — chúng có file .node nhị phân) và vỡ với package dùng __dirname để tìm asset (Prisma engine, template file). Ở backend, bundle dependencies hiếm khi đáng; chỉ làm khi tối ưu cold start serverless và đã test kỹ.
7. package.json chuẩn cho service backend
Ví dụ đầy đủ:
{
"name": "my-api",
"version": "1.0.0",
"type": "module",
"private": true, // chặn `npm publish` nhầm lên registry công khai
"engines": { "node": ">=22.0.0" }, // cảnh báo khi ai đó dùng Node 18
"packageManager": "pnpm@9.12.0", // Corepack ép đúng version cho cả team + CI
"scripts": {
"dev": "tsx watch --env-file=.env src/main.ts",
"build": "npm run typecheck && tsup",
"start": "node dist/main.js",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"format": "prettier --write .",
"test": "vitest run",
"test:watch": "vitest",
"db:migrate": "prisma migrate deploy",
"db:studio": "prisma studio"
}
}Cơ chế của các trường ít người để ý.
engines— không tự chặn, nhưng npm cảnh báo và nhiều PaaS (Render, Railway, Heroku) dùng nó để chọn Node version khi deploy.packageManager+ Corepack — đảm bảo dev/CI/prod dùng cùng một version pnpm, tránh lockfile bị resolve khác nhau..nvmrc(file riêng, chứa đúng22) —nvm usetự chọn version; GitHub Actionssetup-nodeđọc được quanode-version-file.
Pitfall. Quên "private": true ở project công ty → một lệnh npm publish gõ nhầm là source code nội bộ nằm trên npm registry công khai vĩnh viễn. Không undo được (unpublish có giới hạn 72h và vẫn có mirror).
8. Package manager & lockfile
Định nghĩa. Lockfile (package-lock.json, pnpm-lock.yaml) ghim chính xác version của toàn bộ cây dependency, kể cả dependency của dependency.
Tại sao quan trọng. "express": "^5.1.0" nghĩa là "5.1.0 trở lên, dưới 6". Không có lockfile, hôm nay bạn cài 5.1.0, tuần sau CI cài 5.3.2 → build khác nhau, bug xuất hiện ngẫu nhiên mà không ai đổi dòng code nào.
Cơ chế.
npm install— có thể sửa lockfile để thoảpackage.json. Dùng ở máy dev.npm ci— xoánode_modules, cài đúng y hệt lockfile, lỗi ngay nếu lockfile lệchpackage.json. Dùng ở CI và Docker build, luôn luôn.pnpm— lưu package một lần trên đĩa, hardlink vào từng project (tiết kiệm hàng GB) và strict hơn: chặn "phantom dependency" — dùng package mà không khai báo trongpackage.json.
Pitfall. Dùng npm install trong Dockerfile. Nó có thể resolve version khác lockfile → image prod khác hẳn cái bạn test ở local, đúng loại bug khó nhất để truy. Trong Docker luôn là RUN npm ci --omit=dev.
9. ESLint (flat config) + Prettier
Định nghĩa. ESLint bắt lỗi logic/pattern nguy hiểm. Prettier format code (chỉ thẩm mỹ). Hai việc khác nhau, đừng gộp.
Tại sao quan trọng ở BE hơn FE. Ở FE lỗi format là xấu code. Ở BE có những rule bắt bug thật: no-floating-promises (quên await → lỗi bị nuốt, transaction không commit), require-await, no-misused-promises (truyền async function vào chỗ mong đợi hàm sync → Express nuốt lỗi luôn).
Ví dụ — eslint.config.js (flat config, ESLint 9+):
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist/**', 'coverage/**', '*.config.js'] },
js.configs.recommended,
// typeChecked: chậm hơn nhưng mở khoá các rule cần thông tin kiểu — đáng ở BE
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
},
rules: {
'@typescript-eslint/no-floating-promises': 'error', // quên await → lỗi im lặng
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'no-console': ['warn', { allow: ['error'] }], // ép dùng logger (GĐ8)
},
},
);Cơ chế no-floating-promises — vì sao nó đáng giá nhất:
// BUG: quên await. Request trả 200 ngay, nhưng email chưa gửi và nếu lỗi thì
// process nhận unhandledRejection — Node 15+ mặc định CRASH cả server.
app.post('/signup', async (req, res) => {
createUser(req.body); // ← ESLint bắt được dòng này
res.status(201).json({ ok: true });
});Pitfall. Bật rule format trong ESLint (indent, quotes) trong khi vẫn dùng Prettier → hai bên đánh nhau, save file là code nhảy qua nhảy lại. Để Prettier lo format, ESLint lo logic; nếu muốn chắc, thêm eslint-config-prettier để tắt hết rule format của ESLint.
10. strict: true không bảo vệ được biên hệ thống
Định nghĩa. TypeScript chỉ tồn tại lúc compile. Runtime không có type nào cả.
Tại sao quan trọng — đây là hiểu lầm chết người khi từ FE sang. Ở FE, data thường đến từ code của chính bạn. Ở BE, mọi thứ đi vào đều là dữ liệu lạ, không đáng tin: request body, query string, header, biến môi trường, response của API bên thứ ba, row từ DB do raw query. Gắn type cho chúng chỉ là lời hứa, không phải kiểm tra.
Ví dụ — cái sai và cái đúng:
// ❌ SAI: `as` là ép kiểu, KHÔNG kiểm tra gì. Runtime body có thể là bất cứ thứ gì.
app.post('/users', (req, res) => {
const body = req.body as { email: string; age: number };
sendEmail(body.email.toLowerCase()); // body.email = undefined → crash 500
});
// ✅ ĐÚNG: parse + validate ở biên, sau đó type mới có thật
import { z } from 'zod';
const CreateUser = z.object({ email: z.string().email(), age: z.number().int().min(0) });
app.post('/users', (req, res) => {
const parsed = CreateUser.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ errors: parsed.error.issues });
const body = parsed.data; // kiểu suy ra TỪ validator — đã được kiểm chứng
});Ví dụ — env cũng là biên:
// src/config/env.ts — validate MỘT LẦN lúc boot, fail fast
import { z } from 'zod';
const schema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.coerce.number().default(3000), // process.env luôn là string → coerce
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
});
// Sai env → process chết NGAY lúc boot, kèm thông báo rõ.
// Tốt hơn nhiều so với chết lúc 2h sáng ở request thứ 10.000.
export const env = schema.parse(process.env);Pitfall. Đọc process.env.X rải rác khắp codebase. Typo DATABSE_URL cho ra undefined im lặng, rồi lỗi hiện ra ở tận tầng DB dưới dạng thông báo khó hiểu. Quy tắc: process.env chỉ được đọc trong đúng một file — config/env.ts. Chỗ khác import env từ đó.
11. Debug — thay thế cho DevTools
Định nghĩa. Backend không có Chrome DevTools tab Elements/Network. Công cụ tương đương là Node inspector (chính là DevTools protocol) + structured log.
Cơ chế.
# Mở inspector, Chrome vào chrome://inspect là attach được
node --inspect dist/main.js
# Dừng ngay dòng đầu — để debug lỗi lúc boot
node --inspect-brk dist/main.js
# Dev với tsx + inspector
tsx watch --inspect src/main.tsVí dụ — .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [{
"type": "node",
"request": "launch",
"name": "Debug API",
"runtimeExecutable": "tsx",
"args": ["watch", "src/main.ts"],
"envFile": "${workspaceFolder}/.env",
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"] // không nhảy vào code lõi Node khi step
}]
}Pitfall. Deploy production không có source map → stack trace trỏ dist/main.js:1:48211 (một dòng, vì đã minify) — vô dụng. Luôn bật sourcemap: true khi build; Node tự đọc source map từ Node 12+ (bản cũ cần --enable-source-maps). Đừng bao giờ để --inspect mở trên server prod: cổng 9229 hở = toàn quyền thực thi code trên server.
12. Monorepo — khi nào cần, khi nào không
Định nghĩa. Nhiều package trong một repo, chia sẻ code qua workspace (pnpm workspace, npm workspaces, Turborepo/Nx cho caching build).
Tại sao quan trọng. Lý do thật để dùng monorepo là chia sẻ type giữa các service: định nghĩa DTO một lần, cả API và worker/CLI dùng chung, đổi contract là compile lỗi ngay ở mọi nơi.
Ví dụ — cấu trúc thực dụng:
apps/
api/ # NestJS/Express
worker/ # BullMQ consumer — dùng chung DB layer với api
packages/
shared/ # zod schema + type dùng chung
db/ # Prisma schema + client
pnpm-workspace.yamlPitfall. Dựng monorepo + Turborepo từ ngày đầu cho một service. Bạn trả toàn bộ chi phí (build phức tạp, Docker context rắc rối, IDE chậm, resolve type lỗi lạ) mà chưa có lợi ích nào. Quy tắc: bắt đầu single repo. Chỉ tách monorepo khi đã có service thứ hai thật sự cần dùng chung code.
Bài thực hành — dựng skeleton từ số 0
Không copy template. Gõ tay từng bước, mỗi bước chạy thử:
mkdir api && cd api && npm init -y
npm i express zod
npm i -D typescript tsx @types/node @types/express \
eslint @eslint/js typescript-eslint prettier tsup
npx tsc --initRồi lần lượt:
- Sửa
package.json: thêm"type": "module","private": true,"engines", và các script ở mục 7. - Viết
tsconfig.jsontheo mục 2. Chạynpm run typecheck→ phải pass. - Viết
src/config/env.tstheo mục 10. Cố tình xoáDATABASE_URLkhỏi.env→ xác nhận process chết lúc boot với thông báo rõ ràng, không phải chết mơ hồ ở request đầu tiên. - Viết
src/main.tsvới 1 route Express.npm run dev→ sửa file → xác nhận nó tự restart. npm run build && npm start→ phải chạy được. Đây là bước hầu hết mọi người bỏ qua và bị vỡ ở prod.- Thêm alias theo mục 4 → build lại → xác nhận
dist/vẫn chạy. - Viết
eslint.config.jstheo mục 9. Cố tình quên mộtawait→ xác nhậnnpm run lintbắt được. - Đặt
debuggertrong route, chạy VSCode debug, hit endpoint → dừng đúng dòng trong file.ts(không phải.js) → source map đang hoạt động.
Done khi
- [ ] Giải thích được khác biệt type-check vs transpile, và vì sao build bằng esbuild/tsup bắt buộc phải có bước
tsc --noEmitriêng. - [ ] Đọc hiểu từng dòng
tsconfig.json; biết vì sao BE không để"lib": ["DOM"]. - [ ] Xác định được một file là ESM hay CJS chỉ bằng cách nhìn
package.json+ đuôi file. - [ ] Giải thích được vì sao trong ESM+TS phải viết
import './x.js'cho filex.ts. - [ ] Biết
__dirnamekhông tồn tại trong ESM và thay bằng gì. - [ ] Cấu hình path alias và chứng minh
npm run build && npm startchạy được (không chỉnpm run dev). - [ ] Phân biệt
npm installvsnpm ci; biết vì sao Dockerfile luôn dùngnpm ci. - [ ] Có
eslint.config.jsbậtno-floating-promises; giải thích được bug mà rule này chặn. - [ ] Validate
process.envbằng zod tại một file duy nhất, fail-fast lúc boot. - [ ] Debug bằng VSCode, dừng đúng dòng trong file
.tsnhờ source map. - [ ] Nói được vì sao chưa dùng monorepo.
Câu hỏi mở / chưa giải quyết
- ESM hay CJS cho project chính? Nếu học GĐ6 (NestJS) thì Nest kéo bạn về CJS — cân nhắc giữ CJS xuyên suốt để tránh phải chuyển đổi giữa chừng.
tsup(nhanh, gọn) vstscthuần (chuẩn, ít bất ngờ): với NestJS cứ đểnest build; với Express thìtsupđáng thử.- Native Node type-stripping (
--experimental-strip-types) đang trưởng thành dần — theo dõi, nhưng chưa thay đượctsxkhi cần decorator (NestJS) hoặcenum. - Test runner:
vitest(nhanh, config gần như bằng 0, ESM-native) vsjest(hệ sinh thái lớn, NestJS mặc định). Chốt ở GĐ12.
