Що таке package.json і як працює npm?
package.json - це файл конфігурації в корені кожного Node.js проекту. Він містить залежності, визначає скрипти і зберігає метадані. npm читає його, щоб завантажити пакети з публічного реєстру і виконати визначені команди.
Теорія
TL;DR
- package.json - список покупок проекту, npm - служба доставки що все це привозить
dependenciesпотрібні в продакшені;devDependencies- тільки для збірки та тестівpackage-lock.jsonфіксує точні версії, щоб коженnpm installдавав однаковий результат- Ніколи не комітьте
node_modules/; завжди комітьтеpackage-lock.json - У CI/CD пайплайнах використовуй
npm ciзамістьnpm install
Швидкий приклад
mkdir my-app && cd my-app
npm init -y # генерує package.json миттєво
npm install express # додає до dependencies
npm install --save-dev jest # додає до devDependencies
npm start # виконує скрипт "start"{
"name": "my-app",
"version": "1.0.0",
"scripts": { "start": "node index.js" },
"dependencies": { "express": "^4.19.2" },
"devDependencies": { "jest": "^29.0.0" }
}Команда npm start запускає запис "start" зі секції scripts. Ось і вся ідея.
Основні поля
| Поле | Що робить |
|---|---|
name | Назва пакету, унікальна якщо публікуєте в реєстр npm |
version | SemVer: major.minor.patch |
main | Файл, який Node завантажує при require('your-package') |
scripts | Команди доступні через npm run <назва> |
dependencies | Пакети для роботи в продакшені |
devDependencies | Пакети тільки для розробки, виключаються при --production |
peerDependencies | Пакети, які споживач вашої бібліотеки має встановити сам |
engines | Мінімальна версія Node.js для вашого коду |
Версії та SemVer
"express": "4.19.2" // точна версія, ніколи не оновлюється
"express": "^4.19.2" // сумісна: >=4.19.2 <5.0.0 (npm за замовчуванням)
"express": "~4.19.2" // тільки патчі: >=4.19.2 <4.20.0
"express": "*" // будь-яка версія - уникайте в реальних проектахПрефікс ^ npm записує за замовчуванням при npm install. Він дозволяє minor та patch оновлення, які за правилами SemVer мають бути зворотньо сумісними. Префікс ~ строгіший: тільки патчі. Точне закріплення версії звучить надійніше, але означає що ви пропустите патчі безпеки якщо не оновлюєте вручну.
Як працює npm install
Коли запускаєш npm install, npm робить три речі по черзі. Спочатку читає package.json щоб дізнатись потрібні діапазони версій. Потім перевіряє package-lock.json на точні версії з попередньої установки. І нарешті завантажує архіви з registry.npmjs.org та розпаковує їх у node_modules/. Якщо локфайлу ще немає, npm вирішує останні відповідні версії і створює його.
npm ci пропускає крок вирішення повністю. Він читає локфайл напряму і завершується з помилкою якщо той не відповідає package.json. Тому CI/CD пайплайни і використовують саме цю команду: результат передбачуваний, встановлення швидше.
Я бачив як проекти ламались на стейджингу після того як хтось видалив package-lock.json для чистоти - і minor версії пакетів тихо змінили поведінку. Локфайл існує не просто так.
npm скрипти та lifecycle хуки
{
"scripts": {
"start": "node dist/index.js",
"dev": "nodemon src/index.ts",
"build": "tsc",
"test": "jest --coverage",
"prestart": "npm run build",
"posttest": "echo 'Tests done'"
}
}Префікси pre та post - це lifecycle хуки. prestart запускається перед start автоматично, без окремого виклику. Скрипти запускають через npm run build, але npm start та npm test мають вбудовані скорочення без ключового слова run.
Коли що використовувати
- Новий проект з нуля:
npm init -y - Додати бібліотеку для рантайму:
npm install express - Додати інструмент для розробки:
npm install --save-dev typescript jest - Поділитись проектом: закоміть
package.json+package-lock.json, колеги запускаютьnpm install - Деплой або CI/CD:
npm ciзамістьnpm install - Перевірити відомі вразливості:
npm audit
Типові помилки
Не комітити package-lock.json. Без нього npm install завантажує найновішу версію в межах діапазону. Твій ^4.19.2 через місяць може встановити 4.20.1 для колеги якщо вийшла нова minor версія. Речі ламаються.
# Неправильно
echo "package-lock.json" >> .gitignore
git commit -m "cleanup" # колеги отримають різні версії пакетів
# Правильно
git add package-lock.json # завжди комітьте локфайлКомітити node_modules. Ця папка на звичайному проекті важить 200MB+. npm відновить її за кілька секунд з локфайлу. Немає сенсу тримати її в git.
Класти dev-інструменти у dependencies. TypeScript, Jest, nodemon не запускають застосунок в продакшені. Вони мають бути в devDependencies. Якщо потраплять у dependencies, їх встановлять на кожен продакшен деплой без потреби.
Зірочка у полі engines. "node": "*" означає що код може потрапити на Node 12, де немає top-level await та інших нових API. Вказуйте мінімум: "node": ">=20.0.0".
npm install у CI пайплайнах. Ця команда може змінити локфайл якщо знайде розбіжності. Використовуйте npm ci - вона завершується з помилкою при невідповідності, що виявляє проблему замість того щоб приховати її.
Де зустрічається у реальних проектах
- Express:
"start": "node server.js"у scripts,express@^4.19.2у dependencies - Create React App:
react,react-dom,react-scriptsу dependencies;npm startзапускає webpack dev server - Next.js:
"build": "next build","start": "next start"для SSR у продакшені - NestJS:
@nestjs/core,@nestjs/commonу dependencies,"node": ">=18"у engines - Для одного застосунку npm підходить добре. Монорепозиторії з кількома пакетами часто переходять на pnpm для кращої підтримки workspace і меншого використання диска.
Питання на співбесіді
Q: Яка різниця між dependencies та devDependencies?
A: dependencies встановлюються в продакшені. devDependencies пропускаються при npm install --production. Jest, TypeScript та nodemon мають бути в devDependencies, бо в рантаймі вони не потрібні.
Q: Що означає "^4.19.2" у термінах SemVer?
A: ^ дозволяє minor та patch оновлення: будь-яка версія >=4.19.2 <5.0.0. ~ строгіший: тільки патчі, >=4.19.2 <4.20.0. Major версія ніколи не оновлюється автоматично, SemVer вважає їх breaking changes.
Q: Що станеться якщо немає package-lock.json?
A: npm вирішить останню версію що відповідає кожному діапазону і створить новий локфайл. Два окремих npm install в різний час можуть встановити різні версії, звідси класична проблема "у мене працює, у тебе ні".
Q: Яка різниця між npm install та npm ci?
A: npm install вирішує версії і може оновити локфайл як побічний ефект. npm ci читає локфайл точно як він є та завершується з помилкою при невідповідності з package.json. Для автоматизованих пайплайнів підходить другий варіант.
Q: (Senior) Як npm workspaces керують спільними залежностями в монорепозиторії?
A: Поле "workspaces": ["packages/*"] у кореневому package.json каже npm підіймати спільні залежності до кореневого node_modules. Якщо три пакети залежать від React, npm встановить його один раз. Поле overrides дозволяє примусово встановити конкретну версію для всіх суб-пакетів коли виникають конфлікти peer dependencies.
Приклади
Базовий: Express сервер з npm start
// package.json
{
"name": "basic-api",
"version": "1.0.0",
"main": "index.js",
"scripts": { "start": "node index.js" },
"dependencies": { "express": "^4.19.2" }
}// index.js
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello World'));
app.listen(3000, () => console.log('Server on port 3000'));Запускаєш npm install щоб отримати Express, потім npm start. У терміналі з'явиться "Server on port 3000". GET до localhost:3000 поверне "Hello World". З цього мінімального налаштування починається кожен Express проект.
Середній: TypeScript з build і watch скриптами
// package.json
{
"name": "ts-api",
"version": "1.0.0",
"scripts": {
"start": "node dist/index.js",
"dev": "nodemon src/index.ts",
"build": "tsc",
"prestart": "npm run build" // компілює перед запуском
},
"dependencies": {
"express": "^4.19.2",
"dotenv": "^16.4.5"
},
"devDependencies": {
"nodemon": "^3.1.4",
"typescript": "^5.6.2",
"@types/express": "^4.17.21"
},
"engines": { "node": ">=20.0.0" }
}// src/index.ts
import express from 'express';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => res.send(`Running on port ${port}`));
app.listen(port);npm run dev запускає nodemon, який стежить за TypeScript файлами і перезапускає сервер при змінах. npm start спочатку виконає prestart - компілює TypeScript у dist/, потім запускає скомпільований код. Ключовий патерн: nodemon, typescript та @types у devDependencies, express та dotenv у dependencies.
Просунутий: Конфлікт peer dependencies та виправлення через overrides
// packages/ui-lib/package.json (суб-пакет у монорепозиторії)
{
"name": "ui-lib",
"peerDependencies": { "react": "^18.0.0" },
"dependencies": { "lodash": "^4.17.21" }
}Якщо кореневий застосунок встановить React 17, а ui-lib оголошує react@^18 як peer dependency - npm попередить але встановить. В рантаймі компоненти що використовують React 18 API на React 17 рантаймі викидатимуть помилки.
// Кореневий package.json - примусово встановлює єдину версію React
{
"workspaces": ["packages/*"],
"overrides": { "react": "^18.3.0" }
}Поле overrides примушує всі суб-пакети вирішувати React в ^18.3.0, усуваючи невідповідність. Без нього: Module not found або помилки типів у рантаймі. З ним: одна версія React по всьому workspace.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.