Опирается на правила:
NODE-11…NODE-14,NODE-X3,NODE-X4из Node Style Guide → раздел 3. Импорты и модули.
Важно знать
- Только named exports —
export 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';
Группы:
- node builtins — всегда с префиксом
node:(node:fs,node:crypto,node:path). - third-party — пакеты из
node_modules(@nestjs/*,typeorm,zod,rxjs). - path-aliases — внутренние модули через
@app/*или аналогичный alias. - 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 CreateOrderHandler | NODE-11 | export class CreateOrderHandler |
import Handler from './create-order.handler' | NODE-11 | import { CreateOrderHandler } from './...' |
import 'fs' без префикса node: | NODE-12 | import { readFile } from 'node:fs/promises' |
| Ручная расстановка порядка импортов | NODE-12 | simple-import-sort в ESLint |
import { Order } from '../../../../core/order' | NODE-13 | import { Order } from '@app/order/order.aggregate' |
import { CreateOrderCommand } для type-only при verbatimModuleSyntax | NODE-14 | import type { CreateOrderCommand } |
order/index.ts с export * from ... для всего пакета | NODE-X3 | явный import из конкретного файла |
const x = require('...') / module.exports = ... | NODE-X4 | ESM 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.