К содержанию
Первый Коммит

Модуль 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:

JavaScript
// math.js
function add(a, b) {
    return a + b;
}

function multiply(a, b) {
    return a * b;
}

// Экспортируем объект с функциями
module.exports = { add, multiply };

Импортируем и используем функции в другом файле. Создайте файл app.js:

JavaScript
// 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:

JavaScript
// logger.js
class Logger {
    log(msg) { console.log(`[LOG] ${msg}`); }
    error(msg) { console.error(`[ERR] ${msg}`); }
}

module.exports = Logger;  // Экспортируем сам класс, а не объект

Измените файл app.js следующим образом:

JavaScript
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.exportsundefined
__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

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.

JavaScript
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)

Через именованный экспорт модуль может отдать наружу несколько значений. Каждое значение экспортируется под своим именем — и импортировать его нужно точно по этому имени.

JavaScript
// 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 и недоступна извне.

Это ключевое свойство модулей: модуль сам определяет, что видно снаружи, а что скрыто.

Альтернативный синтаксис экспорта — экспорт списком в конце файла:

JavaScript
// 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)

Экспорт по умолчанию — это одно главное значение из модуля. В каждом модуле допускается только один экспорт по умолчанию.

JavaScript
// 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 } из математического модуля.

Важно: комбинировать оба типа экспорта в одном файле возможно, но не рекомендуется — такой модуль сложнее использовать.

Именованный импорт — фигурные скобки обязательны:

JavaScript
// 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, где его можно опустить)

Импорт по умолчанию — без фигурных скобок:

JavaScript
import Calculator from './Calculator.js';

const calc = new Calculator();
console.log(calc.add(10, 20));  // 30
console.log(calc.div(10, 3));   // 3.333...

При импорте по умолчанию переменную можно назвать как угодно:

JavaScript
import MyCalc from './Calculator.js';      // Работает
import Калькулятор from './Calculator.js'; // Тоже работает

Импорт с переименованием (alias):

JavaScript
import { add as сложить, multiply as умножить } from './math.js';

console.log(сложить(2, 3));    // 5
console.log(умножить(4, 5));   // 20

Это полезно, когда имена из разных модулей конфликтуют или когда нужно дать более понятное имя в контексте текущего файла.

Импорт всего модуля как объекта (namespace import):

JavaScript
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:

JavaScript
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, и именованные значения, то импортировать можно таким образом:

JavaScript
import Calculator, { helperFunction } from './Calculator.js';

Динамический импорт

В отличие от статического import, динамический import() загружает модуль во время выполнения (runtime). Он возвращает Promise.

JavaScript
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:

JSON
{
    "port": "3000"
}

Файл config.js читает и разбирает его на верхнем уровне:

JavaScript
// 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:

JavaScript
// app.js
import { config } from './config.js';
// config уже загружен и распарсен к этому моменту
console.log(config.port);  // 3000

В CommonJS использовать await на верхнем уровне невозможно — там пришлось бы оборачивать этот код в async function и вызывать её.

Частые ошибки и подводные камни

1. Пропущено расширение файла

JavaScript
// ❌ ОШИБКА в ESM Node.js
import { add } from './math';

// ✅ Правильно — расширение обязательно
import { add } from './math.js';

2. Попытка использовать require() в ESM

JavaScript
// ❌ ОШИБКА — require не определён в ESM
const fs = require('fs');

// ✅ Правильно
import fs from 'node:fs';

3. В ESM нет глобальных __dirname и __filename. Вместо них:

JavaScript
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, но их всё равно следует избегать:

JavaScript
// 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'
Импорт с aliasimport { 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 с содержимым:

JSON
{
    "name": "my-test-module",
    "version": "1.0.0",
    "type": "module"
}

Теперь создадим новый файл mypath.js с содержимым:

JavaScript
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 с содержимым:

JavaScript
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 на английском языке, русской версии не существует. Ниже приведена небольшая шпаргалка о том, где что искать:
Схема к модулю «Модули и npm»

Теперь создадим новый проект и установим в него какой-нибудь модуль. Для тестирования лучше всего подойдёт модуль cowsay. Он не требует настройки, не зависит от внешних сервисов и сразу выводит результат в консоль в виде ASCII-коровы.

Создадим новую папку testcowsay и инициализируем в ней файл package.json с помощью npm.

Терминал
mkdir testcowsay
cd testcowsay
npm init -y    # -y автоматически заполняет значения по умолчанию

Каждый Node.js-проект начинается с файла package.json — манифеста проекта. Он содержит имя, версию, зависимости и скрипты. Этот файл можно создать вручную или командой npm init, как было сделано выше.

Рассмотрим структуру package.json подробно:

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, — добавьте его):

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"
}

Теперь поле type равно module — проект использует модули ESM. Меняйте именно значение, а не добавляйте вторую строку с type: из двух одинаковых ключей JSON действует последний, и проект останется на CommonJS.

Теперь установим модуль cowsay в проект, выполнив:

Терминал
npm install cowsay@1.6.0

И посмотрим содержимое файла package.json ещё раз:

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 следующим образом:

JavaScript
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. Но реальные проекты используют больше полей:

JSON
{
  "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 позволяет описать несколько публичных модулей и при этом скрыть всё остальное:

JSON
{
  "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:

JavaScript
// types.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

export function formatUser(user: User): string {
  return `${user.name} <${user.email}>`;
}

Второй — main.ts, который импортирует тип и функцию:

JavaScript
// 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-импорт интерфейса.

Войдите, чтобы отмечать пройденные модули и видеть прогресс по курсу.

Введите запрос: название модуля, тема или раздел.

Стрелки — выбор, Enter — открыть. Ищет по названиям модулей, темам и разделам.