Skip to main content

Що таке 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

Швидкий приклад

bash
mkdir my-app && cd my-app npm init -y # генерує package.json миттєво npm install express # додає до dependencies npm install --save-dev jest # додає до devDependencies npm start # виконує скрипт "start"
json
{ "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
versionSemVer: major.minor.patch
mainФайл, який Node завантажує при require('your-package')
scriptsКоманди доступні через npm run <назва>
dependenciesПакети для роботи в продакшені
devDependenciesПакети тільки для розробки, виключаються при --production
peerDependenciesПакети, які споживач вашої бібліотеки має встановити сам
enginesМінімальна версія Node.js для вашого коду

Версії та SemVer

json
"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 хуки

json
{ "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 версія. Речі ламаються.

bash
# Неправильно 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

json
// package.json { "name": "basic-api", "version": "1.0.0", "main": "index.js", "scripts": { "start": "node index.js" }, "dependencies": { "express": "^4.19.2" } }
js
// 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 скриптами

json
// 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" } }
ts
// 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

json
// 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 рантаймі викидатимуть помилки.

json
// Кореневий package.json - примусово встановлює єдину версію React { "workspaces": ["packages/*"], "overrides": { "react": "^18.3.0" } }

Поле overrides примушує всі суб-пакети вирішувати React в ^18.3.0, усуваючи невідповідність. Без нього: Module not found або помилки типів у рантаймі. З ним: одна версія React по всьому workspace.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?