Модуль 2
Модули и npm
- CommonJS
- ES Modules
- require
- import/export
- package.json
- npm
- npx
- Зависимости
#2.1 Зачем нужны модули
Без модулей весь код приложения находился бы в одном файле. Для маленького скрипта это допустимо, но реальные проекты содержат тысячи строк кода. Модули позволяют разбить код на логические части: один файл отвечает за работу с базой данных, другой — за обработку HTTP-запросов, третий — за отправку email.
Модульная система решает несколько задач:
- Организация кода — каждый файл отвечает за одну задачу (принцип единственной ответственности)
- Переиспользование — написанный модуль можно использовать в разных проектах
- Инкапсуляция — модуль экспортирует только то, что нужно другим, скрывая внутреннюю реализацию
- Управление зависимостями — чётко видно, какой модуль от чего зависит
В Node.js существуют две системы модулей: CommonJS (историческая, используется по умолчанию) и ES Modules (современный стандарт JavaScript). Рассмотрим обе.
#2.2 CommonJS-модули
CommonJS — это система модулей, существующая в Node.js с самого начала. Она использует функцию require() для импорта и объект module.exports для экспорта. Без поля "type": "module" в package.json Node.js по умолчанию использует CommonJS.
Для примеров этого раздела отдельный package.json создавать не нужно — без поля "type": "module" файлы .js работают как CommonJS. Создайте для них отдельную папку (например, commonjs-demo). Дальше, в разделе 2.3, мы разберём те же математические функции в стиле ESM, поэтому примеры CommonJS и ESM удобно держать в разных папках: иначе одноимённые файлы math.js будут перезаписывать друг друга, а включённое позже "type": "module" нарушит работу примеров CommonJS.
Рассмотрим экспорт нескольких функций из модуля. Создайте файл math.js:
// math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
// Экспортируем объект с функциями
module.exports = { add, multiply };Импортируем и используем функции в другом файле. Создайте файл app.js:
// app.js
const { add, multiply } = require('./math');
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20Выполните:
node app.jsОбратите внимание: путь начинается с ./ — это означает «файл в той же папке, что и текущий модуль». Без ./ Node.js будет искать модуль в папке node_modules или среди встроенных модулей. Расширение .js можно не указывать.
Если модуль экспортирует одно значение (класс, функцию), удобнее присвоить его напрямую объекту module.exports. Создайте ещё один файл logger.js:
// logger.js
class Logger {
log(msg) { console.log(`[LOG] ${msg}`); }
error(msg) { console.error(`[ERR] ${msg}`); }
}
module.exports = Logger; // Экспортируем сам класс, а не объектИзмените файл app.js следующим образом:
const { add, multiply } = require('./math');
console.log(add(2, 3)); // 5
console.log(multiply(4, 5));
const Logger = require('./logger');
const log = new Logger();
log.log('Сервер запущен');Запустите файл и проверьте результат.
Важная особенность CommonJS: require() работает синхронно. Файл модуля читается и выполняется в момент вызова require(). Результат кешируется — при повторном require() того же файла код не выполняется заново, а возвращается уже готовый объект.
#2.3 ES Modules (ESM)
ES Modules (ESM) — это стандарт модульной системы, принятый в спецификации ECMAScript (ES6, 2015). Он использует ключевые слова import и export для организации кода в отдельные, переиспользуемые модули.
Зачем нужны модули?
Без модулей весь код находился бы в одном файле или в глобальной области видимости, что приводило бы к проблемам:
- Конфликты имён — две переменные с одинаковым именем перезаписывают друг друга
- Сложность поддержки — в файле на 5000 строк невозможно быстро найти нужную функцию
- Невозможность переиспользования — один и тот же код приходится копировать между проектами
Модули решают все эти проблемы: каждый файл — изолированная единица со своей областью видимости; он явно объявляет, что экспортирует (отдаёт наружу) и что импортирует (берёт из других модулей).
Отличие ESM от CommonJS
| Характеристика | CommonJS (require) | ESM (import/export) |
|---|---|---|
| Синтаксис | const x = require('./x') | import x from './x' |
| Загрузка | Синхронная (блокирующая) | Асинхронная (неблокирующая) |
| Время разбора | В момент выполнения (runtime) | До выполнения, на этапе парсинга (статический анализ) |
| Где работает | Только Node.js | Браузеры + Node.js |
| Tree-shaking | Не поддерживается | Поддерживается (бандлеры могут удалить неиспользуемый код) |
| Top-level await | Не поддерживается | Поддерживается |
| this на верхнем уровне | Ссылается на module.exports | undefined |
| __dirname / __filename | Доступны глобально | Нет, используйте import.meta.url |
Tree-shaking — это важное преимущество ESM. Поскольку импорты анализируются статически (до выполнения кода), сборщики (Webpack, Rollup, esbuild) могут определить, какие функции используются, и удалить неиспользуемый код из финальной сборки. С CommonJS это невозможно, потому что require() может вызываться динамически внутри if-блоков.
Сборщик (бандлер) — это инструмент, который собирает множество исходных модулей в один-два готовых файла; чаще всего он нужен для фронтенда, чтобы отправить в браузер компактную сборку вместо сотен отдельных файлов. Подробно бандлеры в этом курсе не рассматриваются — на серверной стороне они обычно не нужны. Здесь достаточно запомнить, что финальная сборка может отличаться от исходников и что статические импорты ESM позволяют исключить из неё лишнее.
Как включить ESM в Node.js
Чтобы использовать ESM в Node.js, нужно сделать одно из двух:
Способ 1: указать "type": "module" в package.json
{
"name": "my-project",
"version": "1.0.0",
"type": "module"
}Строка "type": "module" включает режим ESM сразу для всех файлов .js проекта. Если package.json создан командой npm init -y, поле type в нём уже есть со значением commonjs (так делает npm 11 из Node 24): поменяйте значение, а не добавляйте второе поле.
С полем "type": "module" все файлы .js в проекте интерпретируются как ES-модули. Для отдельного файла в формате CommonJS используйте расширение .cjs.
Способ 2: использовать расширение .mjs
Файлы с расширением .mjs всегда обрабатываются как ES-модули, независимо от настроек package.json. Это удобно, когда нужно использовать ESM в проекте, который в целом работает на CommonJS.
my-project/
├── package.json (без "type": "module")
├── app.js <-- CommonJS (require/module.exports)
├── utils.mjs <-- ESM (import/export)
└── helpers.cjs <-- Явно CommonJSРекомендация: в новых проектах лучше использовать Способ 1 ("type": "module") — это современный подход, и большинство новых библиотек Node.js уже переходят на ESM.
Функции модуля могут экспортироваться наружу, а функции других модулей — импортироваться в него. Существует несколько видов экспорта и импорта.
Именованный экспорт (Named Export)
Через именованный экспорт модуль может отдать наружу несколько значений. Каждое значение экспортируется под своим именем — и импортировать его нужно точно по этому имени.
// math.js (с "type": "module" в package.json)
// Можно ставить export перед каждым объявлением:
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export const PI = 3.14159;
// Приватная функция — НЕ экспортирована, недоступна снаружи
function _internalHelper() {
return 42;
}add, multiply и PI экспортированы — их можно импортировать в другом файле.
_internalHelper не экспортирована: она существует только внутри math.js и недоступна извне.
Это ключевое свойство модулей: модуль сам определяет, что видно снаружи, а что скрыто.
Альтернативный синтаксис экспорта — экспорт списком в конце файла:
// math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
const PI = 3.14159;
// Экспортируем все разом в конце файла
export { add, multiply, PI };Оба варианта полностью эквивалентны. Первый (export перед объявлением) удобнее, когда функций много. Второй удобнее, когда нужно видеть весь «публичный API» модуля в одном месте.
Экспорт с переименованием (alias):
// math.js
function add(a, b) { return a + b; }
export { add as sum }; // снаружи будет доступна как sum, а не addЭкспорт по умолчанию (Default Export)
Экспорт по умолчанию — это одно главное значение из модуля. В каждом модуле допускается только один экспорт по умолчанию.
// Calculator.js
export default class Calculator {
add(a, b) { return a + b; }
sub(a, b) { return a - b; }
mul(a, b) { return a * b; }
div(a, b) {
if (b === 0) throw new Error('Деление на ноль!');
return a / b;
}
}Когда использовать экспорт по умолчанию:
Когда модуль представляет одну сущность — один класс, одну функцию, один объект. Например: класс Calculator, функция createServer, объект конфигурации.
Когда использовать именованный экспорт:
Когда модуль содержит набор утилит — несколько функций, констант. Например: { add, multiply, PI } из математического модуля.
Важно: комбинировать оба типа экспорта в одном файле возможно, но не рекомендуется — такой модуль сложнее использовать.
Именованный импорт — фигурные скобки обязательны:
// app.js
import { add, multiply, PI } from './math.js';
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
console.log(PI); // 3.14159Обратите внимание:
Имена в { } должны точно совпадать с именами экспорта
Расширение .js — обязательно в ESM Node.js (в отличие от CommonJS, где его можно опустить)
Импорт по умолчанию — без фигурных скобок:
import Calculator from './Calculator.js';
const calc = new Calculator();
console.log(calc.add(10, 20)); // 30
console.log(calc.div(10, 3)); // 3.333...При импорте по умолчанию переменную можно назвать как угодно:
import MyCalc from './Calculator.js'; // Работает
import Калькулятор from './Calculator.js'; // Тоже работаетИмпорт с переименованием (alias):
import { add as сложить, multiply as умножить } from './math.js';
console.log(сложить(2, 3)); // 5
console.log(умножить(4, 5)); // 20Это полезно, когда имена из разных модулей конфликтуют или когда нужно дать более понятное имя в контексте текущего файла.
Импорт всего модуля как объекта (namespace import):
import * as math from './math.js';
console.log(math.add(2, 3)); // 5
console.log(math.multiply(4, 5)); // 20
console.log(math.PI); // 3.14159Удобно, когда модуль экспортирует много функций и требуется явно показать, откуда берётся каждая.
Импорт встроенных модулей Node.js:
import { readFile } from 'fs/promises'; // Промис-версия fs
import { join, resolve } from 'path'; // Работа с путями
import { createServer } from 'http'; // HTTP-сервер
// Также можно использовать префикс node: (рекомендуется)
// import { readFile } from 'node:fs/promises';Префикс node: рекомендуется в современном коде. Он явно указывает, что это встроенный модуль Node.js, а не npm-пакет с таким же именем.
Комбинированный импорт (по умолчанию + именованный):
Если модуль экспортирует и default, и именованные значения, то импортировать можно таким образом:
import Calculator, { helperFunction } from './Calculator.js';Динамический импорт
В отличие от статического import, динамический import() загружает модуль во время выполнения (runtime). Он возвращает Promise.
async function loadModule(type) {
if (type === 'math') {
const math = await import('./math.js');
console.log(math.add(2, 3)); // 5
} else {
const utils = await import('./utils.js');
utils.doSomething();
}
}Когда использовать:
- Ленивая загрузка (lazy loading) — загрузить тяжёлый модуль, только когда он действительно нужен
- Условная загрузка — выбрать модуль в зависимости от настроек или окружения
- Загрузка CommonJS-модулей из ESM-кода
Top-level await
В ESM можно использовать await на верхнем уровне файла, без обёртки в async function. Пусть есть файл config.json:
{
"port": "3000"
}Файл config.js читает и разбирает его на верхнем уровне:
// config.js (ESM)
import { readFile } from 'node:fs/promises';
// await прямо на верхнем уровне — работает только в ESM!
const raw = await readFile('./config.json', 'utf-8');
export const config = JSON.parse(raw);Файл app.js импортирует уже готовый объект config:
// app.js
import { config } from './config.js';
// config уже загружен и распарсен к этому моменту
console.log(config.port); // 3000В CommonJS использовать await на верхнем уровне невозможно — там пришлось бы оборачивать этот код в async function и вызывать её.
Частые ошибки и подводные камни
1. Пропущено расширение файла
// ❌ ОШИБКА в ESM Node.js
import { add } from './math';
// ✅ Правильно — расширение обязательно
import { add } from './math.js';2. Попытка использовать require() в ESM
// ❌ ОШИБКА — require не определён в ESM
const fs = require('fs');
// ✅ Правильно
import fs from 'node:fs';3. В ESM нет глобальных __dirname и __filename. Вместо них:
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__filename); // /Users/alex/project/app.js
console.log(__dirname); // /Users/alex/projectПоясним назначение этих переменных: __filename — полный путь к текущему файлу, __dirname — путь к папке, в которой он находится (без имени файла). В CommonJS они доступны сразу как глобальные, а в ESM их нет — вместо них есть import.meta, объект с метаданными модуля, содержащий свойство url (адрес файла в формате file://). Функция fileURLToPath преобразует этот url обратно в обычный путь. В Node 24 доступен и более короткий вариант — import.meta.dirname и import.meta.filename дают то же самое без ручного преобразования.
4. Циклические зависимости
ESM обрабатывает циклические зависимости лучше CommonJS, но их всё равно следует избегать:
// a.js
import { b } from './b.js';
export const a = 'A';
// b.js
import { a } from './a.js'; // a ещё не инициализирована: обращение к ней здесь даст ReferenceError
export const b = 'B';Итоговая сводка
| Операция | Синтаксис |
|---|---|
| Именованный экспорт | export function add() {} или export { add, PI } |
| Экспорт по умолчанию | export default class Calculator {} |
| Именованный импорт | import { add, PI } from './math.js' |
| Импорт по умолчанию | import Calculator from './Calculator.js' |
| Импорт всего | import * as math from './math.js' |
| Импорт с alias | import { add as sum } from './math.js' |
| Динамический импорт | const mod = await import('./math.js') |
| Встроенный модуль | import { readFile } from 'node:fs/promises' |
Для новых проектов рекомендуются ES Modules. В этом курсе далее применяется ESM.
На этом наиболее сложная часть теории завершена.
#2.4 Встроенные модули Node.js
Node.js поставляется с набором встроенных модулей, которые не нужно устанавливать. Они решают основные задачи серверной разработки:
- fs — работа с файловой системой (чтение, запись, удаление файлов)
- path — безопасная работа с путями к файлам (кроссплатформенно)
- http / https — создание HTTP-серверов и выполнение запросов
- os — информация об операционной системе (CPU, память, платформа)
- crypto — криптографические функции (хеширование, шифрование)
- events — система событий (EventEmitter)
- url — парсинг и форматирование URL
- stream — потоковая обработка данных
- util — вспомогательные функции (promisify, inspect)
- child_process — запуск внешних процессов и команд
- Некоторые из встроенных модулей Node.js рассматриваются далее в курсе
Далее создадим собственный модуль и поработаем с ним. Создайте новую папку и в ней новый файл package.json с содержимым:
{
"name": "my-test-module",
"version": "1.0.0",
"type": "module"
}Теперь создадим новый файл mypath.js с содержимым:
import path from 'node:path';
// path.join() — соединяет части пути
console.log(path.join('/users', 'alex', 'docs', 'file.txt'));
// /users/alex/docs/file.txt
// path.resolve() — возвращает абсолютный путь
console.log(path.resolve('src', 'index.js'));
// /home/alex/project/src/index.js
// path.extname() — расширение файла
console.log(path.extname('photo.jpg')); // .jpg
// path.basename() — имя файла
console.log(path.basename('/a/b/c.txt')); // c.txt
// path.dirname() — директория файла
console.log(path.dirname('/a/b/c.txt')); // /a/bЗдесь встроенный в Node.js модуль path подключается импортом по умолчанию. У модуля path есть методы (join(), resolve(), extname(), basename(), dirname() и другие), которые позволяют работать с путями в файловой системе. Его используют для объединения, нормализации, извлечения частей пути и т.д.
Модуль path особенно важен, потому что разделители путей различаются на разных ОС: Windows использует обратный слэш (\), а macOS и Linux — прямой (/). Функции path автоматически используют правильный разделитель.
Поэтому вывод может отличаться от комментариев в примере. Метод path.resolve строит абсолютный путь от текущей папки, так что на Windows выводится путь вида C:\Users\Имя\project\src\index.js (с обратными слэшами), а на macOS/Linux — /home/имя/project/src/index.js. Конкретный путь зависит от расположения проекта на компьютере — это нормально.
Запустим файл командой node mypath.js и проверим вывод.
Далее рассмотрим ещё один встроенный модуль, который предоставляет информацию об операционной системе. Создадим новый файл myos.js с содержимым:
import os from 'node:os';
console.log(os.platform()); // darwin, linux, win32
console.log(os.arch()); // x64, arm64
console.log(os.cpus().length); // Количество ядер CPU
console.log(os.totalmem()); // Общая оперативная память (байт)
console.log(os.freemem()); // Свободная память
console.log(os.homedir()); // Домашняя директория пользователя
console.log(os.hostname()); // Имя компьютераЗапустим файл командой node myos.js и проверим вывод.
Для Node.js уже созданы миллионы различных модулей — это библиотеки и пакеты для решения практически любых задач:
работа с файлами и файловой системой
отправка электронной почты
подключение к базам данных
создание веб-серверов
работа с API
аутентификация и безопасность
логирование
тестирование
и многое другое
Чтобы разработчикам было удобно делиться этими модулями друг с другом, устанавливать их в проекты и управлять версиями, был создан централизованный реестр пакетов — npm registry.
#2.5 npm и package.json
npm (Node Package Manager) — менеджер пакетов, который устанавливается вместе с Node.js. Он решает две задачи: управление зависимостями проекта и доступ к реестру из более чем 2 миллионов пакетов. Для поиска необходимых модулей (зависимостей) обычно используют сайт https://www.npmjs.com
На сайте в строке поиска вводится ключевое слово, например email; по нажатию Search выводятся все найденные модули. При выборе модуля важны следующие характеристики:
- Количество скачиваний в неделю — показатель популярности
- Количество звёзд на GitHub
- Дата последнего обновления
- Наличие документации
Например, для отправки почты часто используют пакет nodemailer (https://www.npmjs.com/package/nodemailer).
Страница пакета содержит:
- описание
- примеры использования
- команду для установки
- список версий
- Сайт реестра npm на английском языке, русской версии не существует. Ниже приведена небольшая шпаргалка о том, где что искать:

Теперь создадим новый проект и установим в него какой-нибудь модуль. Для тестирования лучше всего подойдёт модуль cowsay. Он не требует настройки, не зависит от внешних сервисов и сразу выводит результат в консоль в виде ASCII-коровы.
Создадим новую папку testcowsay и инициализируем в ней файл package.json с помощью npm.
mkdir testcowsay
cd testcowsay
npm init -y # -y автоматически заполняет значения по умолчаниюКаждый Node.js-проект начинается с файла package.json — манифеста проекта. Он содержит имя, версию, зависимости и скрипты. Этот файл можно создать вручную или командой npm init, как было сделано выше.
Рассмотрим структуру package.json подробно:
{
"name": "testcowsay",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
}Разберём поля:
- name — имя проекта (без пробелов, в нижнем регистре)
- version — версия по правилам semver (major.minor.patch)
- description — краткое описание проекта
- main — точка входа, главный файл проекта
- scripts — именованные команды для запуска через npm run
- keywords — ключевые слова для поиска пакета в реестре
- author — автор
- license — лицензия
- type — система модулей проекта: commonjs или module (как включить ESM — ниже)
Раздел scripts — это именованные команды, которые запускаются через npm run. Скрипты start и test особые — их можно запускать без run: npm start, npm test.
Основные команды npm:
npm install cowsay@1.6.0 # Установить пакет (добавляется в dependencies)
npm install -D cowsay@1.6.0 # Установить Dev-зависимость (добавляется в devDependencies)
npm install # Установить все зависимости из package.json
npm uninstall cowsay # Удалить пакет
npm update # Обновить пакеты до последних совместимых версий
npm list # Список установленных пакетов
npm list --depth=0 # Только верхнеуровневые пакеты
npm outdated # Показать устаревшие пакеты
npm run dev # Запустить скрипт из package.jsonРазница между dependencies и devDependencies:
- dependencies — для работы приложения (например, модули express, mongoose),
- devDependencies — только для разработки (тесты, линтеры, сборщики и т.п.).
При деплое на продакшен devDependencies обычно не устанавливаются (npm install --omit=dev).
Остаётся поле type. npm 11, который идёт с Node 24, записывает в него commonjs, а проект должен использовать модули ESM. Поменяем значение на module (если поля type нет — так делает npm 10 из Node 22, — добавьте его):
{
"name": "testcowsay",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module"
}Теперь поле type равно module — проект использует модули ESM. Меняйте именно значение, а не добавляйте вторую строку с type: из двух одинаковых ключей JSON действует последний, и проект останется на CommonJS.
Теперь установим модуль cowsay в проект, выполнив:
npm install cowsay@1.6.0И посмотрим содержимое файла package.json ещё раз:
{
"name": "testcowsay",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"cowsay": "^1.6.0"
}
}В package.json появился раздел dependencies, а в нём — cowsay версии ^1.6.0. Эту запись npm добавил автоматически при установке.
Далее создадим новый файл index.js и импортируем модуль cowsay следующим образом:
import cowsay from "cowsay";
console.log(
cowsay.say({
text: "Hello ESM!",
})
);Запустим файл, выполнив в консоли:
node index.jsПроверим вывод. Он должен выглядеть примерно так:
____________
< Hello ESM! >
------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||Такой вывод означает, что всё сделано правильно.
#2.6 Углублённо: package.json и npm в реальном проекте
В этом разделе разберём поля package.json, которые часто встречаются в production-проектах, и команды npm, которыми пользуются в CI/CD и при работе в команде.
#Полный package.json и поля
Минимальный package.json содержит name и version. Но реальные проекты используют больше полей:
{
"name": "my-api",
"version": "1.2.3",
"description": "REST API на Express",
"type": "module",
"main": "src/app.js",
"exports": "./src/app.js",
"engines": { "node": ">=24" },
"private": true,
"scripts": {
"dev": "node --watch --env-file=.env src/app.js",
"test": "node --test",
"lint": "eslint .",
"start": "node src/app.js"
},
"dependencies": { "express": "^5.2.1" },
"devDependencies": { "eslint": "^10.0.0" }
}Новые поля здесь:
- type: module — включает ESM по умолчанию для всех файлов .js
- main — точка входа (актуально в первую очередь для библиотек)
- exports — современный аналог main (разберём его отдельно ниже)
- engines — требуемая версия Node
- private: true — защита от случайной публикации пакета в реестре npm
#Поле exports — современная замена main
Поле exports управляет тем, какие файлы пакета доступны для импорта снаружи. main задаёт только одну точку входа, а exports позволяет описать несколько публичных модулей и при этом скрыть всё остальное:
{
"name": "my-lib",
"exports": {
".": "./src/index.js",
"./utils": "./src/utils.js",
"./package.json": "./package.json"
}
}Ключ "." задаёт главную точку входа (import ... from "my-lib"), ключ "./utils" — дополнительную (import ... from "my-lib/utils"). Всё, что не описано в exports, недоступно извне (import { внутреннее } from 'my-lib/internal' не сработает). Это инкапсуляция на уровне пакета. Поле также поддерживает условные exports (разные файлы для CommonJS / ESM / TypeScript), но для большинства новых проектов на ESM достаточно простого варианта выше.
#npm ci — установка в CI/CD
В разработке применяется npm install — он читает package.json, разрешает зависимости и обновляет package-lock.json. В CI и при деплое нужна другая команда — npm ci (сокращение от clean install):
- Удаляет node_modules перед установкой — гарантирует чистое состояние
- Устанавливает строго те версии, что в package-lock.json — не устанавливает новые патч-версии
- Завершается ошибкой, если package-lock.json и package.json расходятся — защита от рассинхрона
- Заметно быстрее npm install — не требуется разрешение дерева зависимостей
В Dockerfile и GitHub Actions используйте npm ci, а не npm install:
# Dockerfile
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev # без devDependencies
COPY . .
CMD ["node", "src/app.js"]#npm audit — поиск уязвимостей в зависимостях
npm audit проверяет все пакеты в node_modules на известные уязвимости (CVE) по базе GitHub Advisory:
npm audit # отчёт по всем уязвимостям
npm audit --omit=dev # только production-зависимости
npm audit fix # автоматически обновить туда, где safe (patch)
npm audit fix --force # обновить туда, где major (потенциально breaking)#package-lock.json — должен быть в git
package-lock.json содержит точные версии всех зависимостей (включая транзитивные) и их хеши. Этот файл обязательно коммитится в репозиторий. Причины:
- Воспроизводимость сборок — у всех разработчиков и в CI ставится один и тот же набор версий
- Защита от атак на цепочку поставок (supply chain attacks) — npm проверяет хеши и отказывается устанавливать подменённый пакет
- Аудит истории — видно, когда какая версия обновлялась
Никогда не удаляйте package-lock.json, «чтобы пересобрать всё с нуля». Если возникли проблемы, удаляйте node_modules и запускайте npm ci.
#npx — запуск пакета без установки
npx (Node Package eXecute) запускает CLI-утилиту из npm, не устанавливая её глобально. Это удобно для разовых задач (создание проекта, генерация шаблона):
# Создать новый Express-проект (без глобальной установки)
npx express-generator@4.16.1 my-app
# Запустить конкретную версию ESLint без установки
npx eslint@10.0.0 .
# Прогнать утилиту прямо из репозитория GitHub (опасно — нужна осторожность!)
npx github:owner/repo#2.7 Версионирование (Semver)
npm использует семантическое версионирование (Semantic Versioning):
MAJOR.MINOR.PATCH.
- MAJOR (4.x.x → 5.0.0) — несовместимые изменения API
- MINOR (4.18.x → 4.19.0) — новая функциональность, обратно совместимая
- PATCH (4.18.2 → 4.18.3) — исправления багов
Префиксы в package.json:
- ^4.18.2 (каретка) — разрешает обновления MINOR и PATCH (4.18.2 → 4.99.99), но не MAJOR
- ~4.18.2 (тильда) — разрешает только PATCH (4.18.2 → 4.18.99)
- 4.18.2 (без префикса) — точная версия, никаких обновлений
Файл package-lock.json фиксирует точные версии всех зависимостей (включая вложенные). Его рекомендуется всегда коммитить в git — он гарантирует, что у всех разработчиков одинаковые версии пакетов.
#2.8 Native TypeScript в Node 24
В модуле 1 отмечено, что Node 24 умеет запускать .ts-файлы напрямую. Здесь разберём, как это работает на уровне модулей.
#Базовый пример
Создайте проект с package.json ("type": "module") и два файла. Первый — types.ts:
// types.ts
export interface User {
id: number;
name: string;
email: string;
}
export function formatUser(user: User): string {
return `${user.name} <${user.email}>`;
}Второй — main.ts, который импортирует тип и функцию:
// main.ts
import { formatUser, type User } from './types.ts';
const u: User = { id: 1, name: 'Alex', email: 'a@b.c' };
console.log(formatUser(u));Запустите:
node main.tsВывод: Alex <a@b.c>. Node удаляет все типы (interface, : User, type User), а оставшийся код выполняет как обычный ESM.
Синтаксис TypeScript (interface, : User, type User) в этом курсе не требуется, и запоминать его сейчас не нужно. Здесь лишь показано, что Node 24 умеет запускать такие файлы. Достаточно понимать общую идею: типы — это аннотации, которые Node удаляет перед выполнением, после чего остаётся обычный JavaScript.
#Важные детали
- Расширение в импорте обязательно, причём указывается именно .ts: from './types.ts', а не from './types'
- Используйте импорты с ключевым словом type для type-only символов: import { type User } from './types.ts' — это явно сообщает Node, что User используется только для типов и его можно удалить
- Декораторы не поддерживаются — Node не знает, как их транспилировать. Если нужны декораторы (NestJS, TypeORM), применяйте полноценный tsc или tsx
- Enum не поддерживается — это runtime-конструкция. Используйте as const-объекты или объединения литералов
#Когда использовать native TS, а когда tsc
Native TS подходит для:
- Скриптов и CLI-утилит (там, где раньше ставили ts-node ради удобства)
- Маленьких сервисов, где не нужна сборка
- Прототипирования, экспериментов
tsc (или esbuild / swc) всё ещё нужен, если:
- Нужна проверка типов в CI (tsc --noEmit)
- Используются декораторы (NestJS, TypeORM)
- Целевая платформа — старая версия Node, куда нужно бэкпортировать новые возможности JS
- Проект большой, и сборка выполняет оптимизации (минификация, tree-shaking, bundling)
#2.9 Практика
#Задание 2.1 — проект на ES-модулях и пакеты из npm
- Создайте проект с npm init и включите ES-модули: "type": "module" в package.json
- Найдите модуль с названием nanoid в реестре и установите его в проект
- Установите модуль chalk (https://www.npmjs.com/package/chalk) и выведите цветной текст в консоль. Импортируется он так же, как cowsay: import chalk from 'chalk', а метод для цвета — chalk.blue('текст') (есть и chalk.red, chalk.green и другие). chalk поставляется только как ESM, поэтому поле "type": "module" здесь обязательно
- Установите express (npm install express@5.2.1) и откройте package-lock.json — найдите, какие вложенные (транзитивные) зависимости установились вместе с express. Сравните с chalk и nanoid: у них собственных зависимостей нет, а express добавляет целое дерево пакетов
#Итоги модуля 2
- Модули решают четыре задачи: организация кода по принципу единственной ответственности, переиспользование, инкапсуляция (снаружи видно только экспортированное) и явное управление зависимостями; в Node.js две системы — CommonJS и ES Modules.
- CommonJS — историческая система Node: импорт через require(), экспорт через module.exports; загрузка синхронная, результат кешируется — повторный require того же файла вернёт уже готовый объект.
- ES Modules — стандарт языка: import и export, статический разбор до выполнения (отсюда tree-shaking) и поддержка top-level await; включается полем "type": "module" в package.json или расширением .mjs, а файлы .cjs остаются CommonJS.
- Именованный экспорт (export function add, импорт в фигурных скобках) подходит для набора утилит, экспорт по умолчанию (export default, импорт без скобок и с любым именем) — для одной главной сущности; смешивать оба в одном файле не рекомендуется.
- Особенности ESM в Node: расширение файла в импорте обязательно (./math.js), require недоступен, __dirname и __filename отсутствуют — используйте import.meta.url с fileURLToPath либо import.meta.dirname; циклических зависимостей избегайте.
- Динамический import() загружает модуль во время выполнения и возвращает Promise — для ленивой и условной загрузки; top-level await работает только в ESM и удобен для чтения конфигов при старте.
- Встроенные модули (fs, path, http, os, crypto, events, stream, util) не требуют установки; импортируйте их с префиксом node: (node:path) — так видно, что это не npm-пакет; path.join и path.resolve собирают пути кроссплатформенно, os даёт сведения о системе (platform, cpus, totalmem).
- npm — менеджер пакетов, поставляемый вместе с Node: npm init создаёт package.json (манифест с именем, версией, скриптами и зависимостями), npm install добавляет пакет, npm uninstall удаляет, npm outdated показывает устаревшие; пакеты ищут на npmjs.com и выбирают по скачиваниям, звёздам и дате обновления.
- dependencies — то, что нужно приложению в работе (express, mongoose); devDependencies (флаг -D) — только для разработки: тесты, линтеры, сборщики; при деплое dev-зависимости не устанавливаются.
- Продакшен-поля package.json: engines фиксирует требуемую версию Node, private защищает от случайной публикации, exports объявляет публичные точки входа пакета и скрывает внутренние файлы — инкапсуляция на уровне пакета.
- npm ci — чистая установка строго по package-lock.json для CI/CD и Dockerfile: быстрее npm install и падает при рассинхроне; npm audit ищет известные уязвимости; package-lock.json всегда коммитится в git ради воспроизводимости и защиты от подмены пакетов.
- npx запускает CLI-утилиту без глобальной установки (сначала ищет её в локальном node_modules/.bin); node --run в Node 24 выполняет npm-скрипты быстрее, чем npm run.
- Semver: MAJOR — несовместимые изменения, MINOR — новая обратно совместимая функциональность, PATCH — исправления; каретка разрешает обновления MINOR и PATCH, тильда — только PATCH, версия без префикса фиксируется точно.
- Native TypeScript в Node 24 работает через type stripping: расширение .ts в импортах обязательно, type-only импорты помечайте словом type, enum и декораторы не поддерживаются; для проверки типов в CI и больших проектов по-прежнему нужен tsc.
#Домашнее задание
Соберите собственные мини-модули и попрактикуйтесь в работе с npm — задания идут по нарастанию сложности.
- Создайте модуль temperature.js с функциями celsiusToFahrenheit и fahrenheitToCelsius сначала в стиле CommonJS (module.exports и require), затем скопируйте проект (на Windows — через Проводник, на macOS/Linux — командой cp -r), включите "type": "module" и переведите код на import/export — зафиксируйте в комментариях, какие изменения потребовались.
- Напишите модуль stats.js с именованными экспортами min, max и avg и классом Series в экспорте по умолчанию; в app.js продемонстрируйте четыре вида импорта: именованный, по умолчанию, с переименованием через as и namespace-импорт import * as stats.
- Напишите скрипт sysinfo.js, который через node:os собирает отчёт о системе (платформа, архитектура, ядра, память, домашняя директория) и выводит путь к будущему файлу отчёта, собранный через path.join из трёх частей.
- Найдите на npmjs.com пакет dayjs: оцените число скачиваний, дату последнего релиза и документацию, установите его и выведите текущую дату в трёх форматах; отдельно установите eslint с флагом -D и убедитесь, что он добавлен в devDependencies.
- Разберитесь с версиями: найдите в package-lock.json точную версию dayjs (у самого dayjs вложенных зависимостей нет — это осознанно «нулевой по зависимостям» пакет), затем установите express и посмотрите, сколько вложенных зависимостей появилось у него в package-lock.json. Запустите npm outdated и npm audit, а в файле notes.txt объясните, какие обновления разрешит запись ^1.11.0, какие ~1.11.0 и чем npm ci отличается от npm install.
- (со звёздочкой, продвинутое) TypeScript в этом курсе отдельно не преподаётся — это задание опирается на раздел 2.8 «как Node запускает .ts» и предполагает, что синтаксис типов потребуется разобрать самостоятельно по документации. Напишите на TypeScript модуль config-loader.ts: интерфейс AppConfig и функция loadConfig, которая читает config.json через node:fs/promises с top-level await; запустите main.ts напрямую через node без сборки, помня про обязательное расширение .ts в импортах и type-импорт интерфейса.