Cleaning House in Nx Monorepo, How I Removed Unused Deps Safely
Posted3 months agoActive3 months ago
johnjames.blogTechstory
calmpositive
Debate
40/100
Monorepo ManagementDependency ManagementNx
Key topics
Monorepo Management
Dependency Management
Nx
The author shares their experience of safely removing 120 unused dependencies from an Nx monorepo, sparking discussion on how to maintain a clean and organized codebase.
Snapshot generated from the HN discussion
Discussion Activity
Active discussionFirst comment
39m
Peak period
17
0-6h
Avg / period
7.3
Comment distribution29 data points
Loading chart...
Based on 29 loaded comments
Key moments
- 01Story posted
Sep 28, 2025 at 9:12 PM EDT
3 months ago
Step 01 - 02First comment
Sep 28, 2025 at 9:51 PM EDT
39m after posting
Step 02 - 03Peak activity
17 comments in 0-6h
Hottest window of the conversation
Step 03 - 04Latest activity
Oct 2, 2025 at 10:58 PM EDT
3 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
ID: 45409526Type: storyLast synced: 11/20/2025, 7:45:36 PM
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
Over a decade that's once a month, which is a lot though!
I think sometimes people will hear advice like "pin your deps" and do a `pip freeze | requirements.lock.txt`, without really absorbing that pinning transitive dependencies like this is generally not what you want.
You want a lock file! But you tend to want transitive dependencies that aren't locked down to get upgraded when you upgrade your direct dependencies. But it's a subtlety that can get lost in the noise.
2- I want every install of my project- be it on a dev machine or deploy machine- to have the exact same versions of dependencies, including transitive ones. I don't want to deal with bugs caused by surprise version changes
3- if I upgrade a dependency or remove it, I want the transitive dependencies managed automatically. I don't want orphaned transitive dependencies. In fact, I don't even want to think about them at all other than know that they work and aren't adding bloat or security risks.
You need a package manager with more than a passing thought for handling lock files. For the longest time, npm wasn't it. I'd argue that it still isn't, because "npm install" should NOT be the command used for both "set up a project for the first time" and "add a new package". In the first case, I want a reproducible, deterministic result of a known state. In the second case, I want to modify the dependency graph, producing a new state.
Yes, that's `npm ci`.
With a fresh `npm install` you get the latest packages that satisfy dependencies in the ranges spec'd in package.json. That's basically a crap shoot because even if you specifically pin the versions of all your dependencies, chances are that at least some of your dependencies did not have the good sense to do the same for their dependencies. This is the path to hell.
With a fresh `npm ci` on the other hand, you get back exactly to the known state specified in package-lock.json, where everything is pinned, all the way down. This is the path to happiness.
> That's basically a crap shoot because even if you specifically pin the versions of all your dependencies, chances are that at least some of your dependencies did not have the good sense to do the same for their dependencies.
As I mentioned in my first comment, I don't really want my dependencies to pin their dependencies. I want them to specify a range they work with to minimize the number of redundant copies of common transitive dependencies.
That phrase is frequently way more load-bearing than it should be. Is pnpm's default behavior the correct one? (yarn user here, but open to switching)
No tool can prevent all footguns, but standard / idiomatic / out-of-the-box pnpm usage beats even expert use of npm or yarn. It's different, and better.
And now it's extremely hard to get people to stop. There's so much info out there on the internet that says to use pip freeze that people are going to continue to run into and continue to learn to use.
Glad that we have better tooling nowadays!
There's never enough time to prune the software just like there's never enough time to consider the long term consequences of any decision. The incentive system is usually set up where the focus is always on the next quarter and the short term. And the result is exactly what you'd expect it to be.
At my last company, we had a cleanup project which ended up with a similar number of libraries removed over the epoch which cleaned up+upgraded NPM dependencies.
(1) the number might be multiplied if one library is removed from each package of a monorepo. Eg. If there are 10 packages in the monrepo, then you only need to remove on average 12 NPM libraries.
(2) sometimes updating the upstream libraries to newer versions reduces the size of the upstream dependency tree. Similarly, it was more common for NPM libraries to declare a dependency, whereas more mature libraries might move those to devDependencies or peer dependencies.
(3) removing unnecessary / unused code can unblock you so you can remove a library which is no longer referenced in your repo.
(4) sometimes programmers over engineer early. Sometimes your product requirements change. Reviewing your codebase after each of these conditions allows you to decide if you still need all of the code/dependencies.
(5) as the NodeJS runtime changes, sometimes you can migrate from a 3rd party library to use the built-in tool (we did this with BluebirdJS / Promises).
The likelihood you ever get back to zero copies of that module are fairly low, unless it has become persona non grata for some reason (leftpad). So it's easy to forget to back out the disambiguation entry at the top level. Especially if git blame doesn't make it dead apparent (see also people manually adding entries in non-alphabetic order so that the next 'npm install --save' rearranges five lines).
I think the best solution for the ecosystem might be to have best practices NPM conventions for minimizing dependency redundancy explosion, then forking/migrating most of the popular libraries to use these newer best practices libraries.
Also, it’s best practice to freeze your versions+lockfile, but that also means that version upgrades are manual and expensive friction. If there is a way to reduce the frequency of NPM library version bumps, then the cost of upgrading dependencies drops.
Just incompetent cooks. In one of my jobs, I had to cleanup a bunch of dependencies like "npm" and "install" because the guy typed "npm install" and then pasted "npm install react" or something like that. And that was not noticed for several months.
every time we migrate stuff (like the recent shift to next.js), old apps/libs don’t always get cleaned out. after years and years it’s an npm landfill. knip was basically the bulldozer.
We pre build the dependencies and upload them to a CDN as well, which we can then at build replace the import URLs with the stable CDN ones, it works really well. We have thin wrappers around each dependency to give a stable interface as well.
For what backend TypeScript projects we do have we created typed wrappers around the server framework instead of pre-building because those both deploy differently and have different constraints but this still lets us track and upgrade versions centrally while keeping APIs stable.
Using this approach has had a positive impact on performance across the board as we naturally strive to keep our dependency count low because the maintenance burden is more obvious
Remember not every project is for some amazing product, with a great agile way of working. Some projects are a withdrawals system internal to an insurance company, for example. No one gives a shit there.
Motivating for any kind of tech debt work on such a project is unlikely to be met with approval, and all the devs who really care about such things will likely have left the project long ago.
Without changing the policy around dependencies themselves for a project (which presumably would be an uphill battle for anyone feeling strongly enough about this), the only way to avoid this scenario from happening without tooling would be for people to manually verify that a dependency is still used any time they remove a usage of it. Asking the person who introduces the dependency to keep track of every time someone else introduces another usage of it to the codebase would be burdensome even if you could assume that they wouldn't ever switch jobs/stop contributing to the open source project/take vacations when code is merged in their absence, and realistically I doubt that asking people to notice whenever they remove a use of a dependency and manually verify that the dependency is still used elsewhere would be particularly effective. I don't think there's any way to make this obvious in the absence of tooling, which unfortunately isn't super common in my experience even in projects that make ample use of linting other static checks in CI.
I don't totally agree with the takes that this is from devs not caring about their projects or otherwise phoning it in. Often the projects I've seen struggle with things like this (and even things that are a lot more worrisome than this, like missing or flat-out wrong documentation around stuff like testing locally due to the team relying on unwritten shared knowledge of process) have developers who care deeply about what they're doing, but are stressed, burned-out, and desperately trying to tread water to avoid drowning in the sea of things they'd like to do to improve things. That's not to say that every situation like this is blameless, but unless my experience is greatly out of the ordinary, I'd think that quite a lot of us have been in or at least seen situations like this enough that we should have enough empathy not to jump to conclusions about the state of a project being a direct reflection of how much the devs care. (To be clear, I don't think this is what the parent comment I'm replying to is saying, but it does seem like overall there's a bit of a sentiment similar to this in the thread as a whole, so it feels worth calling out).