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







