Опирается на правила: NODE-11NODE-14, NODE-X3, NODE-X4 из Node Style Guide → раздел 3. Импорты и модули.

Важно знать

  • Только named exportsexport default запрещён: ломает rename-рефакторинг и IDE-автоимпорт.
  • Порядок импортов: node:* builtins → third-party → path-aliases → relative; сортирует ESLint, руками не поддерживать.
  • Path-aliases (@app/* через tsconfig paths) — вместо глубоких ../../../core/....
  • import type { X } для type-only импортов при включённом verbatimModuleSyntax.
  • Барель-файлы (index.ts, реэкспортирующий весь пакет) запрещены — порождают циклические зависимости и замедляют tsc.
  • require(...) / module.exports (CommonJS) в новом коде запрещены — только ESM.
  • ESLint-плагины import/order или simple-import-sort обеспечивают порядок механически — не тратьте время на ручную расстановку.
  • verbatimModuleSyntax в tsconfig.json заставляет tsc падать при type-only импорте без import type — включите, чтобы поймать это на CI.

Структура импортов — индикатор связности модуля. Глубокие relative пути (../../../../shared/...) — сигнал, что модуль знает слишком много о чужой файловой топологии. export default без имени — сигнал, что у символа нет стабильного контракта. Правила раздела устраняют оба источника хрупкости.

Только named exports

NODE-11: export default запрещён для сервисов, классов, функций.

export class CreateOrderHandler {
  async execute(command: CreateOrderCommand): Promise<OrderId> { ... }
}

export class OrderController {
  constructor(private readonly handler: CreateOrderHandler) {}
}

export function buildOrderSummary(order: Order): OrderSummary { ... }

export default создаёт два практических дефекта. Во-первых, импортирующая сторона может дать символу произвольное имя:

import Handler from './create-order.handler';
import CreateIt from './create-order.handler';
import Whatever from './create-order.handler';

Все три — один и тот же класс, но поиск по кодовой базе находит каждый по своему псевдониму. Rename-рефакторинг в IDE переименовывает именованный экспорт везде автоматически; default — не умеет.

Во-вторых, IDE не может предложить автоимпорт для default, если в файле нет явного имени. Named export сразу попадает в индекс.

Единственное исключение — конфигурационные файлы, где формат инструмента требует default:

export default defineConfig({ ... });

Это eslint.config.mjs, prettier.config.mjs, jest.config.ts — не прикладной код.

Порядок импортов

NODE-12: четыре группы, строго по порядку.

import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';

import { Injectable, Inject } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

import { OrderRepository } from '@app/order/order.repository';
import { CustomerRepository } from '@app/customer/customer.repository';

import { CreateOrderCommand } from './create-order.command';
import { OrderCreatedEvent } from './order-created.event';

Группы:

  1. node builtins — всегда с префиксом node: (node:fs, node:crypto, node:path).
  2. third-party — пакеты из node_modules (@nestjs/*, typeorm, zod, rxjs).
  3. path-aliases — внутренние модули через @app/* или аналогичный alias.
  4. relative — файлы из той же директории или соседних.

Сортировка внутри группы — алфавитная, её поддерживает ESLint-плагин simple-import-sort или import/order. Руками расставлять порядок и поддерживать его при каждом добавлении нового импорта — потеря времени и частый источник конфликтов в merge.

Конфигурация в eslint.config.mjs:

import simpleImportSort from 'eslint-plugin-simple-import-sort';

export default [
  {
    plugins: { 'simple-import-sort': simpleImportSort },
    rules: {
      'simple-import-sort/imports': 'error',
      'simple-import-sort/exports': 'error',
    },
  },
];

Path-aliases вместо глубоких relative

NODE-13: @app/* (или аналогичный alias) через paths в tsconfig.json.

import { Order } from '../../../../../../../core/order/order.aggregate';

import { Order } from '@app/order/order.aggregate';

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

Настройка в tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@app/*": ["src/*"]
    }
  }
}

Для NestJS с поддержкой Jest необходимо продублировать маппинг в jest.config.ts через moduleNameMapper:

export default {
  moduleNameMapper: {
    '^@app/(.*)$': '<rootDir>/src/$1',
  },
};

Границы использования: alias @app/* — для cross-module импортов (из order/ в customer/). Внутри одного модуля (order/ → соседний файл в order/) — relative import предпочтительнее: он точно описывает, что зависимость локальна.

import { OrderRepository } from '@app/customer/customer.repository';

import { CreateOrderCommand } from './create-order.command';

import type для type-only импортов

NODE-14: import type при включённом verbatimModuleSyntax.

import type { CreateOrderCommand } from './create-order.command';
import type { OrderSummary } from '@app/order/order.summary';

import { CreateOrderHandler } from './create-order.handler';

verbatimModuleSyntax в tsconfig.json гарантирует, что tsc не будет эмитировать импорт типа в runtime-файл. Без него импорты типов попадают в скомпилированный JS как пустые import {} — бесполезная строка, которая может сломать tree-shaking и cyclic detection.

{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}

Правило простое: если символ используется только в аннотации типа (параметр функции, возвращаемый тип, тип переменной) — import type. Если используется в runtime-выражении (создание экземпляра, вызов метода, передача значения) — обычный import.

import type { Order } from '@app/order/order.aggregate';
import { OrderStatus } from '@app/order/order-status';

function processOrder(order: Order): void {
  if (order.status === OrderStatus.Paid) { ... }
}

Order — только в аннотации параметра, import type. OrderStatus — используется как значение в выражении, обычный import.

Барели запрещены

NODE-X3: index.ts, реэкспортирующий весь пакет ради «короткого импорта».

export * from './order.aggregate';
export * from './create-order.handler';
export * from './order.repository';
export * from './order.controller';

Такой файл кажется удобным — один импорт вместо четырёх. Но он порождает два устойчивых дефекта.

Первый — циклические зависимости. Когда order.aggregate.ts импортирует что-то из customer/, а index.ts реэкспортирует оба модуля, tsc и Node runtime видят граф с циклом. Проявляется как undefined значение в runtime, которое сложно диагностировать.

Второй — скорость tsc. Барель заставляет компилятор вычитать весь пакет даже при импорте одного символа. В монорепо с сотнями барелей cold type-check может занять минуты.

Правильный путь — явный named import из конкретного файла:

import { CreateOrderHandler } from '@app/order/create-order.handler';
import { Order } from '@app/order/order.aggregate';

CommonJS запрещён

NODE-X4: require(...) и module.exports только в новом коде запрещены.

const { createHash } = require('crypto');
module.exports = { CreateOrderHandler };

Стек — ESM ("module": "NodeNext" в tsconfig.json). CommonJS-синтаксис несовместим с import type, verbatimModuleSyntax, tree-shaking и статическим анализом зависимостей. При необходимости интегрировать CJS-пакет — используйте createRequire из node:module как изолированный адаптер, не засоряйте им весь код.

Что запрещено

АнтипаттернПравилоЧто взамен
export default class CreateOrderHandlerNODE-11export class CreateOrderHandler
import Handler from './create-order.handler'NODE-11import { CreateOrderHandler } from './...'
import 'fs' без префикса node:NODE-12import { readFile } from 'node:fs/promises'
Ручная расстановка порядка импортовNODE-12simple-import-sort в ESLint
import { Order } from '../../../../core/order'NODE-13import { Order } from '@app/order/order.aggregate'
import { CreateOrderCommand } для type-only при verbatimModuleSyntaxNODE-14import type { CreateOrderCommand }
order/index.ts с export * from ... для всего пакетаNODE-X3явный import из конкретного файла
const x = require('...') / module.exports = ...NODE-X4ESM import/export

Куда дальше

  • node/naming.md — kebab-case файлы, PascalCase классы, camelCase функции, UPPER_SNAKE_CASE константы.
  • node/expressions.md — unknown + narrowing вместо any, guard clause, типизация публичных сигнатур.
  • node/async.md — async/await, Promise.all, запрет fire-and-forget.
  • node/immutability.md — readonly на полях, as const, spread вместо мутации аргументов.
  • node/tooling.md — полная настройка tsconfig.json со strict + verbatimModuleSyntax, ESLint-конфиг, CI-прогон.
  • Стандарты → Code Style — хаб языковых биндингов: Java, Node, Python, Go.