# npm là gì? Trình quản lý gói mặc định của Node.js

> npm là trình quản lý gói của Node.js, nơi package.json, semver và package-lock.json phối hợp để mỗi lần cài đặt đều cho ra cùng một cây phụ thuộc.

Nguồn: https://camnangai.com/ky-thuat/npm-la-gi  
Xuất bản: 2026-08-07

**npm** là trình quản lý gói (package manager) mặc định cho Node.js, cho phép bạn cài đặt, chia sẻ
  và quản lý các thư viện mã nguồn (dependencies) một cách nhất quán giữa những môi trường phát
  triển khác nhau.

Trả lời thẳng câu hỏi npm là gì: đây là hạ tầng để tái sử dụng mã nguồn và quản lý những cây phụ
thuộc phức tạp trong một dự án [JavaScript](https://camnangai.com/ky-thuat/javascript-la-gi) hiện đại.

Hệ thống npm gồm ba thành phần riêng biệt nhưng vận hành thống nhất: trang web (npmjs.com), giao
diện dòng lệnh (CLI) và kho lưu trữ (Registry). Trang web là nơi bạn tìm kiếm gói, thiết lập hồ sơ
cá nhân và quản lý các tổ chức để điều phối quyền truy cập mã nguồn. Registry là cơ sở dữ liệu
khổng lồ chứa hàng triệu gói mã nguồn JavaScript cùng thông tin meta đi kèm. CLI là công cụ chính
bạn dùng qua terminal để tương tác với hai thành phần còn lại.

Tính đến tháng 9/2022, npm Registry đã ghi nhận hơn 2,1 triệu gói phần mềm, giữ vị thế kho lưu trữ
mã nguồn cho một ngôn ngữ duy nhất lớn nhất thế giới. Nó phục vụ mọi nhu cầu từ dự án cá nhân nhỏ lẻ
đến hệ thống backend doanh nghiệp và các framework frontend phức tạp.


  Với một kỹ sư, npm không dừng ở việc tải thư viện: đây là nơi bạn khoá phiên bản, kiểm tra bảo mật
  và tự động hoá các tác vụ build.

Nắm vững cách npm vận hành là điều kiện tiên quyết để dựng được build pipeline ổn định trong môi
trường [CI/CD](https://camnangai.com/ky-thuat/devops-la-gi) chuyên nghiệp.

![npm là trình quản lý gói của Node.js, hình ảnh chủ đề của bài viết](https://camnangai.com/articles/npm-la-gi/01-infographic-vn.webp)

## npm là gì?

**npm** (Node Package Manager) hoạt động dựa trên sự phối hợp giữa ba thành phần chính. Thành phần
đầu tiên là Registry, thực chất là một cơ sở dữ liệu công khai lưu trữ các tập tin nén (tarball)
của mã nguồn và siêu dữ liệu (metadata) của hàng triệu gói JavaScript. Registry này vận hành trên
nền tảng cơ sở dữ liệu CouchDB, cho phép kỹ sư khắp thế giới truy xuất mã nguồn gần như tức thời
qua mạng lưới phân phối nội dung (CDN).

![Ba thành phần của npm: trang web, giao diện dòng lệnh và kho lưu trữ Registry](https://camnangai.com/articles/npm-la-gi/02-vn.webp)

Thành phần thứ hai là giao diện dòng lệnh (CLI), công cụ được cài đặt tự động khi bạn cài
[Node.js](https://camnangai.com/ky-thuat/nodejs-la-gi).
CLI là cầu nối trực tiếp giữa máy tính cục bộ và Registry. Qua CLI, bạn khởi tạo dự án với
`npm init`, cài thư viện với `npm install`, hoặc xuất bản gói mã nguồn của riêng mình. CLI cung cấp
các thuật toán giải quyết xung đột phiên bản và tối ưu hoá cây phụ thuộc trong thư mục dự án.

Thành phần thứ ba là trang web npmjs.com, đóng vai trò cổng thông tin và quản trị. Tại đây bạn tìm
kiếm thư viện dựa trên độ phổ biến, chất lượng mã nguồn hoặc mức độ bảo trì, đồng thời quản lý các
tổ chức (Organizations) và nhóm (Teams). Điều này quan trọng với doanh nghiệp cần quản lý các gói
riêng tư và phân quyền truy cập cho từng bộ phận phát triển.

Bạn dùng npm cho nhiều việc hơn là tải mã nguồn. Nó tải các công cụ chạy độc lập phục vụ quá trình
phát triển, và chạy gói trực tiếp mà không cần cài đặt vĩnh viễn thông qua `npx`. npx giúp tránh làm ô nhiễm môi trường biến toàn cục và đảm bảo bạn luôn dùng phiên bản
mới nhất của một công cụ trong các tác vụ build một lần.

## package.json: bản khai của một dự án

Tệp `package.json` là bản kế hoạch (manifest) của bất kỳ dự án Node.js nào. Nó định danh dự án qua
tên, phiên bản và các mô tả kỹ thuật, đồng thời liệt kê toàn bộ thư viện phụ thuộc cần thiết. Khi
bạn chuyển dự án giữa các máy tính hoặc máy chủ, bạn không cần mang theo hàng nghìn tệp trong thư
mục thư viện — chỉ cần tệp `package.json` là đủ để tái tạo lại toàn bộ môi trường bằng `npm install`.

![Các loại phụ thuộc trong package.json: dependencies, devDependencies, optionalDependencies và peerDependencies](https://camnangai.com/articles/npm-la-gi/03-vn.webp)

Trong cấu trúc của `package.json`, các phụ thuộc được phân loại theo mục đích dùng lúc chạy hay lúc
phát triển. `dependencies` là các gói bắt buộc để ứng dụng hoạt động thực tế, ví dụ framework
Express. `devDependencies` là các gói chỉ phục vụ quá trình code, test hoặc build, như Jest hoặc
Webpack. Về mặt kỹ thuật, cờ `-S` (hoặc `--save`) đưa gói vào `dependencies`, còn `-D` (hoặc
`--save-dev`) đưa vào `devDependencies`. Từ npm 5, `-S` đã thành mặc định, nhưng nhiều kỹ sư vẫn
viết ra để tường minh hoá ý định trong tài liệu hướng dẫn.

`optionalDependencies` là phần quan trọng nhưng thường bị bỏ qua. Loại này cho phép cài các gói mà
nếu gặp lỗi lúc build — thường do không tương thích hệ điều hành hoặc thiếu trình biên dịch C++ —
npm vẫn tiếp tục quy trình cài đặt thay vì dừng lại. Đổi lại, bạn phải tự viết mã xử lý khi các thư
viện này không tồn tại. Một loại khác là `peerDependencies`, thường thấy khi phát triển plugin hoặc
thư viện con, yêu cầu dự án chính phải có sẵn một phiên bản cụ thể của thư viện nền tảng, ví dụ
[React](https://camnangai.com/ky-thuat/react-la-gi).

Đây là cấu trúc một tệp `package.json` tiêu chuẩn:

```json
{
  "name": "enterprise-backend-service",
  "version": "1.2.4",
  "description": "Hệ thống quản lý logic nghiệp vụ",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.2",
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "jest": "^29.3.1",
    "webpack": "^5.75.0"
  },
  "optionalDependencies": {
    "fsevents": "^2.3.2"
  }
}
```

## Semver: dấu ^ và ~ quyết định bạn nhận bản nào

**npm** quản lý phiên bản theo tiêu chuẩn Semantic Versioning (Semver), với định dạng
`Major.Minor.Patch`. `Major` thay đổi khi có API mới phá vỡ tương thích ngược. `Minor` thay đổi khi
thêm tính năng mới theo cách tương thích ngược. `Patch` dành cho các bản vá lỗi. Hiểu cách npm giải
quyết các dải phiên bản là kỹ năng cốt lõi để tránh lỗi runtime bất ngờ khi cập nhật thư viện.

![Dấu caret cho phép cập nhật cả minor và patch, còn dấu tilde chỉ cho phép patch](https://camnangai.com/articles/npm-la-gi/04-vn.webp)

npm dùng hai tiền tố phổ biến: Tilde (`~`) và Caret (`^`). Dấu Tilde rất thận trọng, chỉ cho phép
cập nhật các bản `Patch`. Ví dụ, `~1.2.3` chấp nhận phiên bản từ 1.2.3 đến dưới 1.3.0. Dấu Caret
linh hoạt hơn và là mặc định của npm hiện nay, cho phép cập nhật cả `Minor` lẫn `Patch` nhưng tuyệt
đối không chạm vào `Major`: `^1.2.3` cho phép cập nhật đến dưới 2.0.0. Nó dựa trên giả định rằng
tác giả thư viện tuân thủ nghiêm ngặt việc không đưa breaking changes vào bản Minor.

Một khái niệm cần nắm là "Major Zero" (v0.x.x). Theo đặc tả Semver, phiên bản 0 dành cho giai đoạn
phát triển ban đầu và API được coi là chưa ổn định. Trong trường hợp này, npm đối xử với dấu Caret
giống hệt Tilde, nghĩa là `^0.1.2` chỉ cho phép cập nhật lên đến dưới 0.2.0. Cơ chế này bảo vệ ứng
dụng khỏi những thay đổi API đột ngột trong giai đoạn thư viện chưa trưởng thành. Khoảng 82% các
gói trên npm Registry vẫn đang ở phiên bản 0.x.x, cho thấy sự do dự của cộng đồng trong việc cam
kết ổn định API ở mức 1.0.0.

Kỹ sư kỳ cựu thường khuyến nghị đưa dự án lên phiên bản 1.0.0 ngay khi mã nguồn đã chạy thật hoặc
API đã ổn định. Kẹt lại ở "Magic Zero" gây khó cho việc quản lý phụ thuộc tự động và tạo tâm lý
không an tâm cho người dùng. Khi API đã định hình, nhảy qua rào cản 1.0.0 là cách tốt nhất để dùng
Semver như một công cụ truyền đạt ý định kỹ thuật.

## npm install làm gì với thư mục node_modules?

Khi bạn chạy `npm install`, CLI thực hiện một thuật toán phân giải cây phụ thuộc. Nó đọc
`package.json`, so sánh với `package-lock.json` nếu có, rồi tải các tarball từ Registry về, giải nén
và đặt vào thư mục `node_modules`. Quy trình này xử lý không chỉ các phụ thuộc trực tiếp mà cả phụ
thuộc gián tiếp — phụ thuộc của phụ thuộc — tạo nên một cấu trúc cây khổng lồ.

![npm v2 lồng các phụ thuộc nhiều tầng, còn npm v3 nâng chúng lên thành cấu trúc phẳng](https://camnangai.com/articles/npm-la-gi/05-vn.webp)

Ở các phiên bản cũ, từ npm v2 trở về trước, cấu trúc `node_modules` được xây theo kiểu lồng nhau,
dẫn đến trùng lặp các gói giống nhau ở nhiều cấp và vượt quá giới hạn đường dẫn trên Windows. Từ
npm v3, thuật toán **hoisting** được áp dụng: npm cố gắng "nâng" các phụ thuộc dùng chung lên cấp
cao nhất của `node_modules`. Cấu trúc thư mục nhờ đó phẳng hơn, tiết kiệm không gian đĩa và tránh
tải lặp cùng một thư viện.

Nếu hai thư viện yêu cầu hai phiên bản không tương thích của cùng một gói — thư viện A cần Lodash
v3, thư viện B cần Lodash v4 — npm thực hiện **deduplication** (khử trùng lặp). Phiên bản được dùng
nhiều nhất được
đưa ra ngoài cùng, còn phiên bản kia được giữ lồng bên trong thư mục `node_modules` của thư viện
yêu cầu nó. Đây là cơ chế giúp npm giải quyết "dependency hell" — địa ngục phụ thuộc — mà vẫn đảm
bảo tính đúng đắn lúc chạy.

Thư mục `node_modules` phình to rất nhanh và có thể chứa hàng chục nghìn tập tin nhỏ. Đó là lý do
kỹ sư luôn đưa thư mục này vào `.gitignore`. Quản lý hàng trăm MB build artifacts trong hệ thống
quản lý phiên bản là không hiệu quả; thay vào đó, bạn dựa vào tính nhất quán của
`package-lock.json` để tái tạo lại đúng thư mục này trên bất kỳ môi trường nào.

## package-lock.json và npm ci: vì sao cài lại luôn giống nhau

`package-lock.json` được tạo ra hoặc cập nhật tự động mỗi khi bạn cài một gói mới. Nó ghi lại trạng
thái chính xác của toàn bộ cây phụ thuộc tại thời điểm đó. Trong khi `package.json`
dùng các dải phiên bản linh hoạt như `^`, tệp lock lưu phiên bản cụ thể kèm địa chỉ tải về và mã
băm toàn vẹn (integrity hash). Chạy cài đặt trên 10 máy khác nhau sẽ cho ra kết quả giống hệt nhau.

![npm install có thể cập nhật lockfile, còn npm ci xoá node_modules và cài đúng theo lockfile](https://camnangai.com/articles/npm-la-gi/06-vn.webp)

Trường `integrity` trong tệp lock dùng chuẩn Subresource Integrity với mã băm SHA-512. Đây là một
chốt chặn bảo mật: nếu kẻ tấn công thay đổi nội dung một tarball nhưng giữ nguyên số phiên bản,
**npm** phát hiện sai khác về mã băm và từ chối cài đặt. Tệp lock cũng ghi lại quan hệ logic giữa
các gói, giúp npm bỏ qua bước tính toán phân giải cây phụ thuộc và tăng tốc độ cài đặt.

Khác biệt giữa `npm install` và `npm ci` là kiến thức bắt buộc cho môi trường chuyên nghiệp.
`npm install` có quyền cập nhật tệp lock nếu thấy phiên bản mới thoả mãn điều kiện trong
`package.json`. Ngược lại, `npm ci` (Clean Install) là lệnh nghiêm khắc dành cho CI/CD: nó xoá sạch
thư mục `node_modules` hiện tại, đọc trực tiếp từ tệp lock, và báo lỗi ngay nếu tệp lock không khớp
với `package.json`. Dùng `npm ci` loại bỏ hoàn toàn các thay đổi ngoài ý muốn khi triển khai.

Cấu trúc trường `integrity` bảo vệ tính toàn vẹn trong `package-lock.json` trông như sau:

```json
"node_modules/lodash": {
  "version": "4.17.21",
  "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
  "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}
```

## npm scripts: chạy tác vụ dự án bằng một câu lệnh

Trường `scripts` trong `package.json` cho phép bạn định nghĩa các bí danh cho những câu lệnh shell
phức tạp. Thay vì bắt mọi thành viên trong nhóm nhớ các tham số dài dòng của Webpack, Docker hay bộ
test, bạn cấu hình chúng một lần rồi chạy bằng `npm run <task-name>`. Cả đội ngũ phát triển dùng
chung một quy trình.

Một cơ chế quan trọng của **npm** scripts là nó tự động thêm thư mục `node_modules/.bin` vào biến
PATH khi thực thi. Điều này cho phép bạn gọi trực tiếp các công cụ dòng lệnh của thư viện — `tsc`,
`eslint`, `prettier` — mà không cần cài chúng toàn cục trên máy. Nó giải quyết triệt để xung đột
phiên bản công cụ giữa các dự án khác nhau trên cùng một máy tính.

npm cũng cung cấp các script vòng đời (lifecycle scripts) như `preinstall`, `postinstall`,
`pretest`, `posttest`. Bạn chạy trực tiếp `npm test` hoặc `npm start` mà không cần từ khoá `run`.
Các script `pre` và `post` tự động chạy trước và sau lệnh chính, cho phép bạn thiết lập môi trường,
dọn dẹp thư mục build hoặc chạy các bước kiểm tra một cách nhất quán.

Ví dụ về các script tự động hoá tác vụ build và test:

```json
{
  "scripts": {
    "clean": "rm -rf dist",
    "prebuild": "npm run clean",
    "build": "webpack --mode production --config webpack.config.js",
    "test": "jest --coverage --verbose",
    "lint": "eslint src/**/*.ts"
  }
}
```

## Install script và chuỗi cung ứng: npm v12 đổi mặc định

Các script vòng đời như `preinstall` mang lại sự tiện lợi, nhưng chúng cũng là kẽ hở lớn nhất cho
các cuộc tấn công chuỗi cung ứng. Những mã độc như sâu Shai-Hulud hay Trojan Miasma RAT lợi dụng cơ chế
này để thực thi mã độc ngay khi người dùng gõ `npm install`. Kẻ tấn công chèn các đoạn mã đánh cắp
token npm, GitHub Personal Access Token hoặc cấu hình cloud ngay trong quá trình cài thư viện.

![npm v12 chuyển ba hành vi cài đặt sang chế độ phải bật thủ công](https://camnangai.com/articles/npm-la-gi/08-vn.webp)

Các chiến dịch hiện đại đã đạt mức độ tinh vi cao. Mã độc có thể đọc trực tiếp bộ nhớ của tiến
trình runner qua `/proc/<Runner.Worker>/mem` trong môi trường [GitHub](https://camnangai.com/ky-thuat/github-la-gi)
Actions để rút OIDC token, rồi gửi thẳng về registry. Chúng còn dùng kỹ thuật đầu độc cache, ghi đè
lên các bản lưu tạm của pnpm hoặc npm để chèn mã độc vào những lần build sau của nhà bảo trì dự án.

Hạ tầng điều khiển của loại mã độc này rất kiên cố với nhiều lớp dự phòng. Nếu máy chủ chính bị
chặn, chúng truy vấn các cổng Ethereum RPC để tìm địa chỉ mới ghi trên smart contract, hoặc dùng
relay của giao thức Nostr và mạng BitTorrent DHT để duy trì liên lạc. Một biến thể còn cài dịch vụ
chạy ngầm kiểm tra token GitHub đã đánh cắp mỗi 60 giây; nếu phát hiện token bị thu hồi, nó thực
thi `rm -rf ~/` để xoá sạch thư mục home của nạn nhân.

npm v12 trả lời rủi ro này bằng cách đổi chính các mặc định. Ba hành vi trước đây tự chạy trong
`npm install` giờ phải bật thủ công: `allowScripts` mặc định tắt, nên các script vòng đời
`preinstall`, `install`, `postinstall` và bước build node-gyp ngầm không còn tự chạy; `--allow-git`
mặc định là `none`, nên phụ thuộc lấy từ Git không còn được phân giải; và `--allow-remote` mặc định
là `none`, nên phụ thuộc tải từ URL từ xa cũng vậy. Để duyệt những script bạn thực sự tin tưởng,
chạy `npm approve-scripts --allow-scripts-pending` rồi commit danh sách cho phép sinh ra trong
`package.json`. Cả ba hành vi này đã có sẵn kèm cảnh báo từ npm 11.16.0, nên bạn kịp chuẩn bị trước
khi nâng cấp.

## npm, Yarn hay pnpm: khác nhau ở đâu?

npm là tiêu chuẩn, còn [Yarn](https://camnangai.com/ky-thuat/yarn-la-gi) và pnpm ra đời để giải quyết các bài toán cụ thể
về hiệu năng và quản lý
đĩa. Khác biệt lớn nhất nằm ở cách tổ chức `node_modules`. **npm** dùng cấu trúc phẳng thông qua
hoisting, và điều đó dẫn tới vấn đề **phụ thuộc ma** (phantom dependencies): một thư viện có thể
truy cập các gói
mà nó không hề khai báo trong `package.json`, chỉ vì các gói đó được nâng lên cấp cha. Hệ quả là
những lỗi rất khó hiểu khi cấu trúc cây phụ thuộc thay đổi.

![npm và Yarn dùng node_modules phẳng, còn pnpm liên kết từ một kho lưu trữ chung](https://camnangai.com/articles/npm-la-gi/09-vn.webp)

pnpm giải quyết triệt để vấn đề này bằng liên kết cứng (hard link) và liên kết tượng trưng
(symlink). pnpm chỉ lưu duy nhất một bản sao của mỗi phiên bản gói trong một kho lưu trữ trung tâm
trên đĩa, rồi tạo symlink trỏ về kho đó trong thư mục dự án. Cách tiếp cận này tiết kiệm dung lượng
đĩa rất lớn và chặn hoàn toàn phụ thuộc ma, vì mã nguồn chỉ nhìn thấy đúng những gì nó đã khai báo.

Về tệp khoá phiên bản, mỗi trình quản lý có định dạng riêng: npm dùng `package-lock.json`, Yarn
dùng `yarn.lock` với định dạng tuỳ chỉnh, còn pnpm dùng `pnpm-lock.yaml`. Định dạng khác nhau nhưng
mục tiêu cuối cùng vẫn là đảm bảo tính nhất quán của cây phụ thuộc. Tốc độ cũng là một điểm khác
biệt: pnpm thường dẫn đầu nhờ khả năng cài song song và cơ chế liên kết tệp hiệu quả hơn việc sao
chép tarball của npm.

Một nguyên tắc quan trọng: tuyệt đối không trộn lẫn các trình quản lý gói trong cùng một dự án.
Chạy cả `npm install` lẫn `yarn install` sẽ tạo ra các tệp lock xung đột, dẫn đến môi trường không
nhất quán và gây khó cho việc gỡ lỗi. Hãy chọn một công cụ phù hợp với hạ tầng của đội — pnpm cho
các dự án monorepo lớn, hoặc npm khi bạn cần tương thích cao nhất với các công cụ CI/CD mặc định.

## Nên bắt đầu với npm từ đâu?

Bắt đầu một dự án chuẩn mực bằng `npm init`. Trong vận hành thực tế, hãy ưu tiên `npm ci` cho tất
cả các pipeline tự động để đảm bảo mọi lần cài đặt đều giống nhau, và luôn commit
`package-lock.json` vào Git để cả đội dùng chung một cây phụ thuộc.

**npm** vẫn là lựa chọn an toàn và phổ biến nhất nếu bạn hiểu rõ ba thứ: cơ chế khoá phiên bản, mã
băm toàn vẹn, và những gì `npm install` được phép tự chạy trên máy bạn. Chạy `npm audit` định kỳ để
vá các lỗ hổng đã biết trong chuỗi cung ứng.

## Tài liệu tham khảo

- [About npm — npm Docs](https://docs.npmjs.com/about-npm)
- [An introduction to the npm package manager — Node.js Learn](https://nodejs.org/learn/getting-started/an-introduction-to-the-npm-package-manager)
- [What is npm? A Node Package Manager Tutorial for Beginners — freeCodeCamp](https://www.freecodecamp.org/news/what-is-npm-a-node-package-manager-tutorial-for-beginners/)
- [package.json — npm Docs](https://docs.npmjs.com/cli/v11/configuring-npm/package-json)
- [Semver: Tilde and Caret — NodeSource](https://nodesource.com/blog/semver-tilde-and-caret)
- [How to Understand package-lock.json in Node.js — OneUptime](https://oneuptime.com/blog/post/2026-01-22-nodejs-package-lock-json/view)
- [npm vs Yarn vs pnpm: Package Manager Selection Guide 2026](https://tomodahinata.com/en/blog/npm-vs-yarn-vs-pnpm-package-manager-comparison-guide)
- [npm install-time security and GAT bypass2fa deprecation — GitHub Changelog](https://github.blog/changelog/2026-07-08-npm-install-time-security-and-gat-bypass2fa-deprecation/)
- [The npm Threat Landscape: Attack Surface and Mitigations — Unit 42](https://unit42.paloaltonetworks.com/monitoring-npm-supply-chain-attacks/)
