Right, npm (and other language-specific package managers) do dependency resolution in a first-class way and Nix doesn't. Nix resolves dependencies in the way that's typical of source-based package managers: when you name a package as a dependency, what you're depending on is whatever version of that package simultaneously inhabits the monorepo of build recipes as your package.
Language-specific package managers like NPM or Cargo or Composer are not designed for a universe in which packages inhabit a monorepo where compatibility is ensured simply by coordination of changes in that repo. They're built on top of a different kind of infrastructure.
They also face different challenges since their focus is much narrower, i.e., libraries in a single language with a single build system, rather than whole applications whose build system can freely change in a fairly open-ended way from version to version.
It would be pretty cool for Nix to have some version constraint solving built in, and for Nixpkgs to host more versions of many packages. There's precedent for that in a way that wouldn't require radical change to Nix and Nixpkgs in other source-based package managers like NetBSD Pkgsrc and Gentoo Portage. It would also be cool to provide some built-in machinery for pulling package recipes out of old versions of Nixpkgs (or searching a global registry of some other kind for them). Whether the latter happens will probably depend on the future of the flakes implementation, and extending it backwards will take manual work. There's already sort of a proof of concept, though, in the implementation of one tool for generating Python packages for use with Nix (mach-nix).
Even with a package.json you will often use `npm install <package_name> --save`.
You don't go chasing a git commit hash on a third party site to do that