Разработка · 7 мин чтения

Дашборд со связкой Vue и Laravel API: архитектура кейса

За последние пару лет я собрал с десяток дашбордов на связке Vue и Laravel API - для интернет-магазинов, логистики и внутренних CRM. Дашборд со связкой Vue и Laravel API - это SPA-фронт, который тянет данные через REST, и Laravel-бэкенд, который отдаёт JSON, считает агрегаты и держит авторизацию. Разберу архитектуру на конкретном кейсе: панель аналитики заказов для магазина на WooCommerce с эквайрингом T‑Bank и доставкой через СДЭК, куда клиент захотел отдельный Vue-дашборд поверх уже существующего Laravel API.

Зачем выносить аналитику в отдельный Vue-дашборд поверх Laravel API

У клиента уже была админка на Laravel с Blade-шаблонами: списки заказов, фильтры, карточки товаров. Работало, но каждый переход между разделами - новая загрузка страницы, 2-3 секунды на средней выборке в 15-20 тысяч заказов. Менеджеры открывали сводку по продажам по 30-40 раз в день, и эти секунды складывались в раздражение и тикеты в поддержку.

Решение - вынести аналитический блок в отдельное SPA на Vue, которое дергает тот же Laravel API через AJAX, без перезагрузки страницы. Переключение между виджетами и фильтрами стало мгновенным, а Laravel остался тем, для чего он и годится - бизнес-логика, очереди, работа с базой. Разделение фронта и бэка по разным репозиториям упростило и деплой: обновление дашборда не требует прогона миграций и не трогает основной интернет-магазин.

Архитектура связки: где Laravel API, где Vue-дашборд

Схема, которую использую чаще всего:

  • Laravel живёт как API-only приложение - только routes/api.php, никаких веб-роутов с Blade для дашборда.
  • Vue-приложение собирается через Vite в статику и раздаётся отдельно: либо тем же Nginx с другого location, либо вообще с отдельного поддомена вроде dashboard.example.ru.
  • Между ними - REST с JSON, авторизация через токены, CORS настроен на конкретный домен фронта, а не на *.
  • Тяжёлые агрегаты (суммы по периодам, воронки, сравнение с прошлым месяцем) считаются в очереди и кэшируются в Redis, а не пересчитываются на каждый запрос.

Важный момент - если дашборд и основной сайт на разных доменах, придётся продумать CORS и cookie-политику заранее, а не патчить постфактум. У меня был случай, когда фронт три дня падал с ошибками авторизации в проде именно из-за того, что SESSION_DOMAIN в Laravel не совпадал с доменом дашборда.

Авторизация: токены Sanctum против сессионной аутентификации

Для связки Vue и Laravel API есть два рабочих варианта авторизации, и выбор зависит от того, на одном домене живут фронт и бэк или на разных.

Вариант Когда использую Особенности
Sanctum SPA (cookie-based) Фронт и API на одном домене или поддомене CSRF-токен через /sanctum/csrf-cookie, сессия в httpOnly cookie, не нужно хранить токен в localStorage
Sanctum API tokens Фронт на отдельном домене, мобильные клиенты, интеграции Bearer-токен в заголовке, срок жизни настраивается, легко отозвать один токен без сброса пароля
Passport (OAuth2) Нужен доступ третьих сторон к API по протоколу OAuth2 Избыточен для внутреннего дашборда, оправдан только при внешних интеграторах

В кейсе с WooCommerce-магазином фронт и API оказались на разных доменах, поэтому взял Sanctum с bearer-токенами: логин отдаёт токен, Vue кладёт его в Pinia store и добавляет в заголовки через axios-интерсептор.

// routes/api.php
Route::post('/login', [AuthController::class, 'login']);

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/dashboard/orders-summary', [DashboardController::class, 'ordersSummary']);
    Route::get('/dashboard/delivery-status', [DashboardController::class, 'deliveryStatus']);
    Route::post('/webhooks/cdek', [CdekWebhookController::class, 'handle'])
        ->withoutMiddleware('auth:sanctum');
});

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Структура Laravel API: ресурсы, пагинация, кэш агрегатов

Отдельный слой API Resources - обязательная вещь, если не хотите отдавать во фронт сырые модели с лишними полями. Каждый эндпоинт дашборда у меня оформлен так: контроллер - сервис с бизнес-логикой - API Resource на выходе. Это разделение спасает, когда фронт просит добавить поле в ответ, а поменять нужно только Resource, не трогая контроллер.

По агрегатам подход такой:

Метод расчёта Скорость отдачи Когда применим
Запрос к БД на лету 2-4 секунды на 20-40 тыс. записей Разовые отчёты, узкая выборка
Кэш в Redis с TTL 80-150 мс Данные, которые не критично видеть с задержкой в 5-10 минут
Предрасчёт в очереди (queue job раз в час) 50-100 мс Тяжёлые агрегаты по большим периодам, дашборды для руководства

В проекте с WooCommerce агрегаты по заказам раньше считались на лету и грузили базу по 3-4 секунды на карточку. Перенёс расчёт в artisan schedule с job раз в 15 минут и кэшем в Redis - отдача упала до 80-120 мс, и база перестала проседать при одновременном заходе нескольких менеджеров.

Пагинацию для таблиц заказов сделал курсорной, а не offset-based - при 50+ тысячах строк offset-пагинация на больших страницах начинает заметно тормозить, курсорная держит стабильное время ответа независимо от глубины прокрутки.

Vue-часть дашборда: Pinia, axios-интерсепторы, виджеты

На фронте стек простой: Vue 3 + Pinia для стейта, axios с общим инстансом и интерсептором, который подставляет токен и ловит 401 для редиректа на логин.

// api/client.js
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'

const api = axios.create({ baseURL: import.meta.env.VITE_API_URL })

api.interceptors.request.use((config) => {
  const auth = useAuthStore()
  if (auth.token) config.headers.Authorization = `Bearer ${auth.token}`
  return config
})

api.interceptors.response.use(
  (res) => res,
  (err) => {
    if (err.response?.status === 401) useAuthStore().logout()
    return Promise.reject(err)
  }
)

export default api

Виджеты дашборда - отдельные компоненты, каждый со своим composable для загрузки данных (например, useOrdersSummary()), чтобы не тащить логику запросов в компонент напрямую. Это упрощает переиспользование одного виджета на разных экранах - у клиента, например, сводка по заказам стоит и на главном экране дашборда, и в отдельном отчёте за период.

Для графиков брал легкие библиотеки без лишнего веса в бандле - тяжёлые чарт-библиотеки на дашборде с 8-10 виджетами ощутимо увеличивают время первой загрузки, особенно если данные обновляются каждые несколько секунд.

Реалтайм-обновления и вебхуки от эквайринга и СДЭК

Часть дашборда - статус заказов и оплат в реальном времени. Два источника событий: вебхук эквайринга T‑Bank при смене статуса платежа и вебхук СДЭК при смене статуса доставки. Оба падают в Laravel как POST-запросы, обрабатываются в очереди (чтобы вебхук не ждал долгую обработку и не отваливался по таймауту), и уже из очереди летит broadcast-событие через Laravel Echo на self-hosted Soketi - Vue-дашборд ловит его и обновляет виджет без перезагрузки страницы.

Проверку подписи вебхука и защиту от повторной обработки одного и того же события я обычно не пишу с нуля каждый раз - в библиотеке готовых скриптов есть отработанные обработчики уведомлений СДЭК и эквайринга, которые беру за основу и адаптирую под конкретный проект.

Ещё источник данных в этом кейсе - лендинг на Tilda с формой заявки: лид падает вебхуком в тот же Laravel API, и дашборд показывает воронку целиком, от заявки на Tilda до оплаченного заказа с трек-номером СДЭК.

Деплой и типичные грабли

По деплою здесь два независимых пайплайна: Laravel API деплоится как обычно, Vue-дашборд собирается через vite build и выкладывается статикой. На CI у меня это два отдельных job, которые не блокируют друг друга - обновление вёрстки дашборда не требует прогона миграций и наоборот.

Из граблей, с которыми сталкивался на практике:

  • CORS настроен на * вместо конкретного домена - работает в деве, ловит блокировку в проде при HTTPS-редиректах.
  • Токен Sanctum без ограничения по времени жизни - если дашборд открыт на общем компьютере в офисе, лучше короткий TTL с рефреш-логикой, чем бессрочный токен.
  • Rate-limit на API не настроен под нагрузку дашборда - при автообновлении виджетов каждые 10-15 секунд несколько открытых вкладок легко упираются в лимит по умолчанию.
  • .env с продовыми ключами эквайринга попадает в публичный репозиторий фронта, если по невнимательности его коммитят вместе с конфигом сборки.

Если нужны алерты в Telegram при просадке метрик - например, конверсия упала ниже порога или очередь вебхуков от СДЭК встала - обычно подключаю бота на aiogram, который дергает отдельный защищённый эндпоинт API раз в несколько минут. Для разовых синхронизаций с внешними таблицами или почтовыми рассылками чаще беру n8n, а не пишу отдельный воркер в Laravel - на разовую интеграцию так уходит в разы меньше времени.

Весь цикл в этом кейсе - доработка Laravel API под нужды дашборда плюс сам Vue-фронт с шестью виджетами - занял пять недель одного разработчика, включая настройку вебхуков и реалтайм-обновлений.

Интерфейс аналитики и отчётов

Дашборд / BI

от 90 000 ₽

Подробнее →

Частые вопросы

Сколько стоит разработать дашборд со связкой Vue и Laravel API?

Дашборд/BI на Vue.js у меня стоит от 90 000 ₽, отдельный API-бэкенд на Laravel - от 100 000 ₽. Если нужен полный цикл - API с нуля плюс Vue-дашборд поверх него, включая авторизацию и вебхуки, - считаю от 190 000 ₽ в зависимости от числа виджетов и источников данных.

Можно ли подключить Vue-дашборд к уже готовому Laravel-проекту?

Да, это самый частый сценарий на практике: у клиента уже есть Laravel-бэкенд для магазина или CRM, и задача - добавить отдельный аналитический слой. Обычно требуется расширить API новыми эндпоинтами под нужды дашборда и настроить Sanctum, если авторизация раньше была только сессионной для Blade-админки.

Какая база данных нужна для дашборда с большими объёмами заказов?

MySQL или PostgreSQL справляются без проблем при условии, что тяжёлые агрегаты не считаются на лету при каждом запросе, а кэшируются или предрассчитываются в очереди. На объёмах свыше 100-150 тысяч заказов имеет смысл добавить индексы под конкретные фильтры дашборда и вынести историю в отдельные партиционированные таблицы.

Нужен ли отдельный сервер под Vue-фронт?

Нет, статику после сборки можно раздавать тем же Nginx, что обслуживает Laravel - достаточно отдельного location или поддомена. Отдельный сервер оправдан только при высокой нагрузке или если фронт и бэк обслуживают разные команды с независимым циклом релизов.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Самозанятый Калинкин Н. А. · работаю с физлицами и юрлицами

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно