What is the difference between dependencies, devDependencies, and peerDependencies in an NPM package.json file? The official documentation doesn’t provide clear explanations. Can someone explain these concepts in simpler terms with practical examples to help distinguish between them?
Dependencies in npm package.json files serve different purposes depending on when the packages are needed. dependencies are packages required for your application to run in production, while devDependencies are only needed during development and testing. peerDependencies are packages that your package expects to be available in the host environment but doesn’t install directly.
Contents
- What are dependencies?
- Understanding devDependencies
- The peerDependencies Explained
- Practical Examples
- When to Use Each Type
- Best Practices and Common Mistakes
- Real-world Scenarios
What are dependencies?
Dependencies in a package.json file are packages that your application needs to function properly in production. These are the core libraries and modules that make your application work when deployed and running for end users.
When you run npm install or npm install --production, npm will install all packages listed in the dependencies section. These packages become available for import in your application code and are included in the node_modules directory.
"dependencies": {
"express": "^4.18.2",
"lodash": "^4.17.21",
"axios": "^1.4.0"
}
In this example:
expressis a web framework needed to serve your applicationlodashprovides utility functions used throughout your codeaxiosis used for making HTTP requests in your application
These are all essential for your application to run properly when deployed to production.
Understanding devDependencies
devDependencies are packages that are only needed during the development and testing process, not when the application is running in production. These include tools for building, testing, linting, and other development-related tasks.
When you run npm install (without flags), npm installs both dependencies and devDependencies. However, when you run npm install --production, npm only installs the dependencies, skipping devDependencies.
"devDependencies": {
"nodemon": "^2.0.22",
"jest": "^29.5.0",
"eslint": "^8.42.0",
"webpack": "^5.88.0"
}
In this example:
nodemonis used for development to automatically restart the server when code changesjestis a testing framework used during developmenteslinthelps maintain code quality and style during developmentwebpackis a bundler used to build the application for production
These tools are essential for development but don’t need to be present in the production environment.
The peerDependencies Explained
peerDependencies are a more complex concept. These are packages that your package expects to be available in the host environment but doesn’t install directly. Instead, it relies on the host application to provide these dependencies.
This pattern is commonly used in libraries and plugins that need to work with a specific framework or runtime. The peer dependency acts as a contract between your package and the host environment.
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
For example, a React component library would declare React and React DOM as peer dependencies because:
- The library itself doesn’t include React
- It assumes the host application already has React installed
- This prevents duplicate React instances which can cause conflicts
When npm installs your package, it won’t automatically install peer dependencies. Instead, it will warn if the required peer dependencies aren’t present in the host environment.
Practical Examples
Let’s look at some practical examples to better understand these concepts:
Example 1: A Web Application
{
"name": "my-web-app",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.4.0"
},
"devDependencies": {
"nodemon": "^2.0.22",
"jest": "^29.5.0"
}
}
What this means:
- When deployed to production, only express and mongoose will be installed
- During development, nodemon and jest will also be available
- In production, the application works with just the core dependencies
Example 2: A React Component Library
{
"name": "my-react-components",
"version": "1.0.0",
"dependencies": {
"styled-components": "^5.3.10"
},
"devDependencies": {
"@types/react": "^18.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
}
What this means:
- The library uses styled-components (a dependency)
- It’s developed with React, React DOM, and TypeScript types (devDependencies)
- It expects host applications to provide React and React DOM (peerDependencies)
- When someone uses this library, they must have React installed in their project
When to Use Each Type
Use dependencies for:
- Core functionality: Libraries your application needs to run
- External APIs: Packages that handle external services
- UI frameworks: React, Vue, Angular, etc.
- Database connectors: Mongoose, Prisma, etc.
Use devDependencies for:
- Development tools: Bundlers, linters, formatters
- Testing frameworks: Jest, Mocha, etc.
- Build tools: Webpack, Babel, etc.
- Type definitions: @types packages
- Documentation generators: JSDoc, etc.
Use peerDependencies for:
- Framework plugins: Libraries that extend frameworks
- Extension modules: Packages that enhance existing functionality
- Version-specific integrations: Libraries designed for specific framework versions
- Avoiding duplicate installations: When you want to share dependencies with the host
Best Practices and Common Mistakes
Best Practices:
- Keep dependencies minimal: Only include what’s absolutely necessary
- Use semantic versioning: Specify version ranges carefully to avoid breaking changes
- Regularly update dependencies: Use
npm outdatedandnpm updateto stay current - Lock your versions: Use
package-lock.jsonfor reproducible builds - Audit for vulnerabilities: Regularly run
npm audit
Common Mistakes:
- Misplacing dependencies: Putting production dependencies in devDependencies
- Ignoring peerDependencies: Not properly declaring required framework versions
- Version conflicts: Not managing version ranges effectively
- Over-including: Adding development tools to dependencies
- Neglecting to update: Using outdated packages with known security issues
Real-world Scenarios
Scenario 1: Building a React App
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"next": "^13.4.0"
},
"devDependencies": {
"@types/node": "^20.4.0",
"@types/react": "^18.2.0",
"typescript": "^5.1.0",
"eslint": "^8.42.0"
}
}
Production deployment: Only React, React DOM, and Next.js are needed
Development: TypeScript, ESLint, and type definitions help during development
Scenario 2: Creating a VSCode Extension
{
"dependencies": {
"vscode": "^1.79.0"
},
"devDependencies": {
"@types/vscode": "^1.79.0",
"@types/node": "^20.4.0"
},
"peerDependencies": {
"vscode": "^1.79.0"
}
}
What this means:
- The extension depends on VSCode API
- It’s developed with VSCode types (for autocompletion)
- It expects VSCode to be available in the host environment
- No duplicate VSCode installation occurs
Sources
- Official npm Documentation - package.json
- MDN Web Docs - npm package.json
- GitHub npm package.json reference
- Node.js Documentation - npm registry
- Medium article - Understanding npm package.json
Conclusion
Understanding the difference between dependencies, devDependencies, and peerDependencies is crucial for effective npm package management. Dependencies are packages your application needs to run in production, devDependencies are development-only tools, and peerDependencies are expected to be provided by the host environment. By properly categorizing your dependencies, you can reduce bundle sizes, avoid conflicts, and ensure your application works smoothly in all environments. Always consider whether a package is needed during production development or testing, and use the appropriate section in your package.json file accordingly.