Node.js

Node.js — среда выполнения JavaScript вне браузера, построенная на движке V8. Она предоставляет API для файловой системы, сети, процессов, потоков и операционной системы.

Простейший файл index.js:

console.log("Hello from Node.js");

Запуск:

node index.js

В Node.js нет браузерных объектов window и document, зато доступны серверные API и глобальный объект process.

console.log(process.version);
console.log(process.platform);
console.log(process.cwd());

Модульная система Node.js

В Node.js используются две основные системы модулей:

В проекте желательно выбрать одну систему и использовать её последовательно.

CommonJS

Файл math.cjs:

function sum(a, b) {
  return a + b;
}

module.exports = { sum };

Файл index.cjs:

const { sum } = require("./math.cjs");
console.log(sum(2, 3));

Экспорт одного значения:

class UserService {}
module.exports = UserService;

В CommonJS доступны __filename и __dirname:

console.log(__filename);
console.log(__dirname);

exports изначально ссылается на module.exports, но переназначать его нельзя:

exports.sum = (a, b) => a + b; // Работает

exports = { sum }; // Не заменит экспорт модуля
module.exports = { sum }; // Работает

ES Modules

Файл math.js:

export function sum(a, b) {
  return a + b;
}

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

Импорт:

import multiply, { sum } from "./math.js";

Чтобы .js считались ESM, указывают в package.json:

{
  "type": "module"
}

Альтернатива — расширение .mjs. Для локальных ESM-импортов обычно требуется расширение файла.

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

const math = await import("./math.js");
console.log(math.sum(2, 3));

В ESM нет __dirname. Переносимый вариант:

import path from "node:path";
import { fileURLToPath } from "node:url";

const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);

Для встроенных модулей рекомендуется префикс node::

import fs from "node:fs";
import path from "node:path";
import http from "node:http";
Возможность CommonJS ESM
Импорт require() import
Экспорт module.exports export
Расширение .cjs .mjs или .js с type: module
Верхнеуровневый await Нет Да
Каталог модуля __dirname import.meta.url

Событийная модель Node.js и libuv

JavaScript обычно выполняется в одном основном потоке. Асинхронность обеспечивают Event Loop, операционная система и библиотека libuv.

import fs from "node:fs";

console.log("Начало");

fs.readFile("data.txt", "utf8", (error, data) => {
  if (error) {
    console.error(error);
    return;
  }

  console.log(data);
});

console.log("Конец");

Сначала выводятся Начало и Конец. Callback вызывается после завершения чтения.

Упрощённо Event Loop содержит фазы таймеров, ввода-вывода, setImmediate() и закрытия ресурсов. Между операциями обрабатываются микрозадачи Promise и очередь process.nextTick().

console.log("start");
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));
console.log("end");

Сначала выполняется синхронный код. process.nextTick() и Promise выполняются до перехода к следующим фазам. Взаимный порядок setTimeout(..., 0) и setImmediate() зависит от контекста.

Долгая синхронная операция блокирует Event Loop:

const start = Date.now();
while (Date.now() - start < 5000) {}

Для тяжёлых вычислений применяют worker_threads, отдельные процессы или фоновые сервисы.


Файловая система: fs

Модуль node:fs имеет callback-, синхронный и Promise-интерфейсы. Для современного асинхронного кода удобен node:fs/promises.

Чтение и запись

import fs from "node:fs/promises";

const text = await fs.readFile("notes.txt", "utf8");
await fs.writeFile("result.txt", "Готово\n", "utf8");
await fs.appendFile("app.log", "Событие\n", "utf8");

Без кодировки readFile() возвращает Buffer:

const data = await fs.readFile("image.png");
console.log(Buffer.isBuffer(data));

Каталоги и файлы

await fs.mkdir("storage/reports", { recursive: true });

const entries = await fs.readdir("storage", {
  withFileTypes: true,
});

for (const entry of entries) {
  console.log(entry.name, entry.isDirectory());
}

await fs.unlink("result.txt");
await fs.rm("storage/temp", { recursive: true, force: true });

Информация о файле:

const stats = await fs.stat("notes.txt");
console.log(stats.isFile(), stats.size, stats.mtime);

Обработка системной ошибки:

try {
  await fs.readFile("config.json", "utf8");
} catch (error) {
  if (error.code === "ENOENT") {
    console.error("Файл не найден");
  } else {
    throw error;
  }
}

Частые коды: ENOENT — не найдено, EACCES — нет доступа, EEXIST — уже существует.

Синхронные методы вроде readFileSync() блокируют основной поток. Их не следует применять внутри обработчиков HTTP-запросов.


Пути: path

node:path составляет и анализирует системные пути.

import path from "node:path";

const filePath = path.join("storage", "reports", "report.json");
console.log(path.basename(filePath));
console.log(path.dirname(filePath));
console.log(path.extname(filePath));
console.log(path.parse(filePath));

path.resolve() строит абсолютный путь, обычно относительно process.cwd():

const absolutePath = path.resolve("storage", "data.json");

Не соединяйте пути вручную через / или \. Не подставляйте непроверенный пользовательский ввод: значение с ../ может вывести путь за разрешённый каталог.


HTTP-сервер без фреймворка

import http from "node:http";

const server = http.createServer((request, response) => {
  const url = new URL(request.url, `http://${request.headers.host}`);

  if (request.method === "GET" && url.pathname === "/") {
    response.writeHead(200, {
      "Content-Type": "text/plain; charset=utf-8",
    });
    response.end("Привет от Node.js");
    return;
  }

  if (request.method === "GET" && url.pathname === "/api/users") {
    response.writeHead(200, {
      "Content-Type": "application/json; charset=utf-8",
    });
    response.end(JSON.stringify([{ id: 1, name: "Анна" }]));
    return;
  }

  response.writeHead(404, {
    "Content-Type": "application/json; charset=utf-8",
  });
  response.end(JSON.stringify({ error: "Not found" }));
});

server.listen(3000, "127.0.0.1", () => {
  console.log("http://127.0.0.1:3000");
});

request — читаемый поток, response — записываемый.

Чтение JSON-тела

function readJson(request, maxBytes = 1_000_000) {
  return new Promise((resolve, reject) => {
    let body = "";
    let size = 0;

    request.setEncoding("utf8");

    request.on("data", (chunk) => {
      size += Buffer.byteLength(chunk);

      if (size > maxBytes) {
        reject(new Error("Request body is too large"));
        request.destroy();
        return;
      }

      body += chunk;
    });

    request.on("end", () => {
      try {
        resolve(body ? JSON.parse(body) : {});
      } catch {
        reject(new Error("Invalid JSON"));
      }
    });

    request.on("error", reject);
  });
}

Без фреймворка разработчик самостоятельно реализует маршрутизацию, лимиты, валидацию, CORS и единый формат ошибок.

Корректное завершение

function shutdown(signal) {
  console.log(`${signal}: завершение сервера`);

  server.close((error) => {
    if (error) {
      console.error(error);
      process.exitCode = 1;
    }
  });
}

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);

server.close() прекращает принимать новые соединения и ожидает завершения активных запросов.


npm, Yarn и pnpm

Это менеджеры пакетов, которые устанавливают зависимости, запускают скрипты и создают lock-файлы.

npm init -y
npm install dotenv
npm install --save-dev vitest

Аналоги:

yarn add dotenv
yarn add --dev vitest
pnpm add dotenv
pnpm add --save-dev vitest

package.json

{
  "name": "node-example",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js",
    "test": "vitest run",
    "debug": "node --inspect src/index.js"
  },
  "dependencies": {
    "dotenv": "^16.0.0"
  },
  "devDependencies": {
    "vitest": "^3.0.0"
  },
  "engines": {
    "node": ">=20"
  }
}

Запуск:

npm run dev
npm test
npm start

Lock-файлы: package-lock.json, yarn.lock, pnpm-lock.yaml. В проекте используют один менеджер и сохраняют его lock-файл в Git.

Для воспроизводимой установки npm-зависимостей в CI:

npm ci

Переменные окружения

Конфигурация доступна через process.env:

const port = Number(process.env.PORT ?? 3000);
const environment = process.env.NODE_ENV ?? "development";

Значения process.env являются строками или undefined, поэтому их нужно преобразовывать и проверять.

Файл .env:

NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/app
API_TOKEN=secret-value

Добавьте секретный файл в .gitignore:

node_modules/
.env
.env.local

В репозитории можно хранить .env.example без секретов.

dotenv

npm install dotenv
import "dotenv/config";
console.log(process.env.PORT);

Современные версии Node.js поддерживают загрузку без пакета:

node --env-file=.env src/index.js

Нельзя хранить реальные секреты в Git, печатать их в логах или передавать серверные переменные клиентскому коду. .env — способ конфигурации, а не шифрование.


Streams

Stream обрабатывает данные частями, не загружая всё в память.

Тип Назначение
Readable Источник данных
Writable Получатель данных
Duplex Чтение и запись
Transform Преобразование данных

Чтение потоком

import fs from "node:fs";

const stream = fs.createReadStream("large-file.txt", {
  encoding: "utf8",
});

for await (const chunk of stream) {
  console.log(chunk.length);
}

pipeline()

import fs from "node:fs";
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";

await pipeline(
  fs.createReadStream("report.csv"),
  createGzip(),
  fs.createWriteStream("report.csv.gz"),
);

pipeline() связывает потоки, передаёт ошибки и корректно уничтожает конвейер при сбое.

Transform Stream

import { Transform } from "node:stream";

const upperCase = new Transform({
  transform(chunk, encoding, callback) {
    callback(null, chunk.toString().toUpperCase());
  },
});

Backpressure возникает, когда источник быстрее получателя. pipe() и pipeline() управляют обратным давлением автоматически. При ручной записи нужно учитывать результат write() и событие drain.

if (!writable.write(chunk)) {
  await new Promise((resolve) => writable.once("drain", resolve));
}

Обработка ошибок в Node.js

try/catch и Promise

async function loadConfig() {
  try {
    const source = await fs.readFile("config.json", "utf8");
    return JSON.parse(source);
  } catch (error) {
    console.error("Ошибка конфигурации", error);
    throw error;
  }
}

Ошибка должна обрабатываться или передаваться выше. Пустой catch скрывает проблему.

Error-first callback

fs.readFile("notes.txt", "utf8", (error, data) => {
  if (error) {
    console.error(error);
    return;
  }

  console.log(data);
});

Событие error

const stream = fs.createReadStream("missing.txt");
stream.on("error", (error) => console.error(error));

try/catch не перехватывает событие, возникшее асинхронно после выхода из блока.

uncaughtException

process.on("uncaughtException", (error) => {
  console.error("Uncaught exception", error);
  process.exitCode = 1;
});

После необработанного исключения состояние программы может быть повреждено. Обработчик нужен для аварийного логирования и контролируемого завершения, а не для продолжения штатной работы.

unhandledRejection

process.on("unhandledRejection", (reason) => {
  console.error("Unhandled rejection", reason);
  process.exitCode = 1;
});

Каждый Promise должен быть ожидаем через await, возвращён вызывающему коду или завершён .catch():

void sendMetrics().catch((error) => {
  console.error("Ошибка отправки метрик", error);
});

Точка входа приложения:

async function main() {
  await startApplication();
}

main().catch((error) => {
  console.error("Startup failed", error);
  process.exitCode = 1;
});

process.exitCode = 1 обычно безопаснее немедленного process.exit(1), который может оборвать запись логов.


Дебаггинг Node-приложений

Запуск инспектора:

node --inspect src/index.js

Остановка до выполнения первой строки:

node --inspect-brk src/index.js

Подключиться можно через отладчик IDE или инструменты разработчика Chromium-браузера.

Инструкция debugger создаёт программную точку останова:

function calculateTotal(items) {
  debugger;
  return items.reduce((sum, item) => {
    return sum + item.price * item.quantity;
  }, 0);
}

В отладчике изучают стек вызовов, переменные, области видимости, асинхронные цепочки и исключения. Порт инспектора нельзя публиковать в открытой сети.

Полезные методы консоли:

console.table([{ id: 1, name: "Анна" }]);
console.trace("Трассировка");
console.time("load");
await loadData();
console.timeEnd("load");

Общий пример

Небольшой JSON API на ESM:

import http from "node:http";
import fs from "node:fs/promises";
import path from "node:path";

const host = process.env.HOST ?? "127.0.0.1";
const port = Number(process.env.PORT ?? 3000);
const dataFile = path.resolve("data/notes.json");

async function initialize() {
  await fs.mkdir(path.dirname(dataFile), { recursive: true });

  try {
    await fs.access(dataFile);
  } catch (error) {
    if (error.code !== "ENOENT") throw error;
    await fs.writeFile(dataFile, "[]\n", "utf8");
  }
}

async function getNotes() {
  return JSON.parse(await fs.readFile(dataFile, "utf8"));
}

function sendJson(response, status, data) {
  response.writeHead(status, {
    "Content-Type": "application/json; charset=utf-8",
  });
  response.end(JSON.stringify(data));
}

const server = http.createServer(async (request, response) => {
  try {
    const url = new URL(request.url, `http://${request.headers.host}`);

    if (request.method === "GET" && url.pathname === "/api/notes") {
      sendJson(response, 200, await getNotes());
      return;
    }

    sendJson(response, 404, { error: "Not found" });
  } catch (error) {
    console.error(error);
    sendJson(response, 500, { error: "Internal Server Error" });
  }
});

async function main() {
  await initialize();
  server.listen(port, host, () => {
    console.log(`Server: http://${host}:${port}`);
  });
}

process.on("SIGTERM", () => server.close());
process.on("SIGINT", () => server.close());

main().catch((error) => {
  console.error("Startup failed", error);
  process.exitCode = 1;
});

Запуск:

node --env-file=.env src/index.js

Краткие рекомендации