on this page
- What Node even is, quickly
- Why the version matters more than you’d think
- Check what you actually have
- Pin the version so everyone matches
- A .nvmrc file
- The engines field in package.json
- Version managers, and what each one is for
- Why CI, your host, and your teammates all run different Node
- Pin it for real, end to end
- Classic traps a mismatch sets for you
- Why this is worth two minutes of setup
You push code that runs perfectly on your laptop. A teammate clones it, runs npm install, and gets a wall of red. Or the deploy that worked yesterday now dies in the build log with some error you’ve never seen. You changed nothing. The code is identical, character for character.
Welcome to the most common ghost in early development: two machines running two different versions of Node, quietly disagreeing about what your code even means.
This guide is about that ghost, where it comes from, and how to pin it down so every machine that touches your project agrees on one thing.
What Node even is, quickly
Node.js (usually just “Node”) is the program that runs JavaScript outside a browser. Your browser has its own JavaScript engine baked in. Node is the version you install on your computer and your servers so JavaScript can run on the command line, on a backend, in a build step. When you type npm run dev or node server.js, Node is the thing actually executing the code.
Here’s the part nobody tells beginners: Node is not one fixed thing. It ships in versions, and they are released constantly. Node 22, Node 24, and so on. Each major version is a meaningfully different runtime.
That difference is the whole problem.
Why the version matters more than you’d think
When the Node version changes, three things underneath your code change with it.
The JavaScript features available. Newer Node versions understand newer JavaScript syntax and methods. Take Array.prototype.findLast, which lets you search an array from the end. It landed in Node 18. Write code using it, run it on your Node 24, and it works fine. Hand that same code to someone on Node 16 and they get findLast is not a function. The code didn’t break. Their runtime simply never learned that word.
The built-in APIs. Node ships libraries for files, networking, and other runtime tasks. Their availability and behavior depend on the version. If a guide uses an API your runtime does not provide, the code can fail before it does any useful work. Check the API’s version history in the Node documentation.
The bundled npm. Node comes with npm (Node Package Manager, the tool that installs the libraries your project depends on) baked in. Different Node versions bundle different npm versions. A newer npm can write a package-lock.json (the file that records the exact versions you installed) in a format an older npm chokes on. So an install that’s clean on your machine can fail on a machine running older Node, before a single line of your actual code runs.
Stack those three up and you get the real headache. “Works on my machine” is not a personality flaw. It’s usually two different Node versions producing two different realities.
Check what you actually have
Before fixing anything, find out what you’re running. Two commands:
node -v
# prints something like v24.11.1
That’s your Node version. The format is major.minor.patch, so v24.11.1 means major version 24. The major number is the one that matters most for compatibility, because that’s where the big behavioral changes land.
npm -v
# prints something like 10.2.4
That’s the npm version that came bundled with your Node. You usually don’t pick this directly. It rides along with whatever Node you installed.
Run both on your machine, then ask a teammate to run them on theirs. If the major Node numbers differ, you’ve found a likely source of “it works for me but not you” before it ever bites.
Pin the version so everyone matches
The fix is to write the intended Node version down somewhere the whole project can see it, instead of leaving each person to guess. There are a few places to write it, and they do different jobs.
A .nvmrc file
A .nvmrc is a one-line text file in your project root that says which Node version this project wants. That’s the entire file:
# .nvmrc
24
You can name a full version (for example, 24.11.1) or just name the major version (24) and let the tooling grab the latest 24.x. This file does nothing on its own. It’s a note. The thing that reads it is a version manager, which we’ll get to in a second. Commit this file. It belongs to the project, not to you.
The engines field in package.json
Your package.json can declare which Node versions your project supports, in a field called engines:
{
"name": "my-app",
"engines": {
"node": "24.x"
}
}
This says “this project expects a Node 24 release.” It does two useful things. First, it documents intent, so anyone reading your package.json knows the target. Second, some hosting providers read engines and use it to pick which Node to run during a build, which is exactly what you want.
Here’s the catch beginners trip on: by default, npm only warns when someone’s Node is outside this range. It prints a yellow message and installs anyway. If you want npm to actually refuse, you have to opt in by adding a file called .npmrc with one line:
# .npmrc
engine-strict=true
With that, an install on the wrong Node version fails loudly instead of mumbling a warning into the void. For a small project that’s often what you want, because a warning nobody reads is barely better than no warning at all.
Version managers, and what each one is for
A version manager lets you install multiple Node versions side by side and switch between them, so you’re not stuck with one global Node for every project you’ll ever touch. This matters the moment you have two projects: an old one on Node 22 and a new one on Node 24. Without a version manager, you end up reinstalling Node whenever you switch projects. There are three common ones.
nvm (Node Version Manager) is the classic. You switch versions per terminal session by hand:
nvm install 24 # download Node 24
nvm use 24 # use it in this shell
The thing to understand: nvm use only affects the terminal window you ran it in. Open a new terminal and you’re back to your default Node. It’s reliable and widely used, but it won’t switch automatically, so it’s easy to forget and end up on the wrong version without noticing.
fnm (Fast Node Manager) is a faster, drop-in alternative. Same idea, but it can auto-switch: set it up to watch for a .nvmrc, and when you cd into a project folder, fnm reads that file and switches Node for you. You stop thinking about it, which is the point. If you’re choosing today and don’t have a reason to pick otherwise, fnm is a comfortable default.
Volta takes a different angle. Instead of switching per shell, Volta pins the toolchain per project and picks the right version automatically whenever you run a command inside that project. You pin once:
volta pin node@24
After that, anyone with Volta who runs node or npm in your project gets Node 24, no manual switching, no remembering. The pin lives in package.json, so it travels with the repo.
Here’s how they line up:
| Tool | What it does | Where it reads the version | Switches automatically? |
|---|---|---|---|
.nvmrc | A one-line file naming the Node version | It’s the file itself; read by nvm and fnm | No, it’s just a note |
engines (package.json) | Declares the supported Node range | package.json; read by npm and some hosts | No, it warns or blocks |
| nvm | Installs and switches Node versions | Reads .nvmrc when you ask it to | No, manual nvm use |
| fnm | Faster nvm-style manager | Reads .nvmrc on entering a folder | Yes, on cd |
| Volta | Pins the toolchain per project | A pin stored in package.json | Yes, per command |
You only need one version manager. The .nvmrc and engines entries are worth having no matter which manager you (or your teammates) use, because they’re plain files anyone can read.
Why CI, your host, and your teammates all run different Node
Left to their own devices, no two environments default to the same Node, and that’s the part that quietly wrecks deploys.
Your CI (Continuous Integration, the service like GitHub Actions that runs your tests and builds when you push) starts from a clean machine every time. Whatever Node that machine defaults to is what you get, unless you tell it otherwise in the workflow config.
Your hosting provider does the same. Cloudflare Pages, Railway, Render: each picks some default Node version for builds unless you pin one. That default also drifts upward over time as the host updates, so a project that built fine in January can fail in June because the host quietly moved its default and nothing in your repo said which Node you needed.
And your teammates? Whatever they happened to install whenever they set up their machine. Could be anything.
This is the connective tissue to deployment. A build that passes on your laptop and fails on the host is, more often than any other single cause, a Node mismatch. The host built your code on a Node your code never met. If you’ve read How Localhost Becomes a Real Website, this is the unglamorous reason behind a chunk of those “works locally, breaks on deploy” mysteries.
Pin it for real, end to end
Concretely, three small moves cover the whole chain.
1. Add a .nvmrc so your local tooling and your teammates’ tooling switch to the right Node:
# .nvmrc
24
2. Add engines to package.json so npm and some hosts know the allowed range:
{
"engines": {
"node": "24.x"
}
}
3. Tell your host explicitly. Hosts choose runtimes in different ways. Cloudflare Pages supports NODE_VERSION; for another host, check its runtime settings and supported version files instead of assuming it reads the same variable:
NODE_VERSION=24
If pinning the host’s Node through a setting feels unfamiliar, the mechanics are the same as any other config value: Environment variables explained walks through how these get set per environment. Some CI setups also let you commit the Node version straight into the workflow file. Here’s that pin in a GitHub Actions step, which reads your .nvmrc so there’s a single source of truth:
# .github/workflows/ci.yml
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
Now the laptop, CI, and the host target the same major version. Use a full version where exact patch matching matters, and check the build logs to confirm what the host selected. That’s the goal: one version, written in places everything can see.
Classic traps a mismatch sets for you
These are the symptoms that look like four different bugs but share one root cause. When you hit one, check Node versions before you lose an afternoon.
| Symptom | What’s really happening | The fix |
|---|---|---|
npm install fails on the host, works for you | Host’s older npm can’t read your newer package-lock.json | Pin the host to your Node version with NODE_VERSION |
| Syntax or API error only in CI | CI runs a Node missing a feature your code uses | Pin CI’s Node via .nvmrc and setup-node |
Error: ... was compiled against a different Node.js version | A native module was built for the wrong Node | Match the Node version, then delete node_modules and reinstall |
| Passes locally, breaks on deploy | Host defaulted to a different Node than yours | Set the host’s Node explicitly; don’t trust the default |
The native module one deserves a flag. Some packages (database drivers, image tools, anything with a C++ part under the hood) compile themselves against the exact Node you installed them on. Switch Node versions without reinstalling and that compiled piece is now built for a runtime that’s gone. The reinstall, after deleting node_modules, rebuilds it for the Node you’re actually on.
Why this is worth two minutes of setup
A pinned Node version means the runtime that builds your app on the host is the same runtime that ran it on your laptop, which is the same one your teammate has, which is the same one CI tested against. The build that passes locally is the build that ships, because there’s nothing left to disagree about.
When you go to put a real project online, half the deploy failures people blame on “the host being weird” are just a version nobody pinned. Write the number down in a .nvmrc, declare it in engines, set it on your host, and you’ve removed one of the loudest, dumbest sources of “but it worked for me” from your life. Same Node everywhere, same build everywhere, one less ghost in the machine.
progress is stored in this browser only