Node.js via mise — .nvmrc / .node-version / package.json engines auto-detection, LTS vs current, the corepack + packageManager field, and when to use the npm: backend for global CLIs. Use when setting up Node for a project or explaining how mise handles Node versions.
Node has the most version-file formats of any language (.nvmrc, .node-version, package.json#engines.node, .tool-versions, mise.toml). mise handles all of them if you opt in, and normalizes the resolution order so there's one clear winner per project.
mise picks the Node version in this order (highest priority first):
mise.toml / .mise.toml [tools] node — explicit wins.mise.local.toml (per-machine override, gitignored)..tool-versions — asdf-compatible.[settings]
idiomatic_version_file_enable_tools = ["node"]
With that setting, mise reads .nvmrc, .node-version, and .package.json#engines.nodeWithout the opt-in, mise ignores the idiomatic files — this is deliberate. Silent reading of untracked files was deemed surprising.
mise.toml[tools]
node = "24" # major
node = "24.1" # major.minor
node = "24.1.0" # exact
node = "lts" # latest LTS (resolves at install time)
node = "latest" # avoid for team projects — moves when new releases land
Team rule: pin to at least major.minor. "latest" is fine for tinkering, terrible for reproducibility.
LTS rule: "lts" resolves to the current LTS major. Fine for long-lived services. For exact reproducibility, pin "22" (the major). Upgrading to a new LTS is then a deliberate commit.
package.json#engines vs mise.toml{
"name": "myapp",
"engines": {
"node": ">=22",
"pnpm": ">=9"
}
}
They coexist: engines.node: ">=22" says "we require at least 22", mise.toml node = "22.12" says "this project uses 22.12 specifically". mise's idiomatic reader treats engines as a loose hint; the explicit [tools] line is authoritative.
packageManagerpackageManager is the modern way to pin pnpm, yarn, or bun:
{
"packageManager": "[email protected]"
}
With corepack enable (mise can do this via a task hook), Node auto-shims pnpm / yarn to the exact version in packageManager. You don't need to install pnpm via mise — corepack handles it.
Wire it in mise.toml:
[tools]
node = "24"
[hooks]
# Run corepack enable once after mise install, so the first `pnpm` invocation works.
enter = "corepack enable 2>/dev/null || true"
This is the cleanest pnpm/yarn pinning story. No npm install -g pnpm drift.
npm: backend for global CLIsmise has an npm: backend for installing npm packages as mise-managed tools:
[tools]
node = "24"
"npm:typescript" = "5.6"
"npm:prettier" = "3.4"
"npm:@anthropic-ai/claude-code" = "latest"
"npm:@google/gemini-cli" = "latest"
Benefits vs npm install -g:
Trade-offs:
PATH fiddling.Use npm: for: anything you'd otherwise npm install -g. Don't use it for project deps — those belong in package.json.
.nvmrc with just a number (22) — mise interprets as 22.x latest. Exact matches like v22.12.0 also work.package.json#engines.node with a range (">=22 <23") — mise picks the highest matching installed version, or the latest in the range if none installed. Can be surprising in CI..tool-versions > .nvmrc > .node-version > engines.node. Don't mix them; pick one.nvm use in shell rc — migrate to mise activate to avoid double-shimming. See mise-migrate-from-nvm.[tools]
node = "24"
[env]
NODE_ENV = { required = "dev or production or test" }
[tasks.dev]
run = "node --watch src/index.js"
[tasks.test]
run = "node --test"
[tools]
node = "24"
[hooks]
enter = "corepack enable 2>/dev/null || true"
[tasks.install]
run = "pnpm install --frozen-lockfile"
sources = ["package.json", "pnpm-lock.yaml", "packages/*/package.json"]
[tasks.build]
depends = ["install"]
run = "pnpm -r build"
# ~/.config/mise/config.toml
[tools]
node = "lts"
"npm:@anthropic-ai/claude-code" = "latest"
"npm:prettier" = "latest"
"npm:typescript-language-server" = "latest"
mise-lang-node-packages — npm vs pnpm vs yarn vs bun, corepack details.mise-migrate-from-nvm — moving off nvm.mise-env-directives — [env] depth.mise-tool-versioning — version pinning across all languages.mise.jdx.dev/lang/node.html.