Fix babel-node: not found on Heroku (devDependencies pruned)
Make a Babel-based Node app start on Heroku: see what the Node.js buildpack removes after the build, then move the compile step to build time so npm start only needs node.
TL;DR#
On Heroku, the Node.js classic buildpack installs your devDependencies, runs the build, then prunes the devDependencies before the app is released. babel-node comes from @babel/node, which Babel's own install line puts in devDependencies, so it is gone when the web process runs npm start. The shell reports babel-node: not found and npm exits with code 127.
The fix that holds up: compile with @babel/cli in a build script, which Heroku runs before pruning, and make start run plain node on the compiled output. Setting the config var NPM_CONFIG_PRODUCTION=false also makes the error go away. It does that by skipping the prune, which leaves Babel compiling your code on every boot in production.
The error#
The Stack Overflow question behind this page dates from 2016. The asker's app ran with npm start and heroku local, but crashed on the dyno:
2016-04-21T22:20:50.467435+00:00 app[web.1]: > babel-node index.js
2016-04-21T22:20:50.479490+00:00 app[web.1]: sh: 1: babel-node: not found
2016-04-21T22:20:50.555769+00:00 app[web.1]: npm ERR! errno ENOENT
2016-04-21T22:20:50.556142+00:00 app[web.1]: npm ERR! qc-server@1.0.0 start: `babel-node index.js`The same failure still happens with current tools. The open-source heroku-buildpack-nodejs (commit baf13a3, run with bin/compile in the heroku/heroku:24-build Docker image on 10 October 2026) built a small app whose start script is babel-node src/index.js, with @babel/core, @babel/node and @babel/preset-env 8.x in devDependencies. The buildpack picked Node 24.21.0 and npm 11.19.0. Running npm start in the built directory afterwards gave:
-----> Pruning devDependencies
removed 158 packages in 547ms
> qc-server@1.0.0 start
> babel-node src/index.js
sh: 1: babel-node: not foundThe shell exited with status 127 (command not found). The modern npm prints less than npm 3 did in 2016 (no ELIFECYCLE block in this run), but the line that matters is the same: the shell cannot find a babel-node command.
Why the buildpack removes babel-node#
Heroku's Node.js Classic Buildpack Builds page describes three main steps: install, prune, cache. It installs both dependencies and devDependencies, and then: "After running the installation, Heroku prunes out the packages declared under devDependencies before deploying the application."
The buildpack source shows the order and the condition. bin/compile installs, runs your build script, saves the cache, and only then calls prune_devdependencies. In lib/package_managers/npm.sh, that function runs npm prune when NODE_ENV is production (the buildpack's default) and NPM_CONFIG_PRODUCTION is empty. The buildpack's bin/release declares the default process web: npm start when there is no Procfile. So the process that starts your app is the one that runs after the prune.
Three facts combine into the crash:
- Babel's @babel/node documentation gives the install command
npm install --save-dev @babel/core @babel/node, so most projects list it underdevDependencies. - The
babel-nodecommand exists only as a link innode_modules/.bin, created by the@babel/nodepackage. The npm run documentation says it "addsnode_modules/.binto thePATHprovided to scripts"; when the package is pruned, the link goes with it, and there is nothing left for the shell to find. The same folder explains why a locally installed compiler works through an npm script but a bare tsc is not recognised in a Windows shell. - Your local
node_modulesstill holds everydevDependency, which is whynpm startandheroku localwork on your machine.
In the reproduction, the build directory after the prune held none of the three Babel packages. npm ls reported them as missing, and no Babel binary was left in node_modules/.bin:
+-- UNMET DEPENDENCY @babel/node@^8.0.0
npm error missing: @babel/node@^8.0.0, required by qc-server@1.0.0Fix: compile at build time, start with node#
Babel's documentation is direct about running babel-node in a deployed app: "You should not be using babel-node in production. It is unnecessarily heavy, with high memory usage due to the cache being stored in memory." It compiles the whole app at every start. The durable fix removes babel-node from the start path instead of fighting the prune.
- Add the Babel command-line compiler to your
devDependencies. It is only needed during the build, so it can be pruned afterwards.
npm install --save-dev @babel/cli @babel/core @babel/preset-env- Add a
buildscript that compilessrctodist, and pointstartat the compiled file. This is thepackage.jsonthat passed in the reproduction:
{
"name": "qc-server",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "babel src -d dist",
"start": "node dist/index.js"
},
"devDependencies": {
"@babel/cli": "^8.0.0",
"@babel/core": "^8.0.0",
"@babel/preset-env": "^8.0.0"
}
}The "type": "module" line is only needed when the compiled output still contains import or export. With Babel 8 and the config below, it did: the compiled dist/index.js kept the import statement from the source. Without that line, Node 24 still ran the file, but first printed this warning:
(node:650) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///tmp/build/dist/index.js is not specified and it doesn't parse as CommonJS.
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.- Keep the Babel config at the project root so the build can find it, then commit and deploy. A JSON config such as this
babel.config.jsonworks whatever the package type is. If you already have aheroku-postbuildscript, Heroku runs it instead ofbuild(per the same Heroku page), so put the compile command there.
{ "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]] }If your config is a babel.config.js written with module.exports, adding "type": "module" breaks the build itself. Run in a node:24-bookworm container, npm run build stopped with:
ReferenceError: module is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/w/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.Renaming the file to babel.config.cjs was the only change needed, and the next build printed Successfully compiled 1 file with Babel. It is the same per-file module decision that produces exports is not defined in ES module scope, which covers the .cjs rename and the other options in detail.
- Add
dist/to.gitignoreso the compiled output is always produced by the build, never committed by hand.
echo "dist/" >> .gitignoreVerify the fix#
Watch the build log on your next deploy: the build line should appear before the prune, and the prune should still remove the Babel packages. In the reproduction with the package.json above, the buildpack logged:
> babel src -d dist
Successfully compiled 1 file with Babel (91ms).
-----> Pruning devDependencies
removed 135 packages in 309msThen npm start ran without any Babel package installed:
> node dist/index.js
qc-server listening on 5000The process exited with status 0 after its log line, because the test server closes itself once it is listening.
On a real app, run heroku logs --tail after the release and confirm that the dyno reaches your listening message instead of crashing with code 127.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
What changes when you only move @babel/node to dependencies#
The accepted answer on the Stack Overflow question installs babel-cli as a regular dependency. That is the legacy Babel 6 package (its npm registry entry still tags a Babel 6 release as latest); from Babel 7 onwards the command lives in @babel/node. Another answer suggests moving the @babel/* packages to dependencies. If you move only @babel/node, the babel-node binary survives the prune, but the error changes. In the reproduction, @babel/core survived too (it is a peer dependency of @babel/node, which npm keeps), while the preset named in the Babel config was pruned:
removed 85 packages in 314ms
babel binaries in node_modules/.bin: babel-node
Error: Cannot find package '@babel/preset-env' imported from /tmp/build/babel-virtual-resolve-base.js
code: 'ERR_MODULE_NOT_FOUND'So "move it to dependencies" only works if you move @babel/core, @babel/node and every preset and plugin in your Babel config, which ships the whole compiler to production. If you see Cannot find package '@babel/preset-env' after a half-done move, that is why.
The NPM_CONFIG_PRODUCTION=false shortcut#
The second Stack Overflow answer sets a config var instead of changing the code. Heroku documents it under "Skip pruning using npm":
heroku config:set NPM_CONFIG_PRODUCTION=falseIn the reproduction, the buildpack then logged Skipping because NPM_CONFIG_PRODUCTION is 'false', kept all three Babel packages, and babel-node src/index.js started the app. It works, but the slug keeps every devDependency, and the app compiles itself on each start, which is what Babel's documentation warns against. Do not confuse it with NPM_CONFIG_PRODUCTION=true: the Heroku page lists that one under "Only install dependencies", so the devDependencies are not installed and the babel command would not exist when the build script runs.
When you no longer need Babel at all#
If your Babel config only existed to turn import and export into something Node could run, you may not need Babel at all: current Node runs ES modules natively. This does not apply if Babel also compiles JSX, TypeScript, decorators or proposal syntax that Node does not support. With "type": "module" in package.json, no Babel packages, and "start": "node src/index.js", the same buildpack run started the app on Node 24.21.0 with an empty dependency tree. Files that still use require or module.exports then need the .cjs extension, for the reason shown in the fix section above. Relative imports also need their extension once Babel is gone: Node resolves ./config/app literally and throws ERR_MODULE_NOT_FOUND, covered in the relative import missing .js fix.
A last trap from Babel's docs: running npx babel-node without @babel/node and @babel/core installed locally makes npx fetch the outdated legacy babel-node 6.x, not the current package.
Pruning is one of several ways a build server differs from your laptop. Another is that the buildpack uses npm ci with your lockfile when one is committed. If that step fails instead with many Missing: … from lock file lines, the lockfile does not match package.json under the npm config the build uses, and the page on npm ci lockfile mismatches caused by legacy-peer-deps covers that failure. For babel-node: not found itself, the rule is simple: whatever npm start calls on Heroku must live in dependencies or be produced by the build, because the devDependencies are gone by the time the dyno starts.
Further reading: Fix EACCES: permission denied, mkdir node_modules/.cache.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix EACCES: permission denied, mkdir node_modules/.cache
Reproduced in Docker: why the cache directory cannot be created, the one-line Dockerfile move that fixes it, and why an old volume keeps the error alive after a rebuild.
Fix esbuild spawnSync ETXTBSY in Docker: Node 20.3 io_uring
Why npm install of esbuild started failing with ETXTBSY on node:20.3 images in June 2023, what changed in libuv and Docker since, and the build fix that holds today.
Fix npm ENOTEMPTY: directory not empty, rename
Read both paths in the rename error, preserve your lockfile, and distinguish a stale retirement directory from an npm regression or a file lock.


