FeaturesTemplatesShowcaseTownie
AI
BlogDocsPricing
Log inSign up
valdottown

valdottown

blog

Val Town's Blog
Public
Like
8
blog
Home
Code
9
components
12
posts
13
routes
6
styles
2
utils
8
IMAGES.md
README.md
TODOs.md
H
index.ts
Branches
5
Pull requests
1
Remixes
16
History
Environment variables
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
posts
/
2025-09-11-gardening-dependencies.md
Code
/
posts
/
2025-09-11-gardening-dependencies.md
Search
9/11/2025
Viewing readonly version of main branch: v539
View latest version
2025-09-11-gardening-dependencies.md
title:
How to keep package.json under control
description:
How I wrangle the many tools that we use to build Val Town
pubDate:
2025-09-11:00:00.000Z
author:
Tom MacWright

Val Town is a React application with a ton of dependencies. It's complicated, and we have to deal with dependency upgrades all the time. We are committing the cardinal sin of overcomplicating the web: our node_modules directory is 863MB as of this writing. Whew!

A boat with boxes that symbolize our dependencies

Is it, though? Are we installing dependencies freely, taking on technical debt left and right? I'd say not really.

The thing is, there's some essential complexity in what we're trying to build. We aren't going to DIY our own TypeScript transpiler or avoid installing CodeMirror and use a textarea for our code editing. I spend a little time every week looking through package.json and thinking which of these can I remove? Sometimes I find a dependency that can be yanked, but a lot of times I come up empty-handed: we actually need all this junk. My ability to judge other people fades as I learn the hard way how principles make contact with reality.

But that's not to say that there's no art in dependency grooming. There are a bunch of techniques and tools that all fit together into a general sort of dependency hygiene that I've developed. I'm not sure I've written it down anywhere in full. Here's a shot.

Read all new dependencies (except React)

Rule #1 is to read. This is very literal: read the source code of any dependency that you're about to introduce into your project. And, of course, the README. I highly recommend doing this the old fashioned way, with your eyes and brain, but LLMs can be helpful too if that's more your speed: but don't offload the whole task to a robot. Actual understanding is the goal, and you can't achieve that secondhand.

Fairly often you'll do this and discover that the new dependency you're adding is just 50 lines long, and is better off vendored rather than installed with NPM: just copy that code over and preserve its open source license in a code comment.

Or you'll realize that the module is 2MB gzipped and introduces 3 new transitive dependencies, but you're only actually using 50 lines of it. Again, this is not a good scenario: you're introducing a lot of surface area that's pure downside. It'll take up more room in node_modules, or it could have security vulnerabilities in the parts that you aren't using and you'll have to triage them just the same.

I make an exception for React and other mega-dependencies: I've seen the inside of React's algorithms and TypeScript compiler code, and decided that I just need to trust the whizzes at those companies.

If you don't read, you won't succeed.

npm ls and reading package-lock.json is your friend

Or, if you're using pnpm, pnpm-lock.yaml and pnpm why. And a similar command for whatever other package manager you're using. The reason is pretty simple: your direct dependencies are inevitably the tip of the iceberg. What really fills up node_modules is all of the stuff they bring along, and that stuff - transitive dependencies - is extremely important.

For example, let's say your project needs to transpile TypeScript. It's fairly likely that you already have a transpiler installed: in our project, we have copies of esbuild that are dragged in by drizzle-kit, by Vite, and by tsx. So using esbuild as a direct dependency costs us nothing: it gets deduped to the same esbuild binary that everything was using under the hood before. This is a useful little game: when you're installing something for your application, if you can find a way to reuse something that's already installed transitively, you can get a dependency for free!

And read package-lock.json or pnpm-lock.yaml. It's not that bad, and you'll learn something. There's a lot in there. It'll familiarize yourself with what modules others rely on, which builds a little PageRank algorithm in your head so that you can recall, off the bat, what you might want to use for something new. Feed your curiosity and open those npmjs.com webpages.

Analyzing the actual size of packages is pretty nifty

There are two distinct impacts of big NPM modules: their contribution to your distributed application, and how much room they take up in node_modules while you're developing the application. It's good to focus on application size first, but both matter: an application that relies on 2 gigabytes of code in node_modules is going to be pretty slow to test in CI, and slower to deploy because it just requires more downloading.

Grand Perspective visualizing dependencies in node_modules

I use Grand Perspective, a vintage application that's been around for 20 years, to keep track of node_modules on disk, but there are a lot of other disk space analyzers worth considering if you're using Linux or Windows.

Analyzing the size of your distributed application is a much more complicated and system-specific. We're using React Router with Vite, so rollup-plugin-visualizer is the solution, but it's different for every bundler and some frameworks have their own solution, like Next.js's bundle analyzer.

What makes a good NPM module

But what are you really looking for? The definition of a good module keeps shifting, but pretty often it'll look something like: a decent history of maintainership, built-in TypeScript types, passing tests, good documentation.

A slang shorthand for this would be that it should have a vibe of competence. Even if you're building something by the seat of your pants and making lots of mistakes, you want the parts that you're using to be solid. An application's bugs are the sum total of the bugs you write and the bugs you inherit from others, so it's actually kind of fair to have higher standards for code that you install than code that you write.

What makes a bad module? Of course something that's abandoned and poorly written is bad, but even worse than that is a module that solves the wrong problem - something that doesn't actually fit the problem you have and instead you have to shift the problem to make it work. You can fix this by reading and spending a little time understanding both your problem and the solution. Or by asking the LLM, you little baby.

Get rid of what you don't use and keep the rest up to date

Renovate and Knip

You should be using Renovate. It'll nag you to keep your modules up to date, and it's better to do that kind of work incrementally rather than in one yearly push.

And you should be using Knip. It's just straight-up magic: it's so fast, extremely accurate. It'll tell you which modules you have specified in package.json but that you aren't actually using. It's super easy to lose track and have a lot of junk lying around from old versions of a project. Get rid of it with Knip! It'll even show you which files your project no longer uses. If there was a Knip t-shirt I'd be wearing it right now, that's how good it is.

Have a quick cheatsheet of people who write good modules

The NPM module ecosystem is made up of people. It's useful to know who those people are! For example, when I'm looking for something related to Promises (or many other topics), I'll check if Sindre Sorhus has already published something. Very often he has!

Other folks who have lots of solid work are good to know, like isaacs, Matteo Collina, Mafintosh.

If you're doing something with Markdown, you should know all the wooorm and unified repos. Nextgen Node.js stuff? Check unjs. Transpiler internals, you should always check Rich Harris's projects, which include so many gems.

Tune in for a follow-up post that just includes some starting points for dependencies that you can even put in an AGENTS.md file if you want to.

Dependencies are an inevitability

That's the truth: we're all building on the shoulders of each other. But there's an art to finding the right shoulders to build on.

In a way, it's frustrating that the web platform, and the NPM module ecosystem, moves so fast and requires so many nitpicky updates and decisions. But that's pretty much the norm: even next-generation languages that have learned the lessons of NPM and Node suffer from bloated package ecosystems. Gardening dependencies is part of the job and we should do it well.

FeaturesVersion controlCode intelligenceCLI
Use cases
TeamsAI agentsSlackGTM
ExploreDocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareersBrandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Terms of usePrivacy policyAbuse contact
© 2025 Val Town, Inc.