Backend на Node.js: от нативного кода к Express

Курс охватывает создание серверов с использованием встроенного модуля HTTP [digitalocean.com](https://digitalocean.com/community/tutorials/how-to-create-a-web-server-in-node-js-with-the-http-module-ru), разработку REST API без сторонних библиотек [statuser.cloud](https://statuser.cloud/blog/rest-api-na-node-bez-express) и плавный переход к использованию фреймворка Express [developer.mozilla.org](https://developer.mozilla.org/ru/docs/Learn_web_development/Extensions/Server-side/Express_Nodejs/Introduction).

1. Основы Node.js: создание HTTP-сервера и работа с файловой системой

Основы Node.js: создание HTTP-сервера и работа с файловой системой

Добро пожаловать в курс «Backend на Node.js». Мы начинаем погружение в серверную разработку, минуя готовые фреймворки вроде Express, чтобы понять, как всё устроено «под капотом». В этой статье мы разберём фундамент любого веб-приложения: как принять запрос от пользователя, обработать его и отправить ответ, а также как работать с файлами на сервере.

Что такое Node.js и зачем он нужен?

Node.js — это не язык программирования и не фреймворк. Это среда выполнения (runtime environment) для JavaScript, построенная на движке V8 от Google Chrome. Она позволяет запускать JavaScript-код вне браузера: на сервере, локальном компьютере или IoT-устройствах.

Ключевая особенность Node.js — это асинхронная модель ввода-вывода, управляемая событиями. В отличие от традиционных многопоточных серверов (например, на базе Apache/PHP), где каждый новый запрос создает отдельный поток, потребляющий память, Node.js работает в одном потоке.

> Node.js позволяет разработчикам использовать JavaScript для создания серверного кода, хотя традиционно этот язык использовался в браузере для создания клиентского кода. > > DigitalOcean

Почему это важно?

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

Модуль HTTP: Сердце сервера

В Node.js встроен мощный модуль http, который позволяет создавать веб-серверы без установки сторонних зависимостей. Для начала работы его необходимо импортировать.

Создание простейшего сервера

Рассмотрим базовый пример создания сервера, который отвечает «Hello World» на любой запрос.

Этот код делает следующее:

  • Преобразует URL запроса в путь к файлу на диске.
  • Определяет расширение файла через path.extname.
  • Выбирает правильный заголовок Content-Type.
  • Читает файл и отправляет его содержимое.
  • Обрабатывает ошибку ENOENT (Error NO ENTry), которая означает, что файл не существует (404).
  • Такой подход лежит в основе работы многих статических серверов, хотя в продакшене для этих целей часто используют Nginx или специальные модули Express.

    > Пример. Вот так выглядит статический сервер на Node.js... var contentType = mimeTypes[extname] || "application/octet-stream"; > > MDN

    Итоги

    Мы разобрали основы создания веб-сервера на чистом Node.js. Эти знания необходимы для понимания того, как работают высокоуровневые фреймворки.

  • Модуль http позволяет создавать сервер, слушать порты и обрабатывать входящие запросы (req) и исходящие ответы (res).
  • Маршрутизация в чистом Node.js реализуется через условные конструкции (if/else) и проверку req.url и req.method.
  • Модуль fs используется для работы с файлами. Для серверов критически важно использовать асинхронные методы (fs.readFile), чтобы не блокировать поток выполнения.
  • MIME-типы обязательны для корректной работы браузера с различными типами файлов (HTML, CSS, JSON, изображения).
  • 2. Маршрутизация запросов и отдача статических файлов без фреймворков

    Маршрутизация запросов и отдача статических файлов без фреймворков

    В предыдущей статье мы создали простейший HTTP-сервер, который умеет отвечать «Hello World». Однако реальные приложения — это не просто один ответ на все случаи жизни. Это сложная система, которая должна отдавать разные данные в зависимости от того, куда зашел пользователь (URL) и как он это сделал (метод запроса). Кроме того, ни один современный веб-сайт не обходится без статики: стилей (CSS), скриптов (JS) и изображений.

    Сегодня мы научимся управлять потоками данных и построим надежный механизм отдачи файлов, учитывая производительность и безопасность, не прибегая к помощи Express.

    Логика маршрутизации (Routing)

    Маршрутизация — это механизм сопоставления URL-адреса и HTTP-метода с конкретной функцией, которая должна обработать этот запрос. В «голом» Node.js объект req (IncomingMessage) предоставляет нам два ключевых свойства: req.url и req.method.

    Эволюция от if/else к структуре данных

    Самый примитивный способ маршрутизации — это цепочка условий if/else. Это работает для 2–3 маршрутов, но становится нечитаемым при масштабировании.

    > Очевидно, что часть кода у нас дублируется. Давайте его упростим... Перепишите приведенный код через оператор switch-case. > > Трепачёв Дмитрий

    Более профессиональный подход в нативном Node.js — использование объекта-словаря (Lookup Object), где ключами являются маршруты, а значениями — функции-обработчики.

    Такая структура позволяет легко добавлять новые маршруты, не создавая «лапшу» из кода. Это разделяет конфигурацию маршрутов и логику запуска сервера.

    Проблема Query-параметров

    Свойство req.url содержит полную строку запроса, включая параметры после знака вопроса. Если клиент запросит /about?ref=google, наш роутер из примера выше вернет 404, так как ключа '/about?ref=google' в объекте нет.

    Для решения этой задачи в современном Node.js используется глобальный класс URL.

    Использование new URL() является стандартом, заменившим устаревший модуль url.parse(), который вы можете встретить в старых туториалах.

    Отдача статических файлов

    Создание статического сервера — задача более сложная, чем кажется. Нам нужно не просто прочитать файл, но и сделать это безопасно и эффективно.

    MIME-типы

    Браузер должен знать, какой тип данных он получает, чтобы правильно их обработать. Если вы отправите CSS-файл с заголовком text/plain, стили не применятся. За это отвечает заголовок Content-Type.

    > Давайте сделаем функцию, которая будет принимать путь к файлу и по расширению этого файла выдавать его mime тип. > > Трепачёв Дмитрий

    Обычно создается словарь соответствий:

    Безопасность: Path Traversal

    Одна из самых критичных уязвимостей при самостоятельной реализации статики — Path Traversal (обход каталогов). Злоумышленник может отправить запрос вида:

    GET /../../../../etc/passwd

    Если мы просто подставим этот URL в функцию чтения файла, сервер может отдать конфиденциальные системные файлы. Чтобы этого избежать, необходимо нормализовать путь и проверять, находится ли он внутри разрешенной публичной папки.

    Производительность: Streams vs Buffer

    В предыдущей статье мы использовали fs.readFile. Этот метод читает весь файл целиком в оперативную память (Buffer) перед отправкой. Для маленьких HTML-файлов это нормально. Но что, если пользователь запросит видеофайл размером 500 МБ?

    Рассчитаем потребление памяти по формуле:

    где — общее потребление памяти, — количество одновременных пользователей, — размер файла.

    Если 100 пользователей одновременно запросят файл размером 100 МБ:

    Серверу потребуется 10 ГБ оперативной памяти, что, скорее всего, приведет к падению процесса с ошибкой Out of Memory.

    Решение — Streams (Потоки). Потоки позволяют читать файл по кусочкам (chunks) и сразу отправлять их в сеть, не загружая весь файл в память. В Node.js объект res является записываемым потоком (Writable Stream).

    Правильная реализация отдачи файла:

    Использование pipe автоматически управляет скоростью передачи данных (backpressure), подстраиваясь под скорость клиента.

    Полный пример статического сервера

    Соберем все знания воедино. Мы создадим сервер, который безопасно отдает статику через потоки и обрабатывает ошибки.

    > Именно поэтому нужно знать как все работает изнутри, чтобы быть готовым сделать свой собственный сервер без каких-либо дополнительных зависимостей. > > MDN

    Разбор ключевых моментов кода

  • new URL(...): Гарантирует, что мы работаем только с путем файла, игнорируя GET-параметры (например, style.css?v=1.2).
  • path.normalize: Устраняет попытки выхода из директории через точки.
  • fs.createReadStream: Обеспечивает минимальное потребление памяти даже при высокой нагрузке.
  • Обработка ошибок потока: Событие error на потоке — единственный способ корректно отловить отсутствие файла при использовании стримов. try/catch здесь не сработает так, как ожидается, из-за асинхронности.
  • Итоги

    Мы реализовали функционал, который обычно скрыт «под капотом» фреймворков. Понимание этих принципов критически важно для написания высокопроизводительного кода.

  • Маршрутизация на чистом Node.js требует ручного парсинга req.url и проверки req.method. Использование словарей (объектов) вместо if/else делает код чище.
  • Класс URL — современный стандарт для работы с адресами, позволяющий легко отделять путь от параметров запроса.
  • Безопасность при работе с файлами требует обязательной нормализации путей для защиты от атак типа Path Traversal.
  • Потоки (Streams) — предпочтительный способ отдачи файлов. Они экономят память и повышают пропускную способность сервера по сравнению с полной загрузкой файла в буфер.