
Аннотация
Работать одному над фронтендом масштабного проекта — это не только вызов, но и абсолютная архитектурная свобода. С другой стороны — это риск утонуть в техническом долге и не довести продукт до релиза.
В этой статье я делюсь практическим опытом создания клиентской части HR-платформы (соискатели, работодатели, вакансии, чаты) с нуля до продакшена силами одного инженера. Я расскажу о выборе стека, адаптации Feature-Sliced Design, укрощении модальных окон, работе с API и глобальном перехвате ошибок. В финале поделюсь результатами «боевого крещения» архитектуры при наплыве пользователей.
Введение — почему это интересно
Существует стереотип, что для создания сложного веб-приложения обязательно нужна команда: тимлид для архитектуры, мидлы для бизнес-логики и тестировщик. Но реалии таковы, что иногда весь фронтенд ложится на плечи одного человека.
Передо мной стояла задача: написать клиентскую часть HR-платформы. Продукт включал личные кабинеты кандидатов и работодателей, систему публикации и импорта вакансий, встроенный чат и информационные разделы (статьи, публикации).
Работать одному круто — ты сам себе архитектор, никто не спорит о код-стайле на часовых митингах. Но цена ошибки возрастает кратно. Выберешь плохую структуру папок — и через три месяца проект превратится в легаси. В этой статье я хочу поделиться тем, какие привычки и шаблоны помогли довести проект до релиза, а чего стоило избегать.
1. Что было важно принять в начале (мой чек-лист)
Перед тем как написать первую строчку кода, я составил для себя жесткие правила игры. Работая в одиночку, дисциплина заменяет код-ревью от старших коллег.

- Выбрать строгую структуру проекта (FSD) и не отступать от нее.
- Сделать единый API-слой для всех HTTP-вызовов (никаких fetch внутри компонентов).
- Перехватывать все серверные ошибки глобально, чтобы не писать try/catch на каждой странице.
- Настроить E2E-тесты для критических путей (я один, я не смогу тестировать руками всё).
- Максимально автоматизировать деплой.
2. Стек технологий — почему именно он
Когда ресурсов мало, инструменты должны помогать, а не вставлять палки в колеса. Я остановился на проверенном и современном наборе:

- React + TypeScript. TypeScript в строгом режиме здесь не просто дань моде. Для соло-разработчика это бесплатный тестировщик, который ловит 80% глупых опечаток (вроде user.id вместо user.uuid) еще на этапе компиляции.
- Vite. После тяжелого Webpack сборка на Vite ощущается как глоток свежего воздуха. Мгновенные перезапуски и HMR экономят десятки минут каждый день. Конфиг vite.config.ts занимает 30 строк и закрывает все потребности.
- Redux (RTK). Встроенного Context API не хватит для сложного приложения с чатами и ролями, он провоцирует лишние рендеры. Redux Toolkit дал мне предсказуемый контейнер состояния.
- Cypress. Быстрая уверенность в работоспособности сценариев (логин, профиль, создание вакансии).
- Docker + Nginx. Готовый и пуленепробиваемый рецепт для стабильного продакшена.
3. Структура проекта (Feature-Sliced Design на практике)
В начале пути всегда есть соблазн сложить все компоненты в папку components. Для пет-проекта это работает, для большой HR-платформы — это путь в ад. Я адаптировал методологию Feature-Sliced Design.
Мой проект разделен на четкие слои:

- app/ — точка входа: инициализация App.tsx, настройка store, провайдеры (with-providers.tsx).
- pages/ — маршруты. Они почти не содержат логики, их задача — просто собрать страницу из фичей.
- widgets/ — крупные агрегаты UI (Header, Footer, SideBar, ModalManager).
- features/ — конкретные бизнес-флоу (авторизация, чат, импорт вакансий).
- entities/ — доменные сущности (vacancy, candidate, employer). Внутри каждой сущности лежат свои папки: api, model, ui, service.
- shared/ — «глупый» переиспользуемый код: UI-библиотека (кнопки, инпуты), глобальный ApiClient, константы.
Это дает понятные границы. Изменение логики карточки вакансии в entities/vacancy физически не может сломать процесс регистрации в features/auth.
4. ApiClient и история про Refresh Token
Разбрасывать запросы к серверу по всем компонентам — ошибка. Я создал единый ApiClient.ts в слое shared. Любой HTTP-запрос в приложении проходит через него.
Реальная проблема, с которой я столкнулся:
Однажды баг с race condition (состоянием гонки) в refresh-token начал сводить меня с ума. Когда у пользователя истекал access_token, страница отправляла сразу 4 параллельных запроса (за профилем, вакансиями, чатами). Все 4 получали 401 ошибку, и приложение 4 раза пыталось обновить токен, сбрасывая сессию.
Как я это решил:
Помощь пришла от перехода на очередь запросов. Прямо в ApiClient я добавил логику: если один запрос ловит 401, клиент переходит в статус «обновляю токен», а все остальные запросы замораживаются в массив.
Как только токен обновлен, замороженные запросы выполняются с новым токеном. Минус — чуть больше кода, плюс — ноль вылетов у пользователей.

5. Глобальный перехват ошибок (чтобы не писать try/catch)
Писать обработку ошибок в каждом компоненте мучительно. Я вынес это в Redux, написав errorListenerMiddleware.ts.
Этот middleware слушает все экшены, которые завершились с ошибкой (rejected). Если сервер отвечает 403 (Нет прав) — всплывает глобальный Toast. Если 500 — Toast «Сервер временно недоступен». А если 401 — пользователя мягко редиректит на логин.
Это сделало компоненты страниц невероятно чистыми: они просто дергают данные из API и рендерят их. О том, что сеть может отвалиться, думает errorListenerMiddleware.

6. Маршрутизация по ролям и ленивая загрузка
Интерфейсы соискателя и работодателя кардинально отличаются. Сердцем маршрутизации стал мой компонент RoleBasedRoute.tsx.
Он оборачивает роуты и проверяет роль юзера из стора. Если соискатель случайно попытается зайти по ссылке /candidates, роутер мгновенно перехватит его и сделает редирект, не дав приложению отправить невалидные запросы на сервер.

UX-совет: показывайте пользователю только те действия, которые ему разрешены, вместо того чтобы бить его по рукам всплывающими окнами «Ошибка доступа» постфактум.
Также я активно использовал Code-Splitting (React.lazy). Модули работодателя физически не загружаются в браузер соискателя, что сильно уменьшило начальный бандл.
7. Укрощение UI: Стили и ModalManager
Модальные окна — вечная боль фронтенда. Если каждый компонент рендерит свою модалку, начинаются конфликты z-index. Я решил это централизованно: создал ModalManager в слое widgets.

В Redux хранится стейт activeModalType. Вызвать окно редактирования профиля можно из любой глубины приложения одной строкой: dispatch(openModal('EDIT_PROFILE')). ModalManager видит изменение стейта и рендерит нужный попап через React Portals поверх всего приложения. Чисто и элегантно.
Стили лежат в двух местах: src/styles/* и index.css отвечают за глобальные токены (переменные цветов, шрифтов), а специфика страниц изолирована в CSS Modules (например, contestInstructionsPage.module.css).
8. Тестирование: Какой охват нужен одному разработчику?
Писать тесты одному кажется потерей времени, пока ты не сломаешь прод. Я выбрал компромисс: юнит-тесты только для сложной бизнес-логики, а основное время — на E2E в Cypress.
В папке cypress/e2e/ лежат сценарии, которые прокликивают критичные пути: регистрация, поиск вакансии, успешный отклик, создание вакансии работодателем.
Если эти тесты зеленые, я уверен, что ядро продукта работает. Плюс строгий ESLint и commitlint для гигиены кода. Эти выходные, потраченные на Cypress, окупились сполна при регрессионном тестировании крупных фичей.

9. Деплой и проверка боем (Стресс-тест)
Я терпеть не могу деплоить руками. В корне проекта лежит Dockerfile, использующий multi-stage сборку. Сначала стартует Node, запускает npm run build через Vite, а затем берет легкий образ Nginx и подкладывает туда готовую папку dist. Файл nginx.conf настроен на отдачу index.html и сжатие статики.

История из продакшена
Теория архитектуры — это прекрасно, но настоящая проверка происходит в бою. На сегодняшний день на платформе успешно зарегистрировано уже более 2 000 пользователей.
Но главное крещение архитектуры состоялось, когда платформа выступила площадкой для масштабного профильного конкурса. В час X мы получили резкий наплыв: около 600 человек одновременно ринулись на сайт. Они читали инструкции, заполняли профили и откликались на вакансии и выполняли тестирование.
Для бэкенда это был огромный стресс. А фронтенд справился безупречно. Поскольку мы использовали статический деплой (SPA, отдаваемый через Nginx), генерация страниц вообще не требовала серверных процессорных мощностей. Nginx просто отдавал кэшированные статические файлы JS и CSS.
Те редкие API-запросы, которые в первые минуты падали по таймауту из-за перегрузки бекенда, мягко перехватывались нашим errorListenerMiddleware, показывая дружелюбные уведомления. Архитектура выдержала пик конкурса без единого сбоя на стороне клиента.
Заключение — что взять с собой
Написать сложный фронтенд в одиночку и довести его до продакшена — абсолютно реальная задача. Ключ к успеху не в бессонных ночах (хотя и в них тоже), а в строгой дисциплине.
Мои главные выводы:
- Структура важнее фичей. FSD спас проект от превращения в легаси.
- Единые точки входа экономят время. ApiClient и errorListenerMiddleware освободили UI от сетевого бойлерплейта.
- Не плодите модалки. Один ModalManager на все приложение решает 99% проблем с интерфейсом.
- Тесты — не роскошь. E2E покрытие критичных путей спасает релизы, когда нет QA-инженера.
- Слушайте пользователей. Иногда быстрая кнопка «Сохранить черновик» важнее идеальной анимации.
Созданная архитектура позволила запустить продукт в сжатые сроки. Она выдержала испытание реальной нагрузкой, и теперь, по мере роста продукта, мы можем легко масштабировать кодовую базу и подключать новых фронтендеров — проект к этому полностью готов.
Список литературы и ресурсы
- Официальная документация React
https://react.dev
- Vite: Next Generation Frontend Tooling
https://vitejs.dev
- Feature-Sliced Design (FSD) Architectural Methodology
https://feature-sliced.design
- Документация Redux Toolkit
https://redux-toolkit.js.org
- Cypress End-to-End Testing
https://www.cypress.io
- Nginx Official Documentation
https://nginx.org/en/docs
