Fullstack-разработка: Node.js и React

Практический курс по созданию веб-приложений, объединяющий серверную логику на Node.js и пользовательские интерфейсы на React [[it-dev-journal.ru](https://it-dev-journal.ru/articles/node-js-i-react-kak-napisat-fulstek-prilozhenie-polnoe-rukovodstvo)]. Вы изучите архитектуру клиент-серверного взаимодействия, настройку окружения и реализацию обмена данными через API [[purpleschool.ru](https://purpleschool.ru/knowledge-base/article/express-react-js)].

1. Настройка окружения и архитектура приложения на Node.js и React

Настройка окружения и архитектура приложения на Node.js и React

Добро пожаловать в курс по Fullstack-разработке. В этой статье мы заложим фундамент вашего будущего приложения. Мы не просто установим программы, но и разберем, как именно взаимодействуют две ключевые технологии: Node.js на сервере и React на клиенте.

Архитектура Fullstack-приложения

Прежде чем писать код, важно понять, как устроена система. Fullstack-приложение на стеке MERN (MongoDB, Express, React, Node.js) или PERN (PostgreSQL) работает по клиент-серверной модели.

  • Клиент (Frontend): Это ваше React-приложение. Оно работает в браузере пользователя. Его задача — отрисовать интерфейс и реагировать на действия пользователя (клики, ввод текста).
  • Сервер (Backend): Это Node.js с фреймворком Express. Он работает на удаленном компьютере (или локально при разработке). Его задача — обрабатывать бизнес-логику, работать с базой данных и отправлять данные клиенту.
  • API (Application Programming Interface): Это набор правил, по которым клиент и сервер общаются. Обычно они обмениваются данными в формате JSON.
  • !Взаимодействие компонентов в Fullstack-архитектуре

    Почему Node.js и React?

    Главное преимущество этой связки — единый язык программирования. И фронтенд, и бэкенд пишутся на JavaScript. Это позволяет:

    * Переиспользовать код (например, функции валидации данных). * Использовать общий менеджер пакетов (npm). * Легче переключаться между задачами сервера и клиента.

    Установка и проверка Node.js

    Основой всего окружения является Node.js. Это среда выполнения, которая позволяет запускать JavaScript вне браузера. Вместе с ней устанавливается npm (Node Package Manager) — инструмент для управления библиотеками.

    Шаг 1: Установка

    Перейдите на официальный сайт nodejs.org и скачайте версию LTS (Long Term Support). Это наиболее стабильная версия, рекомендуемая для большинства проектов.

    Шаг 2: Проверка версий

    После установки откройте терминал (Command Prompt, PowerShell или Terminal на macOS) и введите следующие команды:

    Вы должны увидеть номера версий, например v20.11.0 и 10.2.4. Если вы видите ошибки, попробуйте перезагрузить компьютер или переустановить Node.js.

    Структура проекта

    Для учебного проекта мы будем использовать простую и понятную структуру, разделяющую код клиента и сервера. Создайте главную папку для вашего проекта, например my-fullstack-app.

    Внутри неё мы создадим две подпапки: * client — здесь будет жить React. * server — здесь будет жить Node.js API.

    Такой подход называется монорепозиторием (в упрощенном виде), так как весь код хранится в одном месте.

    Настройка серверной части (Node.js + Express)

    Начнем с бэкенда, так как он будет поставлять данные.

  • Откройте терминал и перейдите в папку проекта:
  • Инициализируйте проект. Флаг -y автоматически ответит «да» на все вопросы конфигурации:
  • В папке появится файл package.json — паспорт вашего проекта, где хранятся список зависимостей и скрипты запуска.

  • Установите Express — минималистичный веб-фреймворк для Node.js:
  • Создание базового сервера

    Создайте в папке server файл index.js и добавьте следующий код:

    ``javascript const express = require('express'); const app = express(); const PORT = 5000;

    // Базовый маршрут (endpoint) app.get('/api/hello', (req, res) => { res.json({ message: 'Привет от сервера Node.js!' }); });

    app.listen(PORT, () => { console.log(Сервер запущен на порту T_{total}SVL50 \times 8 \times 1024 = 40960010 \times 10^6L$) играет огромную роль. Именно поэтому в архитектуре Node.js мы стараемся минимизировать количество запросов и объем передаваемых данных.

    Итоги

    Мы успешно подготовили рабочее место для Fullstack-разработки. Вот ключевые моменты:

    * Архитектура: React отвечает за интерфейс, Node.js — за логику и данные. * Окружение: Node.js и npm необходимы для запуска и управления зависимостями. * Структура: Разделение на client и server упрощает поддержку кода. * Связь: Для взаимодействия между портами (5173 и 5000) необходима настройка CORS на сервере. * Инструменты: Vite — современный стандарт для сборки React-приложений.

    2. Разработка REST API сервера на платформе Node.js

    Разработка REST API сервера на платформе Node.js

    REST (Representational State Transfer) — это архитектурный стиль взаимодействия компонентов распределенного приложения в сети. В контексте Node.js и Express это набор правил, определяющих, как клиент (React) и сервер обмениваются данными. Понимание этих принципов критически важно для создания масштабируемых приложений.

    Принципы REST-архитектуры

    Основная идея REST заключается в том, что все данные на сервере считаются ресурсами. У каждого ресурса есть уникальный идентификатор (URI) и стандартные методы управления им.

    Ключевые характеристики REST API:

  • Клиент-серверная архитектура: Четкое разделение ответственности. Сервер хранит данные, клиент занимается отображением.
  • Stateless (Отсутствие состояния): Сервер не хранит информацию о состоянии сессии клиента между запросами. Каждый запрос должен содержать всю необходимую информацию для его обработки (например, токен авторизации).
  • Единообразие интерфейса: Использование стандартных HTTP-методов (GET, POST, PUT, DELETE).
  • !Цикл запрос-ответ в REST архитектуре

    HTTP-методы и CRUD

    В основе любого API лежат операции CRUD (Create, Read, Update, Delete). В протоколе HTTP им соответствуют определенные глаголы (методы).

    | Операция CRUD | HTTP Метод | Описание | Пример URL | | :--- | :--- | :--- | :--- | | Create (Создание) | POST | Создает новый ресурс | /api/users | | Read (Чтение) | GET | Получает список или один ресурс | /api/users или /api/users/1 | | Update (Обновление) | PUT / PATCH | Обновляет существующий ресурс | /api/users/1 | | Delete (Удаление) | DELETE | Удаляет ресурс | /api/users/1 |

    > Разработка REST API на Node.js требует понимания не только синтаксиса, но и архитектурных паттернов, чтобы приложение было поддерживаемым. habr.com

    Настройка Express для работы с JSON

    По умолчанию Express не умеет читать JSON-данные, приходящие в теле запроса (req.body). Для этого необходимо подключить встроенный middleware.

    Откройте файл server/index.js (созданный в предыдущей статье) и добавьте следующую строку перед объявлением маршрутов:

    Без app.use(express.json()) при попытке отправить данные на сервер вы получите undefined вместо объекта с данными.

    Реализация маршрутов (Routes)

    Рассмотрим создание полноценного API для управления списком задач (Tasks). Поскольку мы пока не подключили базу данных, будем использовать массив в оперативной памяти для имитации хранилища.

    1. Получение данных (GET)

    Метод GET используется для запроса данных. Он не должен изменять состояние сервера.

    2. Создание данных (POST)

    Метод POST принимает данные в теле запроса (req.body) и создает новый ресурс.

    Обратите внимание на коды ответов: 200 — успешно (по умолчанию), 201 — успешно создано, 400 — ошибка клиента (неверные данные).

    3. Параметры маршрута и удаление (DELETE)

    Для действий с конкретным элементом используются параметры маршрута. В Express они обозначаются через двоеточие, например :id. Доступ к ним осуществляется через req.params.

    Теория производительности: Пропускная способность

    При разработке API важно оценивать, какую нагрузку способен выдержать сервер. Node.js работает в однопоточном режиме, используя событийный цикл (Event Loop). Теоретический предел пропускной способности (RPS — Requests Per Second) для CPU-зависимых задач можно оценить по формуле:

    где — максимальное количество запросов в секунду, а — время, которое процессор тратит на обработку одного запроса (в секундах).

    Пример расчета: Если обработка одного запроса (парсинг JSON, логика, сериализация ответа) занимает у процессора 2 миллисекунды (0.002 с), то:

    Это означает, что один процесс Node.js может обработать до 500 таких запросов в секунду. Если добавить тяжелые вычисления, увеличится, а упадет. Именно поэтому в Node.js нельзя блокировать поток долгими вычислениями.

    Структурирование проекта

    Хранить весь код в index.js — плохая практика. В профессиональной разработке используется разделение на слои:

  • Routes (Маршруты): Определяют URL и методы (routes/taskRoutes.js).
  • Controllers (Контроллеры): Содержат логику обработки запроса (controllers/taskController.js).
  • Models (Модели): Отвечают за работу с данными (будет актуально при подключении БД).
  • Пример выноса логики в контроллер:

    Такой подход упрощает тестирование и поддержку кода, особенно когда проект разрастается до десятков эндпоинтов. ru.hexlet.io

    Коды состояния HTTP

    Правильное использование статус-кодов помогает клиенту (React) понять, что произошло на сервере.

    * 2xx (Успех): * 200 OK: Запрос выполнен успешно. * 201 Created: Ресурс успешно создан. * 4xx (Ошибка клиента): * 400 Bad Request: Сервер не может обработать запрос из-за ошибки клиента (например, невалидный JSON). * 401 Unauthorized: Требуется аутентификация. * 403 Forbidden: Доступ запрещен. * 404 Not Found: Ресурс не найден. * 5xx (Ошибка сервера): * 500 Internal Server Error: Непредвиденная ошибка на стороне сервера.

    Итоги

    В этой статье мы реализовали базовый REST API сервер. Ключевые моменты:

  • REST — это стиль архитектуры, использующий HTTP-методы (GET, POST, PUT, DELETE) для управления ресурсами.
  • Middleware express.json() необходим для обработки входящих JSON-данных в POST и PUT запросах.
  • Маршрутизация в Express позволяет обрабатывать динамические параметры URL (например, :id) через req.params.
  • Статус-коды HTTP являются важной частью ответа сервера, сообщая клиенту о результате операции (успех или ошибка).
  • Производительность Node.js сервера напрямую зависит от времени обработки одного запроса процессором, поэтому важно избегать блокирующих операций.
  • 3. Создание интерфейса пользователя с помощью библиотеки React

    Создание интерфейса пользователя с помощью библиотеки React

    В предыдущих статьях мы настроили окружение и создали REST API на Node.js, который умеет отдавать список задач. Теперь пришло время оживить наше приложение, создав клиентскую часть. Мы будем использовать React — библиотеку для создания пользовательских интерфейсов, которая позволяет строить сложные приложения из небольших изолированных кусочков кода, называемых компонентами.

    Компонентный подход и JSX

    React предлагает мыслить не страницами, а компонентами. Компонент — это JavaScript-функция, которая возвращает разметку. Это позволяет повторно использовать код и делает его структуру понятной.

    > Пользовательские интерфейсы можно разбить на небольшие строительные блоки, называемые компонентами. Компоненты позволяют создавать самодостаточные, повторно используемые фрагменты кода. > > Основы React: Построение UI с помощью компонентов

    Для описания внешнего вида компонентов используется синтаксис JSX (JavaScript XML). Он выглядит как HTML, но работает внутри JavaScript.

    Основные отличия JSX от HTML:

  • Вложенность: Компонент должен возвращать только один родительский элемент (обычно div или фрагмент <>...</>).
  • Атрибуты: Вместо class используется className (так как class — зарезервированное слово в JS), а свойства пишутся в camelCase (например, onClick вместо onclick).
  • Интерполяция: Внутри фигурных скобок {} можно писать любое валидное выражение JavaScript.
  • Пример простого компонента:

    Пропсы (Props): Передача данных

    Чтобы компоненты были динамическими, мы передаем в них данные. В React эти данные называются props (свойства). Пропсы передаются от родителя к ребенку и доступны внутри компонента как объект.

    Важно помнить: пропсы доступны только для чтения. Компонент не должен изменять свои пропсы.

    Состояние (State): Интерактивность

    Пропсы позволяют передавать данные, но не позволяют их изменять внутри компонента. Для данных, которые меняются со временем (например, ввод пользователя в форму или список загруженных задач), используется состояние (State).

    Для работы с состоянием используется хук useState.

    Интеграция с Node.js API (useEffect)

    Теперь самое интересное: свяжем наш React-клиент с сервером Node.js, который мы создали в прошлой статье. Для выполнения побочных эффектов (запросов к серверу) используется хук useEffect.

    Мы будем использовать встроенную функцию fetch для получения данных с адреса http://localhost:5000/api/tasks.

    Создание компонента App

    Откройте файл client/src/App.jsx и замените его содержимое на следующий код:

    Обратите внимание на атрибут key в методе map. Он критически важен для алгоритма согласования React, чтобы понимать, какие элементы списка изменились, добавились или удалились.

    Теория: Виртуальный DOM и сложность обновлений

    React работает быстро благодаря концепции Virtual DOM. Вместо того чтобы перерисовывать весь экран при каждом изменении, React создает легкую копию DOM-дерева в памяти, сравнивает её с предыдущей версией и вносит только необходимые изменения.

    Классический алгоритм сравнения двух деревьев имеет сложность . Однако React использует эвристический алгоритм, который снижает сложность до линейной:

    где — вычислительная сложность алгоритма сравнения (diffing algorithm), а — количество элементов в дереве.

    Если бы React использовал стандартный алгоритм, то для отображения 1000 элементов потребовалось бы порядка одного миллиарда операций (). Благодаря оптимизации и использованию ключей (key), React выполняет это за тысячи операций, что делает интерфейс отзывчивым.

    > DOM — это объектное представление HTML-элементов. Он выступает в качестве моста между вашим кодом и пользовательским интерфейсом. > > Основы React: Рендеринг пользовательских интерфейсов (UI)

    Добавление новых задач (POST запрос)

    Чтобы наше приложение стало полноценным, реализуем форму добавления задачи. Нам нужно отправить POST-запрос на сервер и обновить состояние списка задач.

    Добавьте этот код внутрь компонента App (перед return):

    И добавьте форму в JSX разметку:

    Теперь, когда вы нажимаете «Добавить», React отправляет данные на Node.js, получает ответ с созданной задачей (включая id) и обновляет интерфейс без перезагрузки страницы.

    Итоги

    Мы успешно создали клиентскую часть Fullstack-приложения. Вот что мы изучили:

  • Компоненты и JSX: React позволяет строить интерфейс из независимых блоков, используя синтаксис, похожий на HTML.
  • Props и State: Пропсы передают данные вниз по дереву, а состояние (useState) позволяет компонентам реагировать на изменения и хранить данные.
  • Связь с бэкендом: Хук useEffect идеально подходит для выполнения первичной загрузки данных с API при монтировании компонента.
  • Оптимизация: Использование атрибута key в списках помогает алгоритму React эффективно обновлять DOM за время .
  • 4. Интеграция клиентской и серверной частей: HTTP-запросы и CORS

    Интеграция клиентской и серверной частей: HTTP-запросы и CORS

    В предыдущих статьях мы создали сервер на Node.js и клиентское приложение на React. Однако, пока они существуют изолированно друг от друга, наше приложение не может считаться полноценным. В этой статье мы детально разберем механизмы, позволяющие этим двум частям обмениваться данными: протокол HTTP, использование Fetch API и решение проблем безопасности с помощью CORS.

    Анатомия HTTP-запроса

    Взаимодействие между React (клиентом) и Node.js (сервером) происходит по протоколу HTTP. Это протокол типа «запрос-ответ». Клиент всегда инициализирует общение, отправляя запрос, а сервер обрабатывает его и возвращает ответ.

    Согласно Tproger, HTTP-запрос состоит из нескольких ключевых компонентов:

  • Метод (Method): Указывает на действие (GET, POST, PUT, DELETE).
  • URL (Uniform Resource Locator): Адрес ресурса (например, http://localhost:5000/api/tasks).
  • Заголовки (Headers): Метаданные запроса (например, тип контента или информация о браузере).
  • Тело (Body): Данные, передаваемые на сервер (актуально для POST и PUT).
  • Жизненный цикл запроса в React

    Когда вы вызываете fetch в React-компоненте, происходит следующее:

  • Браузер формирует HTTP-пакет.
  • Пакет отправляется через сеть на указанный IP-адрес и порт сервера.
  • Node.js (Express) перехватывает запрос, маршрутизирует его и выполняет логику.
  • Сервер формирует ответ (статус-код, заголовки, тело) и отправляет его обратно.
  • Браузер получает ответ и передает его в JavaScript-код.
  • Fetch API: Современный стандарт

    Для выполнения запросов в современных браузерах используется Fetch API. Это встроенный инструмент, работающий на основе промисов (Promises), что позволяет писать асинхронный код чисто и понятно.

    Рассмотрим пример сложного запроса с отправкой данных:

    Итоги

    В этой статье мы объединили клиент и сервер в единую систему.

  • HTTP-протокол является основой обмена данными. Клиент отправляет запросы (GET, POST и др.), сервер возвращает ответы со статус-кодами.
  • Fetch API — это стандартный способ выполнения асинхронных запросов в React. Не забывайте проверять свойство response.ok.
  • CORS — механизм безопасности браузера, блокирующий запросы между разными источниками (портами). Для решения проблемы необходимо настроить сервер (Express) с помощью пакета cors.
  • Сетевая задержка () играет критическую роль в производительности. Формула напоминает нам, что лишние запросы замедляют приложение.
  • Обработка ошибок на клиенте должна базироваться на HTTP статус-кодах для обеспечения качественного пользовательского опыта.
  • 5. Реализация полноценного CRUD-функционала в связке React и Node.js

    Реализация полноценного CRUD-функционала в связке React и Node.js

    Мы уже прошли большой путь: настроили окружение, запустили сервер на Node.js, создали клиент на React и даже научили их обмениваться данными для получения (Read) и создания (Create) задач. Но полноценное приложение должно уметь больше. В этой статье мы замкнем круг CRUD (Create, Read, Update, Delete), реализовав возможность редактирования и удаления данных.

    Мы разберем, как правильно обрабатывать PUT и DELETE запросы на сервере и как эффективно обновлять состояние React-компонентов, не перезагружая страницу.

    Расширение API на Node.js

    На данный момент наш сервер умеет отдавать список задач и добавлять новые. Чтобы управлять существующими данными, нам нужно научить его понимать, какую именно задачу мы хотим изменить или удалить. Для этого используются параметры маршрута (URL parameters).

    Реализация удаления (DELETE)

    Метод DELETE идемпотентен — это значит, что повторный вызов того же запроса не должен менять состояние сервера (если объект уже удален, повторный запрос просто вернет тот же результат или ошибку 404).

    Откройте файл server/index.js и добавьте следующий маршрут:

    Здесь :id — это динамический параметр. Express автоматически помещает его значение в объект req.params.

    Реализация обновления (PUT)

    Для обновления данных обычно используют методы PUT (полная замена ресурса) или PATCH (частичное обновление). Для простоты мы будем использовать PUT для переключения статуса выполнения задачи и изменения названия.

    Добавьте этот код в server/index.js:

    Алгоритмическая сложность операций

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

    Среднее время поиска в неупорядоченном массиве можно выразить формулой:

    где — среднее время поиска, — количество элементов в массиве, а — время выполнения одной операции сравнения.

    Если у вас 1000 задач, то в среднем серверу придется проверить 500 элементов, чтобы найти нужный. Для маленьких приложений это мгновенно, но в реальных высоконагруженных системах именно поэтому используются базы данных с индексами (B-Tree или Hash Map), которые снижают сложность поиска с до или .

    > В реальных проектах использование базы данных, такой как MySQL или MongoDB, является стандартом для обеспечения надежности и скорости доступа к данным. > > CRUD-приложение на React, Node.js, Express и MySQL

    Модернизация Frontend на React

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

    Рефакторинг: Выделение компонента TaskItem

    В предыдущей статье мы рендерили список прямо в App.jsx. Чтобы добавить кнопки редактирования и удаления для каждой задачи, лучше выделить отдельный компонент. Это улучшит читаемость и производительность.

    Создайте файл client/src/components/TaskItem.jsx (не забудьте создать папку components):

    Этот компонент имеет собственное локальное состояние isEditing, которое управляет режимом отображения: либо текст задачи, либо поле ввода.

    Обновление главного компонента App.jsx

    Теперь подключим логику в App.jsx. Нам нужно написать функции, которые будут отправлять запросы на сервер и обновлять состояние tasks.

    Обратите внимание на принцип иммутабельности (неизменяемости) в React. Мы никогда не меняем массив tasks напрямую (например, через push или splice). Мы всегда создаем новый массив.

    Оптимистичный UI (Optimistic UI)

    В коде выше мы сначала ждем ответа от сервера (await fetch), и только потом обновляем интерфейс (setTasks). Это надежно, но создает небольшую задержку для пользователя. Это называется пессимистичным обновлением.

    Для улучшения UX (User Experience) часто используют оптимистичное обновление. Идея в том, чтобы обновить интерфейс мгновенно, предполагая, что сервер ответит успешно. Если же произойдет ошибка, мы откатываем изменения назад.

    Пример оптимистичного удаления:

    ``javascript const deleteTaskOptimistic = (id) => { // 1. Сразу обновляем UI const previousTasks = [...tasks]; // Сохраняем копию на случай ошибки setTasks(tasks.filter(task => task.id !== id));

    // 2. Отправляем запрос в фоне fetch(http://localhost:5000/api/tasks/RTTO(n)R_{ops}R_{ops}N_{diff}C_{overhead}R_{ops}$ и нагрузку на браузер.

    Итоги

    Мы завершили создание полноценного Fullstack-приложения с CRUD-функционалом. Вот ключевые выводы:

  • Маршрутизация: Для изменения конкретных ресурсов на сервере используются динамические параметры в URL (например, :id), доступные через req.params.
  • Методы HTTP: PUT используется для обновления данных, а DELETE — для их удаления. Это семантически верно и соответствует стандартам REST.
  • Иммутабельность: При обновлении состояния в React (удаление или изменение элемента) мы всегда создаем новые копии массивов с помощью методов .filter() и .map(), а не мутируем старые.
  • UX: Разделение логики на компоненты (TaskItem) и использование оптимистичных обновлений делает интерфейс отзывчивым и удобным для пользователя.
  • Производительность: Правильное использование ключей (key) в списках критически важно для минимизации операций с DOM при обновлении данных.