Node.js and npm
Every JavaScript project Mangrove's engineers own, from partner-api to store-app, starts with a package.json and hundreds of packages someone else wrote. This lesson shows what Node is, how npm decides which versions to install, and how a lockfile and a pinned Node version end "works on my machine".
By the end you can
- Explain what Node adds to JavaScript, and read every important field of a
package.json - Choose between dependencies, devDependencies and peerDependencies
- Read semver ranges (
^,~, exact) and predict which version npm will install - Use the lockfile and
npm cicorrectly, and upgrade safely withnpm outdated,npm updateandnpm audit - Pin the team's Node version with
.nvmrcandengines
The everyday picture
When a Mangrove Market store orders from a supplier, the order form does not only say "rice". It gives an exact item code, and often a rule for substitutes: "Basmati rice 5 kg, any batch from this season is fine, but not a different brand". The supplier picks the newest stock that follows the rule. The goods-received note (GRN) then records exactly which batch arrived, with its lot number.
package.json is the order form: package names, each with a rule for acceptable versions. npm is the supplier, choosing the newest version that follows each rule. And package-lock.json is the GRN: the exact version, download link and fingerprint of every package that arrived. If the next store orders with only the order form, it may get a newer batch. If it orders with the GRN in hand, it gets exactly the same goods.
What Node is, and what package.json says
Node runs JavaScript outside the browser. It is three things together: V8, the same JavaScript engine as Chrome; libuv, a C library that gives Node its event loop and does file and network work in the background (The event loop); and Node's own APIs such as node:fs, node:http and node:crypto. There is no DOM and no window; instead you get files, processes and sockets. Node has an even-numbered LTS line every year; use 22 or 24 for real projects.
node -p "const v = process.versions; ({ node: v.node, v8: v.v8, uv: v.uv })"{ node: '24.21.0', v8: '13.6.233.17-node.53', uv: '1.52.1' }A project is a folder with a package.json. These are the fields you will read in every review:
name,version: matter when you publish a library; for an app, set"private": trueso it can never be published by accident."type": "module":.jsfiles are ES modules (Prototypes, classes and modules). Without it they are CommonJS.scripts: named commands.npm run buildruns thebuildscript;npm testandnpm startwork withoutrun. While a script runs,node_modules/.binis on the PATH, so"lint": "eslint ."finds the project's own ESLint. Arguments after--are passed through:npm run report -- D027.dependencies,devDependencies,peerDependencies: the three lists in the next section.engines: which Node versions the project supports, such as{"node": ">=24"}.
npx runs a package's command: the project's own copy if it is installed, otherwise it downloads it to a cache first. That is how you run npx serve or npx @nestjs/cli new without a global install.
Three kinds of dependency
dependencies are needed when the app runs: NestJS, Prisma Client, a CSV parser. devDependencies are needed only to build and test: TypeScript, Vitest, ESLint, @types/node. The difference matters at deploy time, because a production install with npm ci --omit=dev skips the dev list, making the server image smaller and giving attackers less code to work with. Install with npm install pkg or npm install -D pkg.
peerDependencies are for plugins. @nestjs/testing must use the same @nestjs/common as your app, not a private copy, so it declares @nestjs/common as a peer: "I need it, but the project provides it". If the project's version does not fit, npm 7 and later stop with an ERESOLVE error:
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error
npm error While resolving: partner-api@1.0.0
npm error Found: @nestjs/common@11.2.7
npm error node_modules/@nestjs/common
npm error @nestjs/common@"^11.2.7" from the root project
npm error
npm error Could not resolve dependency:
npm error peer @nestjs/common@"^12.0.0" from @nestjs/testing@12.1.2
npm error node_modules/@nestjs/testing
npm error dev @nestjs/testing@"12" from the root project
npm errorThe message is long, but read it like a stack trace. "Found" is what you have (NestJS 11). "Could not resolve" is what the new package wants (12). The fix is to install matching versions, here @nestjs/testing@11, or to upgrade every @nestjs/* package together. --legacy-peer-deps or --force make the error go away and leave a mismatched install behind, so treat them as a red flag in a PR.
Quick check
The stock API uses Vitest only to run its tests. Which list should Vitest go in?
Vitest is needed to test the code, not to run the server, so it is a dev dependency. In dependencies it would ship to production for nothing. Peer dependencies are only for plugins that must share the host project's copy of a package.
Semver ranges: which version will npm install?
Packages use semantic versioning: MAJOR.MINOR.PATCH, such as 5.6.0. A patch fixes bugs, a minor adds features without breaking anything, a major may break your code. You met it for releases in Branching strategies, releases and versions; here it decides what lands in node_modules. In package.json each dependency has a range, and npm installs the highest published version that satisfies it:
^1.4.2(caret, npm's default): any1.x.xfrom1.4.2up, so minor and patch updates, never2.0.0.~1.4.2(tilde):1.4.xfrom1.4.2up, so patch updates only.1.4.2: exactly that version.1.x,>=1.5.0 <2.0.0,*: an x-range, explicit comparators, anything.
Two details catch people. Below 1.0.0, a caret is stricter, because the package is still unstable: ^0.3.1 means 0.3.x only. And prerelease versions such as 2.1.0-beta.1 are never picked by a normal range; you only get one if your range names a prerelease itself.
Quick check
Published versions are 2.3.1, 2.3.8, 2.4.0 and 3.0.0, and there is no lockfile. Which one does ~2.3.1 install?
A tilde allows patch updates only, and npm picks the highest match, so 2.3.8. The number in the range is the minimum, not the answer. 2.4.0 would need a caret, and 3.0.0 is a new major that neither range allows.
Try the ranges below on a made-up price-format package. Tap ^1.4.2, then ~1.4.2, and watch which versions light up and which one npm picks. Then try ^0.3.1 and *, and look at what happens to 2.1.0-beta.1. Last, tick the lockfile box.
- Caret allows new minors (1.9.3), tilde stops at the patch level (1.4.9). Neither crosses to 2.0.0, and a prerelease is never chosen by accident.
- With a lockfile,
npm ciinstalls exactly the locked version, whatever newer versions exist. The range only matters when the lock is missing or no longer fitspackage.json.
The lockfile, npm ci and node_modules
Ranges mean two installs on different days can give different trees. package-lock.json fixes that. After every install, npm writes down the exact version it chose for every package in the tree, including the dependencies of your dependencies, with the download link and an integrity hash so a changed file is rejected. Commit the lockfile, and review changes to it in PRs like any other code.
Two commands use it differently. npm install uses the lock when it still satisfies package.json, adds what is missing, and updates the lock when you add or change a dependency. npm ci ("clean install") is for CI, Docker builds and new laptops: it deletes node_modules, installs exactly what the lock says, never changes it, and fails if the lock and package.json disagree. That failure is a feature: it catches the PR where someone edited package.json and forgot to commit the new lock.
Where do packages go? npm builds the tree as flat as it can. If two packages need ms and the same version fits both, one copy sits at node_modules/ms. If they need versions that do not overlap, one goes to the top and the other is nested inside the package that needs it. When your code does import 'csv-parse', Node looks for node_modules/csv-parse in the current folder, then the parent folder, and so on up to the root. This is why one npm install can produce hundreds of folders, and why node_modules is never committed.
Quick check
The committed lockfile says csv-parse 5.5.0, package.json says ^5.5.0, and 5.6.0 came out yesterday. What does npm ci install on CI today?
npm ci installs the locked version and ignores newer releases. The two files are still in sync, because 5.5.0 satisfies ^5.5.0, so nothing fails. Only npm install without a lock, or npm update, would move to 5.6.0.
npm install turns ranges into exact versions: it picks the highest published version each range allows, records it in package-lock.json with an integrity hash, and fills node_modules. Later, npm ci reproduces exactly that tree from the lockfile, even after 5.7.0 is published.debug pins ms to 2.0.0 and send needs 2.1.3. Both cannot sit at the top of node_modules, so npm nests the second copy inside send. Where the ranges overlap, npm keeps one shared copy instead ("deduped" in npm ls).Pinning Node, and the rest of the toolbox
The lockfile pins packages, but not Node itself. If one developer runs Node 24 and another Node 22, code that uses a newer feature works for one and crashes for the other. Two small files fix that. .nvmrc holds the version (just 24), and a version manager reads it. "engines": {"node": ">=24"} in package.json documents the same rule; with engine-strict=true in the project's .npmrc, npm refuses to install on the wrong version instead of only warning. Set the same version in CI (node-version-file: .nvmrc in actions/setup-node, see GitHub Actions: continuous integration) and in the Dockerfile.
Which version manager? fnm works on Windows, macOS and Linux and reads .nvmrc and .node-version. nvm is the classic one for macOS and Linux, and nvm-windows is a separate project for Windows. Volta's own README now says it is unmaintained, so do not start new projects on it.
Monorepos. If partner-api and a shared types package live in one repo, npm workspaces link them: list the folders under "workspaces" in the root package.json, and one npm install at the root installs everything and symlinks node_modules/@mangrove/shared to your local folder. npm test --workspaces runs each package's tests; -w apps/api targets one. TypeScript for a JavaScript team uses this to share a type between the API and the app.
pnpm and Yarn are alternative package managers with the same package.json. pnpm stores each version once on disk and links it into projects, which saves a lot of space and is strict about undeclared dependencies; Yarn has its own lockfile and an optional mode without node_modules. Each has its own lockfile, so a team picks one and sticks to it, and the repo should contain only that one lockfile.
Build it
Create a small package with scripts and one dependency, read its lockfile entry, upgrade it safely, then reproduce "works on my machine" and fix it. The outputs below come from Node 24.21 with npm 11; npm 10 on Node 22 prints the same messages. The caret and tilde rules from the semver section are also a Practice exercise below, which you can solve and check in your browser.
-
Create the package and tidy package.json
npm pkg seteditspackage.jsonfrom the terminal;--jsonmakestruea boolean instead of a string.Terminalmkdir sales-report cd sales-report npm init -y npm pkg set description="Daily store sales report" type=module scripts.report="node report.js" npm pkg set private=true --json npm pkg delete main keywords author license scripts.testJSON{ "name": "sales-report", "version": "1.0.0", "description": "Daily store sales report", "scripts": { "report": "node report.js" }, "type": "module", "private": true } -
Add a dependency and read what npm wrote
We install an older version on purpose, so there is something to upgrade later.
Terminalnpm install csv-parse@5.5.0Outputadded 1 package, and audited 2 packages in 1s 1 moderate severity vulnerability To address all issues (including breaking changes), run: npm audit fix --force Run `npm audit` for details.package.jsonnow has the range"csv-parse": "^5.5.0"(npm adds the caret). The lockfile has the exact version, where it came from and its fingerprint:JSON{ "name": "sales-report", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sales-report", "version": "1.0.0", "dependencies": { "csv-parse": "^5.5.0" } }, "node_modules/csv-parse": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.0.tgz", "integrity": "sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw==", "license": "MIT" } } } -
Write the script and run it
JavaScript// report.js: total sales per store from a CSV, optionally for one product name import { readFileSync } from 'node:fs'; import { parse } from 'csv-parse/sync'; const rows = parse(readFileSync('sales.csv', 'utf8'), { columns: true, cast: true }); const search = process.argv[2]; // e.g. "lentils (red)" const match = search ? new RegExp(RegExp.escape(search), 'i') : /.*/; // RegExp.escape: Node 24+ const totals = {}; for (const row of rows.filter((r) => match.test(r.name))) { totals[row.siteId] = (totals[row.siteId] ?? 0) + row.qty * row.priceCents; // integer cents } const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }); for (const [siteId, cents] of Object.entries(totals)) console.log(siteId, usd.format(cents / 100));OutputsiteId,articleCode,name,qty,priceCents D027,A1001,Rice 5 kg,4,840 D027,A2040,Soybean oil 2 L,1,675 G104,A1001,Rice 5 kg,2,840 G104,A3112,Lentils (red) 1 kg,3,290 D027,A3112,Lentils (red) 1 kg,1,290Terminalnpm run report npm run report -- "lentils (red)"Output> sales-report@1.0.0 report > node report.js D027 $43.25 G104 $25.50 > sales-report@1.0.0 report > node report.js lentils (red) G104 $8.70 D027 $2.90The money stays in integer cents until the very last step, where
Intl.NumberFormatturns it into dollars for display (Working with data).RegExp.escapemakes the brackets in "(red)" match literally instead of as regex syntax. It is new in Node 24, which matters in step 7. -
Commit, then see what the lockfile protects you from
Make the project a Git repo (What Git really is), with
node_modulesignored and the lockfile committed.Terminalgit init echo node_modules/ > .gitignore git add . git commit -m "feat: add daily sales report"PowerShellgit init Set-Content .gitignore node_modules/ git add . git commit -m "feat: add daily sales report"Now do what people sometimes do to "fix" an install: delete the lock and install again. (
rm -ralso works in PowerShell, as an alias ofRemove-Item -Recurse.)Terminalrm package-lock.json rm -r node_modules npm install npm ls csv-parse git status --shortOutputsales-report@1.0.0 /home/you/sales-report `-- csv-parse@5.6.0 M package-lock.jsonWithout the lock, the range
^5.5.0picked a newer version than the one you tested. Put the committed lock back and install from it:Terminalgit restore package-lock.json npm ci npm ls csv-parseOutputsales-report@1.0.0 /home/you/sales-report `-- csv-parse@5.5.0Exactly 5.5.0 again, and
git statusis clean. Last, editpackage.jsonby hand to"csv-parse": "^7.0.3"without updating the lock, and runnpm ci:Outputnpm error code EUSAGE npm error npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing. npm error npm error Invalid: lock file's csv-parse@5.5.0 does not satisfy csv-parse@7.0.3 npm errorThat is the error CI shows when a PR forgets the lockfile. Undo the edit with
git restore package.json. -
See what is out of date, and update within the range
Terminalnpm outdatedOutputPackage Current Wanted Latest Location Depended by csv-parse 5.5.0 5.6.0 7.0.3 node_modules/csv-parse sales-report exit 1Read the three columns: Current is installed, Wanted is the highest version your range
^5.5.0allows, Latest is the newest published.npm outdatedexits with code 1 when anything is behind, so it can fail a CI step.npm updatemoves to Wanted, never across a major:Terminalnpm update npm ls csv-parseOutputsales-report@1.0.0 /home/you/sales-report `-- csv-parse@5.6.0The lockfile moved to 5.6.0;
package.jsonstill says^5.5.0, which already allows 5.6.0. This time the change is on purpose: run the report again to check it still works, then commit the lock change on its own branch (Branches and merging). -
Check for known vulnerabilities, and upgrade a major on purpose
Terminalnpm auditOutput# npm audit report csv-parse <7.0.2 Severity: moderate node-csv: Prototype replacement still reachable via columns path - https://github.com/advisories/GHSA-8cw4-87c7-c6xx fix available via `npm audit fix --force` Will install csv-parse@7.0.3, which is a breaking change node_modules/csv-parse 1 moderate severity vulnerability To address all issues (including breaking changes), run: npm audit fix --forceThe fix is a new major, so
npm audit fixwithout--forcewill not apply it, and--forcewould apply it without you reading anything. Do it deliberately instead: read the package's changelog for 6.0.0 and 7.0.0 (6.0.0 renamed several options; this script uses none of them), install the major, and test.Terminalnpm install csv-parse@7 npm pkg get dependencies npm run report -- "lentils (red)"Outputchanged 1 package, and audited 2 packages in 702ms found 0 vulnerabilities { "csv-parse": "^7.0.3" } > sales-report@1.0.0 report > node report.js lentils (red) G104 $8.70 D027 $2.90 -
Reproduce "works on my machine"
Commit, and let a teammate on Node 22 clone the repo. The lockfile is there, so
npm cigives them exactly the same packages. It still fails:Terminalnode -v npm ci npm run report -- "lentils (red)"Outputv22.22.0 added 1 package, and audited 2 packages in 607ms found 0 vulnerabilities > sales-report@1.0.0 report > node report.js lentils (red) file:///home/you/sales-report/report.js:7 const match = search ? new RegExp(RegExp.escape(search), 'i') : /.*/; // RegExp.escape: Node 24+ ^ TypeError: RegExp.escape is not a function at file:///home/you/sales-report/report.js:7:42 at ModuleJob.run (node:internal/modules/esm/module_job:343:25) at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26) at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5) Node.js v22.22.0 exit 1Same packages, different Node. Now fix it for everyone:
Terminalecho 24 > .nvmrc echo engine-strict=true > .npmrc npm pkg set engines.node=">=24"PowerShellSet-Content .nvmrc 24 Set-Content .npmrc engine-strict=true npm pkg set engines.node=">=24"On the Node 22 laptop, the install now stops at once with a clear reason instead of a crash at runtime:
Outputnpm error code EBADENGINE npm error engine Unsupported engine npm error engine Not compatible with your version of node/npm: sales-report@1.0.0 npm error notsup Not compatible with your version of node/npm: sales-report@1.0.0 npm error notsup Required: {"node":">=24"} npm error notsup Actual: {"npm":"10.9.4","node":"v22.22.0"} npm error A complete log of this run can be found in: /home/you/.npm/_logs/2026-10-16T17_43_32_473Z-debug-0.log exit 1The teammate installs the right Node with their version manager. With fnm, run
fnm installandfnm usein the project folder; both read.nvmrc. Addeval "$(fnm env --use-on-cd --shell bash)"to~/.bashrc(or the zsh or PowerShell line from fnm's README) and it switches automatically oncd.
| Command | What it does | Use it |
|---|---|---|
npm install | Installs from the lock if it fits, updates the lock when package.json changes | On your machine, when adding or changing dependencies |
npm ci | Deletes node_modules, installs exactly the lock, fails if out of sync | CI, Docker builds, new laptops |
npm outdated | Current, wanted (by range) and latest versions | Before planning upgrades |
npm update | Moves to the newest version each range allows | Routine minor and patch upgrades, on a branch |
npm audit | Checks the tree against known advisories | In CI, and before releases |
npm ls pkg / npm explain pkg | Who depends on a package, and which copies exist | Finding duplicates and why something is installed |
Bring it to your team
Ask these when a PR touches package.json, or when a build breaks "for no reason".
- Is
package-lock.jsoncommitted, and does this PR's lock diff match the dependency change it describes? - Do CI and the Dockerfile run
npm ci, and does the production image use--omit=dev? - Where is our Node version written down:
.nvmrc,engines, CI and the Dockerfile? Do they agree? - This adds a new dependency: who maintains it, how many packages does it pull in, and could we do without it?
- This is a major upgrade: did someone read the changelog, and which tests cover the code that uses it?
- Who looks at
npm auditor Dependabot alerts, and how fast do we act on a high one?
Red flags
package-lock.jsonin.gitignore, or deleted to make an error go away.npm installin CI or in the Dockerfile.--legacy-peer-depsor--forceadded to make an install pass, with no explanation.npm audit fix --forcerun without reading which majors it changed.- Two lockfiles in one repo (
package-lock.jsonandpnpm-lock.yaml).
Practice
Match versions against caret and tilde ranges
Write satisfies(version, range) for the three range forms in this lesson: exact (1.4.2), caret (^1.4.2) and tilde (~1.4.2). Versions are plain MAJOR.MINOR.PATCH strings, or carry a prerelease tag such as 2.1.0-beta.1, which these ranges never match. Remember the rule below 1.0.0: ^0.3.1 means 0.3.x from 0.3.1 up. pick is written for you: it returns the highest version that satisfies the range, the way npm chooses.
function parse(v) {
const [major, minor, patch] = v.split('.').map(Number);
return { major, minor, patch };
}
function satisfies(version, range) {
if (version.includes('-')) return false; // prereleases never match a normal range
// TODO: handle '^1.4.2', '~1.4.2' and an exact '1.4.2'
return version === range;
}
function pick(versions, range) { // written for you: the highest match
const ok = versions.filter((v) => satisfies(v, range));
ok.sort((a, b) => {
const x = parse(a), y = parse(b);
return x.major - y.major || x.minor - y.minor || x.patch - y.patch;
});
return ok.at(-1) ?? null;
}
const published = ['1.4.2', '1.4.9', '1.5.0', '1.9.3', '2.0.0', '2.1.0-beta.1'];
test('caret allows new minors and patches, never the next major', () => {
expect(satisfies('1.9.3', '^1.4.2')).toBe(true);
expect(satisfies('1.4.1', '^1.4.2')).toBe(false);
expect(satisfies('2.0.0', '^1.4.2')).toBe(false);
expect(pick(published, '^1.4.2')).toBe('1.9.3');
});
test('tilde allows new patches only', () => {
expect(satisfies('1.4.9', '~1.4.2')).toBe(true);
expect(satisfies('1.5.0', '~1.4.2')).toBe(false);
expect(pick(published, '~1.4.2')).toBe('1.4.9');
});
test('below 1.0.0 a caret stays inside the minor', () => {
expect(satisfies('0.3.7', '^0.3.1')).toBe(true);
expect(satisfies('0.3.0', '^0.3.1')).toBe(false);
expect(satisfies('0.4.0', '^0.3.1')).toBe(false);
});
test('an exact version matches only itself, and prereleases never match', () => {
expect(pick(published, '1.4.2')).toBe('1.4.2');
expect(satisfies('2.1.0-beta.1', '^2.0.0')).toBe(false);
});
Show a solution
function parse(v) {
const [major, minor, patch] = v.split('.').map(Number);
return { major, minor, patch };
}
function satisfies(version, range) {
if (version.includes('-')) return false;
const op = range[0] === '^' || range[0] === '~' ? range[0] : '';
const v = parse(version);
const min = parse(op ? range.slice(1) : range);
const cmp = v.major - min.major || v.minor - min.minor || v.patch - min.patch;
if (op === '') return cmp === 0; // exact
if (cmp < 0) return false; // below the minimum
if (op === '~') return v.major === min.major && v.minor === min.minor;
if (min.major > 0) return v.major === min.major; // caret from 1.0.0 up
if (min.minor > 0) return v.major === 0 && v.minor === min.minor; // ^0.3.1
return v.major === 0 && v.minor === 0 && v.patch === min.patch; // ^0.0.3
}
function pick(versions, range) {
const ok = versions.filter((v) => satisfies(v, range));
ok.sort((a, b) => {
const x = parse(a), y = parse(b);
return x.major - y.major || x.minor - y.minor || x.patch - y.patch;
});
return ok.at(-1) ?? null;
}Review a dependency pull request
A teammate's PR for partner-api is titled "Fix install error". The diff changes four things: package.json moves @nestjs/testing from ^11.1.0 to ^12.1.2 while every other @nestjs/* package stays on 11; the CI workflow changes npm ci to npm install --legacy-peer-deps; package-lock.json is deleted; and .gitignore gains the line package-lock.json. Write the review comment you would leave: what is wrong, and what you would ask for instead.
- You name the real problem: a NestJS 12 testing package against NestJS 11, the peer conflict that ERESOLVE reported
- You ask for matching versions: the testing package on 11, or every
@nestjs/*package upgraded together - You ask to keep
npm ciin CI and drop--legacy-peer-deps - You ask for the lockfile to be committed again, taken out of
.gitignore, and its diff reviewed
Show a model answer
"Thanks for chasing this. The install error is the peer conflict: @nestjs/testing@12 needs @nestjs/common@12, and we are on 11. The flag and the deleted lock hide the conflict instead of fixing it, and without a lock every CI build installs whatever is newest that day. Could you install @nestjs/testing@11 for now (or open a separate PR that moves all @nestjs/* packages to 12 together), run npm install so the lock updates, put package-lock.json back in Git and out of .gitignore, and keep plain npm ci in CI? Then the lock diff should show only the testing package changing."
Check yourself
The published versions are 1.4.2, 1.4.9, 1.5.0, 1.9.3 and 2.0.0, and there is no lockfile. What does "^1.4.2" install?
A caret allows minor and patch updates within the same major, and npm picks the highest match, so 1.9.3. Patch-only is the tilde (~1.4.2 would give 1.4.9). Exactly 1.4.2 needs "1.4.2" without a symbol.
CI runs npm ci and fails with "package.json and package-lock.json are not in sync". What most likely happened?
npm ci refuses to guess when the two files disagree. That protects you: the fix is to run npm install locally and commit the lock. Switching CI to npm install or deleting the lock hides the problem and makes builds unreproducible.
Where does TypeScript belong in a NestJS API's package.json?
The server runs the JavaScript that tsc produced, so the compiler is a build tool. Keeping it in devDependencies lets npm ci --omit=dev leave it out of the production image. Peer dependencies are for plugins that must share the host's copy of a package.
Flashcards
Key takeaways
- Node is V8 plus libuv plus system APIs.
package.jsonis the order form: scripts, ranges, engines. - Caret allows new minors, tilde only new patches, and npm installs the highest version that fits. Below 1.0.0 the caret is stricter.
- The lockfile is the GRN: commit it, review it, and use
npm ciin CI and Docker. - Upgrade in steps:
npm outdated,npm updatefor in-range changes, majors one at a time after reading the changelog. - Pin Node with
.nvmrc,enginesandengine-strict, and use the same version in CI and production.
Sources
- package.json, npm Docs
- npm ci, npm Docs
- Semantic Versioning 2.0.0, semver.org