Главная
Все проекты

Rank tracker

Разработка архитектуры и MVP SaaS-сервиса с нуля

Роль

Fullstack Developer / Software Architect (Solo)

Год

2026

Стек

NestJS 11Next.js 16 (React 19)Prisma 7PostgreSQL 16Redis 8BullMQTurborepoDockerDokploy

Задача

Задача

Продуктовая цель:
Разработать с нуля полноценный SaaS-сервис для SEO-специалистов и владельцев бизнеса, позволяющий автоматизировать мониторинг позиций сайтов в Яндексе и Google. Сервис должен избавить пользователей от ручных проверок: достаточно загрузить список ключевых фраз (с учетом региона и типа устройства), после чего система будет по расписанию собирать данные и наглядно показывать динамику ранжирования (рост, падения, попадание в Топ-3) в формате интерактивной пивот-таблицы.

Инженерный вызов:
В рамках pet-проекта стояла задача спроектировать не просто MVP, а production-ready архитектуру, готовую к реальным нагрузкам и масштабированию. Необходимо было:

  1. Построить отказоустойчивый асинхронный конвейер сбора данных (SERP), способный обходить ограничения и квирки внешних API.
  2. Исключить состояние гонки (race conditions) при расчете пользовательских квот (FREE/PAID тарифы) между планировщиком и ручными запусками.
  3. Обеспечить сквозную типобезопасность (End-to-End Type Safety) между независимыми приложениями фронтенда и бэкенда без дублирования кода.
  4. Настроить строгий многоуровневый rate-limiting для защиты как собственного сервера от абьюза, так и платного внешнего API от перерасхода запросов.

Этапы

01

Проектирование и архитектура (Spec-first)

  • Работа началась с составления дизайн-документов (Scope, Goals, Non-Goals) для каждого узла системы.
  • Для обеспечения единого источника правды (Single Source of Truth) был развернут монорепозиторий на базе Turborepo.
  • Схемы валидации написаны на Zod и вынесены в общий пакет @rank-tracker/shared, что гарантировало 100% синхронизацию API-контрактов между бэкендом (NestJS) и фронтендом (Next.js).
02

Безопасность, Auth и работа с данными

  • Настроена база данных PostgreSQL с ORM Prisma (схема прошла через 11 итераций/миграций).
  • Реализована stateless-аутентификация: JWT-токены хранятся исключительно в httpOnly cookies.
  • Для бесшовного пользовательского опыта (UX) на стороне Next.js настроен BFF-прокси, который перехватывает ошибки 401 и делает «тихий» (silent) refresh токенов без перезагрузки страницы.
03

Разработка ядра (Асинхронный SERP-конвейер)

  • Спроектирован механизм сбора позиций.
  • Запущены воркеры на BullMQ. Для защиты от race conditions при списании квот между ночным кроном и ручными проверками были написаны атомарные Lua-скрипты для Redis.
  • Реализован парсер XML-ответов от поисковиков со встроенным механизмом экспоненциальных ретраев для обхода специфичных ошибок API (например, «код 110»).
04

Клиентский интерфейс (Frontend & UX)

  • На базе React 19, Tailwind CSS 4 и компонентов shadcn UI разработан адаптивный дашборд.
  • Главный элемент — сложная интерактивная пивот-таблица, визуализирующая историю позиций.
  • Добавлен отказоустойчивый модуль импорта ключей из CSV/XLSX с автоматическим детектированием и исправлением битой кодировки (win1251).
05

Инфраструктура и CI/CD

  • Приложения упакованы в легковесные Docker-контейнеры (multi-stage сборка).
  • Настроен автоматизированный пайплайн в GitHub Actions: прогон линтеров, запуск юнит-тестов парсера на реальных XML-фикстурах, сборка образов, отправка в GHCR и автоматический деплой на продакшен-сервер через Dokploy.

Процесс

Rank Tracker — SaaS-трекер позиций сайта в Яндексе и Google

Pet-проект: полноценный SaaS-сервис мониторинга позиций сайта в поисковой выдаче — от идеи и спецификаций до CI/CD и продакшен-деплоя.

Стек одной строкой: TypeScript · NestJS 11 · Prisma 7 · PostgreSQL 16 · Redis 8 · BullMQ · Next.js 16 (App Router, React 19) · Tailwind CSS 4 · shadcn/Base UI · TanStack Query 5 · Zod 4 · Turborepo · Docker · GitHub Actions → GHCR · Dokploy


Идея

SEO-специалисту и владельцу сайта нужно знать, на каких позициях его сайт находится в поиске по важным запросам — и как эти позиции меняются со временем. Rank Tracker решает эту задачу:

  • пользователь создаёт проект (домен сайта) и добавляет ключевые фразы — с указанием поисковой системы (Яндекс / Google), региона и типа устройства (desktop / mobile / tablet);
  • система по расписанию (и по ручному запуску) опрашивает поисковую выдачу через SERP-API (xmlstock.com), находит позицию домена и сохраняет её в историю;
  • дашборд показывает пивот-таблицу истории позиций (даты — колонками, свежие слева), где попадание в топ-3 выделено золотым, рост — зелёным, падение — красным, плюс сводные карточки: всего ключей, средняя позиция, количество в топ-3.

Проект спроектирован как SaaS: тарифы FREE / PAID, дневные квоты проверок (FREE — 20/день), защита от абьюза, админ-инструменты для управления тарифами.

Возможности

  • Аутентификация: регистрация с подтверждением email, вход по JWT (access 15 мин + refresh 30 дней с ротацией) в httpOnly-cookies; Cloudflare Turnstile и лимит регистраций с одного IP.
  • Проекты: CRUD, домен уникален в рамках пользователя, настраиваемый интервал автопроверок.
  • Ключевые фразы: массовое добавление вставкой текста или импортом CSV/XLSX (до 2 МБ, автодетект «битой» кодировки win1251); каскадный выбор: поисковик → регион (отфильтрован по движку) → устройство; дедупликация на уровне БД.
  • Проверки позиций: ежедневный cron + ручной запуск (всех ключей или выборочно); Яндекс — топ-100, Google — топ-50; квоты на пользователя в день.
  • Дашборд: пивот-таблица истории за 30 дней с цветовой индикацией трендов, карточки статистики, переключатель Яндекс/Google (состояние в URL).

Архитектура

Монорепозиторий pnpm + Turborepo из трёх воркспейсов: apps/api (NestJS), apps/web (Next.js) и packages/shared — пакет Zod-схем, который делает контракты API едиными для фронта и бэка.

Пайплайн проверки позиций

Ядро проекта — асинхронный конвейер сбора позиций на BullMQ:

Пошагово:

  1. Триггер — ежедневный cron или ручной запуск из UI.
  2. Отбор — планировщик ставит в очередь только ключи, чья последняя проверка старше checkInterval проекта.
  3. Квота — атомарный Lua-скрипт в Redis резервирует слоты под дневным лимитом пользователя. Два режима: reserveExact (всё-или-ничего для ручных проверок, иначе 429 с деталями used/limit) и reserveUpTo (best-effort для ночного cron — берёт сколько осталось).
  4. Очередь — задания с 3 попытками и экспоненциальным backoff; ретраится только то, что помечено как retryable.
  5. Fetch — общий token-bucket на 20 req/s к платному API; Яндекс отдаёт топ-100 одним запросом благодаря deep grouping, Google ограничен 10 результатами на страницу — сервис обходит до 5 страниц с ранним выходом и обрабатывает особенность xmlstock: код 110 («запрос поставлен в очередь») → повтор того же запроса до 4 раз с паузой 7 с.
  6. Парсинг и сохранение — защитный парсинг XML без any, нормализация домена (протокол/www/порт/путь), запись PositionCheck (null = домена нет в топе).

Модель данных

Технологический стек

СлойТехнологии
BackendNestJS 11, Prisma 7 (@prisma/adapter-pg, multi-file схема), PostgreSQL 16, Redis 8 + ioredis, BullMQ, passport-jwt, argon2, nodemailer, fast-xml-parser, Joi (env) + Zod/nestjs-zod (DTO)
FrontendNext.js 16 (App Router, RSC, standalone), React 19, TypeScript, Tailwind CSS 4, shadcn на Base UI, TanStack Query 5, Cloudflare Turnstile
Shared@rank-tracker/shared — Zod 4 схемы и контракты API (единый источник правды для обоих приложений)
Импортcsv-parse, SheetJS (xlsx), iconv-lite
ИнфраTurborepo, Docker multi-stage (turbo prune --docker, node:22-alpine, non-root), docker-compose (Postgres + Redis с ACL), GitHub Actions → GHCR, деплой на Dokploy
ТестыJest 30, юнит-тесты SERP-парсера на реальных XML-фикстурах, e2e через supertest

Инженерные решения

Атомарные квоты на Redis Lua. Ручная проверка и ночной cron могут конкурировать за дневной лимит одного пользователя. Вместо GET+SET с гонкой — Lua-скрипт через EVAL: проверка и резервация происходят атомарно, ключ привязан к дате в таймзоне пользователя сервиса (TTL 25 ч). Перерасход лимита невозможен в принципе.

Единые Zod-контракты вместо tRPC. Одна схема в packages/shared порождает: валидацию запросов в NestJS (createZodDto), типы TypeScript и клиентскую валидацию форм. Фронт и бэк физически не могут разъехаться по формату данных — при этом API остаётся обычным REST, доступным любому клиенту.

BFF-прокси с прозрачным refresh. Токены живут только в httpOnly-cookies — JS их не видит. Route handlers Next.js проксируют запросы к NestJS; при 401 прокси сам вызывает /auth/refresh, подмешивает обновлённые set-cookie в повтор запроса и пробрасывает их браузеру. Пользователь не замечает истечения access-токена.

Двухдвижковый SERP-клиент с обработкой квирков. У Яндекса и Google через xmlstock разная механика: Яндекс отдаёт топ-100 одним запросом (deep grouping), Google — максимум 10 результатов на страницу, поэтому реализован постраничный обход с ранним выходом и ретраями фирменного «кода 110». Ошибки типизированы (SerpFetchError с флагом isRetryable) — политика ретраев очереди опирается на природу ошибки, а не ретраит всё подряд.

Rate limiting на трёх уровнях, каждый на своём месте. Per-IP лимит регистраций (Redis, 3/день — считает и неудачные попытки, чтобы душить перебор капчи), limiter воркера BullMQ (100 jobs/s), in-process token-bucket к платному SERP-API (20 req/s по лимитам Yandex.XML).

Импорт, который переживает реальные файлы. CSV из русского Excel часто приходит в win1251: парсер детектит битую декодировку по символам U+FFFD и перечитывает файл через iconv-lite; поддерживаются разделители ;/, и XLSX. Результат парсинга проходит через ту же Zod-схему, что и ручной ввод.

Безопасность по умолчанию. argon2id для паролей, ротация refresh-токенов, каждый запрос к данным скоупится по userId на уровне сервисов (WHERE {id, userId} — IDOR исключён архитектурно), Turnstile на регистрации, в Redis отключён default-пользователь и создан ACL-пользователь с ограниченными правами.

Проверяемость пайплайна. Юнит-тесты парсера гоняются на реальных записанных XML-ответах обоих поисковиков; для каждого шага конвейера есть smoke-скрипт (test-xmlstock, test-queue, test-processor); админ-CLI (set-user-plan) включён в прод-образ и запускается через tsx прямо в контейнере.

Процесс разработки

  • Spec-first: каждый инкремент пайплайна (клиент xmlstock → Redis compose → очередь → процессор → HTTP-триггер) начинался с дизайн-документа с явными Scope / Goals / Non-Goals (docs/superpowers/specs/).
  • Эволюция схемы БД — 11 Prisma-миграций: от init до отказа от серверных сессий в пользу stateless JWT, каскадных удалений, анти-абьюз полей и переноса поисковой системы с проекта на регион (поддержка Google в тех же проектах).
  • CI/CD: gate из lint + тестов → матричная сборка Docker-образов API и Web → push в GHCR с кэшированием слоёв и тегами latest/sha/tag → деплой на Dokploy. Прод-контейнер API сам применяет миграции при старте (осознанное допущение single-replica, зафиксировано в комментарии).
  • Feature-ветки через git worktree, слияние через PR.

Планы развития

  • Абстракция SearchProvider — подключение DataForSeo / SerpApi как альтернативных источников SERP.
  • Aggregation worker: видимость, динамика средней позиции, рост/падение по проекту.
  • Биллинг для тарифа PAID.
  • Графики истории позиций в дополнение к пивот-таблице.
  • Проверка отдельных слов
  • Экспорт данных в XLSX и CSV
  • Анализ данных таблицы за выбранный период с генерацией краткого отчета с помощью ИИ

Результаты

50

concurrent-воркеров в BullMQ

3

независимых уровня Rate Limiting

2

режима резервирования квот

Экраны