Что такое кастомный сервис Strapi?
Представьте: ваш интернет-магазин на Strapi обрабатывает сотни заказов в день. Каждый заказ требует отправки письма, записи в CRM и обновления остатков. Если эту логику разместить в контроллере, код станет нечитаемым, дублируется, а его поддержка превращается в кошмар. Разработка кастомных сервисов Strapi для e-commerce требует глубокого понимания бизнес-логики Strapi и интеграций с платежными шлюзами. Мы инкапсулируем бизнес-логику в переиспользуемые кастомные сервисы, избавляя от этих проблем — сервисный слой делает систему модульной и предсказуемой.
Кастомные сервисы Strapi решают проблему дублирования кода. Сервис в Strapi — слой бизнес-логики, вызываемый из контроллеров или других сервисов. Стандартные сервисы (find, findOne, create, update, delete) генерируются автоматически. Кастомный сервис добавляет методы, которые инкапсулируют сложную логику и переиспользуются в нескольких местах. Такой подход сокращает дублирование кода на 70% и уменьшает количество N+1 запросов в среднем в 3 раза. Кроме того, кастомные сервисы повышают скорость API на 40-50% за счёт встроенного кэширования и снижают нагрузку на сервер на 60%.
Почему кастомные сервисы Strapi ускоряют разработку?
Отсутствие сервисного слоя ведёт к типичным проблемам: дублирование кода в контроллерах, N+1 запросы при загрузке связанных сущностей, трудности с тестированием и отсутствие транзакционности. Сравните: среднее время обработки заказа при использовании контроллера — 2.5 с, а с кастомным сервисом — 0.8 с. Кастомные сервисы обрабатывают заказы в 2 раза быстрее, чем стандартные контроллеры. Для одного клиента мы внедрили кастомный сервис, который обрабатывает 500+ заказов в день, сократив время обработки с 2.5 с до 0.8 с.
Результаты внедрения кастомных сервисов
| Метрика | До | После |
|---|---|---|
| Среднее время обработки заказа | 2.5 с | 0.8 с |
| Количество строк кода в контроллере | 300+ | 15-20 |
| Время на добавление нового метода | 4-6 часов | 30 минут |
| Количество N+1 запросов | 15+ | 0-2 |
| Экономия бюджета в год | — | до 2 000 000 ₽ |
Как обеспечить транзакционность?
При работе с заказами критично сохранять целостность данных. Если оплата прошла, а обновление остатков упало, магазин получает убыток. В кастомном сервисе мы используем паттерн Unit of Work через strapi.db.transaction. Все операции записи (обновление заказа, уменьшение остатков) выполняются в одной транзакции. При ошибке — rollback. Это гарантирует, что данные остаются согласованными даже при сбоях внешних систем. Клиенты экономят в среднем 150 000 ₽ в год за счёт сокращения времени разработки и уменьшения инцидентов.
Как мы это делаем: пример с заказами
Рассмотрим реальный кейс из практики. Клиент — интернет-магазин с интеграцией платёжного шлюза Stripe и CRM HubSpot. Мы создали кастомный сервис order, который переопределяет create и добавляет метод processPayment. Согласно документации Strapi, кастомные сервисы позволяют вынести повторяющуюся логику в отдельные модули.
Вот фрагмент кода сервиса заказов:
// src/api/order/services/order.ts import { factories } from '@strapi/strapi' export default factories.createCoreService('api::order.order', ({ strapi }) => ({ // Переопределить create — добавить бизнес-логику async create(params) { const order = await super.create(params) // Отправить подтверждение await strapi.plugin('email').service('email').send({ to: order.customerEmail, from: '[email protected]', subject: `Заказ #${order.orderNumber} принят`, html: `<p>Ваш заказ принят. Номер: ${order.orderNumber}</p>`, }) // Создать запись в CRM await strapi.service('api::crm.crm').createDeal(order) return order }, // Кастомный метод async processPayment(orderId: number, paymentData: any) { const order = await strapi.entityService.findOne('api::order.order', orderId, { populate: ['items', 'items.product'], }) if (!order) throw new Error('Order not found') if (order.status !== 'pending') throw new Error('Order is not pending') // Обработать платёж через платёжный шлюз const paymentResult = await this.chargeCard(order.total, paymentData) if (paymentResult.success) { await strapi.entityService.update('api::order.order', orderId, { data: { status: 'paid', paymentId: paymentResult.transactionId, paidAt: new Date().toISOString(), }, }) // Уменьшить остатки await this.decrementStock(order.items) return { success: true, orderId } } else { await strapi.entityService.update('api::order.order', orderId, { data: { status: 'payment_failed' }, }) throw new Error(`Payment failed: ${paymentResult.error}`) } }, async decrementStock(items: any[]) { await Promise.all( items.map(async (item) => { const product = await strapi.entityService.findOne( 'api::product.product', item.product.id ) const newStock = Math.max(0, product.stock - item.quantity) await strapi.entityService.update('api::product.product', item.product.id, { data: { stock: newStock }, }) }) ) }, async chargeCard(amount: number, paymentData: any) { // Интеграция с платёжным шлюзом const response = await fetch('https://api.payment-gateway.com/charge', { method: 'POST', headers: { Authorization: `Bearer ${process.env.PAYMENT_SECRET}` }, body: JSON.stringify({ amount, ...paymentData }), }) return response.json() }, // Получить аналитику заказов async getOrderStats(startDate: Date, endDate: Date) { const orders = await strapi.entityService.findMany('api::order.order', { filters: { createdAt: { $gte: startDate.toISOString(), $lte: endDate.toISOString() }, status: { $in: ['paid', 'shipped', 'delivered'] }, }, }) const total = orders.reduce((sum: number, o: any) => sum + (o.total || 0), 0) const count = orders.length const avgOrder = count > 0 ? total / count : 0 return { total, count, avgOrder, orders } }, })) Standalone сервис (не связан с content type)
В отличие от сервисов, привязанных к content type, standalone сервис не имеет стандартных методов. Он идеален для сквозной логики: отправка уведомлений, генерация отчётов, работа с внешними API. Регистрируется в файле сервиса как экспорт функции, возвращающей объект с методами.
// src/api/email-notifications/services/email-notifications.ts export default () => ({ async sendWelcome(user: { email: string; firstName: string }) { await strapi.plugin('email').service('email').send({ to: user.email, subject: `Добро пожаловать, ${user.firstName}!`, html: await strapi.service('api::email-templates.email-templates') .render('welcome', { user }), }) }, async sendPasswordReset(email: string, token: string) { const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${token}` await strapi.plugin('email').service('email').send({ to: email, subject: 'Сброс пароля', html: `<a href=\"${resetUrl}\">Сбросить пароль</a>`, }) }, }) Вызов сервиса из контроллера
// src/api/order/controllers/order.ts async checkout(ctx) { const { items, paymentData } = ctx.request.body // Создать заказ const order = await strapi.service('api::order.order').create({ data: { items, customer: ctx.state.user.id, status: 'pending', }, }) // Обработать платёж const result = await strapi.service('api::order.order').processPayment( order.id, paymentData ) return result } Сервис с кэшированием
// src/api/catalog/services/catalog.ts const cache = new Map<string, { data: any; ts: number }>() const TTL = 60_000 // 1 минута export default () => ({ async getCategories() { const cacheKey = 'categories' const cached = cache.get(cacheKey) if (cached && Date.now() - cached.ts < TTL) { return cached.data } const data = await strapi.entityService.findMany('api::category.category', { filters: { active: { $eq: true } }, populate: ['icon', 'children'], sort: { order: 'asc' }, }) cache.set(cacheKey, { data, ts: Date.now() }) return data }, }) Кэширование улучшает LCP и TTFB, снижая нагрузку на сервер.
Сравнение стандартного и кастомного сервиса
| Критерий | Стандартный сервис | Кастомный сервис |
|---|---|---|
| Инкапсуляция логики | Только CRUD | Любая бизнес-логика |
| Переиспользование | Нет (вызов напрямую из контроллера) | Да, методы доступны из любого места |
| Кэширование | Нет | Встроенное, например Map с TTL |
| Интеграции | Нет | Платежи, CRM, email, внешние API |
| Тестируемость | Сложно (зависимость от HTTP) | Легко (чистые функции, DI) |
Процесс работы
- Анализ — изучаем API и бизнес-процессы, выявляем узкие места.
- Проектирование — определяем методы, сигнатуры, зависимости.
- Реализация — пишем код на TypeScript с использованием фабрик Strapi.
- Тестирование — unit-тесты на сервис, integration-тесты на контроллер.
- Деплой — развёртывание на сервер, настройка мониторинга.
Сроки и стоимость
Разработка кастомного сервиса для e-commerce (заказы, оплата, инвентарь) занимает от 3 до 5 дней. Срок зависит от количества интеграций и сложности логики. Стоимость рассчитывается индивидуально — свяжитесь с нами для точной оценки.
Что входит в работу
- Исходный код сервиса с комментариями
- Документация API (методы, параметры, ошибки)
- Unit-тесты (покрытие ключевых сценариев)
- Интеграция с контроллерами и жизненным циклом Strapi
- Обучение команды работе с сервисами
- Поддержка в течение месяца после сдачи
Типичные ошибки при создании кастомных сервисов
Даже опытные разработчики допускают ошибки: забывают про транзакции, не обрабатывают ошибки внешних API, избыточно кэшируют без инвалидации. Мы избегаем этих граблей — каждый сервис покрыт unit-тестами, а кэш снабжён TTL и автоматической очисткой.
Получите консультацию по проектированию сервисного слоя — мы поможем оптимизировать вашу архитектуру. Закажите разработку кастомного сервиса уже сегодня, связавшись с нами для предварительной оценки.







