Running Node.js Scripts in JavaScript
How to run plain JavaScript files with Node.js - direct execution, npm scripts, shebang lines, and parsing command-line arguments.
Search across all documentation pages
How to run plain JavaScript files with Node.js - direct execution, npm scripts, shebang lines, and parsing command-line arguments.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card - copy-paste ready.
# Run a script directly
node script.js
# Run with auto-restart on file changes (Node 18.11+)
node --watch script.js
# Run with environment variables
NODE_ENV=production node script.js
# Pass arguments to the script
node script.js --name Alice --verbose// package.json - npm scripts
{
"name": "my-cli",
"version": "1.0.0",
"type": "module",
"bin": {
"my-cli": "./bin/cli.js"
},
"scripts": {
"start": "node script.js",
"dev": "node --watch script.js",
"cli": "node bin/cli.js"
}
}// bin/cli.js - with shebang
#!/usr/bin/env node
import { parseArgs } from 'node:util';
const { values } = parseArgs({
options: {
name: { type: 'string', short: 'n' },
verbose: { type: 'boolean', short: 'v' },
},
});
console.log(`Hello, ${values.name ?? 'world'}!`);# Make the script executable (Unix/macOS only)
chmod +x bin/cli.js
# Then run it directly
./bin/cli.js --name AliceWhen to reach for this: Build scripts, automation tasks, one-off utilities, CLI tools, or anything you don't want to ship through a browser.
A complete file-renamer CLI that lowercases filenames in a directory.
#!/usr/bin/env node
// bin/rename-lower.js
import { readdir, rename } from 'node:fs/promises';
import { join } from 'node:path';
import { parseArgs } from 'node:util';
const { values, positionals } = parseArgs({
options: {
dry: { type: 'boolean', short: 'd' },
help: { type: 'boolean', short: 'h' },
},
allowPositionals: true,
});
if (values.help || positionals.length === 0) {
console.log('Usage: rename-lower <dir> [--dry]');
process.exit(values.help ? 0 : 1);
}
const dir = positionals[0];
try {
const files = await readdir(dir);
for (const file of files) {
const lower = file.toLowerCase();
if (file === lower) continue;
const from = join(dir, file);
const to = join(dir, lower);
if (values.dry) {
console.log(`[dry] ${from} -> ${to}`);
} else {
await rename(from, to);
console.log(`renamed ${from} -> ${to}`);
}
}
} catch (err) {
console.error('Error:', err.message);
process.exit(1);
}// package.json
{
"name": "rename-lower",
"version": "1.0.0",
"type": "module",
"bin": {
"rename-lower": "./bin/rename-lower.js"
}
}# Try it locally
npm link
rename-lower ./photos --dry
rename-lower ./photosWhat this demonstrates:
node:util parseArgs for zero-dependency argv parsingbin field in package.json so npm link exposes the command globallyWhen you type node script.js, Node.js:
"type": "module" in package.json or the file ends in .mjs, otherwise CommonJS.process, console, Buffer, and friends.process.exit().A shebang line like #!/usr/bin/env node tells the operating system's loader which interpreter to use when the file is executed directly. The env tool looks up node in PATH, which is more portable than a hard-coded /usr/local/bin/node.
process.argv is an array where index 0 is the Node.js binary path, index 1 is the script path, and index 2+ are the user-supplied arguments. That is why parseArgs skips the first two by default.
# Pass arguments through an npm script (note the --)
npm run cli -- --name Alice
# Inline environment variables
API_KEY=abc node script.js
# Watch mode (Node 18.11+)
node --watch script.js
# Watch mode with a specific entry
node --watch --watch-path=./src script.js
# Inspect/debug mode
node --inspect-brk script.js
# Parse args with the built-in utility
node -e "console.log(require('node:util').parseArgs({options:{n:{type:'string'}}}))" -- -n hiEven for plain JavaScript scripts, you can opt into type-checking without adding a build step by enabling // @ts-check at the top of the file and creating a jsconfig.json:
// jsconfig.json
{
"compilerOptions": {
"checkJs": true,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"lib": ["ES2022"]
},
"include": ["bin/**/*.js", "scripts/**/*.js"]
}Add JSDoc types for parameters and the editor - and tsc --noEmit - will catch mistakes:
// @ts-check
/** @param {string} name @returns {string} */
function greet(name) {
return `Hello, ${name}!`;
}#!/usr/bin/env node, executing the file directly (./script.js) on Unix shells errors with "exec format error" or runs the file as a shell script.chmod +x. A correct shebang is useless if the file is not executable. Permission must be added with chmod +x bin/cli.js and the bit committed to Git (git update-index --chmod=+x)..js file in a package with "type": "module" is ESM; the same file in a CJS package is CommonJS. Use .mjs/.cjs to be explicit and avoid ambiguity.__dirname is undefined in ESM. In ESM scripts you must reconstruct it: const __dirname = path.dirname(fileURLToPath(import.meta.url)).\r\n) fails on Linux/macOS with "bad interpreter". Force LF via .gitattributes: *.js text eol=lf.-- in npm scripts. npm run cli --name Alice passes --name Alice to npm, not your script. Use npm run cli -- --name Alice.process.argv indexing. Index 0 and 1 are the Node binary and the script path - your user args start at index 2.| Tool | Strengths | Weaknesses |
|---|---|---|
node | Built in, zero config, widely supported | No TS, no watch mode before 18.11 |
bun | Fastest startup, bundled, runs TS/JSX | Newer ecosystem, some API gaps |
deno | Secure by default, native TS, standard library | Different module resolution, smaller ecosystem |
tsx | Fast TS/ESM runner on top of Node | Extra dependency |
pnpm exec | Runs local binaries without global installs | Just a runner, not a runtime |
node script.js explicitly invokes the Node.js binary. ./script.js relies on the operating system to read the shebang line and pick the interpreter - so it only works if the file starts with #!/usr/bin/env node and has the execute bit set.
Use process.argv, which is an array starting with the Node binary (index 0) and the script path (index 1). User arguments start at index 2. For anything beyond trivial flags, use parseArgs from node:util.
It maps a command name to a script file. When the package is installed globally - or linked via npm link - npm creates a symlink in its bin directory so you can run the command from anywhere.
Use Node's built-in --watch flag (Node 18.11+): node --watch script.js. For more control, pair it with --watch-path=./src to restrict the watched directories.
You must separate the npm flags from your script flags with --. For example: npm run cli -- --name Alice. Without the double dash, npm consumes the arguments itself.
Almost always Windows line endings. The shell reads the shebang line including the trailing \r and tries to run /usr/bin/env node\r, which does not exist. Force LF endings for shell and script files via .gitattributes.
Yes. Add // @ts-check to the top of a .js file, create a jsconfig.json with "checkJs": true, and annotate with JSDoc. Running tsc --noEmit surfaces type errors with no runtime footprint.
Use JSDoc tags like /** @param {string} name @returns {Promise<void>} */. Editors and tsc understand these annotations the same way they understand TypeScript types.
process.env is the in-memory environment variables the Node.js process inherited. A .env file is just a text file - Node.js does not parse it automatically. Use --env-file=.env (Node 20.6+) or a library like dotenv.
Call process.exit(1) after logging the error, or throw an uncaught error and let Node exit with code 1 automatically. Reserve non-zero codes for real failures so shell pipelines and CI systems can detect them.
__dirname is a CommonJS-only global that points at the directory of the current script. In ESM you reconstruct it from import.meta.url with fileURLToPath and path.dirname.
Only in ESM - that is, a .mjs file or a file in a package with "type": "module". In CommonJS you must wrap async code inside an async IIFE.
Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥