npx and npm exec let you run Node.js packages without globally installing them, but they do more than just save disk space.

Let’s see npx in action with create-react-app. Normally, you’d npm install -g create-react-app then create-react-app my-app. With npx, it’s just:

npx create-react-app my-app

npx downloads create-react-app (if not already cached), runs it, and then discards it. The same applies to npm exec:

npm exec create-react-app my-app

These tools solve the "dependency management hell" for command-line utilities. Imagine you need a specific version of a linter for a one-off project audit. Installing it globally pollutes your system. npx or npm exec lets you run it just for that task.

Internally, they work by first checking if the command exists in your local node_modules/.bin directory. If it’s there, they execute it directly. If not, they search for it in your PATH (which is how global installations are found). If it’s still not found, they temporarily download the package from the npm registry, execute it, and then clean up the downloaded package.

The core problem these commands solve is the friction of using one-off command-line tools. Before npx/npm exec, the workflow was:

  1. npm install -g <tool>
  2. Run <tool>
  3. npm uninstall -g <tool> (if you were tidy)

This is cumbersome. npx and npm exec streamline this to a single command. They also handle versioning implicitly: if you run npx <package>@<version> ..., it will fetch and run that specific version, ensuring reproducible environments for tasks.

The syntax is straightforward: npx <command> [args...] or npm exec <command> -- [args...]. The -- in npm exec is important; it separates arguments for npm exec itself from arguments intended for the executed command.

A common use case is running outdated packages or testing new ones without commitment. For example, to run eslint version 8.0.0 against your project:

npx eslint@8.0.0 . --ext .js,.jsx

This command will download eslint version 8.0.0, run it with the specified arguments, and then discard it. No global installation, no local devDependencies needed.

The npm exec command also allows you to specify a package source other than the npm registry, like a Git URL. For example, to run a script from a GitHub repository:

npm exec github:user/repo#branch -- script-name --arg1 value1

This command will clone the specified repository, navigate into it, execute script-name with the provided arguments, and then clean up the cloned repository. This is incredibly powerful for running ad-hoc scripts or tools that aren’t published to npm.

When you run a command like npx my-package, it first looks in your project’s node_modules/.bin. If it’s not there, it checks your global npm packages. If it’s still not found, it performs a temporary download from the npm registry. This temporary download is what makes it so convenient. The package is fetched, executed, and then removed from your system’s temporary cache, leaving no trace unless you explicitly tell it to keep it.

One subtle but crucial aspect is how npx handles packages that are already locally installed. If you have my-package as a dependency in your package.json and it’s installed in node_modules, npx my-package will execute the locally installed version, not a temporary download. This behavior is intentional and ensures that you’re using the versions specified in your project’s dependencies when you intend to. This means npx acts as a smart runner, prioritizing local installations before resorting to remote fetches.

The next step after mastering npx and npm exec is understanding how to manage the execution context and environment variables for these temporary packages.

Want structured learning?

Take the full Npm course →