Когда стандартного API Directus недостаточно: кастомные эндпоинты для бизнес-логики
Многие проекты на Directus сталкиваются с задачами, которые не решить встроенным API. Типичный пример: оформление заказа в интернет-магазине. Нужно проверить остатки на складе, создать запись в таблице orders, сформировать платежную сессию Stripe, отправить письмо клиенту. Стандартными методами это превращается в цепочку вебхуков и внешних сервисов, что усложняет поддержку. Кастомный эндпоинт решает проблему одним POST-запросом. Мы разрабатываем такие расширения для e-commerce, SaaS и корпоративных порталов. Наш опыт — более 15 проектов, средний срок разработки — 3 дня. Вы получите API, который идеально соответствует вашим бизнес-процессам без лишних слоёв.
Endpoint Extension — механизм, позволяющий добавить новые маршруты к вашему Directus API через знакомый Express-роутер. Вы получаете полный контроль над логикой и структурой ответа, доступ к сервисам Directus (ItemsService и др.) и схеме базы данных, возможность интеграции с любыми внешними API (платёжные системы, CRM) и гибкое управление доступом на уровне эндпоинта. Каждый эндпоинт проходит код-ревью и покрывается тестами. Опыт работы с Directus — более 4 лет, реализовано свыше 10 проектов.
Какие проблемы мы решаем
Проблема 1: Транзакционная бизнес-логика. При создании заказа нужно атомарно списать товары, создать запись в заказах и инициировать платёж. Кастомный эндпоинт реализует транзакцию с помощью сервисов Directus и платёжного шлюза. Ошибки откатываются, данные консистентны.
Проблема 2: Агрегированные отчёты. Стандартное API не умеет считать сумму продаж по статусам за период. Мы пишем один эндпоинт, который выполняет фильтрацию и агрегацию на сервере, возвращая готовый JSON для дашборда. Время построения отчёта сокращается с нескольких часов до нескольких секунд.
Проблема 3: Вебхуки от внешних систем. Stripe, PayPal и другие сервисы присылают вебхуки, которые нужно обработать и обновить данные в Directus. Кастомный эндпоинт — идеальное место для приёма и верификации вебхуков. Мы гарантируем обработку с uptime 99.9%.
Пример: оформление заказа на Directus
Рассмотрим типичный e-commerce сценарий. Нужен эндпоинт POST /checkout, который принимает корзину, адрес доставки и способ оплаты. На сервере:
- Проверяем аутентификацию пользователя
- Через ItemsService получаем данные о каждом товаре (цена, остаток)
- Если товара недостаточно — возвращаем ошибку 409
- Создаём запись в orders с итоговой суммой
- Создаём платежную сессию Stripe и возвращаем ссылку на оплату
- После успешной оплаты Stripe присылает вебхук, который обновляет статус заказа
// extensions/endpoints/checkout/index.ts
import type { EndpointExtensionContext } from '@directus/types'
import { Router } from 'express'
export default (router: Router, { services, getSchema, env, logger }: EndpointExtensionContext) => {
// POST /checkout — оформление заказа
router.post('/checkout', async (req, res) => {
const schema = await getSchema()
const { ItemsService } = services
// Проверка аутентификации
if (!req.accountability?.user) {
return res.status(401).json({ errors: [{ message: 'Unauthorized' }] })
}
const { items, shipping_address, payment_method } = req.body
if (!items?.length) {
return res.status(400).json({ errors: [{ message: 'Cart is empty' }] })
}
try {
const productsService = new ItemsService('products', { schema, accountability: req.accountability })
// Проверить наличие и посчитать итог
let total = 0
const enrichedItems: any[] = []
for (const item of items) {
const product = await productsService.readOne(item.product_id, {
fields: ['id', 'name', 'price', 'stock'],
})
if (product.stock < item.quantity) {
return res.status(409).json({
errors: [{ message: `Insufficient stock for "${product.name}"` }],
})
}
total += product.price * item.quantity
enrichedItems.push({ ...item, price: product.price, name: product.name })
}
// Создать заказ
const ordersService = new ItemsService('orders', { schema, accountability: req.accountability })
const order = await ordersService.createOne({
user: req.accountability.user,
items: enrichedItems,
total,
shipping_address,
status: 'pending',
date_created: new Date().toISOString(),
})
// Создать платёжную сессию
const paymentSession = await createPaymentSession(order, total, env)
return res.json({
data: {
orderId: order,
paymentUrl: paymentSession.url,
total,
},
})
} catch (error) {
logger.error('Checkout error:', error)
return res.status(500).json({ errors: [{ message: 'Checkout failed' }] })
}
})
// POST /checkout/webhook/stripe
router.post('/webhook/stripe', async (req, res) => {
const sig = req.headers['stripe-signature'] as string
let event
try {
event = verifyStripeWebhook(req.rawBody, sig, env.STRIPE_WEBHOOK_SECRET)
} catch {
return res.status(400).json({ error: 'Webhook signature invalid' })
}
if (event.type === 'checkout.session.completed') {
const session = event.data.object
const orderId = session.metadata?.orderId
if (orderId) {
const schema = await getSchema()
const ordersService = new services.ItemsService('orders', { schema })
await ordersService.updateOne(Number(orderId), {
status: 'paid',
payment_id: session.payment_intent,
paid_at: new Date().toISOString(),
})
}
}
return res.json({ received: true })
})
// GET /reports/sales
router.get('/reports/sales', async (req, res) => {
// Только для admin
if (!req.accountability?.admin) {
return res.status(403).json({ errors: [{ message: 'Admin access required' }] })
}
const { period = 'week' } = req.query
const schema = await getSchema()
const ordersService = new services.ItemsService('orders', { schema, accountability: req.accountability })
const periodDays: Record<string, number> = { day: 1, week: 7, month: 30 }
const days = periodDays[period as string] || 7
const since = new Date(Date.now() - days * 86400000).toISOString()
const orders = await ordersService.readByQuery({
filter: {
date_created: { _gte: since },
status: { _in: ['paid', 'shipped', 'delivered'] },
},
fields: ['id', 'total', 'date_created', 'status'],
limit: -1,
})
const totalRevenue = orders.reduce((sum: number, o: any) => sum + (o.total || 0), 0)
return res.json({
data: {
count: orders.length,
revenue: totalRevenue,
avgOrder: orders.length > 0 ? Math.round(totalRevenue / orders.length) : 0,
period,
},
})
})
// GET /search
router.get('/search', async (req, res) => {
const { q, collections = 'articles,products' } = req.query as { q: string; collections: string }
if (!q || q.length < 2) {
return res.json({ data: [] })
}
const schema = await getSchema()
const collectionList = (collections as string).split(',')
const searchMap: Record<string, string[]> = {
articles: ['title', 'excerpt'],
products: ['name', 'description'],
pages: ['title'],
}
const results = await Promise.all(
collectionList
.filter(c => searchMap[c])
.map(async collection => {
const service = new services.ItemsService(collection, { schema, accountability: req.accountability })
const orFilter = searchMap[collection].map(field => ({
[field]: { _icontains: q },
}))
const items = await service.readByQuery({
filter: { _or: orFilter },
fields: ['id', ...searchMap[collection]],
limit: 5,
})
return items.map((item: any) => ({ ...item, _collection: collection }))
})
)
return res.json({ data: results.flat() })
})
}
async function createPaymentSession(orderId: number, total: number, env: any) {
// Stripe checkout session
const response = await fetch('https://api.stripe.com/v1/checkout/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${env.STRIPE_SECRET_KEY}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
'payment_method_types[]': 'card',
'line_items[0][price_data][currency]': 'rub',
'line_items[0][price_data][unit_amount]': String(Math.round(total * 100)),
'line_items[0][price_data][product_data][name]': `Order #${orderId}`,
'line_items[0][quantity]': '1',
mode: 'payment',
'metadata[orderId]': String(orderId),
success_url: `${env.FRONTEND_URL}/order/${orderId}/success`,
cancel_url: `${env.FRONTEND_URL}/cart`,
}),
})
return response.json()
}
Архитектура решения
Клиент отправляет запрос -> Directus проверяет аутентификацию -> кастомный эндпоинт обрабатывает логику через ItemsService и внешние API. Все запросы логируются, ошибки обрабатываются централизованно. Такой подход избавляет от необходимости создавать отдельный микросервис.
Как кастомные эндпоинты ускоряют разработку?
Кастомный эндпоинт Directus выигрывает по скорости разработки в 3–5 раз по сравнению с написанием отдельного микросервиса. Готовые плагины не дают такой гибкости. В одном из проектов мы внедрили 10+ эндпоинтов за 2 недели, тогда как микросервис потребовал бы месяца. Такой подход сокращает бюджет на интеграцию в 2-3 раза и окупается в течение 2-3 месяцев за счёт сокращения времени разработки.
Что выбрать: кастомный эндпоинт или стандартный API?
| Сценарий | Кастомный эндпоинт | Стандартный API |
|---|---|---|
| Простое создание записи | Избыточно | ✅ |
| Сложная валидация с внешним вызовом | ✅ | Только через кастом |
| Агрегированный отчёт | ✅ | ❌ (только с хуками) |
| Интеграция с платёжным шлюзом | ✅ | ❌ |
| Поиск по нескольким коллекциям | ✅ | ❌ |
Что входит в работу
- Исходный код расширения на TypeScript с комментариями
- Конфигурация package.json для Directus Extension
- Инструкция по развёртыванию (копирование в папку extensions, перезапуск)
- Postman-коллекция с примерами запросов
- Обработка ошибок и валидация входных данных
- Поддержка в течение 30 дней после сдачи
Процесс работы
- Анализ требований — вы описываете нужные эндпоинты, мы уточняем детали
- Проектирование — согласовываем структуру маршрутов и формат ответов
- Реализация — пишем код на TypeScript, подключаем сервисы Directus
- Тестирование — покрываем критичные кейсы unit-тестами, проверяем в окружении, похожем на продакшен
- Деплой — передаём актуальную сборку и документацию
Ориентировочные сроки
| Количество эндпоинтов | Сроки |
|---|---|
| 1–2 простых (валидация, интеграция) | 1–2 дня |
| 3–4 с внешними API (платежи, отчёты) | 3–5 дней |
| 5+ комплексных с вебхуками | 5–7 дней |
Точные сроки рассчитываются после брифинга. Свяжитесь с нами — мы бесплатно оценим ваш проект.
Почему выбирают нас
Опыт работы с Directus более 4 лет: разрабатывали для e-commerce, CMS, SaaS. Гарантия на все расширения — исправляем ошибки бесплатно в течение месяца. Прозрачный код — все изменения в Git, код-ревью обязательно. Поддержка после запуска — консультируем, дорабатываем по необходимости.
Закажите разработку кастомных эндпоинтов Directus под ключ. Получите API, который точно соответствует вашим бизнес-процессам. Для бесплатной оценки вашего проекта свяжитесь с нами — мы подготовим предложение за 1 рабочий день.







