Understanding package.json
A practical guide to package.json, covering project metadata, dependencies, devDependencies, scripts, semantic versioning, peerDependencies, engines, exports and common npm configuration mistakes.
The package.json file is one of the central files in a modern JavaScript project. It describes the project, declares its dependencies, defines executable scripts and can control important parts of how npm and other JavaScript tooling install, build and publish the project.
If you have worked with Node.js, React, Next.js, Vite or another JavaScript ecosystem tool, you have almost certainly encountered package.json. At first it can look like a simple JSON file containing a list of packages, but it supports many different fields and can influence installation, publishing, module resolution, compatibility and development workflows.
Understanding package.json is therefore useful even if you rarely edit it manually. It helps you understand what npm install does, why dependencies are placed in different sections, how npm scripts work, why version ranges contain characters such as ^ and ~, and how a package behaves when it is published for other developers to consume.
What Is package.json?
package.json is a JSON file that describes a JavaScript or Node.js project. It is conventionally placed in the project's root directory.
{
"name": "my-project",
"version": "1.0.0",
"description": "Example JavaScript project",
"scripts": {
"dev": "node src/index.js"
},
"dependencies": {
"express": "^5.1.0"
}
}The file is read by npm and can also be understood by many other tools in the JavaScript ecosystem. Depending on the project, package.json may contain only a few fields or a much larger configuration.
Why Does package.json Matter?
A package.json file provides a standardized place to describe the project's identity, dependencies and commands. This allows developers and automated environments to reproduce the project's setup without manually documenting every package and command.
- Defines the project's package name and version.
- Describes runtime dependencies.
- Separates development-only dependencies from runtime dependencies.
- Defines npm scripts such as dev, build, test and lint.
- Can specify supported Node.js or npm versions.
- Can define how a published package exposes modules.
- Can contain metadata used when publishing a package to npm.
- Can configure package-manager behavior and other ecosystem tools.
How package.json Is Created
A package.json file can be created manually, but npm also provides an interactive command for initializing a project.
npm initnpm asks several questions and generates a package.json file from the answers. For a project where the default answers are acceptable, npm init -y can create the file without the interactive questionnaire.
npm init -yFrameworks and project generators such as Vite, Next.js and other tooling can also create package.json automatically as part of project initialization.
The name Field
The name field identifies the package. It is particularly important when the project is intended to be published to a package registry such as npm.
{
"name": "my-package"
}Published package names have naming rules, and scoped packages can use a scope followed by a slash.
{
"name": "@example/my-package"
}For an ordinary private application, the name still provides useful project metadata even though the project may never be published.
The version Field
The version field specifies the current version of the package. Published packages commonly follow Semantic Versioning conventions.
{
"version": "1.4.2"
}A semantic version normally contains a major, minor and patch component. For example, 1.4.2 means major version 1, minor version 4 and patch version 2.
| Part | Example | Typical meaning |
|---|---|---|
| Major | 1 | Potentially breaking API changes. |
| Minor | 4 | New backward-compatible functionality. |
| Patch | 2 | Backward-compatible bug fixes. |
Semantic versioning describes conventions rather than automatically guaranteeing compatibility. A package can technically publish a version that does not perfectly follow those expectations.
The description Field
The description field provides a short human-readable summary of the package.
{
"description": "Utility functions for processing API responses"
}For published packages, this information can appear in package registry interfaces and helps users understand what the package provides.
The license Field
The license field describes the license under which a package is distributed.
{
"license": "MIT"
}The exact licensing situation of a project can be more complicated than a single package.json field, especially when dependencies use different licenses. For published packages, license information should accurately reflect the project's actual licensing terms.
The author Field
The author field can identify the person or organization responsible for the package.
{
"author": "Example Developer"
}More detailed author information can also be represented as an object, although many projects keep this metadata simple.
The repository Field
The repository field can point to the source repository associated with a package.
{
"repository": {
"type": "git",
"url": "https://github.com/example/my-package.git"
}
}This is particularly useful for published packages because users can quickly find the project's source code and issue tracker.
The keywords Field
The keywords field contains an array of terms describing the package.
{
"keywords": [
"javascript",
"api",
"utilities"
]
}Keywords are mainly useful for packages that are published and discovered through a package registry.
Dependencies
The dependencies field contains packages required by the application at runtime.
{
"dependencies": {
"express": "^5.1.0",
"zod": "^4.1.0"
}
}When npm installs the project, it uses this section to determine which runtime packages need to be installed. A web application may have many dependencies here, including frameworks, database clients, validation libraries and other packages used by the production application.
devDependencies
devDependencies contains packages needed during development or the build process but not normally required as runtime application dependencies.
{
"devDependencies": {
"typescript": "^5.9.0",
"eslint": "^9.0.0",
"vitest": "^3.2.0"
}
}Typical examples include linters, test frameworks, TypeScript, formatters and build tools.
| Section | Typical purpose |
|---|---|
| dependencies | Packages required by the application when it runs. |
| devDependencies | Packages used for development, testing, linting or building. |
| peerDependencies | Packages that the consuming project is expected to provide. |
| optionalDependencies | Dependencies that may be unavailable without making installation fail in the same way as normal dependencies. |
How npm install Handles Dependencies
Running npm install in a project directory reads package.json and installs the dependencies described there. npm also creates or updates package-lock.json, which records a more precise dependency resolution.
npm installThe package.json file describes the requested dependency ranges, while the lockfile records the concrete dependency tree that was resolved. Keeping both files under version control is standard for applications.
package.json vs package-lock.json
These two files have different responsibilities. package.json describes the project and declares dependency requirements. package-lock.json records the exact dependency resolution produced by npm.
| File | Main role |
|---|---|
| package.json | Project metadata, dependency ranges, scripts and package configuration. |
| package-lock.json | Exact resolved dependency tree and package integrity information. |
For applications, the lockfile helps different environments install the same dependency versions. It should generally be committed to version control when the project uses npm.
Understanding Version Ranges
Dependency versions in package.json often contain operators rather than exact versions. These operators define which versions npm can consider acceptable.
| Range | Example meaning |
|---|---|
| 1.4.2 | Only version 1.4.2. |
| ^1.4.2 | Compatible versions according to the caret range. |
| ~1.4.2 | Versions within the corresponding minor release range. |
| >=1.4.2 | Version 1.4.2 or newer, subject to the complete range. |
| * | A very broad version range. |
The exact behavior of a range depends on npm's semver rules, including special handling for zero-major versions. For production projects, it is important to understand the range instead of treating ^ and ~ as interchangeable symbols.
What Does the Caret ^ Mean?
The caret generally allows updates that do not change the left-most non-zero version component under npm's semantic versioning rules.
{
"dependencies": {
"example-package": "^1.4.2"
}
}For a package at version 1.4.2, the range can permit compatible 1.x releases while excluding 2.0.0. The rules are more nuanced for versions beginning with zero, so developers should not reduce the behavior to simply 'allows minor updates'.
What Does the Tilde ~ Mean?
The tilde generally allows patch-level updates within a specified minor release.
{
"dependencies": {
"example-package": "~1.4.2"
}
}This can allow later 1.4.x versions while excluding 1.5.0 and later versions outside the range.
The scripts Field
The scripts field defines commands that can be executed through npm.
{
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest",
"lint": "eslint ."
}
}These scripts provide a consistent interface for common project operations. Instead of requiring every developer to remember a long command, the project can expose a short npm script.
npm run dev
npm run build
npm run test
npm run lintSpecial npm Scripts
Several script names have special behavior in npm. For example, npm run can execute user-defined scripts, while npm test and npm start have dedicated command forms.
npm test
npm startThe exact behavior of lifecycle scripts depends on npm and the script name. Projects should avoid unnecessary lifecycle complexity because automatic script execution can make installation and deployment behavior harder to understand.
Using npm Scripts with Arguments
Arguments can be passed through npm scripts using the double-dash separator.
npm run build -- --mode productionEverything after the separator is passed to the underlying command. This is useful when a project script wraps a CLI tool and developers need to provide additional options.
The main Field
The main field traditionally identifies the primary entry point of a Node.js package.
{
"main": "./dist/index.js"
}When a package is consumed by another project, tools may use main to determine which file should be loaded as the package's default entry point. Modern packages often use the exports field for more explicit control.
The type Field
The type field affects how Node.js interprets .js files in a package.
{
"type": "module"
}With type set to module, .js files in that package scope are treated as ES modules by Node.js. Without it, Node.js uses its default interpretation for .js files based on the package configuration and file extension.
Packages can also use CommonJS explicitly with the .cjs extension and ES modules explicitly with .mjs, regardless of the broader package configuration.
CommonJS vs ES Modules
The type field is particularly important when a package uses modern ECMAScript modules.
{
"type": "module"
}import { createServer } from "node:http";
const server = createServer();A CommonJS package can instead use require and module.exports.
const http = require("node:http");
module.exports = {};When publishing a package, module format should be chosen deliberately because it affects how consumers import and execute the package.
The exports Field
The exports field provides a modern way to define which entry points a package exposes to consumers. It can also provide different files for different environments or module systems.
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}Using exports can make a package's public API more explicit and prevent consumers from importing internal files that are not intended to be public.
The browser Field
Some packages use the browser field to provide browser-specific replacements or entry points. Bundlers and other tooling can interpret this field when building applications for browser environments.
{
"browser": "./dist/browser.js"
}Support for package fields depends on the consuming toolchain, so browser should not be treated as a universal replacement for exports.
peerDependencies
peerDependencies are used when a package expects the application consuming it to provide another package. This is especially common for libraries and plugins that integrate with a host framework.
{
"peerDependencies": {
"react": "^19.0.0"
}
}A React component library, for example, may declare React as a peer dependency because the consuming application should provide its own React installation rather than receiving an unrelated copy bundled as an ordinary dependency.
Why peerDependencies Exist
A peer dependency expresses compatibility with a package that is expected to exist in the consumer's project. This is useful when multiple packages need to share the same host library.
- Plugins can declare which versions of their host framework they support.
- Component libraries can express compatible React versions.
- Build integrations can declare compatible versions of their host tool.
- Consumers receive useful dependency compatibility information.
optionalDependencies
optionalDependencies are dependencies that can be unavailable without being treated exactly like a required dependency. They are useful for packages that can provide enhanced functionality when an optional dependency is present.
{
"optionalDependencies": {
"optional-package": "^2.0.0"
}
}Applications and libraries should still handle the optional dependency being unavailable. Declaring a package as optional does not automatically make application code safe when the package cannot be installed.
The engines Field
The engines field can describe which runtime versions a package expects, such as supported Node.js versions.
{
"engines": {
"node": ">=20"
}
}This is useful for communicating runtime requirements and allowing package-management tooling to warn when the current environment does not satisfy them.
The files Field
When publishing a package, the files field can specify which project files should be included in the published package.
{
"files": [
"dist",
"README.md",
"LICENSE"
]
}This can help keep development-only files out of the published package and make the resulting package smaller and easier to consume.
The bin Field
The bin field is used by packages that expose command-line executables.
{
"bin": {
"my-tool": "./bin/my-tool.js"
}
}After installation, npm can make the declared executable available as a command. This is a common pattern for CLI packages.
The config Field
The config field can provide package-specific configuration values that npm exposes through its configuration mechanisms.
{
"config": {
"port": "3000"
}
}This field is less central to most modern application projects than dependencies and scripts, but it can still appear in packages that use npm configuration features.
The private Field
Setting private to true prevents npm from accidentally publishing the package to a registry.
{
"private": true
}This is common in applications and monorepos that are not intended to be published as npm packages.
The workspaces Field
The workspaces field is commonly used in monorepos where one repository contains multiple packages or applications.
{
"private": true,
"workspaces": [
"packages/*",
"apps/*"
]
}Workspaces allow package managers to understand relationships between packages in the same repository and can simplify dependency management for multi-package projects.
Environment-Specific Dependencies
A common mistake is deciding whether something belongs in dependencies or devDependencies based only on where it is imported during development. The important question is whether the package is needed by the application in the environment where it actually runs.
For example, a TypeScript compiler is commonly a development dependency when TypeScript is compiled before deployment. A package imported by the production server at runtime generally belongs in dependencies.
Installing a Runtime Dependency
npm install adds a package to dependencies by default.
npm install zodThe package is downloaded and package.json is updated with the dependency declaration. The lockfile is also updated to reflect the resolved package tree.
Installing a Development Dependency
Use the -D or --save-dev option when a package belongs in devDependencies.
npm install -D typescriptThis makes the intended role of the package explicit in package.json.
Removing a Dependency
npm uninstall removes a dependency from the project and updates the relevant package metadata.
npm uninstall zodUpdating Dependencies
npm update can update installed packages within the version ranges specified by package.json.
npm updateChanging the declared range in package.json and updating the lockfile are separate concerns. A project should review dependency updates rather than assuming that the newest available release is always appropriate.
npm install vs npm ci
npm install and npm ci are both important commands, but they serve different purposes.
| Command | Typical use |
|---|---|
| npm install | Install dependencies and potentially update the lockfile when dependency declarations change. |
| npm ci | Perform a clean, reproducible installation based on the existing lockfile. |
npm ci is commonly used in continuous integration and deployment environments because it expects the lockfile and package.json to be synchronized and installs from the lockfile without modifying it.
A Typical package.json for an Application
{
"name": "example-app",
"version": "1.0.0",
"private": true,
"description": "Example web application",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint ."
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.0.0",
"vite": "^7.0.0"
},
"engines": {
"node": ">=20"
}
}A Typical package.json for a Published Library
{
"name": "@example/ui",
"version": "2.1.0",
"description": "Reusable UI components",
"license": "MIT",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": [
"dist"
],
"peerDependencies": {
"react": "^19.0.0"
},
"scripts": {
"build": "tsc && vite build"
}
}Library package.json files often contain more metadata because they describe a package that other projects will consume. Entry points, type declarations, peer dependencies and published files can all become part of the package's public contract.
The types Field
The types field identifies TypeScript declaration files for a package.
{
"types": "./dist/index.d.ts"
}This allows TypeScript-aware tooling to locate the declarations that describe the package's API.
The module Field
Some packages use module to point bundlers toward an ES module build.
{
"main": "./dist/index.cjs",
"module": "./dist/index.js"
}module is widely recognized by bundler tooling, but it is not a universal Node.js package-resolution standard in the same way that exports is. Modern package authors should understand how their target tools resolve package entry points instead of adding fields without considering the consumer ecosystem.
Package Scripts and Local Binaries
npm scripts can execute locally installed command-line tools without requiring developers to install those tools globally.
{
"scripts": {
"lint": "eslint .",
"format": "prettier . --write"
}
}When npm executes a script, the project's local node_modules/.bin directory is included in the command lookup path. This is why commands such as eslint can work from an npm script even when the executable is not installed globally.
package.json in Framework Projects
Framework projects often add their own scripts and dependencies to package.json. A Next.js project, for example, commonly contains scripts for development, production builds and starting the production server.
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}The exact versions and scripts depend on the project. The important idea is that package.json provides the dependency and command contract that the framework and deployment environment rely on.
How package.json and node_modules Relate
package.json declares what the project needs, while node_modules contains installed packages on the current machine. node_modules is generated installation output rather than the project's dependency declaration.
For this reason, node_modules is normally not committed to Git. Instead, package.json and the appropriate lockfile are committed so another environment can recreate the dependency installation.
Why node_modules Is Usually Not Committed
A node_modules directory can contain a very large number of files and may include platform-specific binaries. Committing it would make repositories unnecessarily large and make dependency management harder.
node_modules/
.envA typical .gitignore therefore excludes node_modules. The exact .gitignore contents depend on the project, but generated dependencies generally should be recreated from package metadata instead of stored in source control.
Common package.json Mistakes
- Putting a package in dependencies when it is only needed during development.
- Putting a runtime dependency in devDependencies and then omitting it from a production installation.
- Using overly broad version ranges without understanding their implications.
- Editing package.json manually without updating the lockfile.
- Committing node_modules instead of dependency metadata.
- Forgetting to declare a peer dependency required by a published library.
- Publishing internal files because the package does not restrict its published contents.
- Exposing unintended package entry points by relying on implicit module resolution.
- Declaring an unsupported Node.js version in engines.
- Adding scripts that depend on globally installed tools instead of project-local dependencies.
How to Validate package.json
Because package.json is JSON, syntax errors can prevent tools from reading it correctly. A missing comma, extra comma, malformed string or invalid JSON structure can cause installation or tooling commands to fail.
{
"name": "example-app",
"version": "1.0.0",
"scripts": {
"build": "vite build"
}
}Validation should also go beyond JSON syntax. A package.json can be valid JSON while still containing incorrect dependency versions, unsupported fields or incompatible configuration.
Formatting package.json
Consistent formatting makes package.json easier to review and maintain. JSON formatters can normalize indentation and spacing without changing the meaning of the configuration.
Most modern editors can format JSON automatically. A dedicated formatter can also be useful when checking or cleaning a package.json file outside the project.
Checking for Dependency Problems
Dependency management becomes more difficult as a project grows. A package may have direct dependencies, transitive dependencies and peer dependency requirements. Checking the dependency tree can help identify outdated, duplicated or potentially incompatible packages.
npm lsnpm ls displays information about the installed dependency tree. More specialized dependency-checking tools can help identify version problems and unused or inconsistent dependencies.
Using package.json with a CDN
Some JavaScript packages can also be consumed through public CDNs. In these cases, the package name and version from package.json can help identify the corresponding package version and resource URL.
CDN delivery is separate from npm installation. A browser loading a JavaScript file from a CDN does not automatically install the package into node_modules.
https://cdn.example.com/[email protected]/dist/index.jsThe exact URL format depends on the CDN. When using a CDN in production, verify that the package supports browser usage and that the selected version and entry file are appropriate.
package.json for Applications vs Libraries
One of the most important distinctions is whether package.json belongs to an application or a reusable library.
| Concern | Application | Library |
|---|---|---|
| private | Often true when the project is not published. | Usually omitted or false when the package is intended for publication. |
| dependencies | Packages required to run the application. | Packages required by the library itself. |
| peerDependencies | Less common. | Important for host frameworks and shared runtime dependencies. |
| exports | Usually less important. | Important for defining the public package API. |
| files | Usually less important. | Useful for controlling published package contents. |
| types | Not usually needed for an application. | Important for TypeScript libraries. |
package.json Security Considerations
Dependencies are executable code, so package.json is also part of a project's security surface. Adding a package means trusting that package and, indirectly, its dependency tree.
- Review unfamiliar dependencies before adding them.
- Keep dependencies reasonably up to date.
- Review security advisories affecting installed packages.
- Avoid unnecessary dependencies that increase the attack surface.
- Use lockfiles to make installations more predictable.
- Be careful with package lifecycle scripts when installing untrusted packages.
npm can execute package lifecycle scripts during installation and other operations. This is one reason dependency installation should be treated as running third-party code rather than simply downloading static files.
How package.json Fits Into a JavaScript Project
A useful way to think about package.json is as the project's declared package contract. It tells tooling what the project is, what it needs, what commands it exposes and, for published libraries, what other projects can consume.
- package.json declares the project's requirements and metadata.
- The lockfile records a concrete dependency resolution.
- node_modules contains the locally installed dependency tree.
- npm reads package metadata to install packages and execute scripts.
- Build tools use package scripts and dependencies to construct the application.
- Published libraries use fields such as exports, types and peerDependencies to describe their public interface.
A Practical package.json Checklist
- Keep package.json valid JSON.
- Use a meaningful project name.
- Keep the version accurate for packages that are published.
- Put runtime dependencies in dependencies.
- Put development-only tooling in devDependencies.
- Use peerDependencies when consumers are expected to provide a host dependency.
- Use appropriate version ranges instead of blindly copying version strings.
- Define useful scripts for development, builds, testing and linting.
- Specify supported Node.js versions when the project has runtime requirements.
- Use private: true for applications that must not be published to npm.
- Commit package-lock.json for npm-based applications.
- Do not commit node_modules.
- Review dependency updates and security advisories.
- For libraries, explicitly define public entry points and published files when appropriate.
Frequently Asked Questions
What is package.json used for?
package.json describes a JavaScript or Node.js project. It can contain project metadata, dependencies, development dependencies, npm scripts, runtime requirements and package publishing configuration.
What is the difference between dependencies and devDependencies?
dependencies contain packages required by the application at runtime, while devDependencies generally contain packages used for development, testing, linting or building.
Should package-lock.json be committed to Git?
For npm-based applications, committing package-lock.json is generally recommended because it records the concrete dependency tree and makes installations more reproducible.
What is the difference between package.json and node_modules?
package.json declares the project's dependencies and configuration, while node_modules contains the packages actually installed on the current machine.
What does ^ mean before a dependency version?
The caret specifies a semantic-version range. It generally allows compatible updates without changing the left-most non-zero version component, with special rules for zero-major versions.
What is peerDependencies used for?
peerDependencies are used when a package expects the project consuming it to provide another package, which is common for libraries and plugins that integrate with a host framework.
What does private: true do?
It marks the package as private and prevents npm from accidentally publishing it through normal package publishing workflows. It is commonly used by applications and monorepos.
Helpful Package Management Tools
A Package.json Validator can help detect invalid JSON structure and configuration problems, while a Package.json Formatter can normalize formatting and make the file easier to read. An npm Dependency Checker is useful when reviewing installed dependencies and their relationships.
An npm Version Calculator can help determine compatible semantic-version ranges and compare version requirements. When a package also needs to be delivered directly to browsers, a CDN URL Generator can help construct URLs for package assets based on a selected package and version.
Conclusion
package.json is much more than a list of npm packages. It is the central project manifest for many JavaScript and Node.js projects, describing dependencies, development tooling, scripts, runtime requirements and, for published packages, the interface exposed to consumers.
The most important fields to understand first are dependencies, devDependencies, scripts, version, name and engines. As you move into library development, fields such as peerDependencies, exports, types, files, bin and workspaces become increasingly important.
Understanding package.json also means understanding how it works together with package-lock.json and node_modules. package.json describes what the project needs, the lockfile records the resolved dependency tree, and node_modules contains the installed result.
Once these relationships are clear, npm commands and JavaScript project configuration become much easier to understand. Instead of treating package.json as a file that frameworks generate automatically, you can use it deliberately to control dependencies, scripts, compatibility and package behavior.