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 (CJS) —
require()иmodule.exports; - ECMAScript Modules (ESM) —
importиexport.
В проекте желательно выбрать одну систему и использовать её последовательно.
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.
- V8 выполняет JavaScript.
- Event Loop планирует обработчики.
- libuv предоставляет кроссплатформенную работу с сетью, файлами, таймерами и пулом потоков.
- Некоторые операции выполняются средствами ОС, другие — в пуле потоков 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 vitestpnpm add dotenv
pnpm add --save-dev vitestpackage.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"
}
}dependencies— зависимости приложения.devDependencies— инструменты разработки.scripts— команды проекта.type— режим интерпретации.js.private: true— защита от случайной публикации.
Запуск:
npm run dev
npm test
npm startLock-файлы: 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 dotenvimport "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Краткие рекомендации
- Выберите CommonJS или ESM и не смешивайте их без необходимости.
- Используйте префикс
node:для встроенных модулей. - Не блокируйте Event Loop синхронным вводом-выводом и тяжёлыми вычислениями.
- Для Promise API файлов используйте
node:fs/promises. - Для больших данных применяйте Streams и
pipeline(). - Составляйте пути через
node:pathи проверяйте пользовательские значения. - Ограничивайте размер HTTP-запросов и валидируйте входные данные.
- Проверяйте и преобразовывайте
process.envпри запуске. - Не храните секреты в Git и логах.
- Сохраняйте lock-файл и используйте один менеджер пакетов.
- Обрабатывайте каждый Promise и события
error. - После фатальной ошибки корректно завершайте процесс.
- Для пошаговой отладки используйте
node --inspectиnode --inspect-brk.