Как создать веб-сервер в Node.js с использованием модуля HTTP
Введение
Когда вы просматриваете веб-страницу в своем браузере, вы отправляете запрос на другой компьютер в интернете, который затем предоставляет вам веб-страницу в ответ. Этот компьютер, с которым вы общаетесь через интернет, называется веб-сервером. Веб-сервер получает HTTP-запросы от клиента, например вашего браузера, и предоставляют HTTP-ответ, например HTML-страницу или JSON из API.
Модуль Node.js http предоставляет всё необходимое для создания веб-сервера без сторонних зависимостей. Он обрабатывает входящие запросы, парсит HTTP-заголовки, управляет соединениями и отправляет ответы — всё это с помощью простых JavaScript API.
Модуль http — это встроенное решение Node.js для создания HTTP-серверов и клиентов. В отличие от фреймворков, таких как Express.js, он является частью ядра Node.js, что означает, что установка или внешние зависимости не требуются. Это делает его идеальным для лёгких приложений, изучения основ HTTP или когда вы хотите полностью контролировать обработку запросов.
Для того чтобы сервер вернул веб-страницу, используется большое количество программного обеспечения. Оно обычно делится на две категории: фронтенд и бэкенд. Фронтенд-код отвечает за то, как отображается контент, например, цвет панели навигации и оформление текста. Бэкенд-код отвечает за то, как данные передаются, обрабатываются и хранятся. Код, который обрабатывает сетевые запросы из вашего браузера или взаимодействует с базой данных, в основном управляется бэкенд-кодом.
Node.js позволяет разработчикам использовать JavaScript для написания серверного кода, хотя традиционно он использовался в браузере для написания клиентского кода. Совмещение фронтенда и бэкенда таким образом снижает усилия, необходимые для создания веб-сервера, что является одной из основных причин, по которой Node.js популярен для написания серверного кода.
Этот учебник показывает, как создавать готовые к производству веб-серверы с использованием модуля http, который входит в состав Node.js. Вы создадите серверы, которые обрабатывают маршрутизацию, разбирают тела запросов, обслуживают статические файлы, реализуют правильную обработку ошибок и работают как с синтаксисом CommonJS, так и с ES Module.
Упростите развертывание приложений Node.js с помощью платформы App Platform от Linux-Console.net. Разворачивайте Node прямо с GitHub за считанные минуты.
Основные выводы
Прежде чем погрузиться в тему, вот чего вы достигнете:
Создание HTTP-серверов с использованием базового модуля Node.js
httpбез фреймворков:
Вы научитесь запускать полностью функциональный HTTP-веб-сервер, используя только базовый модуль Node.jshttp. Это означает отказ от внешних зависимостей или фреймворков, что поможет вам глубже понять, как серверы работают на низком уровне, а также позволит создавать более легковесные и гибкие реализации для ваших задач.Обрабатывайте разные типы контента (JSON, HTML, CSV, статические файлы) с правильными заголовками:
Вы узнаете, как работать с несколькими типами контента, отвечая HTML для веб-страниц, JSON для API, CSV-файлов или предоставляя статические ресурсы, такие как изображения и скрипты. Вы научитесь правильно задавать HTTP-заголовки, такие какContent-Type, чтобы браузеры и клиенты корректно интерпретировали ваши ответы для обеспечения беспроблемного пользовательского опыта.Реализация маршрутизации с использованием разбора URL-запросов и операторов switch:
Маршрутизация является основой веб-серверов, позволяя обслуживать разную логику для разных URL и конечных точек. Вы увидите методы разбора входящих URL-запросов и структурирования логики сервера (например, с помощью операторов switch), чтобы предоставлять индивидуальные ответы в зависимости от путей и HTTP-методов, имитируя основы работы более крупных фреймворков за кулисами.Обработка POST-запросов путем анализа тел запроса и работы с различными типами контента:
Работа с данными, отправленными пользователем, является ключевой для любого интерактивного приложения. Вы научитесь обрабатывать POST-запросы, включая чтение и разбор тела запроса в виде необработанных данных, JSON или других форматов. В этом разделе рассматривается буферизация входящих данных, их безопасный разбор и формирование ответов в соответствии с полученными данными, предоставляя основы для взаимодействия с пользователем или разработки API.Эффективная отдача статических файлов с файловой системы:
Многие веб-серверы должны напрямую предоставлять пользователям статические ресурсы — изображения, таблицы стилей, файлы JavaScript. Вы узнаете, как считывать файлы с диска и эффективно отправлять их через HTTP, правильно управляя путями к файлам, чтобы избежать проблем с безопасностью, а также изучите оптимизации, такие как потоковая передача файлов, для улучшения производительности при работе с крупными файлами.Реализуйте обработку ошибок с использованием соответствующих HTTP-кодов состояния и ведения логов:
Надежный сервер должен корректно обрабатывать ошибки. Вы узнаете, как обнаруживать, регистрировать и отвечать на ошибки — такие как некорректные запросы или отсутствующие файлы — с использованием стандартных HTTP-кодов состояния, таких как 400, 404 и 500. Вы также изучите основные практики ведения логов, чтобы проблемы были отслеживаемыми, а пользователи получали информативные сообщения об ошибках без раскрытия конфиденциальной информации.-
Используйте современный JavaScript с синтаксисом как CommonJS (
require), так и ES Modules (import):
Node.js поддерживает две системы модулей: CommonJS и ES Modules. Вы будете практиковаться в использовании какrequire(), так иimport, понимая, когда и как применять каждый стиль. Это не только обеспечивает совместимость с различными проектами Node.js, но и помогает писать современный, чистый и легко сопровождаемый код на JavaScript. Применяйте лучшие практики для продакшна, включая кэширование файлов, восстановление после ошибок и меры безопасности:
Наконец, вы ознакомитесь с основными лучшими практиками развертывания веб-серверов Node.js в реальных условиях. Это включает использование кэширования файлов для повышения производительности, внедрение стратегий восстановления после ошибок для поддержания устойчивости сервера, а также применение базовых мер безопасности (таких как санитизация входных данных и корректные HTTP-заголовки) для защиты ваших приложений и пользователей.
Понимание модуля HTTP
Модуль http является низкоуровневым интерфейсом Node.js для работы с HTTP. Он предоставляет:
- Создание сервера:
http.createServer()создает экземпляр HTTP-сервера - Обработка запросов: Входящие запросы представлены объектами
IncomingMessage - Управление ответами: Исходящие ответы используют объекты
ServerResponse - Поддержка потоковой передачи: как запросы, так и ответы являются потоками, что обеспечивает эффективную передачу данных
Почему использовать модуль HTTP вместо Express? Хотя Express.js предоставляет удобные абстракции, нативный модуль http дает полный контроль, меньший размер сборки и глубокое понимание основных принципов HTTP. Это основа, на которой строятся фреймворки. Для простых API, микросервисов или учебных целей он часто является всем, что вам нужно.
Модуль работает на основе событий: когда поступает запрос, сервер вызывает событие 'request' и запускает вашу функцию-обработчик запроса. Такая архитектура позволяет Node.js эффективно обрабатывать множество одновременных подключений на одном потоке благодаря своему циклу событий.
Основные концепции для понимания:
- Неблокирующий ввод-вывод: Модуль
httpиспользует цикл событий Node.js для обработки нескольких соединений без создания отдельных потоков для каждого запроса. Это делает его очень эффективным для операций, связанных с вводом-выводом. - Потоки: Объекты запроса и ответа наследуются от потоков Node.js. Это означает, что большие объемы данных можно обрабатывать по частям, не загружая всё в память — что особенно важно при загрузке файлов или отдаче больших файлов.
- Заголовки и коды статусов: Модуль предоставляет прямой контроль над HTTP-заголовками (
res.setHeader()) и кодами статусов (res.writeHead()), позволяя точно управлять HTTP-семантикой. - Никакой магической маршрутизации: В отличие от фреймворков, встроенной маршрутизации нет. Вы вручную анализируете
req.urlиreq.method, чтобы определить, как отвечать — это дает полную прозрачность в том, как на самом деле работает маршрутизация.
Когда вы вызываете http.createServer(), Node.js создаёт TCP-сервер за кулисами, который прослушивает входящие соединения, разбирает сообщения по протоколу HTTP и генерирует события, которые ваш код может обрабатывать. Понимание этой низкоуровневой работы помогает вам устранять ошибки и принимать обоснованные архитектурные решения.
Предварительные требования
Убедитесь, что Node.js установлен на вашем компьютере для разработки. Этот учебник работает с Node.js 20.x или 22.x LTS (рекомендуется). Чтобы установить Node.js на macOS или Ubuntu, следуйте инструкциям в «Как установить Node.js и создать локальную среду разработки на macOS» или «Как установить Node.js на Ubuntu».
Платформа Node.js поддерживает создание веб-серверов прямо «из коробки». Знание основ Node.js будет полезно. Ознакомьтесь с нашим руководством по созданию и запуску вашей первой программы на Node.js.
Асинхронное программирование используется в некоторых разделах. Если вы новичок в асинхронном коде или модуле
fs, смотрите «Как писать асинхронный код в Node.js».-
Для разделов, ориентированных на производство, знание управления процессами в Linux (systemd/PM2) и обратных прокси (например, Nginx) будет полезным при применении рекомендаций по развертыванию.
Шаг 1 — Создание базового HTTP-сервера
Создайте минимальный HTTP-сервер, который отвечает на любой запрос простым текстом. Этот подход актуален как при использовании Express, так и при создании сервера с нуля. Когда Node.js вызывает http.createServer(), он создает TCP-сервер, который слушает определенный порт. Каждое входящее соединение запускает вашу функцию-обработчик запросов.
Настройте папку для проекта:
mkdir first-servers
cd first-serversТеперь создайте файл, в котором будет храниться код:
touch hello.jsОткройте файл в текстовом редакторе. Мы будем использовать nano, так как он доступен в терминале:
nano hello.jsМы начинаем с загрузки модуля http, который входит в стандартную поставку всех установок Node.js. Добавьте следующую строку в hello.js:
const http = require("http");Модуль http содержит функцию для создания сервера, о которой мы поговорим позже. Если вы хотите узнать больше о модулях в Node.js, ознакомьтесь с нашей статьей «Как создать модуль Node.js».
Нашим следующим шагом будет определение двух констант: хоста и порта, к которым будет привязан наш сервер:
...
const host = 'localhost';
const port = 8000;Как уже упоминалось, веб-серверы принимают запросы от браузеров и других клиентов. Мы можем взаимодействовать с веб-сервером, вводя доменное имя, которое преобразуется в IP-адрес сервером DNS. IP-адрес — это уникальная последовательность цифр, которая идентифицирует устройство в сети, такой как интернет. Для получения дополнительной информации о концепциях доменных имен, ознакомьтесь с нашей статьей «Введение в терминологию, компоненты и концепции DNS».
Значение localhost — это специальный приватный адрес, который компьютеры используют, чтобы ссылаться сами на себя. Обычно это эквивалент внутреннего IP-адреса 127.0.0.1 и он доступен только на локальном компьютере, а не в любых локальных сетях, к которым мы подключены, или в интернете.
Порт — это число, которое серверы используют в качестве конечной точки или «двери» к нашему IP-адресу. В нашем примере мы будем использовать порт 8000 для нашего веб-сервера. Порты 8080 и 8000 обычно используются как порты по умолчанию в разработке, и в большинстве случаев разработчики будут использовать их, а не другие порты для HTTP-серверов.
Когда мы привяжем наш сервер к этому хосту и порту, мы сможем получить доступ к нашему серверу, посетив http://localhost:8000 в локальном браузере.
Давайте добавим специальную функцию, которую в Node.js мы называем обработчиком запросов. Эта функция предназначена для обработки входящего HTTP-запроса и возвращения HTTP-ответа. У этой функции должно быть два аргумента: объект запроса и объект ответа. Объект запроса фиксирует все данные входящего HTTP-запроса. Объект ответа используется для возврата HTTP-ответов серверу.
Мы хотим, чтобы наш первый сервер возвращал это сообщение всякий раз, когда кто-то к нему обращается: "Мой первый сервер!".
Давайте добавим эту функцию следующим образом:
...
const requestListener = function (req, res) {
res.writeHead(200);
res.end("My first server!");
};Функцию обычно называют исходя из того, что она делает. Например, если мы создаем функцию-слушатель запросов для возвращения списка книг, мы, вероятно, назовем её listBooks(). Поскольку это пример, мы будем использовать универсальное имя requestListener.
Все функции-слушатели запросов в Node.js принимают два аргумента: req и res (мы можем назвать их по-другому, если захотим). HTTP-запрос, который отправляет пользователь, фиксируется в объекте Request, который соответствует первому аргументу, req. HTTP-ответ, который мы возвращаем пользователю, формируется во взаимодействии с объектом Response во втором аргументе, res.
Первая строка res.writeHead(200); устанавливает код состояния HTTP-ответа. Коды состояния HTTP показывают, насколько успешно сервер обработал HTTP-запрос. В этом случае код состояния 200 соответствует "OK". Если вам интересно узнать о различных HTTP-кодах, которые могут возвращать ваши веб-серверы, и о том, что они означают, наш гид «Как устранить распространённые ошибки HTTP» является хорошей отправной точкой.
Следующая строка функции, res.end("My first server!");, отправляет HTTP-ответ обратно клиенту, который его запросил. Эта функция возвращает любые данные, которые сервер должен вернуть. В данном случае возвращаются текстовые данные.
Наконец, мы теперь можем создать наш сервер и использовать наш обработчик запросов:
...
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});Сохраните и выйдите из nano, нажав CTRL+X.
В первой строке мы создаём новый объект server с помощью функции createServer() модуля http. Этот сервер принимает HTTP-запросы и передаёт их нашей функции requestListener().
После того как мы создадим наш сервер, мы должны привязать его к сетевому адресу. Мы делаем это с помощью метода server.listen(). Он принимает три аргумента: port, host и функцию обратного вызова, которая вызывается, когда сервер начинает прослушивание.
Все эти аргументы являются необязательными, но рекомендуется явно указывать, какой порт и хост мы хотим использовать для веб-сервера. При развертывании веб-серверов в разных окружениях необходимо знать порт и хост, на которых он работает, чтобы настроить балансировку нагрузки или DNS-алиас.
Функция обратного вызова записывает сообщение в нашу консоль, чтобы мы могли узнать, когда сервер начал принимать соединения.
Вот полный файл hello.js с встроенными комментариями, объясняющими каждую часть:
// Import the built-in http module - no installation needed
const http = require("http");
// Define where the server will listen
const host = 'localhost'; // Only accessible from this machine
const port = 8000; // Common development port
// Request listener: handles every incoming HTTP request
const requestListener = function (req, res) {
// Set HTTP status code to 200 (OK)
res.writeHead(200);
// Send the response body and close the connection
res.end("My first server!");
};
// Create the server with our request listener
const server = http.createServer(requestListener);
// Start listening for connections
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});Этот код создаёт работоспособный HTTP-сервер менее чем за 20 строк. Каждый раз, когда клиент подключается — независимо от URL или HTTP-метода — сервер отвечает «Мой первый сервер!» и кодом состояния 200.
Примечание: Даже если requestListener() не использует объект req, он всё равно должен быть первым аргументом функции.
Менее чем с пятнадцатью строками кода у нас теперь есть веб-сервер. Давайте посмотрим, как он работает, и протестируем его от начала до конца, запустив программу:
node hello.jsВ консоли мы увидим этот вывод:
Server is running on http://localhost:8000Обратите внимание, что приглашение исчезает. Это происходит потому, что сервер Node.js — это длительно работающий процесс. Он завершает работу только в случае ошибки, которая приводит к его сбою и завершению, или если мы останавливаем процесс Node.js, запускающий сервер.
В отдельном окне терминала мы будем взаимодействовать с сервером с помощью cURL, утилиты командной строки для передачи данных по сети. Введите команду, чтобы выполнить HTTP-запрос GET к нашему работающему серверу:
curl http://localhost:8000Когда мы нажмём ENTER, наш терминал покажет следующий результат:
My first server!Мы теперь настроили сервер и получили первый ответ от сервера.
Давайте разберём, что произошло, когда мы тестировали наш сервер. С помощью cURL мы отправили запрос GET на сервер по адресу http://localhost:8000. Наш сервер на Node.js прослушивал соединения с этого адреса. Сервер передал этот запрос функции requestListener(). Функция вернула текстовые данные со статус-кодом 200. Затем сервер отправил этот ответ обратно в cURL, который отобразил сообщение в нашем терминале.
В производственной среде обычно не привязываются к localhost — вместо этого используют 0.0.0.0, чтобы принимать подключения с любого сетевого интерфейса. Однако для локальной разработки localhost безопаснее и предотвращает внешний доступ к вашему серверу разработки. Также, если порт 8000 уже используется, вы получите ошибку EADDRINUSE — просто выберите другой порт или остановите процесс, использующий этот порт, с помощью lsof -i :8000 (macOS/Linux).
Прежде чем мы продолжим, давайте остановим наш работающий сервер, нажав CTRL+C. Это прерывает выполнение сервера и возвращает нас к командной строке.
На большинстве веб-сайтов, которые мы посещаем, или в API, которые мы используем, ответы сервера редко бывают в виде простого текста. Обычно мы получаем HTML-страницы и данные в формате JSON. На следующем этапе мы узнаем, как возвращать HTTP-ответы в общих форматах данных, с которыми мы сталкиваемся в интернете.
Если вы хотите узнать о том, как делать HTTP-запросы из Node.js (на стороне клиента), ознакомьтесь с нашим руководством «Как создать HTTP-клиент с использованием Core HTTP в Node.js».
Шаг 2 — Возврат различных типов контента
Ответ, который мы возвращаем с веб-сервера, может иметь различные форматы. Ранее упоминались JSON и HTML, а также мы можем возвращать другие текстовые форматы, такие как XML и CSV. Наконец, веб-серверы могут возвращать нетекстовые данные, такие как PDF, архивированные файлы, аудио и видео.
В этой статье, помимо обычного текста, который мы только что вернули, вы узнаете, как возвращать следующие типы данных:
- JSON
- CSV
- HTML
Все три типа данных основаны на тексте и являются популярными форматами для передачи контента в интернете. Многие языки программирования и инструменты для серверной разработки поддерживают возврат этих различных типов данных. В контексте Node.js нам нужно сделать две вещи:
- Установите заголовок
Content-Typeв наших HTTP-ответах с соответствующим значением. - Убедитесь, что
res.end()получает данные в правильном формате.
Давайте посмотрим это на практике с несколькими примерами. Код, который мы будем писать в этом разделе и в последующих, во многом похож на код, который мы писали ранее. Большинство изменений находятся внутри функции requestListener(). Давайте создадим файлы с этим «шаблонным кодом», чтобы будущие разделы было легче понимать.
Создайте новый файл с именем html.js. Этот файл будет использоваться позже для возврата HTML-текста в HTTP-ответе. Здесь мы разместим код шаблона и скопируем его на другие серверы, которые возвращают различные типы.
В терминале введите следующее:
touch html.jsТеперь откройте этот файл в текстовом редакторе:
nano html.jsДавайте скопируем «шаблонный код.» Введите это в nano:
const http = require("http");
const host = 'localhost';
const port = 8000;
const requestListener = function (req, res) {};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});Сохраните и выйдите из html.js с помощью CTRL+X, затем вернитесь к терминалу.
Теперь давайте скопируем этот файл в два новых файла. Первый файл будет использоваться для возврата данных CSV в HTTP-ответе:
cp html.js csv.jsВторой файл вернёт JSON-ответ на сервере:
cp html.js json.jsОставшиеся файлы будут для последующих упражнений:
cp html.js htmlFile.js
cp html.js routes.jsТеперь мы готовы продолжить наши упражнения. Давайте начнем с возвращения JSON.
Обработка JSON
JavaScript Object Notation, обычно называемый JSON, является текстовым форматом обмена данными. Как следует из названия, он основан на объектах JavaScript, но является независимым от языка, что означает, что его могут использовать любые языки программирования, которые умеют разбирать его синтаксис.
JSON часто используется API для приема и возврата данных. Его популярность объясняется меньшим размером передаваемых данных по сравнению с предыдущими стандартами обмена данными, такими как XML, а также наличием инструментов, которые позволяют программам разбирать данные без чрезмерных усилий. Если вы хотите узнать больше о JSON, вы можете прочитать наше руководство по работе с JSON в JavaScript.
Откройте файл json.js с помощью nano:
nano json.jsПри возврате JSON необходимо установить заголовок Content-Type в application/json. Без этого браузеры и API-клиенты будут обрабатывать ответ как обычный текст, из-за чего fetch().json() не сработает. Устанавливайте заголовки до вызова writeHead() — после отправки заголовков изменить их уже невозможно.
Распространённая ошибка: Вызов res.writeHead(200) перед res.setHeader() блокирует заголовки. Если пытаться установить заголовки после writeHead(), они будут проигнорированы. Всегда сначала вызывайте setHeader(), а затем writeHead().
Измените функцию requestListener():
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(`{"message": "This is a JSON response"}`);
};
...Код статуса 200 указывает на успешное выполнение. Тело ответа содержит корректный JSON.
Почему использовать JSON.stringify(), а не ручное создание строк? Ручное написание JSON подвержено ошибкам. Если ваше сообщение содержит кавычки, переносы строк или специальные символы, их нужно экранировать вручную. JSON.stringify() делает это автоматически:
const data = {
message: "This is a JSON response",
timestamp: Date.now(),
note: 'He said "hello"' // Quotes automatically escaped
};
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(JSON.stringify(data));Остерегайтесь циклических ссылок: Если ваш объект ссылается сам на себя, JSON.stringify() выдаст ошибку: TypeError: Converting circular structure to JSON. Это часто случается с результатами запросов к базе данных. Всегда проверяйте структуру ваших данных.
Что произойдет, если попытаться отправить объект напрямую? Модуль http принимает только строки или буферы. Если вы попробуете res.end({ message: "test" }), Node.js выдаст: TypeError [ERR_INVALID_ARG_TYPE]: Первый аргумент должен быть строкой или буфером. Сначала необходимо преобразовать объекты в строки JSON.
Проверьте, что заголовок установлен правильно: Используйте флаг -i с curl, чтобы увидеть заголовки ответа:
curl -i http://localhost:8000Вы должны увидеть Content-Type: application/json в заголовках. Если вы видите text/plain или отсутствует Content-Type, это значит, что вы либо вызвали setHeader() после writeHead(), либо полностью забыли это сделать.
Типичный сценарий отладки: Если ваш фронтенд на JavaScript получает undefined при вызове fetch('/api').then(r => r.json()), проверьте заголовки с помощью curl -i. Content-Type должен быть точно application/json (для некоторых строгих парсеров учитывается регистр).
Сохраните и закройте json.js, нажав CTRL+X. Теперь давайте запустим сервер с помощью команды node:
node json.jsВ другом терминале давайте подключимся к серверу с помощью cURL:
curl http://localhost:8000Когда мы нажмем ENTER, мы увидим следующий результат:
{"message": "This is a JSON response"}Теперь мы успешно вернули JSON-ответ, так же, как во многих популярных API, с которыми мы создаем приложения. Обязательно завершите работу сервера с помощью CTRL+C, чтобы вернуться к стандартной командной строке. Далее давайте рассмотрим другой популярный формат передачи данных: CSV.
Обработка CSV
Формат файла Comma Separated Values (CSV) — это текстовый стандарт, который обычно используется для представления табличных данных. В большинстве случаев каждая строка отделяется переводом строки, а каждый элемент строки — запятой.
В нашем рабочем пространстве откройте файл csv.js в текстовом редакторе:
nano csv.jsДавайте добавим следующие строки в нашу функцию requestListener():
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
};
...На этот раз наш Content-Type указывает на то, что возвращается CSV-файл, так как его значение — text/csv. Второй заголовок, который мы добавляем, — это Content-Disposition. Этот заголовок сообщает браузеру, как отображать данные, особенно в самом браузере или как отдельный файл.
Когда мы возвращаем ответы в формате CSV, большинство современных браузеров автоматически загружают файл, даже если заголовок Content-Disposition не установлен. Однако при возвращении CSV-файла мы всё же должны добавить этот заголовок, так как он позволяет задать имя CSV-файла. В этом случае мы сообщаем браузеру, что этот CSV-файл является вложением и должен быть загружен. Затем мы указываем браузеру, что имя файла — oceanpals.csv.
Давайте запишем данные CSV в HTTP-ответ:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
res.writeHead(200);
res.end(`id,name,email\n1,Sammy Shark,shark@ocean.com`);
};
...Верните статус 200/OK с данными CSV. Столбцы разделяются запятыми, а строки — символом новой строки (\n). У нас есть две строки: строка заголовка и одна строка с данными.
Особенности форматирования CSV: Простой пример выше работает, но для генерации CSV в продакшне требуется экранирование. Если ваши данные содержат запятые (например, "Smith, John") или кавычки, это нарушит формат CSV. Правильное экранирование CSV заключается в обрамлении значений кавычками и удвоении внутренних кавычек: "Smith, John" превращается в "Smith, ""John""" в CSV. Для продакшен-использования используйте библиотеку вроде csv-stringify или корректно экранируйте значения, чтобы избежать ошибок парсинга.
Что происходит без Content-Disposition? Большинство браузеров скачивают файл, если установлен Content-Type: text/csv, но имя файла может быть либо путём из URL, либо чем-то общим. Заголовок attachment;filename=oceanpals.csv гарантирует, что браузер сохранит файл с конкретным именем. Без attachment некоторые браузеры пытаются показать CSV прямо в окне, что часто выглядит неаккуратно.
Мы протестируем этот сервер в браузере. Сохраните csv.js и выйдите из редактора с помощью CTRL+X.
Запустите сервер с помощью команды Node.js:
node csv.jsВ другом терминале давайте подключимся к серверу с помощью cURL:
curl http://localhost:8000Консоль покажет следующее:
id,name,email
1,Sammy Shark,shark@ocean.comЕсли мы перейдем по адресу http://localhost:8000 в нашем браузере, будет загружен CSV-файл. Его имя будет oceanpals.csv.
Выйдите из запущенного сервера с помощью CTRL+C, чтобы вернуться к обычной командной строке.
После того как мы рассмотрели возврат данных в формате JSON и CSV, мы охватили два популярных случая для API. Давайте перейдем к тому, как мы возвращаем данные для сайтов, которые люди просматривают в браузере.
Обслуживание HTML
HTML (HyperText Markup Language) — это самый распространенный формат, используемый, когда мы хотим, чтобы пользователи взаимодействовали с нашим сервером через веб-браузер. Он был создан для структурирования веб-контента. Веб-браузеры предназначены для отображения HTML-контента, а также любых стилей, которые мы добавляем с помощью CSS, другой фронтенд-технологии, позволяющей изменять внешний вид наших сайтов.
Давайте снова откроем html.js в нашем текстовом редакторе:
nano html.jsИзмените функцию requestListener(), чтобы она возвращала соответствующий заголовок Content-Type для HTML-ответа:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/html");
};
...Теперь вернём HTML-контент пользователю. Добавьте выделенные строки в html.js, чтобы это выглядело так:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(`<html><body><h1>This is HTML</h1></body></html>`);
};
...Сначала мы добавляем код состояния HTTP. Затем мы вызываем response.end() с аргументом-строкой, содержащей допустимый HTML. Когда мы откроем наш сервер в браузере, мы увидим HTML-страницу с одним заголовком, содержащим This is HTML.
Давайте сохраним и выйдем, нажав CTRL+X. Теперь запустим сервер с помощью команды node:
node html.jsМы увидим Server is running on http://localhost:8000, когда наша программа запустится.
Теперь откройте браузер и перейдите по адресу http://localhost:8000. Наша страница будет выглядеть так:
Давайте остановим работающий сервер с помощью CTRL+C и вернемся к стандартной командной строке.
Обычно HTML пишут в отдельном файле, отдельно от серверного кода, такого как наши программы на Node.js. Далее давайте посмотрим, как мы можем возвращать HTML-ответы из файлов.
Шаг 3 — Обслуживание HTML-страницы из файла
Мы можем отдавать HTML как строки в Node.js пользователю, но предпочтительнее загружать HTML-файлы и отдавать их содержимое. Таким образом, по мере роста HTML-файла нам не придется поддерживать длинные строки в коде Node.js, что делает его более лаконичным и позволяет работать с каждой частью нашего сайта отдельно. Такая «разделённость ответственности» распространена во многих веб-разработках, поэтому полезно знать, как загружать HTML-файлы для поддержки этого в Node.js.
Для обслуживания HTML-файлов мы загружаем HTML-файл с помощью модуля fs и используем его данные при формировании HTTP-ответа.
Сначала мы создадим HTML-файл, который веб-сервер будет возвращать. Создайте новый HTML-файл:
touch index.htmlТеперь откройте index.html в текстовом редакторе:
nano index.htmlНаша веб-страница будет минимальной. У неё будет оранжевый фон, а в центре будет отображаться текст с приветствием. Добавьте этот код в файл:
<!DOCTYPE html>
<head>
<title>My Website</title>
<style>
*,
html {
margin: 0;
padding: 0;
border: 0;
}
html {
width: 100%;
height: 100%;
}
body {
width: 100%;
height: 100%;
position: relative;
background-color: rgb(236, 152, 42);
}
.center {
width: 100%;
height: 50%;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-family: "Trebuchet MS", Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 144px;
}
p {
font-size: 64px;
}
</style>
</head>
<body>
<div class="center">
<h1>Hello Again!</h1>
<p>This is served from a file</p>
</div>
</body>
</html>На этой единственной веб-странице отображаются две строки текста: Hello Again! и This is served from a file. Строки расположены в центре страницы, одна над другой. Первая строка текста отображается как заголовок, то есть она будет крупной. Вторая строка текста будет немного меньше. Весь текст будет белого цвета, а фон веб-страницы оранжевый.
Хотя это не является темой данной статьи или серии, если вы хотите узнать больше о HTML, CSS и других технологиях фронтенда, вы можете ознакомиться с руководством Mozilla «Начало работы в сети».
Это всё, что нам нужно для HTML, так что сохраните файл и выйдите с помощью CTRL+X. Теперь мы можем перейти к коду сервера.
Для этого упражнения мы будем работать с htmlFile.js. Откройте его в текстовом редакторе:
nano htmlFile.jsТак как нам нужно прочитать файл, давайте начнем с импорта модуля fs:
const http = require("http");
const fs = require('fs').promises;
...Этот модуль содержит функцию readFile(), которую мы будем использовать для загрузки HTML-файла на месте. Мы импортируем вариант с промисами в соответствии с современными лучшими практиками JavaScript. Мы используем промисы, так как это синтаксически более лаконично, чем колбэки, которые пришлось бы использовать, если бы мы присвоили fs просто через require('fs'). Чтобы узнать больше о лучших практиках асинхронного программирования, вы можете прочитать наше руководство «Как писать асинхронный код в Node.js».
Мы хотим, чтобы наш HTML-файл читался, когда пользователь обращается к нашей системе. Давайте начнем с изменения requestListener(), чтобы читать файл:
...
const requestListener = function (req, res) {
fs.readFile(__dirname + "/index.html")
};
...Мы используем метод fs.readFile() для загрузки файла. Его аргумент имеет вид __dirname + "/index.html". Специальная переменная __dirname содержит абсолютный путь к месту, где выполняется код Node.js. Затем мы добавляем /index.html, чтобы загрузить HTML-файл, который мы создали ранее.
Теперь давайте вернём HTML-страницу после её загрузки:
...
const requestListener = function (req, res) {
fs.readFile(__dirname + "/index.html")
.then(contents => {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(contents);
})
};
...Если промис fs.readFile() выполнится успешно, он вернёт свои данные. Мы используем метод then(), чтобы обработать этот случай. Параметр contents содержит данные HTML-файла.
Сначала мы устанавливаем заголовок Content-Type в значение text/html, чтобы сообщить клиенту, что мы возвращаем HTML-данные. Затем мы записываем код состояния, чтобы указать, что запрос был успешным. Наконец, мы отправляем клиенту загруженную HTML-страницу с данными из переменной contents.
Метод fs.readFile() иногда может давать сбой, поэтому мы должны обрабатывать этот случай при возникновении ошибки. Добавьте это в функцию requestListener():
...
const requestListener = function (req, res) {
fs.readFile(__dirname + "/index.html")
.then(contents => {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(contents);
})
.catch(err => {
res.writeHead(500);
res.end('Internal Server Error');
return;
});
};
...Сохраните файл и выйдите из nano с помощью CTRL+X.
Когда обещание сталкивается с ошибкой, оно отклоняется. Мы обрабатываем такой случай с помощью метода catch(). Он устанавливает код статуса 500, что означает, что произошла внутренняя ошибка, и отправляет пользователю удобное сообщение об ошибке. В рабочей среде никогда не следует раскрывать пользователю подробности ошибки, так как это может привести к утечке конфиденциальной информации.
Запустите наш сервер с помощью команды node:
node htmlFile.jsВ веб-браузере перейдите по адресу http://localhost:8000. Вы увидите эту страницу:
Теперь вы вернули HTML-страницу с сервера пользователю. Вы можете завершить работу запущенного сервера, нажав CTRL+C. Когда вы это сделаете, вы увидите возвращение командной строки терминала.
При написании кода такого рода для продакшена, вы, возможно, не захотите загружать HTML-страницу каждый раз при получении HTTP-запроса. Хотя эта HTML-страница примерно 800 байт, более сложные сайты могут занимать мегабайты. Большие файлы могут загружаться долго. Если ваш сайт ожидает большой трафик, лучше загружать HTML-файлы при запуске и сохранять их содержимое. После их загрузки вы можете настроить сервер и начать принимать запросы по адресу.
Чтобы продемонстрировать этот метод, давайте посмотрим, как мы можем переделать наш сервер, чтобы он стал более эффективным и масштабируемым.
Эффективная подача HTML
Вместо того чтобы загружать HTML при каждом запросе, на этом этапе мы загрузим его один раз в начале. Запрос будет возвращать данные, которые мы загрузили при запуске.
В терминале повторно откройте скрипт Node.js с помощью текстового редактора:
nano htmlFile.jsДавайте начнем с добавления новой переменной перед созданием функции requestListener():
...
let indexFile;
const requestListener = function (req, res) {
...Когда мы запустим эту программу, эта переменная будет содержать содержимое HTML-файла.
Теперь давайте перенастроим функцию requestListener(). Вместо загрузки файла она будет возвращать содержимое indexFile:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(indexFile);
};
...Далее мы перенесем логику чтения файла из функции requestListener() в запуск нашего сервера. Внесите следующие изменения при создании сервера:
...
const server = http.createServer(requestListener);
fs.readFile(__dirname + "/index.html")
.then(contents => {
indexFile = contents;
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});
})
.catch(err => {
console.error(`Could not read index.html file: ${err}`);
process.exit(1);
});Сохраните файл и выйдите из nano с помощью CTRL+X.
Код, который читает файл, похож на то, что мы писали в нашей первой попытке. Однако теперь, когда мы успешно читаем файл, мы сохраняем его содержимое в глобальную переменную indexFile. Затем мы запускаем сервер с помощью метода listen(). Главное здесь то, что файл загружается до запуска сервера. Таким образом, функция requestListener() гарантированно вернёт HTML-страницу, так как indexFile больше не является пустой переменной.
Наш обработчик ошибок также был изменён. Если файл не удаётся загрузить, мы перехватываем ошибку и выводим её в консоль. Затем мы выходим из программы Node.js с помощью функции exit(), не запуская сервер. Так мы можем увидеть, почему чтение файла не удалось, устранить проблему и затем снова запустить сервер.
Мы теперь создали разные веб-серверы, которые возвращают пользователю различные типы данных. Пока что мы не использовали данные запроса для определения того, что должно быть возвращено. Нам нужно будет использовать данные запроса при настройке разных маршрутов или путей на сервере Node.js, поэтому далее давайте посмотрим, как они работают вместе.
Шаг 4 — Управление маршрутами с использованием объекта HTTP-запроса
Большинство веб-сайтов, которые мы посещаем, или API, которые мы используем, обычно имеют более одного конечного пункта, чтобы мы могли получить доступ к различным ресурсам. Хорошим примером может служить система управления книгами, которая может использоваться в библиотеке. Она должна не только управлять данными о книгах, но и управлять информацией об авторах для удобства каталогизации и поиска.
Хотя данные о книгах и авторах связаны между собой, это два разных объекта. В таких случаях разработчики программного обеспечения обычно создают код для каждого объекта на разных эндпоинтах, чтобы показать пользователю API, с каким типом данных он взаимодействует.
Давайте создадим новый сервер для небольшой библиотеки, который будет возвращать два разных типа данных. Если пользователь перейдёт по адресу нашего сервера /books, он получит список книг в формате JSON. Если он перейдёт по адресу /authors, он получит список информации об авторах в формате JSON.
Пока сервер возвращает одинаковый ответ на каждый запрос. Чтобы поддерживать несколько ресурсов, храните данные в переменных и используйте условную маршрутизацию. Например:
const books = JSON.stringify([
{ title: "The Alchemist", author: "Paulo Coelho", year: 1988 },
{ title: "The Prophet", author: "Kahlil Gibran", year: 1923 }
]);
const authors = JSON.stringify([
{ name: "Paulo Coelho", countryOfBirth: "Brazil", yearOfBirth: 1947 },
{ name: "Kahlil Gibran", countryOfBirth: "Lebanon", yearOfBirth: 1883 }
]);
const requestListener = function (req, res) {
res.setHeader("Content-Type", "application/json");
switch (req.url) {
case "/books":
res.writeHead(200);
res.end(books);
break;
case "/authors":
res.writeHead(200);
res.end(authors);
break;
default:
res.writeHead(404);
res.end(JSON.stringify({error:"Resource not found"}));
}
}Тестируйте с помощью curl http://localhost:8000/books или /authors, чтобы увидеть соответствующие данные. Любой другой URL возвращает ошибку 404.
Проблема с простым сопоставлением строк: req.url — это обычная строка, которая включает всё, что идёт после домена — пути, параметры запроса и фрагменты. Если кто-то посещает /books?id=123, ваш case "/books" не сработает, потому что req.url равно /books?id=123, а не /books. Параметры запроса нарушают маршрутизацию: /books?sort=name не совпадёт с /books. URL-кодированные символы могут нарушать маршрутизацию: /books%20reviews (пробел закодирован как %20) не совпадёт с /books reviews.
Используйте API URL вместо этого: API URL автоматически разделяет путь и параметры запроса, декодирует URL-кодированные символы и правильно обрабатывает крайние случаи.
const host = req.headers.host || 'localhost:8000';
const url = new URL(req.url, `http://${host}`);
switch (url.pathname) {
case '/books':
// Validate HTTP method - only GET allowed for this endpoint
if (req.method !== 'GET') {
res.writeHead(405, { 'Allow': 'GET' });
return res.end(JSON.stringify({ error: 'Method not allowed' }));
}
res.writeHead(200);
return res.end(books);
case '/authors':
if (req.method !== 'GET') {
res.writeHead(405, { 'Allow': 'GET' });
return res.end(JSON.stringify({ error: 'Method not allowed' }));
}
res.writeHead(200);
return res.end(authors);
default:
res.writeHead(404);
return res.end(JSON.stringify({ error: 'Resource not found' }));
}Зачем проверять HTTP-метод? Один и тот же путь может поддерживать разные HTTP-методы (GET для получения данных, POST для создания). Без этой проверки POST-запрос к /books вернул бы список книг вместо создания новой, что нарушает принципы REST API. Код состояния 405 Method Not Allowed с заголовком Allow сообщает клиентам, какие методы допустимы для этого эндпоинта.
Обработка строки запроса: API URL автоматически разделяет путь (/books) и параметры запроса (?id=123), поэтому /books?id=123 корректно направляется к эндпоинту книг, и при этом вы всё ещё можете получить доступ к url.searchParams.get('id'), если это необходимо.
Протестируйте различные сценарии:
curl http://localhost:8000/books
curl http://localhost:8000/authors
curl http://localhost:8000/books?id=123 # Query string doesn't break routing
curl -X POST http://localhost:8000/books # Returns 405 Method Not Allowed
curl http://localhost:8000/nonexistent # Returns 404Шаг 5 — Обработка POST-запросов и разбор тела запроса
До сих пор мы работали только с GET-запросами. Большинство API должны принимать данные от клиентов, что обычно происходит через POST-запросы. Модуль http требует ручного разбора тел запросов, которые приходят в виде потоков.
Ключевое понимание: В отличие от GET-запросов, где данные передаются через URL, тело POST-запроса приходит потоком. Это означает, что данные поступают не сразу, а частями. Если пытаться разбирать JSON до того, как все части придут, данные будут неполными или поврежденными. Сначала необходимо собрать все части, а затем разбирать JSON.
Вот что происходит, если пытаться анализировать слишком рано:
// WRONG - Body isn't complete yet
req.on('data', chunk => {
const data = JSON.parse(chunk); // Will fail - chunk is only part of the body
});
// CORRECT - Collect all chunks first
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
const data = JSON.parse(body); // Now body is complete
});Создайте сервер, который принимает POST-запросы с JSON-данными. Создайте новый файл:
touch post.js
cp routes.js post.jsОткройте post.js:
nano post.jsВот готовый к производству парсер JSON, который учитывает вопросы безопасности:
const MAX_BODY = 1 * 1024 * 1024; // 1MB limit prevents memory exhaustion
function parseJSONBody(req, res) {
return new Promise((resolve, reject) => {
let body = '';
let received = 0;
// Validate Content-Type before processing
const contentType = req.headers['content-type'] || '';
if (!contentType.startsWith('application/json')) {
res.writeHead(415, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unsupported Content-Type' }));
return reject(new Error('Unsupported Content-Type'));
}
// Collect data chunks
req.on('data', chunk => {
received += chunk.length;
// Prevent memory exhaustion from huge payloads
if (received > MAX_BODY) {
res.writeHead(413, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Payload too large' }));
req.destroy(); // Stop receiving more data
return reject(new Error('Payload too large'));
}
body += chunk.toString(); // Convert Buffer to string
});
// All chunks received, now parse
req.on('end', () => {
try {
if (!body) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Empty request body' }));
return reject(new Error('Empty request body'));
}
const data = JSON.parse(body);
// Validate it's an object (not a string, number, etc.)
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON object' }));
return reject(new Error('Invalid JSON object'));
}
resolve(data);
} catch (parseError) {
// JSON.parse() throws for malformed JSON
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Malformed JSON' }));
console.error('JSON parse error:', parseError.message);
}
});
// Handle network errors
req.on('error', (error) => {
console.error('Request error:', error.message);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Request error' }));
});
});
}Зачем проверять Content-Type? Если клиент отправляет данные формы с Content-Type: application/x-www-form-urlencoded, попытка разобрать их как JSON завершится неудачей. Код состояния 415 Unsupported Media Type сообщает клиенту, что их Content-Type неприемлем.
Зачем ограничивать размер тела? Без ограничения злоумышленник может отправить гигабайты данных, что приведет к исчерпанию памяти сервера. Ответ 413 Payload Too Large и req.destroy() немедленно прекращают соединение, когда предел превышен.
Зачем проверять результат парсинга? JSON.parse('"hello"') выполняется успешно, но возвращает строку, а не объект. Ваш код может ожидать, что data.property будет работать, но строки не имеют свойств. Проверка гарантирует, что вы получите настоящий объект.
Используйте эту функцию в вашем обработчике POST:
const requestListener = async function (req, res) {
if (req.method === 'POST' && req.url === '/books') {
const bookData = await parseJSONBody(req, res);
// Now use bookData safely
// ...
}
};Шаг 6 — Обслуживание статических файлов
Многие веб-приложения нуждаются в предоставлении статических ресурсов, таких как изображения, CSS-файлы, JavaScript-файлы или другие ресурсы. Хотя модуль http не имеет встроенной функции обслуживания статических файлов, как express.static в Express, вы можете реализовать это с помощью модулей fs и path.
Критическая проблема безопасности: Если вы наивно используете path.join(__dirname, 'public', req.url) без проверки, злоумышленник может получить доступ к любому файлу на вашем сервере с помощью атак обхода каталогов. Запрос вроде GET /../../../etc/passwd может раскрыть системные файлы. Проверка с помощью path.resolve() гарантирует, что разрешённый путь никогда не выйдет за пределы каталога public.
Вот что может пойти не так без правильной проверки пути:
// VULNERABLE - Allows directory traversal
const filePath = path.join(__dirname, 'public', req.url);
// Request: /../../../etc/passwd
// Resolves to: /etc/passwd (outside public directory!)
// SECURE - Validates path stays within public directory
const safePath = path.resolve(publicDir, '.' + reqPath);
if (!safePath.startsWith(publicDir)) {
res.writeHead(403);
res.end('Forbidden');
return;
}
// Request: /../../../etc/passwd
// safePath: /home/user/app/public/../../../etc/passwd
// Resolved: /etc/passwd
// Check fails because /etc/passwd doesn't start with /home/user/app/publicСоздайте защищённый сервер статических файлов:
touch static.jsОткройте static.js:
nano static.jsОбслуживайте статические файлы эффективно и безопасно, используя потоковую передачу с помощью fs.createReadStream() и безопасное разрешение путей:
const http = require("http");
const fs = require("fs");
const path = require("path");
const host = 'localhost';
const port = 8000;
const mimeTypes = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.pdf': 'application/pdf'
};
const publicDir = path.resolve(__dirname, 'public');
const requestListener = (req, res) => {
// Normalize the request path
let reqPath = req.url === '/' ? '/index.html' : req.url;
// Strip query string - ?id=123 shouldn't affect file path
reqPath = reqPath.split('?')[0];
// Resolve path and check it stays within publicDir
// The '.' prefix prevents absolute paths from being resolved incorrectly
const safePath = path.resolve(publicDir, '.' + reqPath);
// Critical security check: ensure resolved path is within publicDir
if (!safePath.startsWith(publicDir)) {
res.writeHead(403, { 'X-Content-Type-Options': 'nosniff' });
res.end('Forbidden');
return;
}
// Check if file exists and is actually a file (not a directory)
fs.stat(safePath, (err, stat) => {
if (err || !stat.isFile()) {
res.writeHead(404, { 'X-Content-Type-Options': 'nosniff' });
res.end('File not found');
return;
}
// Determine MIME type from file extension
const ext = path.extname(safePath).toLowerCase();
const contentType = mimeTypes[ext] || 'application/octet-stream';
res.setHeader("Content-Type", contentType);
res.setHeader("X-Content-Type-Options", "nosniff"); // Prevents MIME sniffing attacks
res.writeHead(200);
// Stream file instead of loading into memory
// Critical for large files - won't consume all memory
const stream = fs.createReadStream(safePath);
stream.pipe(res);
stream.on('error', () => {
// File might be deleted between stat() and createReadStream()
res.writeHead(500, { 'X-Content-Type-Options': 'nosniff' });
res.end('Server error');
});
});
};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
console.log(`Serving static files from: ${publicDir}`);
});Создайте каталог public и добавьте тестовый файл:
mkdir public
echo "<h1>Static File Served!</h1>" > public/index.htmlЗапустите сервер:
node static.jsОткройте в браузере http://localhost:8000, чтобы посмотреть обслуживаемый статический файл.
Почему использовать потоковую обработку вместо fs.readFile()? Для видеофайла размером 100 МБ, fs.readFile() загружает весь файл в память перед отправкой. С fs.createReadStream() данные передаются напрямую с диска в сеть маленькими частями, используя минимальное количество памяти независимо от размера файла.
Зачем проверять stat.isFile()? Если кто-то запрашивает путь к директории, например /images/, fs.stat() сработает, но isFile() вернёт false. Без этой проверки вы попытаетесь транслировать директорию, что приведёт к ошибке. Эта проверка гарантирует, что вы обслуживаете только настоящие файлы.
Проверьте защиту от обхода пути:
curl http://localhost:8000/../../../etc/passwd
# Should return 403 Forbidden, not the system fileШаг 7 — Обработка ошибок и ведение логов
Надёжная обработка ошибок имеет решающее значение для производственных серверов. Модуль http не обеспечивает автоматическую обработку ошибок, поэтому её нужно реализовать самостоятельно.
Правильная обработка ошибок включает в себя перехват асинхронных ошибок, их корректное журналирование, отправку пользователям удобных для понимания сообщений об ошибках и предотвращение сбоев сервера из-за необработанных ошибок. Давайте улучшим наш сервер, добавив комплексную обработку ошибок и журналирование:
const http = require("http");
const fs = require('fs').promises;
const host = 'localhost';
const port = 8000;
// Simple logging utility
const log = {
info: (msg) => console.log(`[INFO] ${new Date().toISOString()} - ${msg}`),
error: (msg, err) => console.error(`[ERROR] ${new Date().toISOString()} - ${msg}`, err),
warn: (msg) => console.warn(`[WARN] ${new Date().toISOString()} - ${msg}`)
};
const requestListener = async function (req, res) {
try {
log.info(`${req.method} ${req.url} - ${req.headers['user-agent'] || 'Unknown'}`);
if (req.url === '/') {
try {
const data = await fs.readFile(__dirname + '/index.html');
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(data);
} catch (error) {
log.error('Failed to read index.html', error);
res.writeHead(500);
res.end('<h1>Internal Server Error</h1><p>Unable to load page.</p>');
}
} else if (req.url === '/error') {
throw new Error('Simulated error for testing');
} else {
res.writeHead(404);
res.end('<h1>Not Found</h1><p>The requested resource was not found.</p>');
}
} catch (error) {
log.error('Unhandled error in request handler', error);
const errorMessage = process.env.NODE_ENV !== 'production'
? error.message
: 'An unexpected error occurred';
res.writeHead(500);
res.end(`<h1>Internal Server Error</h1><p>${errorMessage}</p>`);
}
};
const server = http.createServer(requestListener);
// Handle server-level errors
server.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
log.error(`Port ${port} is already in use`);
} else {
log.error('Server error', error);
}
process.exit(1);
});
// Handle client connection errors
server.on('clientError', (err, socket) => {
log.warn('Client error: ' + err.message);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(port, host, () => {
log.info(`Server is running on http://${host}:${port}`);
});Корректное завершение работы (SIGTERM/SIGINT)
При запуске под процесс-менеджерами, такими как PM2 или Docker, обрабатывайте сигналы завершения, чтобы корректно закрыть сервер:
const connections = new Set();
server.on('connection', socket => {
connections.add(socket);
socket.on('close', () => connections.delete(socket));
});
const shutdown = (signal) => {
log.warn(`${signal} received: shutting down gracefully...`);
server.close(() => {
log.info('Server closed.');
process.exit(0);
});
setTimeout(() => {
log.warn('Forcing socket close...');
connections.forEach(s => s.destroy());
process.exit(1);
}, 10000).unref();
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));Показаны основные методы обработки ошибок:
- Блоки try-catch вокруг асинхронных операций
- Обработчики ошибок на уровне сервера для проблем с подключением и портом
- Обработка ошибок клиента для некорректных запросов
- Ведение журналов с отметками времени и уровнями серьезности
- Ошибки с учётом окружения (не показывайте трассировки стека в продакшене)
- Элегантные ответы об ошибках с соответствующими кодами состояния HTTP
Лучшие практики: В продакшене рассмотрите возможность использования структурированных библиотек логирования, таких как Winston или Pino, и отправляйте критические ошибки в сервисы отслеживания ошибок, такие как Sentry или Rollbar. В этом примере используется простое логирование в консоль в образовательных целях.
Шаг 8 — Использование синтаксиса модулей ES (ESM)
Современный Node.js поддерживает модули ES (ESM) с использованием синтаксиса import/export вместо CommonJS require/module.exports. Чтобы использовать ESM, либо назначьте вашему файлу расширение .mjs, либо установите "type": "module" в вашем package.json.
Синтаксис ESM использует import вместо require и export вместо module.exports. Вот тот же базовый сервер, использующий синтаксис ES Modules:
Создайте server.mjs:
touch server.mjs
nano server.mjsimport http from 'http';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
// ES Modules don't have __dirname, so we recreate it
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const host = 'localhost';
const port = 8000;
const requestListener = async (req, res) => {
try {
if (req.url === '/') {
const data = await fs.readFile(path.join(__dirname, 'index.html'));
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(data);
} else {
res.writeHead(404);
res.end('Not found');
}
} catch (error) {
console.error('Error:', error);
res.writeHead(500);
res.end('Server error');
}
};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});Запусти это:
node server.mjsКлючевые различия: В ESM __dirname недоступен — его нужно получать из import.meta.url. Также пути к файлам в операциях с fs могут требовать явного разрешения. В остальном API модуля HTTP остаётся таким же.
В качестве альтернативы создайте package.json с "type": "module":
{
"type": "module"
}Затем используйте файлы .js с синтаксисом ESM:
import http from 'http';
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('ESM Server');
});
server.listen(8000);Производственные соображения
При переходе от разработки к эксплуатации необходимо учитывать несколько важных факторов:
Поддержка HTTPS
Модуль http обслуживает только HTTP. Для HTTPS используйте модуль https аналогичным образом:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('path/to/private-key.pem'),
cert: fs.readFileSync('path/to/certificate.pem')
};
const server = https.createServer(options, requestListener);Безопасность: Никогда не сохраняйте приватные ключи или сертификаты в системе контроля версий. Используйте переменные окружения или безопасные сервисы управления секретами в продакшене.
Настройка обратного прокси
В production-окружении серверы Node.js обычно работают за обратными прокси, такими как Nginx или Apache. Прокси выполняет завершение SSL, обслуживание статических файлов и балансировку нагрузки. Ваш сервер Node.js должен:
- Слушать только на
localhostили127.0.0.1 - Читать заголовки
X-Forwarded-Forдля получения IP-адресов клиентов - Доверяйте заголовкам прокси только от вашего прокси-сервера
Смотрите наше руководство по настройке Nginx в качестве обратного прокси на Ubuntu для подробностей установки.
Управление процессами
Для производства используйте менеджеры процессов, такие как PM2, для управления:
- Автоматический перезапуск при сбоях
- Развертывания без простоя
- Управление журналами
- Режим кластера для использования нескольких ядер процессора
Узнайте больше в нашем руководстве «Как настроить приложение Node.js для продакшена на Ubuntu».
Оптимизация производительности
Кэширование файлов: Загружайте статические файлы при запуске (как показано в Шаге 3), вместо того чтобы читать их с диска при каждом запросе. Это значительно снижает нагрузку на ввод-вывод.
Передавайте большие файлы потоками: Для файлов размером более 1 МБ используйте
fs.createReadStream()и передавайте их в ответ через pipe, вместо того чтобы загружать весь файл в память.Пул соединений: Для подключений к базе данных используйте пул соединений, чтобы избежать накладных расходов на создание новых подключений для каждого запроса.
Сжатие: Используйте модуль
zlibдля gzip-сжатия текстовых ответов, чтобы уменьшить трафик:const zlib = require('zlib'); const gzip = zlib.createGzip(); res.setHeader('Content-Encoding', 'gzip'); gzip.pipe(res); gzip.write(data); gzip.end();Режим кластера: Используйте модуль Node.js
cluster, чтобы создавать дочерние процессы, равные числу ядер CPU, максимизируя производительность для операций, нагружающих процессор.Тайм-ауты запросов: Установите разумные значения тайм-аутов, чтобы предотвратить использование ресурсов медленными клиентами:
server.setTimeout(30000); // 30 secondsМониторинг памяти: Отслеживайте использование памяти, чтобы вовремя обнаруживать утечки. В рабочей среде контролируйте размер кучи и внедряйте аккуратные перезапуски, если память растет без ограничений.
Лучшие практики кэширования и сжатия
Для дальнейшего улучшения производительности и уменьшения времени отклика применяйте кеширование и сжатие там, где это целесообразно.
Включите кэширование для статических файлов:
Добавьте заголовкиCache-ControlиETagв ответы с вашими статическими файлами. Например:const stat = fs.statSync(safePath); res.setHeader('Cache-Control', 'public, max-age=86400'); // cache for 1 day res.setHeader('ETag', `"${stat.size}-${stat.mtime.getTime()}"`);Браузеры и CDN будут использовать кэшированные копии до тех пор, пока не истечет срок действия ETag или кэша.
Используйте сжатие gzip или Brotli:
Текстовые ответы, такие как HTML, CSS, JS и JSON, сжимаются очень эффективно. Вы можете применить сжатие с помощью модуля Nodezlib:const zlib = require('zlib'); const stream = fs.createReadStream(safePath); res.setHeader('Content-Encoding', 'gzip'); stream.pipe(zlib.createGzip()).pipe(res);Это снижает использование пропускной способности и ускоряет доставку для клиентов с медленным соединением.
Избегайте чрезмерного кэширования динамических конечных точек:
Для API или часто обновляемых данных устанавливайте более короткие сроки хранения в кэше (например,max-age=60) или используйтеno-store, когда это необходимо.
Эти оптимизации дополняют потоковую передачу статических файлов и корректное завершение работы, помогая вашему серверу Node.js обеспечивать высокую производительность при сохранении безопасности и предсказуемости.
Используйте модуль Node.js http для минимальных API, пользовательской маршрутизации или изучения внутренностей HTTP.
Используйте Express/Fastify, когда вам нужны промежуточные обработчики, шаблонизация или быстрая разработка прототипов.
Устранение распространённых проблем
Порт уже используется (EADDRINUSE)
Ошибка: Ошибка: listen EADDRINUSE: адрес уже используется :::8000
Решение:
- Найдите процесс, использующий порт:
lsof -i :8000(macOS/Linux) илиnetstat -ano | findstr :8000(Windows) - Завершите процесс или используйте другой порт
- Добавьте обработку ошибок (как показано в Шаге 7), чтобы аккуратно их перехватывать
Тело запроса не получено
Проблема: тело POST-запроса пустое или не определено
Решение: Тела запросов являются потоками — необходимо собирать их по частям. Всегда используйте шаблон, показанный в Шаге 5:
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
// Now parse body
});Ошибки CORS в браузере
Проблема: Браузер блокирует запросы из-за политики CORS
Решение: Добавьте заголовки CORS в ваш ответ:
res.setHeader("Access-Control-Allow-Origin", "*"); // Or specific domain
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");Безопасность: Использование "*" для Access-Control-Allow-Origin разрешает любые источники. В продакшене указывайте ваш фронтенд-домен явно.
Статические файлы возвращают 404
Проблема: Статические файлы не найдены, хотя они существуют
Решение:
- Проверьте разрешение пути к файлу
- Проверьте, что
__dirnameуказывает на правильный каталог - Убедитесь, что права доступа к файлу позволяют его чтение
- Проверьте, что очистка пути не удаляет допустимые символы
Часто задаваемые вопросы
Что такое модуль HTTP в Node.js?
Модуль http является встроенным модулем Node.js для создания HTTP-серверов и клиентов. Он предоставляет низкоуровневые API для обработки HTTP-запросов и ответов без необходимости использования внешних зависимостей. Этот модуль является частью ядра Node.js, поэтому он доступен в каждой установке Node.js.
Как создать простой веб-сервер на Node.js?
Создайте сервер с помощью http.createServer() и функции-обработчика запросов:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World');
});
server.listen(8000);Это создает сервер, прослушивающий порт 8000, который отвечает на все запросы «Hello World».
В чем разница между модулем HTTP и Express.js?
Модуль http является базовой функциональностью Node.js, предоставляющей низкоуровневую обработку HTTP. Express.js — это фреймворк, построенный поверх http, который добавляет маршрутизацию, middleware, шаблонизацию и другие удобства. Модуль http дает полный контроль, но требует больше ручной настройки, тогда как Express.js предоставляет абстракции за счет дополнительных зависимостей.
Используйте модуль http, когда:
- Создание лёгких API или микросервисов
- Изучение основ HTTP
- Вы хотите отсутствие зависимостей
- Полный контроль над обработкой запросов и ответов
Используйте Express.js, когда:
- Создание сложных приложений с множеством маршрутов
- Вам нужна экосистема промежуточного ПО
- Быстрое прототипирование является приоритетом
- Вам нужны общепринятые шаблоны
Для всестороннего введения в Express.js и того, как он упрощает веб-разработку, смотрите наше руководство по основам Node.js Express.
Могу ли я создать REST API с использованием Node.js без Express?
Да, абсолютно. Модуль http вполне подходит для создания REST API. Вам потребуется вручную обрабатывать:
- Разбор маршрута (с использованием
req.urlиreq.method) - Разбор тела запроса (сбор фрагментов потока)
- Форматирование ответа
- Обработка ошибок
Для простых API это часто предпочтительнее, чем добавление зависимостей от фреймворка. Смотрите шаги 4 и 5 в этом руководстве для примеров маршрутизации и обработки POST-запросов.
Как обрабатывать разные маршруты с использованием модуля HTTP в Node.js?
Разберите req.url и используйте условную логику или оператор switch:
const requestListener = (req, res) => {
switch (req.url) {
case '/':
res.writeHead(200);
res.end('Home page');
break;
case '/about':
res.writeHead(200);
res.end('About page');
break;
default:
res.writeHead(404);
res.end('Not found');
}
};Для более сложной маршрутизации вы можете разбирать пути URL и строки запросов с помощью модуля url, а также обрабатывать различные HTTP-методы.
Как отправлять HTML или JSON ответы с помощью модуля HTTP?
Установите заголовок Content-Type соответствующим образом и отправьте данные:
HTML:
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end('<h1>Hello</h1>');JSON:
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(JSON.stringify({ message: 'Hello' }));Заголовок Content-Type сообщает клиенту, как интерпретировать данные ответа.
Какой порт мне использовать для локального сервера Node.js?
Обычные порты для разработки:
- 8000 или 8080: Общая разработка HTTP
- 3000: Популярно среди фреймворков, таких как Express
- 5000: Также распространено
Любой порт выше 1024 подходит для пользователей без прав администратора. Порты ниже 1024 требуют прав администратора. Проверьте доступность с помощью lsof -i :PORT (macOS/Linux) перед использованием.
Как обрабатывать ошибки 404 на HTTP-сервере Node.js?
Добавьте случай по умолчанию в вашу логику маршрутизации:
const requestListener = (req, res) => {
if (req.url === '/') {
res.writeHead(200);
res.end('Home');
} else {
res.writeHead(404);
res.end('Not Found');
}
};Всегда устанавливайте код состояния 404 для несуществующих маршрутов, чтобы соответствовать стандартам HTTP.
Как мне обслуживать статические файлы без Express?
Используйте модуль fs для чтения файлов и модуль path для безопасного разрешения путей:
const fs = require('fs').promises;
const path = require('path');
const requestListener = async (req, res) => {
const filePath = path.join(__dirname, 'public', req.url);
try {
const data = await fs.readFile(filePath);
res.writeHead(200);
res.end(data);
} catch (error) {
res.writeHead(404);
res.end('File not found');
}
};Всегда проверяйте пути, чтобы предотвратить атаки обхода директорий (см. шаг 6 для безопасной реализации).
Могу ли я использовать ES-модули (import) с модулем HTTP в Node.js?
Да, Node.js поддерживает ES-модули. Используйте один из вариантов:
- Файл с расширением
.mjs:
import http from 'http';
// ... rest of code- Установите
"type": "module"вpackage.json, затем используйте файлы.js:
{
"type": "module"
}Обратите внимание, что у ES Modules нет __dirname — получите его из import.meta.url, как показано в Шаге 8.
Как разобрать тело POST-запроса с помощью модуля HTTP?
Тела POST-запросов поступают как потоки. Соберите части, затем разберите:
const parseBody = (req) => {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
try {
resolve(JSON.parse(body)); // For JSON
} catch (error) {
reject(error);
}
});
req.on('error', reject);
});
};Этот шаблон работает для JSON, данных формы или любого текстового типа содержимого.
Подходит ли модуль HTTP для использования в продакшене?
Да, модуль http готов к использованию в продакшене и используется многими приложениями Node.js. Однако учтите:
- Управление процессами: Используйте PM2 или аналогичные инструменты для автоматического перезапуска
- Обратный прокси: Работает за Nginx/Apache для SSL и балансировки нагрузки
- Обработка ошибок: Реализуйте комплексную обработку ошибок и ведение журналов
- Безопасность: Проверяйте вводимые данные, очищайте пути, устанавливайте ограничения на размер запросов
- Производительность: Кэшировать статические файлы, использовать кластеризацию для операций, нагружающих процессор
Многие производственные приложения используют модуль http напрямую, особенно микросервисы и легковесные API. Для получения подробных рекомендаций по развертыванию в производственной среде см. «Как настроить приложение Node.js для работы в производственной среде на Ubuntu 22.04».
Заключение
В этом руководстве вы создали производственные HTTP-серверы на Node.js с нуля. Вы начали с базового текстового ответа, затем перешли к обслуживанию нескольких типов контента (JSON, CSV и HTML). Вы реализовали маршрутизацию для обработки разных URL, разбирали тела POST-запросов, безопасно обслуживали статические файлы и добавили полное управление ошибками и ведение логов. Вы также изучили синтаксис как CommonJS, так и ES Modules, а также вопросы, связанные с развертыванием в производственной среде.
Теперь вы готовы создавать веб-серверы, которые справляются с различными задачами без зависимости от фреймворков. Обладая этими знаниями, вы можете:
- Создавайте лёгкие REST API для микросервисов
- Обслуживайте многостраничные сайты с правильной маршрутизацией
- Обрабатывать загрузку и скачивание файлов
- Реализовать собственную логику промежуточного программного обеспечения
- Принимайте обоснованные решения о том, когда фреймворки приносят пользу, а когда создают ненужную сложность
Следующие шаги:
- Углубитесь в основы Node.js: вернитесь к серии «Как программировать на Node.js» для полного охвата темы
- Изучите официальную документацию: документация по модулю HTTP Node.js содержит подробные справочники по API и примеры продвинутого использования
- Когда вам нужны возможности фреймворка: Рассмотрите основы Express.js для быстрой разработки с поддержкой middleware и шаблонов
Понимание этих основ HTTP делает вас более квалифицированным разработчиком на Node.js. Независимо от того, используете ли вы фреймворки или работаете напрямую с модулем http, теперь вы понимаете основные механизмы, которые обеспечивают работу всех веб-приложений на Node.js. Эти знания помогают вам устранять ошибки, оптимизировать производительность и разрабатывать системы, которые эффективно масштабируются.
Спасибо, что учитесь вместе с сообществом Linux-Console.net.

