«Благодаря Node.js мы можем выполнять JavaScript на сервере — раньше вы могли запускать его только в браузере», — именно эта архитектурная свобода превратила MERN-стек в золотой стандарт для fullstack-разработки. В этом материале мы разбираем пошаговое создание CRUD-приложения, от проектирования NoSQL-структуры в MongoDB до деплоя полноценного монорепозитория на Render.
🚀 Глава 1: Знакомство с MERN-стеком и построение фундамента API 0:00
Разработка с нуля: от первой строчки до деплоя 0:00
Этот комплексный курс по MERN-стеку разработан специально для тех, кто хочет пройти путь от абсолютного новичка до полноценного развертывания готового проекта. Ментор Барак предлагает построить отзывчивое приложение для создания заметок, используя связку MongoDB, Express, React и Node.js. В финальной версии программы пользователи смогут не просто добавлять записи, но и полноценно обновлять или удалять их. Кроме того, проект будет включать продвинутые функции, такие как ограничение частоты запросов, что редко встречается в обучающих материалах для новичков, но крайне важно для реальных резюме.
Для успешного прохождения курса потребуется установить Node.js версии LTS с официального сайта и настроить редактор кода VS Code. Структура проекта предполагает разделение на две ключевые директории: одна для бэкенда, где будет создаваться API, и вторая для фронтенда на базе React. Работа начнется именно с серверной части, и только после полной готовности API фокус сместится на интерфейс.
Анатомия стека MERN: сила единого языка 3:23
Прежде чем приступать к написанию кода, необходимо разобраться в самой концепции MERN-стека. Каждая буква в этой аббревиатуре обозначает отдельный слой современного веб-приложения:
- M (MongoDB) — документоориентированная база данных, выступающая основным хранилищем информации.
- E (Express) — минималистичный веб-фреймворк для серверной разработки.
- R (React) — библиотека для создания динамических пользовательских интерфейсов.
- N (Node.js) — программная платформа, выступающая в роли среды выполнения кода.
Главное преимущество этой комбинации заключается в том, что разработчику не нужно переключаться между разными экосистемами: и на фронтенде, и на бэкенде используется один и тот же язык — JavaScript. Изучив его основы, можно закрывать задачи на всех уровнях приложения. Express предоставляет готовую и стабильную «коробку инструментов» для маршрутизации и обработки ошибок, избавляя от необходимости изобретать велосипед. В свою очередь, Node.js позволяет выполнять JavaScript прямо на сервере, а не только внутри клиентского браузера. Для работы с фронтендом потребуются лишь самые базовые знания React, включая компоненты и хуки состояния.
Настройка окружения и первый запуск сервера 6:04
Практическая часть начинается в папке бэкенда через терминал VS Code. Команда npm init -y мгновенно инициализирует Node.js-проект, создавая файл package.json для управления зависимостями. На этом этапе устанавливается фреймворк Express. Важный нюанс: автор намеренно фиксирует версию 4.18.2. Несмотря на недавний релиз пятой версии Express, именно четвертая остается стандартом индустрии и используется в абсолютном большинстве коммерческих кодовых баз. Фиксация версии гарантирует, что код не устареет со временем.
Основным файлом сервера по конвенции становится server.js. По умолчанию Node.js использует систему CommonJS (require), но для работы с современным синтаксисом ES-модулей (import express from "express") необходимо внести правку в package.json, добавив свойство "type": "module". Чтобы запускать сервер удобной командой, в блоке скриптов настраивается специальный ключ "dev": "node server.js". Локальный сервер настраивается на прослушивание порта 501, сигнализируя об успешном старте через консоль.
Архитектура API и цикл «запрос-ответ» 13:04
Любое full-stack приложение функционирует на основе взаимодействия клиента, сервера и базы данных. Когда пользователь совершает действие (например, удаляет заметку), клиент отправляет запрос на сервер. Сервер обрабатывает его, вносит изменения в базу данных и возвращает ответ об успешном выполнении. Это непрерывный жизненный цикл «запрос-ответ».
Интерфейсом такого взаимодействия выступает API (Application Programming Interface). Лучше всего его суть передает классическая аналогия с официантом в ресторане. Посетитель (клиент) делает заказ (запрос), официант (API) относит его на кухню (сервер) и возвращает готовое блюдо (ответ).
Но почему нельзя связать фронтенд напрямую с базой данных? Ответ кроется в безопасности. Около 90% пользователей имеют добрые намерения, однако всегда существует прослойка злоумышленников. Если предоставить клиенту прямой доступ к базе, вредоносный пользователь сможет украсть чужие пароли или полностью очистить таблицы данных. API служит жестко контролируемым барьером, который выполняет строго определенные и безопасные сценарии.
Протокол HTTP, методы REST API и коды состояний 17:41
В подавляющем большинстве случаев современные веб-сервисы строятся по архитектуре REST API с использованием протокола HTTP. Взаимодействие опирается на четыре базовых метода:
- GET — получение данных (например, загрузка ленты постов).
- POST — создание новой записи.
- PUT — обновление существующей информации.
- DELETE — удаление данных.
Каждый метод четко описывает намерение клиента. На стороне сервера настраиваются соответствующие маршруты (эндпоинты). Например, при обращении методом GET к /api/notes сервер возвращает статус 200 OK и массив заметок. Если бы создавалась новая запись методом POST, корректным ответом стал бы статус 201 Created.
Все ответы сервера сопровождаются числовыми статус-кодами, разделенными на классы:
- 100–199 (Информационные) — редко встречаются в повседневной практике.
- 200–299 (Успешные) — сигнализируют о том, что всё прошло по плану (200 OK, 201 Created).
- 300–399 (Перенаправления) — указывают, что ресурс перемещен. Пример: код 301 (Moved Permanently) используется при миграции сайта с HTTP на безопасный HTTPS-протокол.
- 400–499 (Клиентские ошибки) — возникают, когда пользователь или браузер передали некорректные данные. Самый известный — 404 Not Found, возникающий при обращении к несуществующему адресу. Также важны 401 (не авторизован) и 403 (доступ запрещен). Позже по курсу разработчики также столкнутся с кодом 429, означающим превышение лимита запросов.
- 500–599 (Серверные ошибки) — происходят по вине бэкенда, даже если запрос клиента был идеальным. Сюда относятся общая ошибка 500 (Internal Server Error) и код 503 (Service Unavailable), сообщающий о перегрузке или технических работах, который часто можно увидеть на заглушках крупных сервисов, таких как GitHub.
🛠️ Оптимизация процесса разработки, рефакторинг архитектуры и запуск MongoDB Atlas 25:05
Автоматизация сервера с Nodemon и разделение скриптов окружения 25:05
Разработка серверной части приложения на начальном этапе часто сопряжена с рутинными действиями, которые существенно замедляют рабочий процесс. Каждый раз, когда в код бэкенда вносятся изменения — например, корректируется текстовый ответ эндпоинта — разработчику приходится вручную останавливать текущий процесс в терминале и запускать его заново. Это быстро становится утомительным занятием. Для решения этой проблемы применяется популярная утилита Nodemon, которая автоматически отслеживает любые изменения в файлах проекта и мгновенно перезапускает сервер на лету.
Установка Nodemon выполняется через пакетный менеджер npm одной простой командой: npm install nodemon -D. Флаг -D (или --save-dev) здесь критически важен, так как он регистрирует пакет исключительно как зависимость для разработки (dev dependency). В продакшене автоматический перезапуск не просто бесполезен, но и вреден для общей стабильности и производительности системы.
После успешной установки необходимо настроить правильные сценарии запуска в конфигурационном файле package.json. Для этого создается четкое разделение на два ключевых скрипта:
-
dev: запускает сервер через командуnodemon server.jsдля активной и комфортной разработки. -
start: запускает сервер напрямую через классическийnode server.jsдля продакшена.
Такой подход гарантирует, что после развертывания готового приложения на удаленном облачном сервере система не будет тратить лишние вычислительные ресурсы на отслеживание изменений кода, поскольку в продакшене кодовая база статична.
Архитектурный рефакторинг: маршруты, контроллеры и структура проекта 32:55
По мере добавления новых эндпоинтов для тестирования базовых HTTP-методов (POST, PUT, DELETE) и работы с динамическими параметрами путей вроде /:id, монолитный файл server.js начинает стремительно разрастаться. Если оставить всю логику обработки запросов в одном месте, файл быстро превысит тысячу строк кода, став абсолютно нечитаемым и непригодным для долгосрочной поддержки. Хорошая практика промышленной разработки требует грамотного разделения кодовой базы на специализированные архитектурные слои.
Первым шагом масштабного рефакторинга становится выделение маршрутов в отдельную структуру. В корне проекта создается папка routes, а внутри нее — специализированный файл notesRoutes.js. С помощью встроенного инструмента express.Router() создается изолированный объект маршрутизатора. Это архитектурное решение позволяет полностью избавиться от рутинного дублирования базового URL. Вместо того чтобы прописывать полный путь /api/notes для каждого эндпоинта, этот общий префикс выносится в основной файл server.js через middleware-функцию app.use('/api/notes', notesRoutes). В самом же файле маршрутов остаются только чистые относительные пути.
Однако маршруты должны лишь связывать URL с обработчиками, а не содержать саму бизнес-логику приложения. Для изоляции логики вводится классический слой контроллеров. Создается директория controllers и файл notesController.js. Туда переносятся все анонимные функции-обработчики запросов, которые получают понятные именованные экспорты, такие как getAllNotes или createNote.
Для финальной оптимизации структуры бэкенда все рабочие директории (маршруты, контроллеры, серверная точка входа) переносятся в единую папку src (source). Это изолирует исходный код приложения от корневых конфигурационных файлов. После такого переноса важно не забыть обновить пути запуска в package.json, указав новую точку входа как src/server.js, чтобы предотвратить падение приложения при старте.
Интеграция облачной NoSQL базы данных MongoDB Atlas 44:20
Ранее в разговоре авторы касались настройки окружения Node.js и Express, но полноценному веб-приложению необходим надежный слой постоянного хранения данных. Перед подключением базы данных важно разобраться в теории и провести сравнение: чем NoSQL решения отличаются от классических реляционных SQL баз данных (таких как MySQL или PostgreSQL).
В SQL-базах вся информация строго структурирована в виде таблиц с фиксированными строками и столбцами, что сильно напоминает работу с электронными таблицами. Они идеально подходят для систем со сложными, жестко заданными связями между сущностями, например, для банковских платформ. В свою очередь, MongoDB — это документоориентированная NoSQL база данных, хранящая информацию в гибком формате, аналогичном JSON. Это делает её оптимальным выбором для быстро меняющихся структур данных и огромных массивов информации, где на первом месте стоит скорость разработки и горизонтальная масштабируемость.
Для развертывания базы данных используется облачная платформа MongoDB Atlas, предоставляющая полностью бесплатный кластер без необходимости привязки банковской карты. Процесс первичной настройки на сайте mongodb.com состоит из нескольких последовательных шагов:
-
Создание нового изолированного проекта на платформе, который в рамках урока получил название "thinkboard".
-
Развертывание бесплатного облачного кластера с сохранением базовых региональных настроек по умолчанию.
-
Генерация безопасных учетных данных администратора и копирование строки подключения (connection string) для интеграции в кодовую базу бэкенда.
Финальный критически важный этап конфигурации облака — настройка сетевого доступа (Network Access) в панели управления Atlas. По умолчанию облако блокирует абсолютно все внешние запросы в целях безопасности. Чтобы избежать досадных ошибок авторизации на этапе локальной разработки, в белый список временно добавляется адрес 0.0.0.0/0, что разрешает безопасный доступ к базе данных с любого IP-адреса в мире. В самом конце этого этапа разработчик инициализирует установку ORM-библиотеки Mongoose версии 7.0.3, которая потребуется для дальнейшей работы.
🔌 Безопасное подключение к MongoDB и проектирование моделей данных 50:22
Интеграция Mongoose и защита конфигурации через переменные окружения 50:22
После успешного развертывания облачной базы данных критически важным шагом становится организация стабильного, отказоустойчивого и безопасного подключения к ней из разрабатываемого Node.js-приложения. Для реализации этой задачи в структуре проекта создается выделенная директория config, внутри которой размещается файл db.js, полностью инкапсулирующий в себе логику взаимодействия с базой данных. Процесс подключения организуется в виде асинхронной функции connectDB. Использование конструкции try...catch здесь строго обязательно, поскольку операции ввода-вывода при работе с удаленными серверами СУБД всегда сопряжены с рисками сетевых задержек или обрывов связи. Главным рабочим инструментом выступает популярная ODM-библиотека Mongoose, чей метод mongoose.connect() принимает строку подключения в качестве аргумента и возвращает промис. Если аутентификация завершается неудачей — к примеру, из-за опечатки в пароле пользователя — приложение мгновенно перехватывает исключение. В блоке catch разработчик не просто выводит лог ошибки в консоль, но и принудительно останавливает выполнение всего Node-процесса с помощью команды process.exit(1), где статус 1 явным образом сигнализирует системе о критическом системном сбое.
По умолчанию Mongoose пытается подключиться к стандартной системной базе данных с предопределенным именем test. Чтобы изолировать данные текущего проекта, имя целевой базы данных переопределяется вручную на notes_db — его необходимо прописать непосредственно перед условным знаком вопроса в URI-строке подключения. На этом этапе разработки отчетливо заявляет о себе фундаментальная проблема безопасности: если оставить секретную строку подключения с открытыми учетными данными администратора прямо в исходном коде, то при первой же синхронизации проекта с публичным репозиторием на GitHub вся база данных окажется под угрозой несанкционированного доступа.
Для решения этой проблемы в стек технологий внедряется специализированный пакет dotenv, позволяющий полностью вынести конфиденциальные параметры во внешний изолированный файл .env, который хранится исключительно на локальной машине разработчика. Пошаговая интеграция этого механизма выглядит следующим образом:
-
Установка официального пакета из реестра npm выполняется стандартной командой
npm install dotenv. -
Создание секретной переменной окружения
MONGO_URIв корневом.env-файле, куда копируется полная строка подключения без кавычек. -
Инициализация конфигурации пакета в главном файле бэкенда
server.jsпосредством вызова методаdotenv.config().
После активации данной конфигурации глобальный объект Node.js process.env открывает динамический доступ ко всем объявленным секретам. Теперь в метод подключения Mongoose вместо уязвимого хардкода передается безопасная ссылка process.env.MONGO_URI. По точно такой же схеме в конфигурационный файл выносится и порт сервера PORT. Чтобы гарантировать непрерывную работу бэкенда в любых условиях, разработчик применяется классический паттерн логического ветвления: если системная переменная порта по какой-то причине вернет undefined, сервер автоматически запустится на резервном значении порта по умолчанию.
Проектирование схем и создание моделей данных Mongoose 59:45
Когда защищенное соединение с базой данных успешно установлено, архитектура приложения требует перехода к следующему этапу — строгому структурированию информации. В экосистеме Mongoose центральными сущностями для управления данными выступают схемы (Schemas) и модели (Models). Для аккуратного разделения ответственности в структуре бэкенда создается новая папка models, в которой размещается файл Note.js. Соответствуя устоявшимся стандартам веб-разработки, файлы моделей всегда именуются в единственном числе и с заглавной буквы. Схема берет на себя роль детального чертежа или контракта, который жестко регламентирует анатомию будущего документа в NoSQL-базе, предопределяя типы полей и правила валидации.
Проектирование начинается с импорта модуля Mongoose и вызова конструктора new mongoose.Schema(), куда передается конфигурационный объект с описанием структуры будущей заметки:
-
Поле
title(заголовок создаваемой заметки) — настраивается как строковый тип (type: String) и объявляется строго обязательным для заполнения с помощью параметраrequired: true. -
Поле
content(текстовое содержимое заметки) — конфигурируется аналогичным образом как обязательная текстовая строка.
Помимо базовых кастомных полей, для полноценного ведения логов и последующего отображения истории изменений в интерфейсе приложению требуются временные метки. Вместо того чтобы вручную объявлять поля дат и настраивать логику их перезаписи при каждом обновлении, Mongoose предоставляет разработчикам удобную встроенную опцию — передачу конфигурационного объекта { timestamps: true } вторым аргументом в конструктор схемы. Данный параметр автоматически обязывает СУБД самостоятельно создавать и модифицировать системные поля createdAt (дата создания) и updatedAt (дата последнего редактирования) для каждого документа.
Завершающим штрихом проектирования становится компиляция спроектированной схемы в полноценную рабочую модель при помощи метода mongoose.model('Note', noteSchema), которая затем экспортируется из файла по умолчанию. Важно подчеркнуть подспудную магию Mongoose: при создании модели с именем 'Note' библиотека автоматически сопоставит ее с коллекцией во множественном числе и нижнем регистре, то есть notes. Полученная модель превращается в мощный объектный интерфейс. Как было вскользь упомянуто далее в ходе беседы, эта модель немедленно легла в основу написания базовых CRUD-контроллеров для выборки и сохранения данных, а также потребовала подключения встроенного middleware для корректного парсинга входящего JSON-трафика.
🛠️ Глава 4. Полноценный бэкенд-CRUD, магия Middleware и облачная защита с Upstash Redis 1:15:23
Расширение возможностей CRUD: обновление, удаление и умная сортировка заметок 1:15:23
Разработка бэкенда для приложения заметок выходит на финишную прямую, требуя от разработчика построения надежной архитектуры обработки данных. Нам необходимо расширить базовую функциональность и превратить её в полноценный CRUD-сервис. Начнем с реализации контроллера обновления записей. При отправке PUT-запроса важно не просто возвращать абстрактное сообщение об успехе <a class="ts" data-seconds="4523" href="#t=4523" title="Смотреть с 1:15:23" aria-label="Смотреть с 1:15:23"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>, но и скрупулезно обрабатывать пограничные сценарии. Например, если клиент передает несуществующий или деформированный ID, сервер не должен падать в ошибку — вместо этого мы перехватываем управление, проверяем результат поиска на ложность и возвращаем статус-код 404 Not Found с сообщением "Note not found" <a class="ts" data-seconds="4595" href="#t=4595" title="Смотреть с 1:16:35" aria-label="Смотреть с 1:16:35"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>.
При модификации документов через Mongoose критически важно передавать опцию { new: true } в метод обновления <a class="ts" data-seconds="4622" href="#t=4622" title="Смотреть с 1:17:02" aria-label="Смотреть с 1:17:02"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. По умолчанию данная библиотека возвращает исходную версию документа, существовавшую до применения изменений. Опция new: true заставляет базу данных отдавать уже измененный объект, который мы можем сразу же транслировать обратно на фронтенд в теле ответа. Стоит отметить гибкость Mongoose: сервис поддерживает частичные обновления, поэтому если в запросе передается только новое текстовое содержимое, заголовок заметки останется нетронутым <a class="ts" data-seconds="4703" href="#t=4703" title="Смотреть с 1:18:23" aria-label="Смотреть с 1:18:23"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>.
Следующим логическим шагом становится удаление сущностей. Метод Note.findByIdAndDelete() извлекает идентификатор напрямую из параметров адресной строки req.params.id <a class="ts" data-seconds="4745" href="#t=4745" title="Смотреть с 1:19:05" aria-label="Смотреть с 1:19:05"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Здесь применяется аналогичная валидация существования записи. Если удаление прошло успешно, сервер отправляет явный статус 200 и текстовое уведомление "Note deleted successfully" для поддержания консистентности API <a class="ts" data-seconds="4785" href="#t=4785" title="Смотреть с 1:19:45" aria-label="Смотреть с 1:19:45"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>.
Для окончательного завершения этого слоя пишется контроллер получения конкретной заметки по идентификатору <a class="ts" data-seconds="4855" href="#t=4855" title="Смотреть с 1:20:55" aria-label="Смотреть с 1:20:55"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Автор сознательно отказывается от построения сложных абстрактных функций-оберток над асинхронными вызовами, подчеркивая, что классическая конструкция try/catch является абсолютно легитимным, надежным и понятным решением для разработчиков начального уровня <a class="ts" data-seconds="4949" href="#t=4949" title="Смотреть с 1:22:29" aria-label="Смотреть с 1:22:29"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>.
Финальный штрих в логике данных — обеспечение правильного вывода. Чтобы конечный пользователь видел свежие записи первыми, в контроллер чтения базы внедряется сортировка .sort({ createdAt: -1 }) <a class="ts" data-seconds="5003" href="#t=5003" title="Смотреть с 1:23:23" aria-label="Смотреть с 1:23:23"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Отрицательный индекс инвертирует хронологический порядок Mongoose <a class="ts" data-seconds="5074" href="#t=5074" title="Смотреть с 1:24:34" aria-label="Смотреть с 1:24:34"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. При этом демонстрируется чистота кода: неиспользуемые аргументы функций, такие как объект запроса request, заменяются символом нижнего подчеркивания _, что является общепринятым стандартом в JavaScript-сообществе <a class="ts" data-seconds="5112" href="#t=5112" title="Смотреть с 1:25:12" aria-label="Смотреть с 1:25:12"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>.
Что такое Middleware: промежуточное ПО изнутри и создание кастомного логгера 1:25:24
Понятие промежуточного программного обеспечения (или Middleware) традиционно окружено ореолом ложной сложности, однако его архитектурный паттерн предельно прост. Middleware представляет собой функцию, которая встраивается в жизненный цикл обработки запроса, запускаясь строго в промежутке между получением сетевого пакета сервером и отправкой финального ответа клиенту <a class="ts" data-seconds="5206" href="#t=5206" title="Смотреть с 1:26:46" aria-label="Смотреть с 1:26:46"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Разработчики ранее уже сталкивались со встроенным решением express.json() — именно этот промежуточный слой отвечает за парсинг входящих потоков данных в формате JSON. Без него Express просто не сможет распознать тело запроса, и свойство req.body вернет undefined <a class="ts" data-seconds="5137" href="#t=5137" title="Смотреть с 1:25:37" aria-label="Смотреть с 1:25:37"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>.
Кастомные middleware-функции внедряются в конвейер приложения с помощью метода app.use() <a class="ts" data-seconds="5247" href="#t=5247" title="Смотреть с 1:27:27" aria-label="Смотреть с 1:27:27"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Такая функция оперирует тремя ключевыми объектами: request, response и специальным колбэком next. Роль next() фундаментальна: это триггер, передающий управление следующему звену в цепочке обработчиков. Если разработчик забудет вызвать next(), обработка запроса навсегда заблокируется на текущем этапе, а клиент так и не дождется ответа от сервера <a class="ts" data-seconds="5274" href="#t=5274" title="Смотреть с 1:27:54" aria-label="Смотреть с 1:27:54"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>.
Для демонстрации концепции создается кастомный логгер, динамически выводящий в консоль метод запроса (req.method) и целевой маршрут (req.url) <a class="ts" data-seconds="5315" href="#t=5315" title="Смотреть с 1:28:35" aria-label="Смотреть с 1:28:35"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Подобный механизм трассировки незаменим в реальной разработке. Спектр применения промежуточного ПО невероятно широк:
-
Проверка авторизации и аутентификации пользователей (например, валидация сессии в Instagram перед публикацией фотографии)
<a class="ts" data-seconds="5424" href="#t=5424" title="Смотреть с 1:30:24" aria-label="Смотреть с 1:30:24"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. -
Фильтрация и валидация входящих параметров.
-
Защита инфраструктуры от перегрузок и вредоносных действий со стороны клиентов.
Именно к последнему сценарию безопасности бэкенд-систем мы и переходим.
Защита от перегрузок: ограничение частоты запросов с Upstash Redis 1:30:11
Ограничение частоты запросов (Rate Limiting) — это стратегически важный механизм безопасности, контролирующий интенсивность обращений пользователей к веб-ресурсу <a class="ts" data-seconds="5478" href="#t=5478" title="Смотреть с 1:31:18" aria-label="Смотреть с 1:31:18"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. В отсутствие такого барьера злоумышленники или некорректно написанные скрипты могут атаковать эндпоинт /api/notes лавиной бесконечных запросов. Это способно вызвать отказ в обслуживании (DDoS) и падение физического сервера <a class="ts" data-seconds="5491" href="#t=5491" title="Смотреть с 1:31:31" aria-label="Смотреть с 1:31:31"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Общепринятым стандартом для сигнализации о превышении лимита активности является HTTP-статус 429 Too Many Requests <a class="ts" data-seconds="5556" href="#t=5556" title="Смотреть с 1:32:36" aria-label="Смотреть с 1:32:36"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>.
Для построения масштабируемого облачного счетчика в проект интегрируется Upstash Redis <a class="ts" data-seconds="5583" href="#t=5583" title="Смотреть с 1:33:03" aria-label="Смотреть с 1:33:03"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. В отличие от MongoDB, Redis функционирует как сверхбыстрое хранилище пар «ключ-значение» в оперативной памяти <a class="ts" data-seconds="5596" href="#t=5596" title="Смотреть с 1:33:16" aria-label="Смотреть с 1:33:16"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. В панели управления Upstash разворачивается бесплатная база данных под кодовым именем "thinkboard" <a class="ts" data-seconds="5622" href="#t=5622" title="Смотреть с 1:33:42" aria-label="Смотреть с 1:33:42"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Конфиденциальные параметры подключения — rest-URL и секретный токен доступа — немедленно инкапсулируются в защищенный файл .env для предотвращения утечки данных <a class="ts" data-seconds="5662" href="#t=5662" title="Смотреть с 1:34:22" aria-label="Смотреть с 1:34:22"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>.
На стороне бэкенда разворачиваются две целевые зависимости: пакет для лимитирования @upstash/ratelimit (версия 2.0.5) и клиент для связи @upstash/redis (версия 1.34.9) <a class="ts" data-seconds="5716" href="#t=5716" title="Смотреть с 1:35:16" aria-label="Смотреть с 1:35:16"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Конфигурационный файл config/upstash.js аккумулирует настройки лимитера <a class="ts" data-seconds="5770" href="#t=5770" title="Смотреть с 1:36:10" aria-label="Смотреть с 1:36:10"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Для наглядной проверки работы алгоритма выбирается строгая конфигурация типа «sliding window» (скользящее окно), которая жестко лимитирует трафик до 10 запросов за 20 секунд на один ключ <a class="ts" data-seconds="5797" href="#t=5797" title="Смотреть с 1:36:37" aria-label="Смотреть с 1:36:37"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>.
Финальным этапом становится создание изолированного слоя middleware в директории middleware/rateLimiter.js <a class="ts" data-seconds="5867" href="#t=5867" title="Смотреть с 1:37:47" aria-label="Смотреть с 1:37:47"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Эта асинхронная функция перехватывает трафик и обращается к методу rateLimit.limit() с уникальным маркером запроса <a class="ts" data-seconds="5962" href="#t=5962" title="Смотреть с 1:39:22" aria-label="Смотреть с 1:39:22"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Если лимит исчерпан, выполнение прерывается возвратом статуса 429 и JSON-объектом с требованием повторить попытку позже <a class="ts" data-seconds="5989" href="#t=5989" title="Смотреть с 1:39:49" aria-label="Смотреть с 1:39:49"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>. Если же пользователь действует в рамках дозволенного, вызов next() беспрепятственно продвигает запрос к бизнес-логике приложения <a class="ts" data-seconds="6016" href="#t=6016" title="Смотреть с 1:40:16" aria-label="Смотреть с 1:40:16"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></a>.
🚀 Настройка фронтенда: React, Vite и навигация 146:14
Разработка клиентской части MERN-приложения начинается с инициализации проекта с помощью Vite, который является стандартом индустрии благодаря своей скорости и удобству. Для создания приложения используется команда npm create vite@latest, после чего в папке проекта необходимо установить все зависимости через npm install и запустить сервер для разработки командой npm run dev [146:40–147:09].
После очистки boilerplate-кода — удаления стандартных CSS-файлов и содержимого App.jsx — проект структурируется под будущий функционал. Для управления навигацией внедряется библиотека React Router (ранее известная как react-router-dom), которая позволяет организовать переходы между тремя основными разделами приложения: домашней страницей, страницей создания заметок и страницей просмотра деталей конкретной записи [147:36–148:02].
Для интеграции маршрутизации всё приложение оборачивается в компонент BrowserRouter в файле main.jsx. Это дает возможность определять пути (routes) в компоненте App.jsx, сопоставляя их с соответствующими компонентами страниц (HomePage, CreatePage, NoteDetailPage), причем последний путь является динамическим, принимая id заметки [149:09–151:21]. Параллельно для удобного вывода уведомлений пользователю в систему добавляется библиотека React Hot Toast, позволяющая с минимальными усилиями реализовать всплывающие сообщения об успешных действиях или ошибках [151:45–152:53].
🎨 Стилизация: Tailwind CSS и DaisyUI 153:06
Для обеспечения современного внешнего вида и высокой скорости верстки используется Tailwind CSS (версии 3.x для обеспечения максимальной стабильности в текущих проектах). Этот фреймворк позволяет описывать стили прямо в JSX-разметке с помощью набора утилитарных классов, что избавляет от необходимости постоянно переключаться между файлами стилей и логикой компонентов [153:06–153:32].
Интеграция Tailwind включает в себя:
- Установку зависимостей и генерацию конфигурационного файла
tailwind.config.js. - Настройку подключения стилей в
index.css. - Установку расширения Tailwind IntelliSense для VS Code, обеспечивающего автодополнение классов [154:23–155:58].
Для дальнейшего ускорения разработки в проект интегрируется DaisyUI — библиотека компонентов, построенная поверх Tailwind. Она позволяет не писать десятки классов для стандартных элементов, а использовать готовые семантические названия, такие как btn или btn-primary, что существенно сокращает объем кода [156:22–156:48]. Для проекта была выбрана цветовая тема Forest, которая настраивается в файле конфигурации и активируется через атрибут data-theme в основном компоненте приложения. Эта система тем позволяет буквально в одну строку менять дизайн всего интерфейса, переключаясь между доступными вариациями [159:53–201:28].
Ранее в разговоре
В предыдущих частях обсуждались вопросы настройки бэкенда, архитектуры API, интеграции MongoDB Atlas, а также реализация механизма ограничения частоты запросов (rate limiting) с помощью Upstash Redis и оптимизация порядка подключения к базе данных перед стартом сервера [144:14–146:01].
🌐 Настройка взаимодействия с API и отображение данных заметок 2:12:40
В процессе разработки полноценного MERN-приложения взаимодействие между фронтендом и бэкендом неизбежно сталкивается с политикой безопасности браузеров. Одной из центральных задач на данном этапе является корректная настройка механизма Cross-Origin Resource Sharing (CORS).
По умолчанию браузеры блокируют запросы, если фронтенд (например, работающий на localhost:5173) пытается получить данные с другого домена (API-сервера). Чтобы решить эту проблему, необходимо настроить соответствующее промежуточное ПО (middleware) на стороне сервера.
- Для реализации используется пакет
cors. Установка выполняется командойnpm install cors(в примере используется версия 2.8.5). - В основном файле сервера
server.jsнеобходимо импортировать библиотеку и добавитьapp.use(cors())в конвейер обработки запросов. - Важным нюансом является порядок подключения middleware: конфигурация CORS должна быть инициализирована перед ограничителем частоты запросов (rate limiter), чтобы сервер мог корректно отправлять ответы об ошибках, не блокируя их из-за политики безопасности.
Для более строгой безопасности рекомендуется указывать конкретный origin: app.use(cors({ origin: 'http://localhost:5173' })), разрешая доступ только вашему фронтенду.
🧩 Создание адаптивной сетки заметок и компонента NodeCard 2:22:31
После успешного получения данных с API — процесс которого ранее затрагивался в главе об архитектуре и CRUD-операциях — необходимо визуализировать список заметок на главной странице. Для этих целей идеально подходит CSS Grid, позволяющий создавать гибкие макеты.
Реализация сетки в React-компоненте выглядит следующим образом:
- Использование классов Tailwind CSS:
grid grid-cols-1, с переходом наmd:grid-cols-2иlg:grid-cols-3для обеспечения адаптивности на разных размерах экранов. - Создание отдельного компонента
NodeCardдля отображения каждой карточки заметки. Каждая карточка оборачивается вLinkизreact-router-dom, что позволяет перенаправлять пользователя на страницу детального просмотра конкретной записи при клике.
📅 Утилиты форматирования дат 2:28:32
Сырые данные из базы данных часто содержат даты в формате ISO, которые неудобны для чтения пользователем. Чтобы сделать интерфейс более дружелюбным, необходимо написать вспомогательную утилиту.
В папке lib/utils.js создается функция formatDate, которая принимает объект даты и преобразует его в читаемый вид. Этот подход позволяет централизовать логику форматирования и использовать её во всём приложении. В компоненте NodeCard мы просто оборачиваем node.createdAt в эту функцию, предварительно преобразовав значение через new Date(). Это завершает базовое визуальное оформление главной страницы, подготавливая её к интеграции функциональности удаления и редактирования, которые будут рассмотрены в следующих главах.
📝 Разработка формы создания заметок и архитектурная оптимизация Axios 2:31:48
Проектирование интерфейса и управление состоянием формы 2:31:48
Переходя к созданию новой страницы приложения, разработчик приступает к реализации интерактивной формы добавления заметок (create page). Работа начинается с проектирования реактивного состояния React, необходимого для отслеживания пользовательского ввода в реальном времени. Для этого инициализируются три ключевых хука useState:
-
title— строка для хранения заголовка заметки, по умолчанию пустая. -
content— строка для тела заметки, также инициализируемая пустым значением. -
loading— логический флаг (по умолчаниюfalse), управляющий отображением процессов отправки.
Флаг loading играет критическую роль в UX: в момент отправки формы он переключается в значение true, блокируя интерфейс от повторных случайных кликов.
Визуальный каркас страницы выстраивается с использованием контейнерной сетки Tailwind. Основной блок центрируется по горизонтали с помощью утилиты mx-auto и получает ограничение по максимальной ширине max-w-2xl. В верхней части формы размещается навигационная ссылка «Back to notes» для возврата на главную страницу, оформленная в виде прозрачной кнопки с иконкой ArrowLeftIcon из библиотеки lucide-react. Сама форма оборачивается в компонент карточки DaisyUI (базовые принципы стилизации которого подробно разбирались ранее в главе 5) с классом card-body. Поля ввода привязываются к соответствующим состояниям через обработчики событий: текстовое поле ввода заголовка обновляет состояние посредством setTitle(e.target.value), а многострочное поле textarea аналогичным образом синхронизируется с переменной content. Кнопка отправки формы динамически реагирует на статус запроса: если флаг loading активен, её текст меняется на «Creating...», а сам элемент становится неактивным (disabled).
Клиентская валидация и интеграция с API 2:38:06
Логика отправки формы инкапсулируется в асинхронной функции handleSubmit. Первым обязательным шагом внутри обработчика становится вызов e.preventDefault(), который предотвращает стандартное поведение браузера по перезагрузке страницы при отправке HTML-формы. Перед непосредственной отправкой данных на сервер реализуется строгая клиентская валидация. Чтобы исключить отправку пустых строк или полей, заполненных исключительно пробелами, к значениям title и content применяется метод .trim(). Если хотя бы одно из полей оказывается пустым, приложение прерывает выполнение функции и вызывает всплывающее уведомление toast.error с текстом «All fields are required».
При успешном прохождении валидации запускается контролируемый блок try...catch...finally. Сначала флаг loading переводится в положение true. Внутри блока try инициируется асинхронный HTTP-запрос методом POST через библиотеку Axios на локальный эндпоинт сервера бэкенда. В теле запроса передается объект, содержащий валидированные заголовок и содержимое заметки. В случае успешного сохранения документа в базе данных, библиотека react-hot-toast выводит сообщение «Note created successfully», а пользователя мгновенно перенаправляет на главную страницу при помощи хука useNavigate из пакета React Router. Блок finally гарантирует, что независимо от исхода сетевой операции, флаг loading вернется в состояние false, возвращая кнопке активность.
Особое внимание уделено обработке потенциальных ошибок бэкенда. Программа проверяет статус ответа: если error.response.status равен 429, это свидетельствует о срабатывании механизма ограничения частоты запросов Upstash Redis, внедренного ранее в главе 4. В таком случае пользователю выводится кастомный тост с иконкой черепа (skull icon), работающий на протяжении 4000 миллисекунд, с предупреждением: «Slow down, you are creating notes too fast».
Оптимизация архитектуры: глобальный экземпляр Axios 2:45:50
Дублирование жестко закодированных URL-адресов бэкенда (например, http://localhost:5000/api/...) в каждом отдельном компоненте создает серьезные проблемы при масштабировании и поддержке кодовой базы. Для устранения этой избыточности разработчик применяет паттерн создания изолированного глобального экземпляра Axios. В директории проекта создается конфигурационный файл axios.js, где с помощью метода axios.create() инициализируется кастомный инстанс, экспортируемый под именем API. В конфигурационном объекте задается свойство baseURL, указывающее на корневой путь серверного API.
Автор акцентирует внимание на важнейшем архитектурном правиле: базовый URL должен заканчиваться строго на префиксе /api, но не должен включать в себя путь конкретного ресурса, такого как /notes. Это стратегическое решение нацелено на будущее расширение функционала приложения. Если со временем в систему добавится, к примеру, модуль аутентификации пользователей, запросы должны будут уходить на эндпоинт /api/users. Благодаря гибкой настройке базового URL, разработчику не придется создавать новые конфигурационные файлы — достаточно будет использовать относительный путь API.post('/users').
После создания инстанса проводится рефакторинг существующих компонентов: в коде главной страницы и формы создания длинные строки запросов заменяются на лаконичные вызовы вроде API.get('/notes').
В качестве финального штриха улучшения клиентского интерфейса в этой главе затрагивается интеграция логики удаления элементов (что является частью CRUD-функционала, специфика бэкенд-реализации которого закреплена за главой 4). В компоненте NoteCard на кнопку удаления вешается асинхронный обработчик handleDelete, где вызов e.preventDefault() предотвращает нежелательный переход по ссылке на страницу детального просмотра. После подтверждения действия через системное диалоговое окно window.confirm, отправляется сетевой запрос API.delete. Чтобы интерфейс обновлялся мгновенно без перезагрузки всей страницы, в компонент передается функция изменения состояния setNodes, которая с помощью метода filter налету исключает удаленный объект из локального массива по его идентификатору _id.
📝 Управление данными: просмотр и обновление заметок 2:57:19
Для обеспечения полноценной работы приложения необходимо реализовать страницу детального просмотра, которая позволяет не только ознакомиться с содержимым заметки, но и внести в нее изменения или удалить ее.
Динамическая маршрутизация и useParams 2:59:05
Когда пользователь переходит к конкретной заметке, в URL-адресе передается уникальный идентификатор (ID). Для его извлечения на React-компоненте используется хук useParams. В коде это выглядит как деструктуризация объекта, возвращаемого хуком: мы извлекаем id (имя переменной должно совпадать с тем, что указано в настройках маршрутизации).
Использование деструктуризации и обертывание в фигурные скобки при выводе в консоль (console.log({ id })) является удобной практикой, позволяющей сразу увидеть в терминале структуру объекта и его значение.
Загрузка данных заметки 3:00:14
Для загрузки данных используется хук useEffect, который срабатывает каждый раз при изменении id в URL. Основная логика вынесена в асинхронную функцию fetchNote, которая выполняет GET-запрос к API.
- Состояния: Мы управляем тремя состояниями:
node(хранит данные заметки),loading(индикатор загрузки для UI) иsaving(индикатор для процесса обновления). - Обработка: В блоке
try/catch/finallyмы получаем ответ от сервера и обновляем состояние, при этом блокfinallyгарантирует отключение индикатора загрузки даже в случае ошибки.
Ранее в разговоре они касались архитектуры API и обработки CORS, что позволило успешно реализовать этот запрос.
Редактирование и удаление записей 3:06:27
Для обновления заметки мы используем HTTP-метод PUT. Функция handleSave проверяет наличие обязательных полей (заголовок и контент), а затем отправляет обновленный объект заметки на сервер по адресу /nodes/${id}.
Что касается удаления, функция handleDelete запрашивает подтверждение у пользователя. Если получено согласие, выполняется DELETE-запрос к API, после чего пользователь перенаправляется на главную страницу через navigate('/'). Эти операции гарантируют, что состояние UI всегда соответствует актуальным данным в базе данных.
🚀 Финальный аккорд: раздача статики и деплой MERN-приложения на Render 3:20:49
Конфигурация Express для продакшена и динамический API URL 3:20:49
Когда разработка всех базовых компонентов MERN-стека завершена, наступает критический этап — объединение фронтенда и бэкенда в единое целое для продакшен-среды. В режиме локальной разработки приложение традиционно использует два разных домена, однако для развертывания конфигурация кардинально меняется. На стороне сервера в файле server.js настраивается глобальный обработчик GET-запросов app.get('*'). Если входящий запрос не относится к существующим API-эндпоинтам, Express должен перенаправлять пользователя на собранное React-приложение. Для этого используется метод res.sendFile() в связке с утилитой путей path.join(__dirname, '../frontend/dist/index.html').
Эта логика изолируется внутри условной конструкции, проверяющей глобальную переменную окружения: if (process.env.NODE_ENV === 'production'). Такой подход гарантирует, что раздача статических файлов клиентского приложения активируется исключительно после деплоя на продакшен-хостинг Render.com. Синхронно меняется и логика работы с CORS, которая была детально настроена на ранних этапах курса для решения проблемы кросс-доменных запросов. Автор отключает CORS для продакшена: if (process.env.NODE_ENV !== 'production') app.use(cors()). Поскольку теперь и фронтенд, и бэкенд будут обслуживаться в рамках одного домена, необходимость в CORS полностью отпадает.
Параллельно изменения вносятся в клиентскую часть, а именно в конфигурационный файл axios.js. В процессе разработки базовый URL жестко ссылался на локальный порт localhost:5001. Для стабильной работы в облаке этот адрес должен определяться динамически. Используя внутренние механизмы сборщика Vite, автор задействует проверку среды через import.meta.env.MODE === 'development'. Если текущий режим соответствует разработке, Axios шлет запросы на локальный хост, в противном случае базовым путем становится относительный роут /api. Это исключает появление жестко прописанных адресов в кодовой базе.
Настройка скриптов автоматизации и локальное тестирование 3:22:51
Важным шагом в подготовке monorepo-репозитория является централизация команд сборки и запуска в корневом файле package.json. Для успешного старта платформе Render требуется единая команда запуска, которая прописывается в секции scripts как "start": "npm run start --prefix backend". Это позволяет активировать бэкенд-сервер напрямую из корня проекта, управляя подпапками monorepo. Перед отправкой кода в облако критически важно протестировать продакшен-сборку локально, чтобы исключить непредвиденные сбои на сервере.
В терминале выполняется команда npm run build, которая инициирует установку зависимостей для обеих частей приложения и создает оптимизированную версию клиентской сборки в папке dist. При первой попытке запустить проект через корневой скрипт npm run start автор сталкивается с ошибкой 404 Not Found. Причина кроется в отсутствии явно переданной переменной окружения NODE_ENV. После исправления команды запуска на NODE_ENV=production npm run start система отрабатывает корректно. Обновление страницы localhost:5001 наглядно доказывает успех: по адресу бэкенд-сервера теперь успешно отображается полноценный интерфейс React-приложения.
Корневой файл package.json берет на себя важнейшую функцию — он объясняет удаленному серверу Render, как пошагово работать с monorepo, где папки фронтенда и бэкенда разделены. Поскольку папка node_modules традиционно находится в файле .gitignore и отсутствует в удаленном репозитории GitHub, Render должен сначала установить абсолютно все пакеты, собрать оптимизированный продакшен-вариант фронтенда, а затем запустить бэкенд, который подхватит скомпилированные статические ассеты. Убедившись в полной работоспособности схемы, автор фиксирует изменения через Git последовательными командами git add ., git commit -m "prepared for the deployment" и отправляет их на GitHub с помощью git push.
Развертывание веб-сервиса на Render.com и магия CI/CD 3:28:33
Процесс деплоя разворачивается на облачной платформе Render.com, предоставляющей удобный бесплатный тарифный план для веб-сервисов. Автор авторизуется на сервисе через аккаунт GitHub и выбирает целевой репозиторий проекта. При настройке параметров веб-сервиса большинство полей остаются дефолтными, но ключевые команды переопределяются вручную:
- Build Command:
npm run build - Start Command:
npm run start
Особое внимание уделяется переменным окружения в панели управления Render. Автор копирует туда все конфигурационные ключи, включая секретные URI базы данных, за исключением NODE_ENV. Платформа Render по умолчанию определяет среду исполнения как продакшен, поэтому принудительно выставлять это значение в панели нет необходимости. Касаясь специфики бесплатного тарифа, спикер озвучивает важное практическое предупреждение:
«Поскольку это бесплатный тариф, сервис становится неактивным через 15 минут простоя. Если вы не заходили на сайт в течение этого времени, при следующем визите загрузка займет около одной минуты. Чтобы проект работал без пауз, можно перейти на минимальный платный тариф за 7 долларов в месяц».
Запуск процесса деплоя активирует интерактивные логи сборки. Платформа последовательно скачивает зависимости бэкенда и фронтенда, компилирует React-приложение и выводит сообщение об успешном завершении фронтенд-билда. В финале логов отображаются заветные системные уведомления: облачная база данных MongoDB успешно подключена, сервер запущен, а статус сервиса меняется на Live. Клик по сгенерированному URL-адресу открывает готовое приложение прямо в браузере. Тестирование на реальном сервере подтверждает полную исправность CRUD-функционала: тестовая заметка создается, мгновенно отображается в интерфейсе, успешно редактируется и удаляется.
Финальные штрихи и автоматический редеплой 3:32:45
Перед окончательным завершением проекта автор вносит последнее косметическое исправление в файл конфигурации upstash.js. Ранее в разговоре они касались ограничения частоты запросов с Upstash Redis, но оставленный в коде текстовый комментарий не соответствовал измененным лимитам. Текст комментария оперативно обновляется до актуальных продакшен-значений: «100 запросов в минуту / 60 секунд».
Этот финальный коммит с сообщением "rate limiter updated" отправляется в удаленный репозиторий GitHub. На этом примере спикер демонстрирует автоматическую работу встроенного механизма непрерывной интеграции и развертывания (CI/CD): Render мгновенно фиксирует новые изменения в ветке репозитория и самостоятельно запускает повторный деплой, избавляя разработчика от ручной пересборки проекта. Резюмируя итоги создания полноценного приложения с нуля, автор отмечает, что детальный разбор всех нюансов превратил часовую запись в монументальный трехчасовой курс, подкрепленный интерактивными схемами архитектуры.