Node.js — серверный JavaScript
Node.js — это среда выполнения JavaScript вне браузера. Она позволяет писать серверы, утилиты, ботов и целые веб-приложения на одном языке с фронтендом. Этот курс проведёт тебя от установки до деплоя первого приложения.
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
| Область | Примеры |
|---|---|
| Веб-серверы и API | Express, Fastify, NestJS |
| Утилиты и сборка | Webpack, Vite, ESLint |
| Боты и автоматизация | Telegram-боты, скрипты для CI |
| CLI-инструменты | npm, npx, многие разработчики пишут свои |
| Игры и приложения в реальном времени | чаты, онлайн-редакторы, Socket.IO |
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 — версию менеджера пакетов.
Но также пригодится:
- VS Code — редактор с отличной поддержкой JS.
- Git — контроль версий.
- Терминал — на Windows подойдёт PowerShell или Windows Terminal.
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.
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 |
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 {
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 |
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 |
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-объектами.
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. Советы и типичные ошибки
По ходу написания первых проектов ты встретишь одинаковые грабли. Вот список самых частых проблем и их решений.
- EADDRINUSE — порт занят. Смени порт или убей процесс, занимающий его.
- Cannot find module — пакет не установлен либо путь до модуля неверный. Выполни npm install.
- Callback hell — вложенные колбэки превращают код в кашу. Используй async/await и промисы.
- Блокировка событийного цикла — синхронные тяжёлые операции в обработчике запроса «замораживают» весь сервер. Выноси их в async функции или отдельные процессы.
// так делать плохо: сервер зависнет, пока читается файл
const data = readFileSync("big.log");
// так правильно: не блокируем событийный цикл
const data = await readFile("big.log", "utf-8");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;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: 'Внутренняя ошибка' });
}
});- 404 — ресурс не найден, 400 — неверный запрос, 401 — нет авторизации.
- Ошибки валидации возвращайте клиенту явно, без стектрейса.
- Не логируйте пароли, токены и тело запроса целиком.
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);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 Threads | CPU-интенсивные расчёты |
| Child process | внешние CLI и изоляция |
| cluster | масштабирование по ядрам |
15. Масштабирование: cluster и PM2
Один процесс Node.js использует одно ядро. Чтобы задействовать все ядра, поднимите несколько процессов: встроенный модуль cluster разводит воркеры на каждое ядро, а PM2 легко управляет их жизненным циклом в проде.
npm i -g pm2
pm2 start app.js -i max
pm2 save && pm2 startupPM2 даёт мониторинг, автоматический перезапуск при падении и zero-downtime обновление: pm2 reload app --update-env перезапускает воркеры по одному. Между воркерами не делится память — общее состояние (сессии, счётчики) храните в Redis, иначе ответы будут несогласованными.
| Инструмент | Сильная сторона |
|---|---|
| cluster | встроенный, без зависимостей |
| PM2 | процесс-менеджер, reload, мониторинг |
| Docker + orchestration | реплики и автоперезапуск в кластере |
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.
- Включайте noUncheckedIndexedAccess: массив без проверки может вернуть undefined.
- Для типов из внешних API используйте код-генерацию (env-типы), а не ручной дубль.
- Проверяйте типы в CI: tsc --noEmit на каждый pull request.
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.
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 и ваша наблюдаемость.
- Запускайте под непривилегированным пользователем (USER node).
- Устанавливайте зависимости в runtime только с --omit=dev.
- Сканируйте образ на уязвимые пакеты перед релизом.
Что дальше
Ты прошёл путь от установки Node.js до деплоя приложения: разобрался с модулями, файловой системой, событиями, потоками и базами данных. Следующий логичный шаг — фреймворк Express, который сделает написание серверов в разы быстрее. Если хочешь упорядочить знания основ, загляни в курс JavaScript, а для понимания обмена данными — в HTTP и DNS.