Розробка кастомних ендпоінтів Directus

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка кастомних ендпоінтів Directus
Середня
~2-3 робочих дні
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка кастомних енпойнтів Directus

Endpoint Extensions додають кастомні маршрути до API Directus. Використовуються для бізнес-операцій: оформлення замовлення, інтеграція з платіжними шлюзами, вебхуки від зовнішніх сервісів, агреговані звіти.

Базовий Endpoint Extension

// 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()
}

Реєстрація енпойнта

// package.json
{
  "directus:extension": {
    "type": "endpoint",
    "path": "dist/index.js",
    "source": "src/index.ts"
  }
}

Маршрути будуть доступні за адресою /checkout, /checkout/webhook/stripe, /reports/sales, /search.

Графік

Розробка 4–6 кастомних енпойнтів з інтеграцією платіжної системи та звітами — 3–4 дні.