Кастомные эндпоинты Directus: расширяем REST API под любую бизнес-логику

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Кастомные эндпоинты Directus: расширяем REST API под любую бизнес-логику
Средний
~2-3 дня
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1358
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1250
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    956
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1188
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    929
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    947

Когда стандартного 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, который принимает корзину, адрес доставки и способ оплаты. На сервере:

  1. Проверяем аутентификацию пользователя
  2. Через ItemsService получаем данные о каждом товаре (цена, остаток)
  3. Если товара недостаточно — возвращаем ошибку 409
  4. Создаём запись в orders с итоговой суммой
  5. Создаём платежную сессию Stripe и возвращаем ссылку на оплату
  6. После успешной оплаты 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 дней после сдачи

Процесс работы

  1. Анализ требований — вы описываете нужные эндпоинты, мы уточняем детали
  2. Проектирование — согласовываем структуру маршрутов и формат ответов
  3. Реализация — пишем код на TypeScript, подключаем сервисы Directus
  4. Тестирование — покрываем критичные кейсы unit-тестами, проверяем в окружении, похожем на продакшен
  5. Деплой — передаём актуальную сборку и документацию

Ориентировочные сроки

Количество эндпоинтов Сроки
1–2 простых (валидация, интеграция) 1–2 дня
3–4 с внешними API (платежи, отчёты) 3–5 дней
5+ комплексных с вебхуками 5–7 дней

Точные сроки рассчитываются после брифинга. Свяжитесь с нами — мы бесплатно оценим ваш проект.

Почему выбирают нас

Опыт работы с Directus более 4 лет: разрабатывали для e-commerce, CMS, SaaS. Гарантия на все расширения — исправляем ошибки бесплатно в течение месяца. Прозрачный код — все изменения в Git, код-ревью обязательно. Поддержка после запуска — консультируем, дорабатываем по необходимости.

Закажите разработку кастомных эндпоинтов Directus под ключ. Получите API, который точно соответствует вашим бизнес-процессам. Для бесплатной оценки вашего проекта свяжитесь с нами — мы подготовим предложение за 1 рабочий день.

Разработка API: REST, GraphQL, WebSocket, tRPC

К нам приходит клиент с Postman-коллекцией на 200 эндпоинтов и говорит: «Всё работает, но фронтенд тормозит». Открываем Network-вкладку — 47 последовательных запросов на загрузку одной страницы дашборда. Каждый ждёт предыдущего. Это не проблема скорости сервера — это проблема архитектуры API. За 10 лет на рынке мы перепроектировали не один десяток таких интеграций, и гарантируем: правильный протокол и контракт решают проблему на корню.

Когда REST перестаёт справляться

REST хорошо работает для простых CRUD-операций. Но как только рядом с веб-интерфейсом появляется мобильное приложение, начинается over-fetching: мобилка запрашивает /api/users/123 и получает объект на 4KB, хотя ей нужны только name и avatar. Умножьте на список из 50 пользователей — 200KB трафика вместо 8KB.

GraphQL решает это через selection sets. Клиент описывает именно те поля, которые ему нужны, и сервер возвращает ровно их. На проекте с React Native + Next.js мы переехали с REST на Apollo Server: размер payload на главном экране упал с 340KB до 28KB — экономия трафика составила 92%. Сертифицированные инженеры команды подтверждают: типичные боли при внедрении GraphQL — N+1 query. Резолвер для поля author у поста вызывает SELECT * FROM users WHERE id = ? для каждого поста в списке. На странице с 20 постами — 21 запрос к базе. Решается через DataLoader — он батчит запросы и превращает их в один SELECT * FROM users WHERE id IN (...).

Что такое tRPC и чем он лучше REST/GraphQL?

Если весь стек на TypeScript (Next.js + Node/Bun), tRPC убирает целый слой проблем. Вы определяете процедуру на сервере — клиент получает полный тайп-сейфти автоматически, без генерации кода и без Swagger. Переименовали поле в схеме Zod — TypeScript подсветит все места на фронтенде, где оно используется. tRPC уменьшает количество кода в 2 раза по сравнению с REST + Swagger + openapi-typescript: не нужно поддерживать отдельную спецификацию и генерировать типы — всё выводится из рантаймовых валидаторов. Однако tRPC не подходит, если API потребляют сторонние клиенты или мобильные приложения на других языках — в таких случаях используем GraphQL или REST с OpenAPI-спецификацией.

WebSocket и реальное время: когда SSE, когда WS?

HTTP-поллинг каждые 5 секунд — это иллюзия реального времени с задержкой до 5 секунд и бесполезной нагрузкой на сервер. Для чатов, live-нотификаций, совместного редактирования — WebSocket или Server-Sent Events. SSE — однонаправленный поток от сервера к клиенту, работает поверх обычного HTTP, автоматически переподключается. Подходит для нотификаций, стриминга данных, прогресс-баров. WebSocket — двунаправленный, нужен для чатов и коллаборативных фич. Опыт показывает: 80% задач «реального времени» решаются через SSE, а не WebSocket — меньше инфраструктурных сложностей.

Типичная ошибка: открывать WebSocket-соединение на каждый компонент страницы. На одном проекте дашборд открывал 12 параллельных WS-соединений. Правильно — один connection manager на уровне приложения, подписки через него. В результатах работы мы всегда передаём схему соединения и готовое решение.

Протокол Типизация Over-fetching Версионирование Real-time
REST Слабая (OpenAPI) Присутствует URL / Header Поллинг
GraphQL Сильная (SDL) Нет Deprecation Subscriptions
tRPC Полная (TypeScript) Нет TypeScript checks Subscriptions (optional)

Swagger / OpenAPI как контракт

Документация, написанная постфактум — устаревает на следующий день после релиза. Мы пишем спецификацию OpenAPI 3.1 до начала разработки, она становится контрактом между фронтендом и бэкендом. Фронтенд генерирует типы через openapi-typescript, бэкенд валидирует входящие данные через сгенерированные схемы. Расхождение контракта с реализацией ловится на CI, а не на ревью. Для Laravel — l5-swagger или dedoc/scramble. Для Node.js — @fastify/swagger или Zod + zod-to-openapi.

Как правильно аутентифицировать API?

JWT с долгоживущими access-токенами без ротации — источник проблем при компрометации. Правильная схема: access-токен на 15 минут, refresh-токен на 30 дней с ротацией при каждом использовании. Refresh-токен хранится в httpOnly cookie, access-токен — в памяти (не в localStorage). Для межсервисного взаимодействия — API Keys с scope-ограничениями или mTLS. OAuth 2.0 с PKCE для публичных клиентов (SPA, мобилки).

Версионирование и обратная совместимость

Ломающие изменения в API без версионирования ломают клиентов. Три подхода мы используем в проектах:

Метод Пример Когда применять
URL-версионирование /api/v2/ REST API с долгой поддержкой legacy
Header-версионирование Accept: application/vnd.api+json;version=2 Минимальные изменения в URL
Эволюционное (deprecation) Добавление полей, deprecated-директива GraphQL Для GraphQL — плавный вывод полей

Обратную совместимость мы гарантируем через автомат-проверки (oasdiff) на CI.

Как мы разрабатываем API: пошаговый план

  1. Аналитика — аудит текущих интеграций, составление схемы данных, выбор протокола (REST/GraphQL/tRPC/WebSocket).
  2. Проектирование контракта — OpenAPI или SDL (GraphQL) до первой строки кода.
  3. Разработка — реализация по контракту, модульные тесты на каждый эндпоинт.
  4. Нагрузочное тестирование — k6: 500 виртуальных пользователей, 10 минут, p95 latency ≤ 200ms.
  5. Деплой — CI/CD с проверкой обратной совместимости, автоматическая публикация документации.
  6. Обучение команды — передача Postman-коллекции или Playground, инструкция по подключению.
Типичные ошибки, которые мы исключаем
  • N+1 при запросах без DataLoader.
  • Отсутствие rate limiting — DDOS через неавторизованные эндпоинты.
  • Хранение access-токена в localStorage.
  • Открытие множества WebSocket-соединений вместо одного connection manager.
  • Документация, не обновлённая после релиза.

Что входит в работу (deliverables)

  • OpenAPI 3.1 спецификация (или SDL для GraphQL).
  • Сгенерированные клиентские типы для TypeScript / Dart / Kotlin.
  • Набор автотестов с покрытием всех эндпоинтов (модульные + интеграционные).
  • Нагрузочные тесты (k6) и отчёт (p50/p95/p99 latency, RPS).
  • Документация в Swagger UI / Redoc / GraphiQL.
  • Обучение команды (2–4 часа воркшопа).
  • Поддержка в течение 30 дней после сдачи (по договору).

Наш опыт

  • 10+ лет на рынке разработки API.
  • 200+ завершённых проектов (REST, GraphQL, WebSocket, tRPC).
  • 50+ сертифицированных инженеров (AWS, Kubernetes, API Design).
  • Экономия на трафике в среднем 85% при переходе с REST на GraphQL для мобильных приложений.
  • 100% обратная совместимость — ни одного сломанного клиента за последние 3 года.

Сроки

Разработка API для типового SaaS-проекта с 30–50 эндпоинтами: от 3 до 8 недель в зависимости от сложности бизнес-логики и количества внешних интеграций. Миграция существующего REST API на GraphQL — от 2 до 6 недель. Добавление WebSocket-слоя к готовому бэкенду — от 1 до 3 недель. Стоимость рассчитывается индивидуально после аудита. Получите консультацию — свяжитесь с нами, чтобы обсудить ваш проект.