За последние пару лет я собрал с десяток дашбордов на связке 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 или поддомена. Отдельный сервер оправдан только при высокой нагрузке или если фронт и бэк обслуживают разные команды с независимым циклом релизов.