// blog/developer/
Back to Blog
Developer · August 1, 2026 · 8 min read · Updated May 22, 2026

package.json Setup: The Fields Most Node Projects Get Wrong

package.json Setup: The Fields Most Node Projects Get Wrong

Every Node.js project lives or dies by its package.json. It declares the name, version, dependencies, scripts, and module entry points. npm init creates a minimal one. The minimum is rarely what you actually want.

A tuned package.json saves time for everyone who touches the project. Scripts automate common tasks. The engines field stops version mismatches in CI. A correct exports block keeps the package working in both ESM and CommonJS consumers.

This guide covers the fields that matter in 2026 and the patterns experienced Node teams already use. Internal tool, web app, or open-source package: getting package.json right at the start saves hours of debugging later.

Keep the file readable with the JSON Formatter. Consistent indentation and stable key ordering make the diffs clean in code review.

* * *

Fields Beyond `name` and `version`

Everyone fills in name, version, and description. These fields are where the real wins are.

type: "module" for ESM, omit for CommonJS. In 2026 most new projects should ship "type": "module".

engines: the Node and npm versions your project supports.

`json "engines": { "node": ">=20.0.0", "npm": ">=10.0.0" } `

With engine-strict=true in .npmrc, this blocks installs on incompatible versions.

packageManager: pin the package manager via Corepack.

`json "packageManager": "pnpm@9.1.0" `

exports: modern entry points. Replaces main for dual ESM/CJS packages.

`json "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs", "types": "./dist/index.d.ts" } } `

files: whitelist of what ships when you publish. Everything else is excluded.

`json "files": ["dist", "README.md", "LICENSE"] `

sideEffects: false if your package has no side effects. Bundlers (Webpack, Rollup) use this to tree-shake aggressively.

Validate the whole structure with the JSON Schema Validator against the official package.json schema. It catches typos that produce cryptic npm errors.

* * *

Scripts That Pay Off

The scripts field is where most of your workflow lives. A pattern that holds up on real projects:

`json "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "eslint . --ext .ts,.tsx --fix", "format": "prettier --write .", "format:check": "prettier --check .", "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", "validate": "npm run typecheck && npm run lint && npm run test", "prepare": "husky", "precommit": "lint-staged" } `

Naming conventions worth copying:

  • noun:verb for related commands: test, test:watch, test:coverage.
  • validate runs everything CI runs.
  • prepare runs automatically after npm install (good for setting up git hooks).

npm runs prescript before script and postscript after. prebuild and postbuild are the common ones. Use them for setup and cleanup, not surprise side effects.

For portability, avoid shell-specific syntax inside scripts. Use cross-env for environment variables and rimraf instead of rm -rf. Otherwise Windows contributors will hate the project.

Terminal showing npm init output
Terminal showing npm init output
* * *

Dependency Rules

  • dependencies vs devDependencies: runtime code goes in dependencies. Build tools, test runners, linters, and formatters go in devDependencies. Mix them up and production installs balloon.
  • peerDependencies: for plugins and libraries that must share a dep with the host. A React component library peer-depends on React, never bundles it:

`json "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } `

  • Version ranges: ^1.2.3 allows 1.x.x (minor and patch). ~1.2.3 allows 1.2.x (patch only). 1.2.3 pins exact. Default to ^ for most, exact for critical infra.
  • Lock files: commit package-lock.json, pnpm-lock.yaml, or yarn.lock. The lock file is what makes installs reproducible across machines and CI.
  • Audit: run npm audit regularly. Address high and critical severities fast. Lower severities can ride the normal upgrade cadence.
  • Overrides: fix a vulnerable transitive dep without waiting on the parent maintainer.

`json "overrides": { "vulnerable-package": "^2.0.1" } `

  • Bundle weight: check bundlephobia.com before adding any dependency. A 50 KB utility for a function you could write in ten lines is rarely worth it.
* * *

Publishing to npm

For published packages, these fields earn their keep:

  • name: must be unique on npm. Check with npm search . Scoped packages (@org/package) sidestep naming conflicts.
  • version: semver. Start at 1.0.0 for the first stable release. Patch (1.0.1) for bug fixes, minor (1.1.0) for new features, major (2.0.0) for breaking changes.
  • license: "MIT", "Apache-2.0", "ISC", or "UNLICENSED" for private packages.
  • repository:

`json "repository": { "type": "git", "url": "https://github.com/user/repo.git" } `

  • keywords: 5-10 relevant tags so npm search can find you.

Pre-publish checklist:

  1. npm pack --dry-run to see what would ship and the size.
  2. Verify files includes only what is needed.
  3. Run the test suite one more time.
  4. Bump the version with npm version patch | minor | major.
  5. npm publish (or npm publish --access public for scoped packages).

Automate with semantic-release or changesets so commit messages drive the version. Manual version management ends in mistakes.

Node.js project structure in code editor
Node.js project structure in code editor
* * *

FAQ

npm, yarn, or pnpm?

All three are production-ready in 2026. pnpm is fastest and the most disk-efficient. yarn has the strongest monorepo workspaces. npm is the default and ships with Node. Pick one per team and lock it via the packageManager field.

What does the caret (^) actually mean?

The caret allows updates that do not change the leftmost non-zero digit. ^1.2.3 allows 1.2.3 up to (but not including) 2.0.0. ^0.2.3 allows 0.2.3 up to 0.3.0 only. This mirrors semver's rule that major bumps signal breaking changes.

How do I make a package work with both `require()` and `import`?

Use conditional exports with both import (ESM) and require (CJS) entries. Build dual output with tsup, unbuild, or esbuild. Test both entry points before you publish; subtle bugs only show up in one or the other.

Should I commit `node_modules`?

No. Put node_modules/ in .gitignore and commit the lock file. Reproducible installs without the cost of versioning thousands of dependency files.