← назад к разделу

Реальный проект — это не один файл, а десятки и сотни. Чтобы код одного файла мог пользоваться функциями другого, существуют модули. А чтобы подключать чужой код (библиотеки) и не копировать его вручную, существует npm — пакетный менеджер из мира Node.js. Разберём оба по порядку.

Зачем нужны модули

Если бы весь код жил в одной глобальной области, любые две переменные с одинаковым именем конфликтовали бы, а понять, откуда взялась функция, было бы невозможно. Модуль решает это просто: каждый файл — отдельное пространство имён. Внутри файла всё своё и приватное; наружу видно только то, что вы явно экспортировали, а взять чужое можно только через явный импорт.

Короткая формула: один файл — один модуль, и связи между файлами видны в строках import.

ES-модули: import и export

Современный стандарт — ES modules (ESM). Это синтаксис import/export, единый для браузера и для Node. Именно его вы пишете в TypeScript по умолчанию.

// file: math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

// "export default" — одно главное значение на модуль
export default class Calculator {
  // ...
}
// file: app.ts
import Calculator, { add, PI } from "./math"; // default + именованные
import { add as plus } from "./math";          // переименование при импорте
import * as math from "./math";                // всё пространство модуля разом

console.log(add(2, 3)); // 5
console.log(math.PI);   // 3.14159

Что важно запомнить:

  • Именованный экспорт (export function add) импортируется в фигурных скобках по тому же имени.
  • Экспорт по умолчанию (export default) — один на файл, имя при импорте выбираете сами.
  • Путь к своему файлу начинается с ./ или ../. Путь без точки (import { z } from "zod") — это пакет из node_modules.

CommonJS: require и module.exports

До появления ESM в Node была своя система модулей — CommonJS (CJS). Её вы будете встречать в унаследованном коде и многих библиотеках:

// экспорт
function add(a, b) {
  return a + b;
}
module.exports = { add };

// импорт
const { add } = require("./math");

Чем CommonJS отличается от ES-модулей на практике:

  • require() — обычный вызов функции, его можно поставить в середине файла или внутри условия. import — это объявление, которое поднимается наверх и выполняется до остального кода.
  • CommonJS грузит модули синхронно; ESM спроектирован под асинхронную загрузку и статический анализ (бандлеры умеют выкидывать неиспользуемый экспорт).
  • ESM — это будущее и стандарт; CommonJS — наследие, которое ещё долго будет рядом.

В Node то, какую систему использует ваш .js-файл, определяет поле "type" в package.json: "type": "module" — это ESM, "type": "commonjs" (или отсутствие поля) — CommonJS. В TypeScript вы почти всегда пишете import/export, а уже компилятор и настройки решают, во что это превратится.

package.json — паспорт проекта

Любой Node-проект описывается файлом package.json в корне. Это обычный JSON с метаданными проекта и, главное, списком зависимостей. Создать его можно командой npm initnpm init -y — со значениями по умолчанию).

{
  "name": "my-service",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/app.js",
    "dev": "tsx watch src/app.ts",
    "test": "vitest"
  },
  "dependencies": {
    "express": "^4.19.2",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "typescript": "^5.4.5",
    "tsx": "^4.10.0",
    "vitest": "^1.6.0"
  }
}

dependencies против devDependencies

Зависимости делятся на две группы, и разница не косметическая:

  • dependencies — пакеты, без которых приложение не работает в продакшене. Например, веб-фреймворк express или валидатор zod. Они уезжают на сервер вместе с приложением.
  • devDependencies — инструменты, нужные только во время разработки и сборки: сам компилятор typescript, тестовый раннер, линтеры. На рабочем сервере собранного приложения они не нужны.

Команды кладут пакет в нужную группу: npm install express — в dependencies, npm install --save-dev typescript (короче npm i -D typescript) — в devDependencies. Правильное разделение уменьшает размер продакшен-сборки и снижает поверхность для уязвимостей.

semver — версии вида 4.19.2

Номер версии пакета — это semver (semantic versioning), три числа MAJOR.MINOR.PATCH:

  • MAJOR (4.x.x) — несовместимые изменения, старый код может сломаться.
  • MINOR (x.19.x) — новые возможности без поломок.
  • PATCH (x.x.2) — исправления, ничего не ломающие.

В package.json перед версией обычно стоит значок, который задаёт допустимый диапазон обновлений:

  • ^4.19.2 — «карет»: разрешено всё до следующего MAJOR (4.x.x, но не 5.0.0). Самый частый вариант.
  • ~4.19.2 — «тильда»: разрешены только PATCH-обновления (4.19.x).
  • 4.19.2 — строго эта версия, без обновлений.

Точные версии, которые реально установились, фиксируются в файле package-lock.json (у npm). Его коммитят в репозиторий — он гарантирует, что у вас и у коллеги соберётся ровно одно и то же.

node_modules и установка

Команда npm install (без аргументов) читает package.json, скачивает все зависимости и складывает их в папку node_modules в корне проекта. Туда же подтягиваются зависимости ваших зависимостей — поэтому папка получается огромной.

Два правила про node_modules:

  • Не коммитьте её в git — она восстанавливается из package.json + lock-файла одной командой. Добавьте node_modules/ в .gitignore.
  • Если что-то «сломалось непонятно почему», частое лечение — удалить node_modules и lock-файл и поставить заново.

npm, pnpm и другие

npm — менеджер по умолчанию, идёт вместе с Node. Есть альтернативы с тем же package.json: pnpm (хранит пакеты в одном общем месте и делает ссылки — экономит диск и ставит быстрее) и yarn. Для начала достаточно npm; pnpm стоит знать, потому что его часто выбирают в новых проектах.

Скрипты npm

Блок scripts в package.json — это именованные команды проекта. Запускаются они через npm run <имя>:

npm run build   # выполнит "tsc" — скомпилирует TypeScript
npm run dev     # запустит dev-режим с автоперезагрузкой
npm test        # для test/start/stop слово run можно опустить

Скрипты избавляют от запоминания длинных команд и документируют, как с проектом работать: открыв package.json, любой человек сразу видит, как его собрать, запустить и протестировать.

Коротко

  • Модуль — это файл; наружу видно только export, взять чужое можно только через import.
  • ES modules (import/export) — современный стандарт; CommonJS (require) — наследие в Node-коде и старых библиотеках.
  • package.json — паспорт проекта: метаданные, зависимости и скрипты.
  • dependencies нужны в продакшене, devDependencies — только для разработки и сборки.
  • semver MAJOR.MINOR.PATCH; ^ разрешает обновления до следующего MAJOR, lock-файл фиксирует точные версии.
  • node_modules восстанавливается из package.json, в git не коммитится.
  • npm scripts запускаются через npm run <имя> и документируют работу с проектом.

Что почитать дальше

  • Основы JavaScript для TypeScript — язык, поверх которого всё это работает.
  • Инструменты: компилятор и tsconfig — как tsc и настройки превращают ваши модули в исполняемый код.
  • Асинхронность и event loop — почему ESM грузится асинхронно и как Node это исполняет.