a snapshot of the terminal showing the output of env dash dash help command

Node.js shebang

Alex Ewerlöf (moved to substack)
5 min readAug 29, 2020

--

JavaScript is an interpreted language and its source code needs to be fed to some interpreter to run. If you want to run a JavaScript file using Node.js, you normally run this command:

$ node yourfile.js

By typing the name of the interpreter (node), you are explicitly telling the shell how to run your script.

But that knowledge can be put inside the script itself so it can be run directly as if it was a binary:

$ ./yourfile.js

This will work only if you have execution permission on that file (you can set that with chmod u+x yourfile.js for example) and have set the right “shebang”.

Shebang

Shebang or hashbang (#!) is the first line of the file which tells the OS which interpreter to use. It typically looks like this:

#!/absolute/path/to/the/interpreter [optional params]

Shebang is a OS feature and can be used to run any interpreted language: Python, Perl, etc. For Node.js it can (but often doesn’t) look like this:

#!/usr/bin/node

Node.js will happily ignore this as a comment only if it is the very first line of the file (it won’t work even if there’s an empty line or //comment line before it). Browsers ignore it too (Chrome 74+, FF 67+).

Most people have a Node.js binary or symlink sitting at /usr/bin/node. If Node.js is not at /usr/bin/node, the OS will complain. For example bash would say bad interpreter: No such file or directory script won't execute. But is there a way to tell the OS to run the script with Node.js no matter where it is installed?

#!node doesn’t work because shebang requires an absolute path.

Say hello to env

env is primarily intended to run a command in a modified environment. The emphasis here being “a command” because env is almost always at /usr/bin/env while “a command” can be anything that’s on the PATH.

If instead of /usr/bin/node we write/usr/bin/env node, we’re telling OS to run env and env will run node and node will in turn execute the script.

The short answer

This is the most common shebang for Node scripts:

#!/usr/bin/env node

However, env has a few other tricks up its sleeve that we can use.

Pass parameters to Node.js

Passing the -S option to env causes it to parse whatever comes after which opens up a new door: passing parameters to the command.

For example let’s say we would like to run node with a special flag to enable ESM modules when running the current file. We could use this shebang:

#!/usr/bin/env -S node --experimental-module

Another example: if we want to run another script before running the current script, we can use Node’s -r option:

#!/usr/bin/env -S node -r ./my/other/file.js

Or to open up the inspection port:

#!/usr/bin/env -S node --inspect

Please note that if you run the script like node yourfile.js , Node.js will not try to parse arguments from the shebang. It’ll just ignore it. It is the kernel which uses the shebang prior to running the file in order to figure out how to run it.

Set environment variables

Remember we said env can run a command in a modified environment? That’s actually where it gets its name and it’s very powerful. Let’s say we want our script to run in production mode. We could set the NODE_ENV environment variable:

#!/usr/bin/env -S NODE_ENV=production node

Without that, the NODE_ENV would be undefined or whatever is set at the user’s terminal when running the script.

Node.js respects a bunch of environment variables. For example we can use the NODE_OPTIONS to pass some CLI flags like this:

#!/usr/bin/env -S NODE_OPTIONS=--experimental-modules node

Start with an empty environment

If we wanted the script to run without access to any environment variable at the user’s terminal, we could run it with the -i flag which stands for “ignore environment”:

#!/usr/bin/env -S -i node

A mere - implies -i, so we can also write it like this:

#!/usr/bin/env -S - node

Force-disable DEBUG

Maybe we don’t want to clear all environment variables but block-list a few of them. An example is DEBUG (if you’re using the popular debug package). Maybe we don’t want the users of the scripts to set the flag when running it as a script. Then we’d use the -u flag which stands for unset environment variable.

#!/usr/bin/env -S -u=DEBUG - node

If the user runs the script as DEBUG=* ./yourfile.js they cannot see any debugging info but you can still run it as DEBUG=* node ./yourfile.js see the DEBUG output.

Lock the Node.js runtime version

Sometimes you want to lock the node version that is used to run a script. Prior to NPM@3 we could use engineStrict, but that feature is removed and now we can only set the engines in package.json which may or may not exist next to the script and depends on setting the engine-strict config flag.

But there’s an easier way. Since node is also an NPM package, and npx allows running any NPM package, you can write:

#!/usr/bin/env -S npx node@6

This may try download the requested version of Node upon running the script (so it won’t work without internet connection if that particular version of node does not exist in the NPX cache).

Tip: you can check the node version using process.version

Run it with TypeScript

There’s no rule that says we have to run node. Assuming typescript and ts-node are available globally (npm -i g typescript ts-node), we could specify ts-node as the interpreter:

#!/usr/bin/env ts-node

And let it run the file as a TypeScript program.

In any of these examples, the file can have the .js extension or whatever else you prefer. It can even be without extentions!

Have I missed something? Do you have any tip you’d like to add? Please let me know in the comments or reach out on twitter: @alexewerlof

References

Did you like what you read? Follow me here or on LinkedIn. I write about technical leadership and web architecture. If you want to translate or republish this article, here’s a quick guide.

--

--