Node.js — среда выполнения JavaScript вне браузера: тот же язык, но вместо DOM и window у вас файловая система, сеть и процессы операционной системы. Проект на Node.js начинается с npm init и файла package.json, где включают ES-модули полем "type": "module". Первый сервер пишется на встроенном модуле node:http без зависимостей, настройки читаются из переменных окружения через флаг --env-file, а --watch перезапускает процесс при изменении кода.
После материала вы сможете
- создать npm-проект с ES-модулями, скриптами и lock-файлом и объяснить разницу между
npm installиnpm ci - разбить код на модули с
import/exportи использоватьnode:fs/promises,node:pathиnode:events - написать HTTP-сервер на
node:httpс маршрутамиGET /healthиGET /services, кодами 404 и 405 и журналом запросов - читать настройки из переменных окружения через
--env-fileи запускать сервер в режиме--watch
Что знать заранее
Проверено на
Node.js 24 LTS (24.18)npm 11.16curl 8.21
Сверено с документацией: 11 октября 2026 г.
Содержание
- Что такое Node.js и чем он отличается от браузера
- npm-проект: package.json, зависимости и lock-файл
- ES-модули: import, export и что изменилось после require
- Встроенные модули: fs, path и events
- Первый HTTP-сервер и маршрутизация вручную
- Переменные окружения, --env-file и режим --watch
- Упражнения и почему дальше нужен фреймворк
Что такое Node.js и чем он отличается от браузера
В уроке «Что такое backend» мы разобрали, что делает сервер. Теперь — инструмент, на котором этот сервер можно написать, не уходя из JavaScript. Node.js — открытая кроссплатформенная среда выполнения JavaScript. Внутри неё работает V8 — тот же движок, что исполняет JavaScript в Chrome, — но снаружи нет браузера: нет document, window и DOM. Вместо них — стандартная библиотека для работы с файлами, сетью, процессами и потоками данных.
Документация Node.js подчёркивает главную особенность: приложение работает в одном процессе и не создаёт поток на каждый запрос. Операции ввода-вывода — чтение файла, запрос к базе, обращение к сети — неблокирующие: пока ответ не пришёл, Node.js обслуживает другие запросы, а затем возвращается к вашему коду через промис или колбэк. Это та же модель цикла событий, которую вы видели в уроке об асинхронном JavaScript, только применённая к серверу.
Что доступно
- Браузер
- DOM,
window,fetch,localStorage, cookies - Node.js
fs,http,path,process,fetch, потоки и процессы ОС
Версия JavaScript
- Браузер
- Та, что у браузера посетителя
- Node.js
- Та, что вы установили на сервер
Модули
- Браузер
- Только
import(ES-модули) - Node.js
- ES-модули и CommonJS (
require)
Доступ к ОС
- Браузер
- Изолирован песочницей
- Node.js
- Полный: файлы, сеть, переменные окружения
Важное следствие из последней строки таблицы: код на сервере видит ваши ключи, базу и файловую систему, поэтому всё, что вы читаете из запроса, нужно проверять. Для уроков используйте актуальную LTS-ветку — версию с долгосрочной поддержкой. На дату проверки это Node.js 24 (кодовое имя Krypton); код урока проверен на 24.18. Проверьте свои версии:
$ node -v
v24.18.0
$ npm -v
11.16.0npm-проект: package.json, зависимости и lock-файл
Любой проект на Node.js — это папка с файлом package.json: в нём имя, версия, скрипты запуска и список зависимостей. Создаёт его команда npm init, а ключ -y принимает значения по умолчанию без вопросов. Дальше удобно править файл не руками, а через npm pkg set — так не сломаете JSON. Создадим проект API со списком услуг:
mkdir services-api
cd services-api
npm init -y
npm pkg set type=module
npm pkg set scripts.dev="node --watch --env-file=.env server.js" scripts.start="node server.js"
npm pkg set private=true --json
npm pkg delete scripts.test main{
"name": "services-api",
"version": "1.0.0",
"description": "",
"scripts": {
"dev": "node --watch --env-file=.env server.js",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"private": true,
"dependencies": {
"zod": "^4.6.6"
}
}"type": "module"— файлы.jsв проекте считаются ES-модулями. npm 11 по умолчанию записывает"type": "commonjs"(настройкаinit-type), поэтому значение меняем явно. Документация Node.js советует всегда указывать полеtype, а не полагаться на автоопределение"private": true— npm откажется публиковать проект в реестр: для приложения, в отличие от библиотеки, это защита от ошибкиscripts— команды проекта.npm run devзапустит сервер для разработки,npm start— без флагов разработки. Начиная с Node.js 22 скрипты можно запускать и встроенной командойnode --run dev
Пакеты из реестра npm ставит npm install <имя>. Установим Zod — библиотеку валидации, которая понадобится в следующем уроке про Express:
$ npm install zod
added 1 package, and audited 2 packages in 2s
1 package is looking for funding
run `npm fund` for details
found 0 vulnerabilities
$ npm pkg get dependencies
{
"zod": "^4.6.6"
}
$ npm ls
services-api@1.0.0 …/services-api
`-- zod@4.6.6dependencies в package.json
- Зачем
- Список пакетов, нужных приложению в работе;
devDependencies— только для разработки и тестов (npm install -D) - В Git?
- Да
^4.6.6
- Зачем
- Диапазон версий: подойдёт любая совместимая 4.x не ниже 4.6.6, но не 5.0
- В Git?
- —
package-lock.json
- Зачем
- Точное дерево установленных версий с контрольными суммами. Документация npm: файл предназначен для коммита, чтобы команда, CI и сервер ставили одно и то же
- В Git?
- Да
node_modules/
- Зачем
- Сами пакеты. Восстанавливаются из lock-файла
- В Git?
- Нет, в
.gitignore
ES-модули: import, export и что изменилось после require
Модуль — файл со своей областью видимости: наружу видно только то, что вы явно экспортировали. Node.js поддерживает две системы модулей. CommonJS с require() — историческая, её вы встретите в старых проектах и примерах. ES-модули с import и export — стандарт языка, тот же, что в браузере. Новые проекты пишут на ES-модулях. Вынесем настройки и загрузку данных в отдельные модули:
const port = Number(process.env.PORT ?? 8080);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error(`PORT must be an integer from 1 to 65535, got "${process.env.PORT}"`);
}
export const config = {
port,
host: process.env.HOST ?? "127.0.0.1",
appName: process.env.APP_NAME ?? "services-api",
};import { readFile } from "node:fs/promises";
import { join } from "node:path";
const file = join(import.meta.dirname, "..", "data", "services.json");
export async function loadServices() {
const text = await readFile(file, "utf8");
return JSON.parse(text);
}[
{ "id": "web", "title": "Сайт для бизнеса", "category": "web" },
{ "id": "crm", "title": "CRM-система", "category": "software" },
{ "id": "bot", "title": "Telegram-бот", "category": "integrations" }
]- Встроенные модули импортируют с префиксом
node:—node:fs/promises,node:http. Так сразу видно, что это не пакет из npm с похожим именем - В относительном импорте расширение файла обязательно:
./src/config.js, а не./src/config— так требует документация ESM, как и в браузере require,__dirnameи__filenameв ES-модулях не определены. Вместо двух последних —import.meta.dirnameиimport.meta.filename, стабильные с Node.js 24awaitможно писать на верхнем уровне модуля, без обёртки в async-функцию — им мы загрузим данные до старта сервера
$ node a.mjs # import { config } from "./config"
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '…/config' imported from …/a.mjs
Did you mean to import "./config.js"?
$ node b.mjs # const fs = require("node:fs")
ReferenceError: require is not defined in ES module scope, you can use import instead
$ node c.mjs # console.log(__dirname)
ReferenceError: __dirname is not defined in ES module scopeВстроенные модули: fs, path и events
Стандартная библиотека Node.js покрывает большую часть задач простого сервера — без единой зависимости. В src/services.js уже работают два модуля: readFile из node:fs/promises читает файл и возвращает промис, а join из node:path склеивает путь с правильными разделителями для Windows и Unix. Используйте промисную версию fs: синхронные функции вроде readFileSync останавливают весь процесс, пока диск не ответит, и на сервере допустимы разве что при старте.
node:fs/promises
- Для чего
- Файлы и папки
- Пример
await readFile(path, "utf8"),writeFile,mkdir
node:path
- Для чего
- Пути без ручной склейки строк
- Пример
join(import.meta.dirname, "data")
node:http
- Для чего
- HTTP-сервер и клиент
- Пример
createServer((req, res) => …)
node:events
- Для чего
- Подписка на события
- Пример
emitter.on("name", handler)
node:process / process
- Для чего
- Окружение, аргументы, завершение
- Пример
process.env.PORT,process.exit(1)
Модуль node:events стоит понять сразу, потому что на нём построена половина Node.js: HTTP-сервер, запрос, ответ, потоки — всё это объекты EventEmitter. Один объект генерирует событие методом emit, а подписчики, зарегистрированные через on, вызываются по очереди, синхронно. Так удобно развязывать код: модуль заказов не знает, кто будет слать письма и считать статистику.
import { EventEmitter } from "node:events";
const orders = new EventEmitter();
orders.on("created", (order) => console.log(`email: order #${order.id} received`));
orders.on("created", (order) => console.log(`stats: +${order.total} KZT`));
orders.once("created", () => console.log("first order today!"));
orders.emit("created", { id: 1, total: 12000 });
orders.emit("created", { id: 2, total: 8500 });
console.log("listeners:", orders.listenerCount("created"));
orders.emit("error", new Error("payment service timeout"));
console.log("this line is never printed");$ node events-demo.js
email: order #1 received
stats: +12000 KZT
first order today!
email: order #2 received
stats: +8500 KZT
listeners: 2
…/events-demo.js:13
orders.emit("error", new Error("payment service timeout"));
^
Error: payment service timeout
at …/events-demo.js:13:22
Emitted 'error' event at:
at …/events-demo.js:13:8
Node.js v24.18.0Первый HTTP-сервер и маршрутизация вручную
Устройство запросов и ответов, заголовки и коды мы подробно разобрали в уроке «HTTP и JSON», где на node:http уже был сервер заказов. Здесь сервер собирается из модулей проекта и получает две вещи, без которых не обходится ни один backend: журнал запросов и обработку ошибки запуска. Маршрутов два: GET /health — проверка, что процесс жив (её опрашивают хостинг и балансировщик), и GET /services — список услуг из файла.
import { createServer } from "node:http";
import { config } from "./src/config.js";
import { loadServices } from "./src/services.js";
const services = await loadServices();
function sendJson(res, status, data, headers = {}) {
const body = JSON.stringify(data);
res.writeHead(status, {
"Content-Type": "application/json; charset=utf-8",
"Content-Length": Buffer.byteLength(body),
...headers,
});
res.end(body);
}
const routes = {
"/health": () => ({ status: "ok", uptime: Math.round(process.uptime()) }),
"/services": () => services,
};
const server = createServer((req, res) => {
const started = performance.now();
res.on("finish", () => {
const ms = (performance.now() - started).toFixed(1);
console.log(`${req.method} ${req.url} ${res.statusCode} ${ms}ms`);
});
const { pathname } = new URL(req.url, "http://localhost");
const route = routes[pathname];
if (!route) return sendJson(res, 404, { error: "Not found" });
if (req.method !== "GET") {
return sendJson(res, 405, { error: "Method not allowed" }, { Allow: "GET" });
}
sendJson(res, 200, route());
});
server.on("error", (error) => {
console.error(`Cannot start server: ${error.code} ${error.message}`);
process.exit(1);
});
server.listen(config.port, config.host, () => {
console.log(`${config.appName}: http://${config.host}:${config.port}`);
});Данные загружаются до старта
await loadServices()на верхнем уровне: если файл битый, сервер не запустится вовсе — лучше, чем ответы с ошибкой.Маршруты — объект
Ключ — путь, значение — функция, которая возвращает данные. Нет ключа — 404. Путь есть, а метод не GET — 405 с заголовком
Allow, как требует RFC 9110.Журнал через событие
res.on("finish")срабатывает, когда ответ передан операционной системе. В этот момент известны код ответа и длительность — одна строка лога на запрос.Ошибка запуска
server.on("error")перехватывает, например,EADDRINUSE— порт занят — и завершает процесс с понятным сообщением и кодом 1.
Запустите npm run dev и во втором терминале отправьте запросы:
$ curl -si http://127.0.0.1:8080/health
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 26
Date: Sun, 11 Oct 2026 23:13:57 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"status":"ok","uptime":1}
$ curl -s http://127.0.0.1:8080/services
[{"id":"web","title":"Сайт для бизнеса","category":"web"},{"id":"crm","title":"CRM-система","category":"software"},{"id":"bot","title":"Telegram-бот","category":"integrations"}]
$ curl -si -X POST http://127.0.0.1:8080/services
HTTP/1.1 405 Method Not Allowed
Content-Type: application/json; charset=utf-8
Content-Length: 30
Allow: GET
Date: Sun, 11 Oct 2026 23:13:57 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"error":"Method not allowed"}
$ curl -s http://127.0.0.1:8080/price
{"error":"Not found"}Переменные окружения, --env-file и режим --watch
Порт, адрес базы, ключи сервисов отличаются между ноутбуком, тестовым и боевым сервером, поэтому их не пишут в код, а читают из переменных окружения — объекта process.env. Для разработки их удобно держать в файле .env, а Node.js умеет загрузить его сам, без пакета dotenv: флаг --env-file стабилен с версии 24.10.
PORT=8080
HOST=127.0.0.1
APP_NAME=InterLAB services APInode_modules/
.env$ node server.js
services-api: http://127.0.0.1:8080
$ PORT=8081 node --env-file=.env server.js
InterLAB services API: http://127.0.0.1:8081
$ PORT=abc node server.js
Error: PORT must be an integer from 1 to 65535, got "abc"
at …/src/config.js:4:9
$ node --env-file=.env.local server.js
C:\Program Files\nodejs\node.exe: .env.local: not found
$ node --env-file=.env server.js # second instance, port already in use
Cannot start server: EADDRINUSE listen EADDRINUSE: address already in use 127.0.0.1:8080- Все значения из окружения — строки.
PORT=8080придёт как"8080", поэтомуconfig.jsпереводит его в число и проверяет диапазон. Ошибку конфигурации лучше получить при старте, чем при первом запросе - Если переменная задана и в окружении, и в файле, побеждает окружение — так на сервере можно переопределить значение из
.env --env-fileзавершает процесс, если файла нет. Для необязательного файла есть--env-file-if-exists, а из кода —process.loadEnvFile()- Секреты в продакшене задают средствами хостинга или менеджера секретов, а не файлом рядом с кодом. Подробнее — в уроке о секретах и переменных окружения
Флаг --watch перезапускает процесс, когда меняется точка входа или любой импортированный модуль, — в логе выше это строка Restarting 'server.js'. Проверка на Node.js 24.18 показала: перезапуск срабатывает и при изменении файла из --env-file, но не при правке data/services.json — файл читается через fs, а не импортируется. Чтобы следить за произвольными папками, используйте --watch-path=.; учтите, что этот флаг отключает автоматическое отслеживание импортов, поэтому указывайте папки целиком.
$ npm run dev
> services-api@1.0.0 dev
> node --watch --env-file=.env server.js
InterLAB services API: http://127.0.0.1:8080
GET /health 200 2.4ms
GET /services 200 0.3ms
POST /services 405 0.3ms
GET /price 404 0.3ms
Restarting 'server.js'
InterLAB services API: http://127.0.0.1:8080
GET /services 200 2.7msУпражнения и почему дальше нужен фреймворк
Сервер работает, но посмотрите, сколько пришлось сделать руками: сравнивать пути, вытаскивать параметры регулярным выражением, собирать JSON-ответ, помнить про 405. Для POST добавятся чтение тела, разбор JSON, проверка Content-Type, валидация и единый формат ошибок — в уроке «HTTP и JSON» это заняло десятки строк на один маршрут. Именно это берёт на себя фреймворк. В следующем уроке перенесём API на Express: маршруты с параметрами, middleware, валидация через Zod и центральный обработчик ошибок.
- В
package.jsonуказаны"type": "module"и"private": true, lock-файл в Git,node_modules— нет - Встроенные модули импортируются с
node:, относительные импорты — с расширением.js - Файлы читаются через
node:fs/promises, пути строятся черезpath.joinиimport.meta.dirname - Настройки читаются из
process.env, приводятся к нужному типу и проверяются при старте .envв.gitignore, рядом лежит.env.exampleбез секретов- Для разработки —
node --watch --env-file=.env, для продакшена — чистыйnode server.js
Как проверено9 · пройденосверено 11 октября 2026 г.
- запущено, работает
package.json (npm init -y + npm pkg set)
команды выполнены во временном проекте scratchpad/kb/verify/nodejs-osnovy/services-api, npm 11.16.0; package.json в статье — содержимое файла после команд
- запущено, работает
npm install zod / npm pkg get dependencies / npm ls
реальный вывод, zod 4.6.6, package-lock.json lockfileVersion 3; путь проекта в выводе сокращён
- запущено, работает
src/config.js, src/services.js, data/services.json, server.js
Node.js 24.18.0: node --check всех файлов; npm run dev; curl -si /health (200), /services (200), POST /services (405 + Allow), /price (404) — реальный вывод в статье; код в статье вставлен сборочным скриптом из проверенных файлов
- запущено, работает
Запуски с переменными окружения и ошибками
node server.js без .env (значения по умолчанию), PORT=8081 поверх --env-file (окружение важнее), PORT=abc (ошибка конфигурации), --env-file=.env.local (not found, exit 9), второй экземпляр (EADDRINUSE, exit 1); вывод стека сокращён
- запущено, работает
--watch
npm run dev: перезапуск при изменении src/services.js и .env; изменение data/services.json перезапуска не вызвало; --watch-path=. перезапускает при изменении data/services.json
- запущено, работает
Ошибки ESM (расширение, require, __dirname)
три отдельных .mjs-файла, реальные сообщения Node.js 24.18, пути сокращены
- запущено, работает
events-demo.js
node events-demo.js — вывод подписчиков, once, падение на error без обработчика (exit 1); стек сокращён
- запущено, работает
Решение упражнения GET /services/:id
server-ex2.js: /services/crm → 200, /services/seo → 404, /services → 200; сборочный скрипт сверяет фрагмент с проверенным файлом
- запущено, работает
node --run start
сервер стартовал через node --run start (Node.js 24.18)
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.