Package.json Generator

Fill the fields on the left and a complete package.json builds itself on the right, with keys in the order npm writes them. Names, versions and dependency ranges are checked as you type, so a broken manifest never reaches your project root.

Package.json builder

Start from

Lowercase, no spaces. Scoped names take the form @team/package.

Three numbers, dot separated. Prerelease tails such as 1.0.0-rc.2 pass.

The file loaded by require or import when someone pulls in your package.

package.json
Then runnpm install

What package.json actually controls

One JSON file in your project root decides which packages get installed, which Node version is allowed to run the code, what happens when someone types npm run build, and what the registry shows if you publish. Node itself reads only a handful of those keys. The rest are read by npm, by pnpm and yarn, by bundlers, and by your editor. Getting the file right once saves the class of bug where a project installs fine on your laptop and dies in CI.

Every key falls into one of four jobs. Identity keys describe the package to humans and to the registry. Resolution keys tell a loader which file to open. Dependency keys drive the install. Constraint keys refuse work that would break.

KeyJobWho reads itRequired
nameIdentity on disk and in the registrynpm, the resolverTo publish or install
versionWhich release this code isnpm, every range solverTo publish
mainEntry file for require and legacy importsNode, bundlersNo, defaults to index.js
typeReads .js files as ESM or CommonJSNodeNo, defaults to commonjs
exportsLocks down which paths outsiders reachNode 12.7 and newer, bundlersNo, but it overrides main
scriptsNamed shell commandsnpm run, pnpm, yarn, bunNo
dependenciesInstalled for everyone who uses the packageEvery package managerNo
devDependenciesInstalled only in the package itselfEvery package managerNo
enginesNode and package manager version gatenpm warns, pnpm blocksNo
privateRefuses npm publish when truenpmNo
filesWhitelist of what ships in the tarballnpm pack and publishNo

Version ranges and what the caret really allows

A dependency line is a range, not a pin. The characters in front of the number decide how much drift you accept the next time someone runs an install on a clean checkout. Most published packages sit on a caret, and most of the surprise upgrades people report come from misreading it.

RangeReads asAcceptsRefuses
^4.17.1Compatible with 4.17.14.17.2, 4.18.0, 4.99.95.0.0
~4.17.1Patch updates only4.17.2, 4.17.94.18.0
4.17.1Exactly this build4.17.1Everything else
>=4.17.1 <5Explicit windowAny 4.x at or above 4.17.15.0.0
*Any published versionAll of themNothing
^0.4.2Patch only, because major is zero0.4.30.5.0

That last row catches experienced developers. Below version 1.0.0 the caret narrows to the behaviour of a tilde, because semver treats a zero major as unstable and lets the minor number carry breaking changes. A package sitting on ^0.x gives you far less freedom than the same caret on ^1.x, and pinning early releases exactly is the safer habit.

The lockfile, not this file, decides the exact version installed. package.json states a range and package-lock.json records the single build that satisfied it. Commit both. Deleting the lockfile to fix an install is the fastest way to turn a working range into a broken one.

Runtime, dev and peer dependencies

Put a package in the wrong bucket and either your users download build tooling they never run, or your production server crashes on a missing module. The rule is short: if the shipped code imports it, it belongs in dependencies.

BucketInstalled for consumersTypical members
dependenciesYesexpress, react, database drivers, anything an import reaches at runtime
devDependenciesNovitest, eslint, typescript, bundlers, type definition packages
peerDependenciesNot installed, only demandedThe framework a plugin attaches to, so one copy of react serves the whole tree
optionalDependenciesAttempted, failure ignoredPlatform specific binaries with a pure JavaScript fallback

Applications get some slack here. If you deploy a built bundle rather than publishing to the registry, the split matters mostly for install speed and image size. Libraries get none. A TypeScript library that lists typescript under dependencies forces a compiler download on every user who only wanted a function.

Scripts, and the hooks that run without being asked

Anything under scripts becomes npm run name. Four names skip the run word entirely: start, test, stop and restart. The node_modules/.bin folder is prepended to PATH inside a script, so vitest resolves without a relative path or an npx prefix.

"scripts": {"dev": "node --watch server.js","build": "tsup src/index.ts --format esm,cjs --dts","pretest": "eslint .","test": "vitest run","prepublishOnly": "npm run build"}

The pre and post prefixes create automatic hooks. Running npm test above fires pretest first, and a failing lint stops the test run before a single spec executes. prepublishOnly fires on publish and nowhere else, which is the right place for a build step you never want running during a normal install.

HookFires whenGood for
prepareAfter install from source, and before publishCompiling a package installed straight from git
prepublishOnlyBefore npm publish onlyBuild and test gate on releases
pre<name>Before the matching scriptLint before test, clean before build
post<name>After the matching scriptSize reports, cache warming

Naming rules that break a publish

The name field has the strictest validation in the file, and npm applies it at publish time rather than at install time. Fixing it late means a rename across your imports and your README.

  • Lowercase only. Mixed case names exist in the registry from before the rule landed, and none can be created now.
  • 214 characters maximum, counting the scope on a scoped name.
  • No leading dot or underscore. Both are reserved by npm for internal use.
  • URL safe characters only. Letters, digits, hyphens, dots and underscores. No spaces, no slashes outside a scope.
  • No name that reads like an existing one. npm rejects a name a typosquat check flags against a popular package, so expres and react-dom- both fail.
  • Scoped names take a slash, as in @yourteam/parser. A scope is the only place a slash is legal, and scoped packages publish privately by default unless you pass an access flag.

Module type and the entry point fields

The type field changes how Node reads every .js file in the package. Set it to module and import works while require throws. Leave it out and the opposite holds. File extensions override the setting per file: .mjs is always ESM and .cjs is always CommonJS, whatever type says.

For a library published to the registry, exports replaces main and does more. It maps public paths, hides internals, and hands different files to different loaders.

"type": "module","main": "./dist/index.cjs","exports": {".": {"types": "./dist/index.d.ts","import": "./dist/index.js","require": "./dist/index.cjs"},"./package.json": "./package.json"}

Two details bite here. Once exports exists, any path not listed becomes unreachable, so a consumer importing yourpkg/dist/util.js gets a resolution error rather than the file. And types has to come first inside each condition, because the resolver takes the first match and TypeScript never gets a look otherwise. The generator above writes main, which suits applications and small packages. Add exports by hand when you start shipping dual builds.

Where this generator stops

The output covers the fields a normal project sets by hand. Several real fields sit outside it, on purpose.

  • No exports, imports or typesVersions map. These are nested conditional structures that a form flattens badly. Write them directly once your build produces more than one artifact.
  • No workspaces. Monorepo roots need a workspaces array plus per package manifests, and the layout choice belongs to your repository rather than a form.
  • No dependency version lookup. Nothing here contacts the registry, so a version you type is a version you get. Run npm outdated after the first install to see what is current.
  • No name availability check. Validation covers the format rules, not whether the name is already taken. npm view yourname answers that in a second.
  • Overwriting an existing file loses your edits. If the project already has a manifest, copy the fields you want across rather than replacing the file, since your lockfile is tied to what is in there now.
  • Nothing is uploaded. The whole page runs in your browser, which also means nothing is saved. Download or copy before you close the tab.

Checking the file before you commit it

Four commands catch nearly every mistake a hand written manifest carries.

  1. Save the output as package.json in your project root, then run npm install. A malformed range or an unknown package fails here, loudly.
  2. Run npm pkg fix. It repairs the repository URL format, normalises the license identifier and corrects a few common shape errors in place.
  3. Run npm pack --dry-run. It lists exactly which files a publish would ship, which is where an oversized tarball or a missing dist folder shows up.
  4. Run npm run with no arguments to print every script name. A typo in a script key is invisible until the moment CI needs it.

For an application rather than a package, add one more habit: commit the lockfile, and run npm ci instead of npm install in your pipeline. The first command respects the lockfile exactly and fails when it disagrees with package.json. The second quietly rewrites the lockfile to match, which is how a green local build turns into a red deploy.

Package.json questions that come up mid install

Name rules, caret ranges, the dependency split and the fields npm reads at publish time.

What is package.json used for?

It records the identity of a Node project and everything a package manager needs to reproduce it: the dependency ranges, the scripts behind npm run, the entry file, and the Node version the code expects. npm reads it on every install and the registry reads it on every publish.

How do I create package.json without a generator?

Run npm init in the project folder and answer the prompts, or npm init -y to accept the defaults and edit afterwards. This page suits the case where you want the finished shape in front of you first, with templates and validation, rather than answering questions in a terminal.

What is the difference between dependencies and devDependencies?

Dependencies install for everyone who installs your package. DevDependencies install only when someone works on the package itself, so test runners, linters and bundlers belong there. If the shipped code imports it at runtime, it goes in dependencies.

What does the caret in ^1.2.3 allow?

Any version up to but excluding the next major, so 1.2.3 through 1.99.99 but never 2.0.0. Below 1.0.0 it narrows: ^0.4.2 accepts patch updates only, since semver treats a zero major as unstable and lets the minor number carry breaking changes.

Why does npm reject my package name?

Most rejections come from an uppercase letter, a space, a leading dot or underscore, or a name close enough to a popular package that the typosquat check blocks it. The validation strip under the output flags the format rules as you type. Availability needs npm view yourname.

Do I need the version field for a private app?

npm install works without it, but tooling that reads the manifest often assumes it exists, and a missing version breaks any workflow that tags releases from it. Keep 1.0.0 or 0.1.0 in place and set private to true, which is what the publish block checkbox does.

What happens when I set type to module?

Node reads every .js file in the package as an ES module, so import works and require throws. Extensions still win per file: .mjs stays ESM and .cjs stays CommonJS whatever type says. Switch an existing CommonJS project carefully, since __dirname and require both disappear.

Should I commit package-lock.json?

Yes, for applications and libraries both. package.json holds ranges and the lockfile holds the exact builds that satisfied them, which is what makes an install reproducible. Use npm ci in CI so the pipeline fails on a mismatch instead of silently resolving new versions.