Two lists, one simple question
Your package.json keeps packages in two groups. To decide which group a package
belongs in, ask one question: "Does the finished, shipped project need this to work, or
do I only need it while building?"
dependencies— needed to build or run the actual project.devDependencies— only needed during development (testing, type-checking, deploying, generating images).
This project's real split
Here's how this very website divides its packages — a useful real-world example:
dependencies (needed to build the site)
- astro — the framework that generates the website. Without it, there's no site.
- tailwindcss & @tailwindcss/vite — produce all the styling.
- three — the 3D library the engine visualisation needs to run in the browser.
devDependencies (helpful tools only)
- typescript & @astrojs/check — check the code for mistakes while developing.
- @types/node — type hints that make editing nicer; not shipped.
- playwright — runs automated browser tests.
- wrangler — uploads the finished site to Cloudflare. A deploy tool, not part of the site.
- satori & @resvg/resvg-js — generate social-preview images at build time.
See the pattern? The dependencies are the ingredients of the website itself. The devDependencies are the kitchen tools — essential for cooking, but they don't go on the plate.
How you choose the list when installing
The install command decides the list for you — the -D flag is the whole difference:
Pick the right list
$ npm install three # → dependencies (project needs it)
$ npm install -D playwright # → devDependencies (dev tool only) - Run in the integrated terminal
- Open package.json afterwards to see which list the package landed in
- Got it wrong? Uninstall and reinstall with or without -D
Plain 'npm install <pkg>' goes to dependencies. Add -D (or --save-dev) to put it in devDependencies instead.
Does the difference actually matter?
For a site you build and deploy yourself, both lists install the same way when you run
npm install locally, so it might feel academic. It starts to matter in two cases:
- Lean production installs — running
npm install --omit=devskips devDependencies, for a smaller, faster install on a server that only needs to run the result. - Publishing a package — if you share your package, anyone installing it gets your
dependenciesbut not yourdevDependencies. Putting a build-only tool in the wrong list would force it onto all your users.
Even when it doesn't change behaviour, keeping the split tidy makes package.json readable — anyone can see at a glance what the project truly relies on.
"Dependencies of dependencies"
You list only a handful of packages, yet node_modules holds hundreds. That's
because each package has its own dependencies, which have theirs, and so on. These are
called transitive dependencies — npm fetches the entire tree automatically so
you never have to chase them down yourself.
You may also see peerDependencies in some packages. That's a package saying "I work alongside X, but you should install X yourself" — common for plugins that attach to a framework. As a beginner you rarely set these; just install whatever a warning tells you is missing.
Up next: those scripts in package.json — how npm run dev and npm run build actually work.