node app.js is the common case, but the binary accepts a program in four ways. Pointing it at a directory (node .) runs the main entry of the package.json there. -e evaluates a string, -p evaluates and prints the result, and a program arriving on standard input is read to EOF and executed. A file starting with #!/usr/bin/env node runs as a command on macOS and Linux; Windows ignores shebangs, so npm 2,036 generates a .cmd shim.
node server.js --port 8080 # a file, with arguments of your own
node . # the "main" entry of ./package.json
node -p "[1,2,3].map(n => n * 2).join(',')"
node -e "console.log(process.argv)" alpha beta2,4,6 [ 'C:\\Program Files\\nodejs\\node.exe', 'alpha', 'beta' ]
Watch the second result. With a script file, process.argv[1] is that file and your arguments start at index 2 — hence the process.argv.slice(2) idiom. With -e there is no script path, so arguments start at index 1 and that idiom silently drops one. node --check file.js reports syntax errors without running the file, a cheap pre-commit guard.
The REPL starts when you run node with no arguments. It is a full runtime: require(), dynamic import() and top-level await all work, and every built-in module is preloaded. Two magic variables make it far more useful — _ holds the last evaluated expression, _error the last uncaught one.
> users.map(u => u.name.toUpperCase())
[ 'ADA', 'LINUS' ]
> _.join(' & ')
'ADA & LINUS'
> users[9].name
Uncaught TypeError: Cannot read properties of undefined (reading 'name')
> _error.name
'TypeError'Dot commands control the session: .editor opens a multi-line buffer that evaluates only on Ctrl+D, .save out.js and .load in.js write and replay a session, and .break abandons a half-finished expression. History persists in .node_repl_history in your home directory; set NODE_REPL_HISTORY to another path, or to an empty string to keep pasted secrets out of it.