Directus Custom Endpoints Development

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Directus Custom Endpoints Development

Endpoint Extensions add custom routes to the Directus API. Used for business operations: checkout, payment gateway integration, webhooks from external services, aggregated reports.

Basic 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 — checkout
  router.post('/checkout', async (req, res) => {
    const schema = await getSchema()
    const { ItemsService } = services

    // Authentication check
    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 })

      // Check availability and calculate total
      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 })
      }

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

      // Create payment session
      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 only
    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()
}

Endpoint registration

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

Routes will be available at /checkout, /checkout/webhook/stripe, /reports/sales, /search.

Timeline

Development of 4–6 custom endpoints with payment system integration and reports — 3–4 days.