Node.js is the engine under your JavaScript car. If the engine is too old, the car coughs. If it is too new, a weird light may blink. So it helps to tell everyone which Node version your project expects.
TLDR: Put your Node version in package.json using the engines field. The most common setting is "node": ">=18" or an exact range like "node": "18.x". This tells people, tools, and hosting platforms what Node version your app needs. For stricter local control, add tools like Volta, nvm, or npm’s engine-strict.
Why specify a Node version?
Imagine a team of developers. Sam uses Node 16. Mia uses Node 20. The server uses Node 18. Everyone runs the same project. But the project acts different on each machine. Fun? Not so much.
Node versions matter because features change. Some APIs appear in newer versions. Some behavior changes. Some packages require a minimum Node version. Even tiny differences can cause big bugs.
By specifying the Node version, you make your project say:
“Hello, dear human. Please run me with this kind of Node. I am picky, but polite.”
The main way: use the engines field
The standard place to specify Node version is the engines field in package.json.
{
"name": "my-fun-app",
"version": "1.0.0",
"engines": {
"node": ">=18"
}
}
This says: “Use Node 18 or newer.” Simple. Clean. Friendly.
You can also be more specific:
{
"engines": {
"node": "18.x"
}
}
This says: “Use any Node 18 version.” For example, Node 18.17.0 is fine. Node 20 is not.
Or you can use a range:
{
"engines": {
"node": ">=18 <21"
}
}
This says: “Use Node 18, 19, or 20. But not 17. Not 21.” Very tidy.
Understanding version symbols
Node versions use semantic versioning. That sounds fancy. It just means versions look like this:
18.19.1
That means:
- 18 is the major version.
- 19 is the minor version.
- 1 is the patch version.
The major version is the big one. It can bring major changes. The patch version is usually tiny fixes.
Here are common symbols:
>=18means Node 18 or newer.<21means older than Node 21.18.xmeans any Node 18 version.^18.0.0usually means version 18, but not 19.~18.19.0usually means 18.19 patch versions only.
For most apps, ">=18" or ">=18 <21" is enough.
Does engines force the version?
Here is the funny bit. The engines field is like a sign on a door. It says, “Please wear shoes.” But it may not tackle barefoot people.
By default, npm often shows a warning if the Node version is wrong. It may not stop installation. You might see something like:
npm WARN EBADENGINE Unsupported engine
This warning is useful. It tells you something is off. But if you want npm to be stricter, you need another setting.
Make npm strict with engine-strict
If you want npm to reject the wrong Node version, use engine-strict.
You can add a .npmrc file to your project:
engine-strict=true
Now npm will be less chill. If someone uses the wrong Node version, npm can fail the install. This is useful for teams. It prevents “works on my machine” goblins.
Specify package manager too
Node is not the only tool in town. You may also want to specify npm, pnpm, or Yarn. This helps people install packages the same way.
Use the packageManager field:
{
"packageManager": "npm@10.2.3"
}
Or:
{
"packageManager": "pnpm@9.0.0"
}
This is not exactly the Node version. But it is part of the same “please use the right tools” story.
Use Volta for extra control
Volta is a tool that pins Node and package managers per project. It is smooth. It is fast. It is like a tiny butler for JavaScript tools.
You can add a volta field to package.json:
{
"volta": {
"node": "20.11.1",
"npm": "10.2.4"
}
}
When someone enters the project folder, Volta can automatically use the right Node version. No drama. No chanting commands under a full moon.
This is great for teams. It is also great for your future self. Future you has enough problems.
What about nvm?
nvm is another popular way to manage Node versions. It usually uses a file called .nvmrc.
20.11.1
That file is not inside package.json. But many projects use both:
package.jsonwithenginesfor tools and platforms..nvmrcfor local developer setup.
This combo is very common. It is clear. It is friendly. It works well.
What version should you choose?
Good question. Do not just pick a random number. Node versions have different support timelines.
In many cases, choose an LTS version. LTS means Long Term Support. It is the safe family car. Not the rocket skateboard.
For production apps, LTS is usually best. It gets security updates. It is stable. Hosting providers support it well.
For experiments, you can use newer versions. Just know they may surprise you. Sometimes surprises are cake. Sometimes they are bees.
Examples for different projects
Here are some simple patterns.
For a modern app:
{
"engines": {
"node": ">=20"
}
}
For a safer production app:
{
"engines": {
"node": ">=18 <21"
}
}
For a very strict app:
{
"engines": {
"node": "20.11.1"
}
}
Exact versions can be useful. But they can also be annoying. Every tiny Node update needs a change. Most teams prefer a range.
Hosting platforms care too
Many hosting platforms read package.json. The engines field can tell them which Node version to use during build and runtime.
This can prevent nasty deployment bugs. Your app works locally. Then it explodes online. Why? The server used a different Node version. The engines field helps stop that story.
A good final setup
Here is a nice, practical package.json setup:
{
"name": "space-toast-app",
"version": "1.0.0",
"engines": {
"node": ">=20 <21"
},
"packageManager": "npm@10.2.4"
}
And if you use Volta:
{
"volta": {
"node": "20.11.1",
"npm": "10.2.4"
}
}
If you want strict npm installs, add this to .npmrc:
engine-strict=true
Final thoughts
Specifying Node version in package.json is a small move. But it saves time. It reduces confusion. It makes your project easier to run.
Use engines.node as your main signal. Add packageManager if you want consistent installs. Add Volta or .nvmrc if you want smoother local setup.
Your project will be happier. Your teammates will be happier. And the bug goblins will have to find a new hobby.