נקודות קצה מותאמות אישית של Directus: הרחבת REST API לכל לוגיקה עסקית

כאשר ה-API הסטנדרטי של Directus אינו מספיק לתהליכים עסקיים מורכבים, אנו בונים נקודות קצה מותאמות אישית הפותרות משימות בבקשה אחת. הצוות שלנו מספק הרחבות במפתח מלא—מתכנון ועד יישום—המותאמות ללוגיקה ולאינטגרציות שלך. התוצאה היא REST API גמיש ואמין שגדל יחד עם העסק שלך.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
נקודות קצה מותאמות אישית של Directus: הרחבת REST API לכל לוגיקה עסקית
בינוני
~2-3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1306
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1049
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

Custom Directus Endpoints: הרחבת REST API לכל לוגיקה עסקית

פרויקטים רבים על Directus נתקלים במשימות שלא ניתן לפתור עם ה-API המובנה. דוגמה טיפוסית: ביצוע הזמנה בחנות מסחר אלקטרוני. צריך לבדוק מלאי, ליצור רשומה בטבלת ההזמנות, ליצור סשן תשלום של Stripe ולשלוח אימייל ללקוח. בשיטות סטנדרטיות, זה הופך לשרשרת של webhooks ושירותים חיצוניים, מה שמסבך את התחזוקה. נקודת קצה מותאמת אישית פותרת את הבעיה עם בקשת POST אחת. אנו מפתחים הרחבות כאלה למסחר אלקטרוני, SaaS ופורטלים ארגוניים. הניסיון שלנו: מעל 15 פרויקטים, זמן פיתוח ממוצע של 3 ימים. אתם מקבלים API שתואם בדיוק לתהליכים העסקיים שלכם ללא שכבות מיותרות.

הרחבת נקודת קצה היא מנגנון המאפשר להוסיף נתיבים חדשים ל-API של Directus באמצעות נתב Express מוכר. אתם מקבלים שליטה מלאה על הלוגיקה ומבנה התגובה, גישה לשירותי Directus (ItemsService וכו') ולסכמת מסד הנתונים, יכולת אינטגרציה עם כל API חיצוני (מערכות תשלום, CRM), וניהול גמיש של הרשאות ברמת נקודת הקצה. כל נקודת קצה עוברת בדיקת קוד ומכוסה בבדיקות. ניסיון עם Directus: מעל 4 שנים, יותר מ-10 פרויקטים שבוצעו.

אילו בעיות אנו פותרים

בעיה 1: לוגיקה עסקית טרנזקציונית. בעת יצירת הזמנה, יש צורך לנכות מלאי באופן אטומי, ליצור רשומת הזמנה וליזום תשלום. נקודת קצה מותאמת אישית מיישמת את הטרנזקציה באמצעות שירותי Directus ושער תשלום. שגיאות מתבטלות, והנתונים נשארים עקביים.

בעיה 2: דוחות מצטברים. ה-API הסטנדרטי אינו יכול לחשב סיכומי מכירות לפי סטטוס לאורך תקופה. אנו כותבים נקודת קצה אחת שמבצעת סינון ואגרגציה על השרת, ומחזירה JSON מוכן לדשבורד. זמן הפקת הדוח יורד מכמה שעות לכמה שניות.

בעיה 3: Webhooks ממערכות חיצוניות. Stripe, PayPal ושירותים אחרים שולחים webhooks שצריך לעבד ולעדכן נתונים ב-Directus. נקודת קצה מותאמת אישית היא המקום האידיאלי לקבלת ואימות webhooks. אנו מבטיחים עיבוד עם זמינות של 99.9%.

דוגמה: תשלום ב-Directus

נבחן תרחיש טיפוסי של מסחר אלקטרוני. אנו צריכים נקודת קצה // 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() } שמקבלת עגלת קניות, כתובת משלוח ואמצעי תשלום. על השרת:

  1. אימות התחברות המשתמש
  2. שימוש ב-ItemsService לקבלת נתונים עבור כל מוצר (מחיר, מלאי)
  3. אם המלאי אינו מספיק, החזרת שגיאה 409
  4. יצירת רשומת הזמנה עם הסכום הכולל
  5. יצירת סשן תשלום של Stripe והחזרת קישור התשלום
  6. לאחר תשלום מוצלח, Stripe שולח webhook שמעדכן את סטטוס ההזמנה
// 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–3 ומחזירה את עצמה תוך 2–3 חודשים בזכות זמן פיתוח מופחת.

מה לבחור: נקודת קצה מותאמת אישית או API סטנדרטי?

תרחיש נקודת קצה מותאמת אישית API סטנדרטי
יצירת רשומה פשוטה מוגזם
ולידציה מורכבת עם קריאה חיצונית רק דרך מותאם אישית
דוח מצטבר ❌ (רק עם hooks)
אינטגרציה עם שער תשלום
חיפוש רב-אוסף

מה כלול

  • קוד מקור של ההרחבה ב-TypeScript עם הערות
  • קובץ package.json מוגדר עבור Directus Extension
  • הוראות פריסה (העתקה לתיקיית extensions, הפעלה מחדש)
  • אוסף Postman עם בקשות לדוגמה
  • טיפול בשגיאות וולידציית קלט
  • 30 ימי תמיכה לאחר המסירה

תהליך

  1. ניתוח דרישות — אתם מתארים את נקודות הקצה הנדרשות, ואנו מבהירים פרטים
  2. עיצוב — מסכימים על מבנה הנתיבים ופורמט התגובה
  3. יישום — כתיבת קוד ב-TypeScript, חיבור שירותי Directus
  4. בדיקות — כיסוי מקרים קריטיים בבדיקות יחידה, בדיקה בסביבה דמוית ייצור
  5. פריסה — מסירת הבנייה והתיעוד

לוחות זמנים משוערים

מספר נקודות קצה לוח זמנים
1–2 פשוטות (ולידציה, אינטגרציה) 1–2 ימים
3–4 עם APIs חיצוניים (תשלומים, דוחות) 3–5 ימים
5+ מורכבות עם webhooks 5–7 ימים

לוחות זמנים מדויקים מחושבים לאחר בריף. צרו קשר — נבחן את הפרויקט שלכם.

למה לבחור בנו

מעל 4 שנות ניסיון עם Directus: פיתחנו עבור מסחר אלקטרוני, CMS ו-SaaS. אחריות על כל ההרחבות — אנו מתקנים באגים בחינם למשך חודש. קוד שקוף — כל השינויים ב-Git, עם בדיקת קוד חובה. תמיכה לאחר ההשקה — אנו מייעצים ומשפרים לפי הצורך.

הזמינו נקודות קצה מותאמות אישית של Directus במפתח מוכן. קבלו API שתואם בדיוק לתהליכים העסקיים שלכם. להערכה חינמית של הפרויקט שלכם, צרו קשר — נכין הצעה תוך יום עסקים אחד.