Node.js — серверный JavaScript

Node.js — это среда выполнения JavaScript вне браузера. Она позволяет писать серверы, утилиты, ботов и целые веб-приложения на одном языке с фронтендом. Этот курс проведёт тебя от установки до деплоя первого приложения.

Уровень: начальный 18 разделов 3–4 часа

1. Что такое Node.js

Раньше JavaScript работал только в браузере. В 2009 году Райан Даль взял движок V8 из Chrome и встроил его в отдельную программу — так появился Node.js. Теперь JS-код можно запускать на сервере, как обычный исполняемый файл.

Ключевая особенность Node.js — асинхронная модель ввода-вывода и однопоточный цикл событий. Файловые операции, сетевые запросы и чтение баз данных не блокируют выполнение: пока сервер ждёт ответа от базы, он продолжает обслуживать другие запросы.

// как выглядит главная идея: колбэк вызывается, когда операция закончена
fs.readFile("data.txt", (err, data) => {
  if (err) { console.error(err); return; }
  console.log(data.toString());
});
console.log("Меня напечатают сразу, не дожидаясь чтения файла");

Где применяется Node.js

ОбластьПримеры
Веб-серверы и APIExpress, Fastify, NestJS
Утилиты и сборкаWebpack, Vite, ESLint
Боты и автоматизацияTelegram-боты, скрипты для CI
CLI-инструментыnpm, npx, многие разработчики пишут свои
Игры и приложения в реальном временичаты, онлайн-редакторы, Socket.IO
Node.js не подходит для тяжёлых вычислений (парсинг больших данных, машинное обучение). Если нужно много считать — лучше Python или Go. Но как веб-сервер Node.js превосходен.

2. Установка и nvm

Самый простой способ — скачать установщик с официального сайта nodejs.org и установить LTS-версию. На Linux и macOS удобнее использовать менеджер версий nvm, который позволяет переключаться между версиями Node.js.

# Windows — можно поставить через winget
winget install OpenJS.NodeJS.LTS

# macOS / Linux — менеджер версий nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
nvm install 20
nvm use 20

Проверь установку: команда node -v выводит версию среды, а npm -v — версию менеджера пакетов.

Всегда выбирай LTS-версию для продакшн-проектов. «Current» версии выходят быстро, содержат новинки, но и чаще ломают совместимость. nvm именно для того и нужен, чтобы не привязываться к одной версии.

Но также пригодится:

Если установка прошла, но в PowerShell команда node не находится — перезапусти терминал или перезагрузи переменные окружения. Подробнее про инструменты читай в курсе Linux.

3. Первый сервер

Начнём с классического примера. Встроенный модуль http умеет создавать HTTP-сервер без сторонних библиотек.

const http = require("http");

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
  res.end("Привет, мир!");
});

server.listen(3000, () => {
  console.log("Сервер запущен: http://localhost:3000");
});

Сохрани файл как server.js и запусти командой node server.js. Открой в браузере http://localhost:3000 и увидишь «Привет, мир!». Чтобы остановить сервер — нажми Ctrl+C.

Порт 3000 — популярный по умолчанию для Node-проектов. Если он занят, Node выдаст ошибку EADDRINUSE — просто поменяй номер порта.

Современный синтаксис import

Вместо require всё чаще используют стандартный import. Для этого в package.json добавь поле "type": "module".

import http from "node:http";

const server = http.createServer((req, res) => {
  res.end("ES-модули работают");
});
server.listen(3000);

4. Модули и npm

Модули — это способ разбить код на переиспользуемые части. Свои модули создаются через export и import, а чужие ставятся через npm — крупнейший реестр пакетов в мире.

// math.js
export function add(a, b) { return a + b; }
export const PI = 3.14159;

// main.js
import { add, PI } from "./math.js";
console.log(add(2, 3));      // 5
console.log(PI.toFixed(2));  // 3.14

Команды npm

npm init -y           # создать package.json
npm install express   # добавить пакет в зависимости
npm install -D vitest # зависимость только для разработки
npm uninstall express # удалить
npx nodemon server.js # запустить с автоматической перезагрузкой

Файл package.json — «паспорт» проекта: в нём имя, версия, скрипты и список зависимостей. Достаточно скопировать этот файл на другую машину и выполнить npm install — все пакеты подтянутся сами.

Файл / папкаНазначение
package.jsonописание проекта, зависимости, скрипты
package-lock.jsonточные версии всех зависимостей
node_modules/установленные пакеты (не редактировать, не коммитить)
.gitignoreисключает node_modules из Git
Папку node_modules никогда не добавляют в Git — она может весить сотни мегабайт. Обязательно напиши в .gitignore строку node_modules/. Пакет restore на новой машине выполняется командой npm install.

5. Файловая система: модуль fs

Модуль fs (file system) работает с файлами и папками. У каждого метода есть синхронная версия (с суффиксом Sync) и асинхронная.

import { readFile, writeFile, mkdir } from "node:fs/promises";

// асинхронное чтение с await
const data = await readFile("notes.txt", "utf-8");
console.log(data);

// запись (создаст файл, если его нет)
await writeFile("log.txt", "новая запись\n", { flag: "a" });

// создание папки с вложенными директориями
await mkdir("backup/2026", { recursive: true });

Версия на промисах node:fs/promises с await читается лучше всего и не приводит к аду колбэков.

В верхнеуровневом коде (файл-модуль) await работает без обёртки. Но внутри обычной функции его нужно обернуть в async — например, async function start() { ... }.
Проверяй код ошибок: при чтении несуществующего файла промис отклонится. Оберни вызов в try/catch и выведи понятное сообщение, а не просто красный стек ошибок.
try {
  const data = await readFile("missing.txt", "utf-8");
} catch (err) {
  console.error("Не удалось прочитать файл:", err.code);
}

6. События и потоки

Модуль events реализует паттерн «издатель-подписчик». Объект EventEmitter позволяет генерировать собственные события и реагировать на них. На нём построены почти все внутренности Node.js.

import { EventEmitter } from "node:events";

class Order extends EventEmitter {
  pay() {
    this.emit("paid", { id: 1, total: 2500 });
  }
}

const order = new Order();
order.on("paid", (info) => {
  console.log(`Заказ ${info.id} оплачен на ${info.total} руб.`);
});
order.pay();

Потоки (streams) передают данные частями, не загружая всю информацию в память. Это важно для больших файлов и видео: сервер «потоково» отдаёт файл, а посетитель смотрит его ещё во время загрузки.

import { createReadStream } from "node:fs";
import { createServer } from "node:http";

const server = createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "video/mp4" });
  createReadStream("movie.mp4").pipe(res);
});
server.listen(3000);

Виды потоков

ПотокНаправлениеПример
Readableтолько чтениечтение файла, ответ от сервера
Writableтолько записьзапись в файл, отправка ответа
Transformчтение + записьсжатие gzip, шифрование
Duplexдвустороннеесокеты, WebSocket
Обычный fs.readFile загружает весь файл в память. Для файла в 2 ГБ это плохо. createReadStream читает его кусками по 64 КБ — потребление памяти не зависит от размера файла.

7. Модуль http и обработка запросов

Объект запроса req хранит метод, URL, заголовки и тело запроса; объект ответа res позволяет отправить статус, заголовки и содержимое. Напишем маленький роутер без фреймворков.

import { createServer } from "node:http";

const server = createServer((req, res) => {
  const url = new URL(req.url, "http://localhost");
  console.log(req.method, url.pathname);

  if (req.method === "GET" && url.pathname === "/") {
    res.writeHead(200, { "Content-Type": "text/html" });
    res.end("<h1>Главная</h1><a href='/users'>Пользователи</a>");
  } else if (req.method === "GET" && url.pathname === "/users") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify([{ id: 1, name: "Иван" }]));
  } else {
    res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
    res.end("Страница не найдена");
  }
});

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

Структура HTTP-запроса

ЧастьОписание
МетодGET, POST, PUT, PATCH, DELETE
URLпуть и query-параметры: /users?page=2
Заголовкиметаданные: Content-Type, Authorization
Телоданные в POST: JSON, form-data
Ручное написание роутера полезно для понимания HTTP, но в реальных проектах используют фреймворк Express (следующий урок). Он берёт на себя маршрутизацию, парсинг тела и обработку ошибок.

8. Работа с базами данных

Node.js не имеет встроенной СУБД. Для старта идеально подходит SQLite — файл-база без отдельного сервера. Для больших проектов — PostgreSQL. Подключение к базе всегда асинхронное, поэтому используем await.

npm install better-sqlite3
        
import Database from "better-sqlite3";

const db = new Database("app.db");
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE
  )
`);

db.prepare("INSERT INTO users (name, email) VALUES (?, ?)")
  .run("Иван", "ivan@mail.ru");

const rows = db.prepare("SELECT * FROM users").all();
console.log(rows);

Для PostgreSQL удобен драйвер pg или ORM — например, Prisma или Sequelize. ORM позволяет работать с таблицами как с обычными JavaScript-объектами.

Никогда не собирай SQL-запросы склейкой строк с данными пользователя — это путь к SQL-инъекциям. Всегда используй плейсхолдеры (? или $1) и prepared statements. Подробнее в курсе SQL.
Перед продакшеном обязательно включи переменные окружения: пароль, строку подключения и порт храни в .env, а не в коде. Секреты никогда не попадают в Git.

9. Деплой Node.js-приложения

Локально приложение работает на localhost. Чтобы его увидели пользователи, нужен сервер с установленным Node.js и инструмент для управления процессом — например, pm2. Он перезапускает приложение при падении и хранит логи.

# на сервере (Ubuntu)
sudo apt install nodejs npm
npm ci
npm run build

# менеджер процессов pm2
npm install -g pm2
pm2 start server.js --name my-app
pm2 save
pm2 logs

Дальше нужно привязать домен и настроить обратный прокси. Nginx принимает входящие запросы на 80/443 порту и перенаправляет их на localhost:3000. За HTTPS отвечает бесплатный сертификат Let's Encrypt.

# /etc/nginx/sites-available/my-app
server {
  server_name example.ru;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }
}

Варианты деплоя

СпособКогда выбирать
VPS + pm2полный контроль, нужен опыт с Linux
Render / Railwayбыстрый старт, автодеплой из Git
Dockerодинаковая среда везде, воспроизводимость
Cloud Run / AWSмасштабирование нагруженных сервисов
Перед деплоем прогони тесты: npm test. Lint и typecheck из npm run выловят ошибки, которые дорого исправлять в проде. Подробнее — Хостинг и деплой.

10. Советы и типичные ошибки

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

Синхронная версия fs.readFileSync в обработчике HTTP-запроса — частая ошибка новичков. Сервер будет работать, но при первом же большом файле застынет для всех пользователей.
// так делать плохо: сервер зависнет, пока читается файл
const data = readFileSync("big.log");

// так правильно: не блокируем событийный цикл
const data = await readFile("big.log", "utf-8");
Правило простое: если операция ждёт ввода-вывода (сеть, диск, база) — почти всегда есть смысл выбрать асинхронный вариант. Это главное, чем Node.js отличается от классических многопоточных серверов.

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

Не храните секреты в коде. Node.js читает переменные окружения из process.env, а файл .env подгружается через пакет dotenv (в Node 20+ доступен встроенный флаг --env-file).

// .env
DATABASE_URL=postgres://user:pass@localhost:5432/shop
JWT_SECRET=fa4b0c1e2ff1090a8b7c6
PORT=3000

// config.js
import 'dotenv/config';

const config = {
  dbUrl: process.env.DATABASE_URL,
  secret: process.env.JWT_SECRET,
  port: process.env.PORT || 3000
};
export default config;
Файл .env обязательно добавляйте в .gitignore. Утёкший секрет считайте скомпрометированным: поменяйте его, а не просто «спрячьте» commit.
Для стейджинга используйте отдельные .env.development/.env.production и загрузку по актуальной среде — так исключаются случайные «прод-тайны» в разработке.

12. Обработка ошибок и логирование

Незавершённые promise-реджекты роняют процесс. Единая стратегия: http-ошибки с кодами 4xx/5xx, логирование в структурированном формате и обязательный возврат ответа при каждой ветке.

app.use('/api', async (req, res, next) => {
  try {
    const data = await loadData(req.params.id);
    res.json({ ok: true, data });
  } catch (err) {
    console.error(JSON.stringify({
      at: new Date().toISOString(),
      method: req.method,
      url: req.originalUrl,
      error: err.message
    }));
    res.status(500).json({ ok: false, error: 'Внутренняя ошибка' });
  }
});
Для продакшена рассмотрите pino или Winston вместо console.log: они пишут JSON-логи, которые удобно агрегировать и искать по трейсу запроса.

13. Работа с внешними API: fetch

Начиная с Node 18 встроен fetch, поэтому не нужен отдельный node-fetch или axios для простых запросов. Обрабатывайте сетевые ошибки и таймауты явно.

const res = await fetch('https://api.example.com/rates', {
  signal: AbortSignal.timeout(5000)
});

if (!res.ok) {
  throw new Error(`API вернул ${res.status}`);
}
const rates = await res.json();
console.log(rates);
res.json() бросает исключение при не-JSON ответе (например, HTML-страница ошибки). Проверяйте content-type и оборачивайте парсинг в try/catch перед использованием данных.
Добавляйте ретраи с экспоненциальной задержкой для идемпотентных запросов и ограничивайте число параллельных запросов через пул или пагинацию — так вы не уроните внешний сервис и сами не поймаете 429.

14. Worker Threads и тяжёлые вычисления

Главный поток Node.js однопоточный. Долгие CPU-задачи (обработка изображений, парсинг больших файлов, шифрование) блокируют весь процесс — выносите их в worker-потоки.

// worker.js
const { parentPort } = require('worker_threads');

parentPort.on('message', (chunk) => {
  const result = expensiveParse(chunk);
  parentPort.postMessage(result);
});

// main.js
const { Worker } = require('worker_threads');
const worker = new Worker('./worker.js');
worker.postMessage(data);
worker.on('message', (result) => console.log(result));
МеханизмЗадача
Worker ThreadsCPU-интенсивные расчёты
Child processвнешние CLI и изоляция
clusterмасштабирование по ядрам
Обмен данными между потоками копируется через structured clone. Для больших кусков используйте SharedArrayBuffer или передачу буфером, чтобы не пересылать данные дважды.

15. Масштабирование: cluster и PM2

Один процесс Node.js использует одно ядро. Чтобы задействовать все ядра, поднимите несколько процессов: встроенный модуль cluster разводит воркеры на каждое ядро, а PM2 легко управляет их жизненным циклом в проде.

npm i -g pm2
pm2 start app.js -i max
pm2 save && pm2 startup

PM2 даёт мониторинг, автоматический перезапуск при падении и zero-downtime обновление: pm2 reload app --update-env перезапускает воркеры по одному. Между воркерами не делится память — общее состояние (сессии, счётчики) храните в Redis, иначе ответы будут несогласованными.

ИнструментСильная сторона
clusterвстроенный, без зависимостей
PM2процесс-менеджер, reload, мониторинг
Docker + orchestrationреплики и автоперезапуск в кластере
Сколько бы воркеров вы ни подняли, уязвимое место останется у базы данных: масштабируйте приложение вместе с СУБД, иначе рост процессов лишь перенесёт очередь.
При размещении на нескольких серверах добавьте балансировщик — например, Nginx с round-robin — а сами серверы держите одинаковыми по конфигурации.

16. TypeScript на Node.js

Типизация превращает Node.js-проекты в более предсказуемые: интерфейсы описывают контракты данных, а проверка типов ловит ошибки до запуска. Современные версии Node.js исполняют TypeScript без отдельной сборки — достаточно флага --experimental-strip-types (в новых стабильно) либо запускать через tsx.

interface Product {
  id: string;
  name: string;
  price: number;
}

async function getProduct(id: string): Promise<Product> {
  const row = await db.query('SELECT * FROM products WHERE id = ?', [id]);
  return row[0];
}

Строгая типизация особенно полезна для обработки данных из API и баз: JSON-ответы и строки из БД приводятся к известным типам, а расплывчатые any запрещаются в линтере. Для продакшена собирайте проект компилятором tsc в dist/ и запускайте скомпилированный JavaScript.

Сетевая и файловая работа всё равно требует проверок в рантайме: типы — это контракт на этапе разработки, а не валидация входящих данных.

17. WebSocket и realtime-приложения

Для чатов, уведомлений и живых табло нужен двусторонний канал: WebSocket устанавливает одно TCP-соединение, по которому данные летят в обе стороны без пересоздания запросов. Стандартный пакет ws проще всего, а Socket.IO добавляет комнаты, переподключение и фолбэк на polling.

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 3001 });

wss.on('connection', (ws) => {
  ws.on('message', (msg) => {
    wss.clients.forEach((c) => c.send(msg.toString()));
  });
});

Продумайте два момента: редис-адаптер для множества воркеров (сообщение, отправленное одной нодой, должно дойти до клиентов на других) и сердцебиение, чтобы закрывать «висящие» соединения. За веб-платформой балансировщиков (например, Nginx) включите заголовок Upgrade.

Ограничивайте число сообщений и размер payload: WebSocket без лимитов позволяет одному клиенту просто разливаться по всей инфраструктуре.
Для простых уведомлений «сервер → клиент» достаточно Server-Sent Events (SSE) — они дешевле и уже поддерживаются браузерами.

18. Контейнеризация Node.js: надёжный деплой

Контейнер фиксирует окружение: версию Node.js, зависимости и переменные, поэтому приложение одинаково работает локально и на сервере. Правильная сборка использует multi-stage: сперва устанавливаются зависимости и собирается артефакт, потом создаётся маленький runtime-образ.

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

FROM node:22-alpine
ENV NODE_ENV=production
USER node
COPY --from=build /app /app
WORKDIR /app
CMD ["node", "src/index.js"]

Перезапускайтесь корректно: процесс должен слушать SIGTERM, завершать текущие запросы и закрывать пулы соединений (graceful shutdown). Порт пробрасывайте через переменные окружения, а файлы логов пишите в stdout — так их соберёт Docker и ваша наблюдаемость.

Не храните секреты в образе: пароли и ключи передавайте через secrets-механизм оркестрации или переменные окружения хоста.

Что дальше

Ты прошёл путь от установки Node.js до деплоя приложения: разобрался с модулями, файловой системой, событиями, потоками и базами данных. Следующий логичный шаг — фреймворк Express, который сделает написание серверов в разы быстрее. Если хочешь упорядочить знания основ, загляни в курс JavaScript, а для понимания обмена данными — в HTTP и DNS.

← Вернуться к курсам