ReactTypeScriptJavaScript

Как бы я спроектировал marketplace для электроники на React + NestJS + TypeScript

18 декабря 2025
12 мин чтения

Если бы в 2025 году мне нужно было спроектировать marketplace для электроники с нуля, я бы начинал не со стека, а с неприятной правды про сам продукт. Marketplace электроники — это не “интернет-магазин, только побольше”. Это система, в которой одновременно живут витрина, каталог, сложная фильтрация, карточка товара, поиск, корзина, оформление заказа, личный кабинет, логистика, промо-логика, отзывы, остатки, цены, маркетинговые страницы, продавцы, интеграции, админка и, скорее всего, еще B2B- или сервисные контуры. Внешне пользователь видит каталог и карточку товара. Внутри это уже давно не просто фронтенд с бэкендом, а довольно плотная прикладная платформа.

Именно поэтому хороший проект такого продукта не начинается с вопроса “React или Next?”, “Nest или что-то полегче?”, “Prisma или ORM X?”. Все это важно, но вторично. Если архитектура изначально мыслится как витрина плюс заказ, через год команда почти неизбежно захочет переписывать половину системы. Потому что marketplace электроники очень быстро обрастает не только новыми экранами, но и новыми типами поведения: сложной каталогизацией, вариативностью товарных данных, пересечением поисковых и коммерческих сценариев, контентом, промо, персонализацией, сервисными ограничениями и несколькими уровнями внутренних интерфейсов. Хорошая новость в том, что React, NestJS и TypeScript в 2025 году — вполне зрелая база для такой системы. React 19 стабилен с декабря 2024 года, NestJS 11 вышел в январе 2025 года, а TypeScript 5.9 доступен с августа 2025 года, так что стек уже не выглядит “на грани эксперимента”, а вполне соответствует production-реальности конца 2025 года.

Но зрелость стека сама по себе ничего не гарантирует. На таком продукте можно построить как очень живучую систему, так и очень убедительную будущую катастрофу. Поэтому, если бы я проектировал такой marketplace сегодня, я бы сознательно искал границу между удобством и управляемостью почти в каждом слое: в каталоге, в API, в shared типах, в поиске, в checkout, в личном кабинете, в админке и в интеграциях. Не потому, что люблю усложнять. А потому, что именно такие продукты умирают не от отсутствия фич, а от расползания границ.

Я бы начал с деления продукта на режимы, а не на страницы

Одна из самых частых ошибок в e-commerce-проектах — мыслить систему страницами: каталог, карточка, корзина, checkout, профиль. Для первого месяца так удобно. Для второго года — вредно. Потому что разные участки marketplace живут в принципиально разных режимах, и архитектура должна отражать именно это. Я бы выделил как минимум четыре больших режима.

Первый — витринно-контентный: главная, брендовые страницы, акции, подборки, SEO-категории, статьи, landing pages. Здесь важны скорость первого рендера, индексируемость, кэширование, предсказуемый HTML и аккуратная server-first подача. React 19 и современный full-stack React уже давно двигают экосистему в сторону более естественного server/client разделения, но это не значит, что вся витрина автоматически должна становиться “полным full-stack чудом”. Для таких страниц я бы выбирал максимально документную модель и очень дозированную интерактивность.

Второй режим — исследовательский каталог: фильтры, сортировки, листинги, результаты поиска, брендовые и категорийные выдачи. Это уже не просто страницы, а интерфейс исследования ассортимента. Тут важны кэш запросов, сохранение параметров в URL, быстрый возврат назад, стабильный scroll restoration, предсказуемая модель фильтрации и очень аккуратное отношение к клиентскому состоянию.

Третий режим — транзакционный: корзина, checkout, применение промокодов, адреса, доставка, оплата, подтверждение заказа. Здесь цена ошибки выше, чем в каталоге, а UX должен быть менее “богатым” и более надежным. Я бы проектировал этот слой гораздо консервативнее: меньше магии, больше явных шагов, больше контрактной строгости на API, больше внимания к optimistic behavior только там, где он действительно не ухудшает доверие.

Четвертый режим — кабинетно-операционный: профиль, заказы, возвраты, сервисные обращения, wishlist, подписки на снижение цены, seller-панель, админка и внутренние инструменты команды. Это уже совсем другой тип приложения: долгие сессии, больше локального состояния, больше рабочих сценариев, больше потребность в устойчивой модульной архитектуре.

Если не признать эти режимы сразу, система начнет разъезжаться. Команда будет пытаться применить одни и те же правила к витрине, каталогу и checkout, а это почти гарантированный путь к внутреннему конфликту архитектуры.

На фронтенде я бы строил не “один большой app”, а несколько согласованных зон

Если говорить о React-части, я бы сознательно избегал идеи одного огромного фронтенда, где все живет по одним и тем же законам. У продукта такого типа слишком разная природа экранов. Я бы делал единый репозиторий и единый design/platform слой, но внутри него — разные архитектурные режимы.

Для витринных и контентных страниц я бы выбирал server-first подачу с очень аккуратной интерактивностью. React 19 делает full-stack сценарии реалистичнее, особенно вокруг форм, actions и серверного контура, но для маркетинговых и SEO-важных участков мне по-прежнему важнее предсказуемый HTML, быстрый первый рендер и минимизация ненужного клиентского кода, чем желание “везде использовать новую модель”.

Для каталога и поиска я бы строил клиентскую оболочку поверх серверно-подготовленного старта. То есть initial state выдачи и SEO-значимые части — с сервера, а дальше уже очень продуманная клиентская жизнь: URL как источник фильтров, кэш по ключу запроса, быстрый back navigation, controlled infinite scroll только там, где он действительно полезен, и очень жесткое ограничение на хаос в модульной структуре.

Для checkout я бы был наиболее осторожен. В 2025 году очень легко увлечься красивыми мутационными сценариями, optimistic UI и actions everywhere. Но checkout для электроники — это место, где пользователь должен доверять системе. Я бы использовал React 19 там, где он действительно уменьшает glue code вокруг форм и отправки шагов, но не превращал бы критический транзакционный поток в playground новых паттернов только потому, что они эстетически приятны.

Для кабинета и внутренних интерфейсов я бы проектировал более “приложенческий” React: модульные feature-области, разделение UI, data и use-case логики, ясный data layer и более строгую организацию общего состояния.

State management я бы делил по природе состояния, а не по любви к одному инструменту

В marketplace электроники очень легко сделать одну из двух ошибок. Либо сложить все в один глобальный store ради “целостности”. Либо, наоборот, понадеяться, что локальные hooks и кэш запросов чудом решат весь state landscape. Оба пути плохо стареют.

Я бы разделил состояние минимум на четыре класса. Server state: товары, цены, остатки, списки, карточка, заказы, профили, промо-данные, все, что приходит с сервера и живет по законам кэша, инвалидации и повторного чтения. UI state: открытые панели, сортировки интерфейса, режимы отображения, локальные контролы. Workflow state: многошаговый checkout, черновики форм, сценарии возврата, сервисные потоки. И cross-session preference state: избранное, недавно просмотренное, предпочтения доставки, настройки отображения.

Я бы не пытался доказать миру, что один store manager обязан решать все это одинаково красиво. В 2025 году это уже не выглядит зрелостью. Гораздо зрелее — подобрать инструмент под класс состояния и удерживать правила, где какое состояние живет. Главный принцип здесь простой: чем системнее и чувствительнее состояние к согласованности и воспроизводимости, тем более структурированным должен быть слой, который им управляет. Чем локальнее и UI-ориентированнее оно по природе, тем меньше смысла тащить его в тяжелую централизованную схему.

Каталог и карточка товара я бы проектировал вокруг product view model, а не вокруг сырой серверной сущности

Очень многие marketplace-проекты плохо стареют из-за того, что фронтенд слишком близко живет к серверной товарной модели. Для электроники это особенно опасно. Один товар может иметь сложные атрибуты, вариации, совместимые аксессуары, условия доставки, промо-правила, статусы availability, трейд-ин, гарантию, seller-dependent поля, контентные блоки и кучу всего еще. Если дать фронтенду “одну общую product entity” и надеяться, что UI сам ее переварит, карточка и каталог быстро превратятся в слой случайных адаптеров и условий.

Я бы строил явные view models под главные сценарии: product list item, product card summary, product detail, product compare item, checkout line item, order line snapshot. Да, это больше форм данных. Но это защищает границы между слоями и делает эволюцию системы дешевле. Фронтенду нужен не “товар вообще”, а товар в конкретной форме для конкретного экрана.

На бэкенде это означает, что API не должно быть просто отражением внутренней persistence-модели. На фронтенде — что shared types не должны диктовать UI свою внутреннюю серверную анатомию. Это как раз тот случай, где full-stack TypeScript дает огромный соблазн сделать “одну общую Product”, и именно здесь я бы сознательно сопротивлялся.

Поиск я бы выделил в отдельную архитектурную подсистему

Для marketplace электроники поиск — это не “еще один endpoint рядом с каталогом”. Это отдельная зона продукта с собственными законами. Пользователь может искать по модели, бренду, характеристике, артикулу, совместимости, категории, пользовательскому намерению. Результаты могут смешивать товары, бренды, категории, аксессуары и промо-объекты. Сортировки и фильтры должны быть очень быстрыми, а сама логика ранжирования редко живет в том же слое, что CRUD по товарам.

Поэтому я бы не прятал поиск внутри “catalog module” как будто это просто разновидность list endpoint. Я бы выделил отдельный поисковый контур: индексирование, read model под выдачу, фасетные фильтры, возможно, отдельный search backend или хотя бы отдельный query layer на NestJS. Фронтенд при этом должен общаться не с товарной сущностью, а с выдачей как с самостоятельным контрактом.

Это дорого в начале, но окупается невероятно быстро. Почти все большие проблемы marketplace не в том, как красиво показать карточку, а в том, как удержать каталог и поиск от превращения в гигантскую кашу из product data, search logic и UI-specific костылей.

На NestJS я бы строил backend как модульную систему use-case областей, а не как слой сущностей

Если говорить о NestJS-части, я бы сознательно избегал наивной схемы “users module, products module, orders module” как основного архитектурного каркаса. Не потому, что такие модули не нужны, а потому, что marketplace-поведение редко укладывается в сущностные коробки. Оформление заказа пересекает корзину, пользователя, цены, доставку, промо, наличие и оплату. Карточка товара пересекает каталог, отзывы, контент, остатки, медиа и seller-данные. Возврат — это уже отдельная бизнес-область, а не просто метод в orders service.

NestJS 11 к концу 2025 года уже выглядит вполне зрелой базой для такого подхода: современный Node baseline, Express 5 по умолчанию, более внятные lifecycle- и middleware-правила делают фреймворк хорошим кандидатом на корпоративно-живучий backend-каркас.

Я бы строил backend вокруг use-case областей: catalog browsing, product presentation, search, cart, checkout, pricing, promotions, fulfillment, account, reviews, content, seller operations, admin operations. Внутри этих областей уже могут жить свои query services, command services, policies, adapters и инфраструктурные детали. Но внешняя форма системы должна совпадать с реальными потоками продукта, а не с красивой диаграммой таблиц.

Контракт API я бы делал удобным для фронтенда, а не “чистым” по ощущениям backend-команды

Marketplace для электроники почти гарантированно будет жить на большом количестве экранов, форм и витринных сценариев. Поэтому качество API-контракта напрямую определит стоимость фронтенда через полгода. Я бы не делал API как “максимально абстрактно-RESTful отражение доменной модели”. Я бы делал его как набор очень последовательных контрактов потребления.

Что это значит на практике? Единая модель пагинации. Ясная схема ошибок. Предсказуемые envelopes там, где они реально нужны. Разные response DTO под list, detail, compact и transactional use cases. Четкое различие между input и output моделями. И, очень важно, отдельное уважение к BFF- или frontend-facing read contracts там, где фронтенду нужна более плоская и удобная форма данных, чем серверной предметной модели.

Для NestJS + OpenAPI это означает, что документация должна быть не просто красивой Swagger-страницей, а реальным контрактным слоем. И если где-то я бы сознательно позволил себе дублирование, то именно здесь: лучше несколько осмысленных response-моделей, чем одна “универсальная” сущность, которая потом отравит и UI, и серверный код.

Shared TypeScript-слой я бы держал очень скучным

В full-stack TypeScript 2025 года один из самых опасных соблазнов — тащить в shared все, что приятно поделить. Я бы делал наоборот. Shared слой в таком проекте должен быть максимально скучным. Контрактные DTO. Схемы валидации. Публичные enum. Typed clients. Event payload contracts. Но точно не серверная внутренняя логика, не “общая ProductEntity для всех”, не полу-доменные сервисы и не загадочный пакет, в котором “лежит вообще все полезное”.

Почему так строго? Потому что marketplace неизбежно будет эволюционировать асимметрично. Фронтенд и backend почти наверняка захотят смотреть на товар, заказ, доставку и промо по-разному. Если shared слой будет слишком умным, он очень быстро превратится в цемент между слоями. А такие продукты и так достаточно сложны, чтобы не усиливать связанность добровольно.

Поэтому я бы придерживался простого правила: делить договоренности, а не внутренности. Full-stack TypeScript дает невероятное удобство, но платить за него внутренней сцепкой системы — слишком дорогой обмен.

Checkout я бы проектировал как отдельную машину состояний, а не как “еще одну форму”

Очень многие e-commerce команды теряют управляемость именно здесь. Корзина и checkout кажутся просто последовательностью экранов, а потом внезапно выясняется, что там живет половина бизнеса: адреса, самовывоз, доставка, сроки, платежные ограничения, промокоды, валидация товара, перерасчет цены, региональные правила, проверки наличия, антифрод, подтверждение контактов и еще десяток вещей.

Если относиться к checkout как к обычной форме, он быстро станет самым токсичным слоем системы. Я бы изначально проектировал его как отдельный workflow-контур с явными состояниями, шагами и правилами переходов. На фронтенде — отдельный use-case слой, а не просто набор разрозненных form-компонентов. На бэкенде — отдельный orchestration service, а не несколько loosely coupled endpoint’ов, которые “вместе образуют checkout”.

React 19 здесь реально полезен в части form/mutation ergonomics, но это не повод думать, что сам workflow magically организуется хорошо. Наоборот: чем удобнее стала формная glue-логика, тем важнее не забыть, что checkout — это все еще сложный прикладной сценарий, а не только хороший DX вокруг submit.

Админку и seller-интерфейсы я бы не смешивал с клиентским storefront ни по модели, ни по ритму изменений

Еще один типичный источник проблем — желание держать “весь фронтенд вместе”, как будто витрина, кабинет пользователя, админка и seller-панель являются почти одним приложением. На короткой дистанции это удобно. На длинной — вредно. У этих интерфейсов разный UX, разная частота изменений, разная степень интерактивности и разная цена внутреннего технического долга.

Я бы сознательно разводил storefront и operational interfaces. Общий design/platform слой — да. Общие domain contracts — ограниченно, да. Но не одну общую UI-архитектуру без учета того, что admin и seller panel обычно гораздо ближе к “рабочему приложению”, чем к customer-facing фронтенду. Они должны иметь право жить немного по другим правилам, с более тяжелым state layer, с более приложенческими паттернами и с большим вниманием к долгой сессии, а не к первому рендеру.

Интеграции я бы проектировал как внешнюю среду, а не как “еще один модуль”

У marketplace электроники почти наверняка будут интеграции: ERP, PIM, склад, доставка, платежи, CRM, email/SMS, маркетплейсные каналы, supplier feeds, ценовые правила, контентные импорты. Одна из самых опасных ошибок — начать относиться к ним как к обычной части прикладного кода. На практике они живут по другим законам: нестабильность, ретраи, расхождения форматов, eventual consistency, зависимость от внешней инфраструктуры и отдельный мониторинг.

Поэтому я бы очень четко отделял интеграционный слой от внутренних use-case областей. NestJS для этого хорош именно как модульный каркас с явными adapters, lifecycle hooks и инфраструктурными сервисами. Но здесь как раз нельзя впадать в иллюзию, будто TypeScript и общая монорепа все гармонизируют сами. Внешняя система все равно остается внешней системой, даже если у вас красивый typed adapter.

Итог

Если бы я проектировал marketplace для электроники на React + NestJS + TypeScript в конце 2025 года, я бы не пытался сделать “один идеальный full-stack app”. Я бы строил набор согласованных, но разных по природе подсистем. Витрину — как server-first контентно-коммерческий слой. Каталог и поиск — как самостоятельный исследовательский контур. Checkout — как отдельный workflow со своей машиной состояний. Кабинеты, seller-панель и админку — как более приложенческие интерфейсы. Backend — как модульную систему use-case областей, а не как каталог сущностей. Shared слой — как скучный набор контрактов, а не как общий мозг всей системы.

Такой подход менее “монолитно элегантен” на диаграмме, зато гораздо лучше переживает рост продукта. А для marketplace это и есть главное. Здесь почти все со временем усложняется: каталог, цены, поиск, seller-процессы, промо, логистика, контент, возвраты. Если система изначально строится так, будто это просто витрина плюс заказ, она очень быстро начинает сопротивляться собственному бизнесу.

Если сформулировать главный вывод совсем прямо, он будет таким: я бы проектировал такой marketplace не вокруг стека, а вокруг разных режимов продукта и их границ — а React, NestJS и TypeScript использовал бы как зрелые инструменты для удержания этих границ, а не для их стирания ради удобства. Именно это, как мне кажется, и отличает архитектуру, которую через год хочется развивать, от архитектуры, которую через год уже хочется переписать.