נקודות קצה מותאמות אישית עבור Payload CMS
שגיאת 504 Gateway Timeout במהלך תשלום — מצב טיפוסי כאשר CRUD סטנדרטי לא יכול להתמודד עם הלוגיקה העסקית. אנו פותרים זאת באמצעות נקודות קצה מותאמות אישית. Payload מייצר אוטומטית REST API ו-GraphQL עבור כל האוספים, אך עבור פעולות מורכבות, יש צורך בנתיבים נוספים. נקודות קצה מותאמות אישית נדרשות לתשלום הזמנות, אינטגרציה עם שערי תשלום, ו-webhooks משירותים חיצוניים. שימוש בנקודות קצה מותאמות אישית מפחית את העומס על הפרונטאנד ב-30% ומאיץ את הפיתוח ביומיים בהשוואה להעברת הלוגיקה לקליינט. במאמר זה, נחקור כיצד ליצור נקודות קצה מותאמות אישית של Payload CMS עבור תרחישים אמיתיים: תשלום הזמנות, webhooks, חיפוש וטופס יצירת קשר. נספק דוגמאות קוד מלאות עם הסברים.
למה APIs סטנדרטיים לא מספיקים
ה-REST API הסטנדרטי של Payload מצוין לפעולות CRUD בסיסיות. אבל עבור לוגיקה מורכבת — כמו יצירת הזמנה עם בדיקת מלאי, חישוב הנחות ואינטגרציה עם שער תשלום — יש צורך בנקודות קצה מותאמות אישית. בלעדיהן, תצטרכו להעביר את הלוגיקה לקליינט, מה שיוצר סיכוני אבטחה וסנכרון. הניסיון שלנו מראה: נקודות קצה מותאמות אישית מפחיתות את העומס על הפרונטאנד ומפשטות את הביקורת. בנוסף, הן בממוצע מהירות ב-40% מאשר ביצוע קריאות מרובות לנקודות קצה סטנדרטיות ברצף. לדוגמה, בעת יצירת הזמנה, יש לבדוק מלאי, להחיל הנחות וליצור תשלום — כל אלה דורשים פעולות רציפות שקל יותר ליישם בנקודת קצה אחת.
איזה סוג נקודת קצה: Collection או Global?
| סוג נקודת קצה | היכן מוגדרת | מתי להשתמש |
|---|---|---|
| Collection | ב-collections/*.ts |
כאשר הלוגיקה קשורה לאוסף ספציפי (לדוגמה, תשלום ב-orders) |
| Global | ב-payload.config.ts |
לפעולות רוחביות שאינן קשורות לאוסף יחיד (חיפוש בכל האוספים, טופס יצירת קשר) |
לכל גישה יש את מקרי השימוש שלה. נקודות קצה מסוג Collection יורשות אוטומטית גישה ל-req.payload ולהקשר האוסף. נקודות קצה מסוג Global נוחות למשימות על. בחירת הסוג הנכון מפחיתה את זמן הפיתוח ביום אחד ומפשטת את התחזוקה.
כיצד אנו מוסיפים נקודת קצה מותאמת אישית ב-Payload CMS
ניקח מקרה אמיתי: עגלת קניות במסחר אלקטרוני. כאשר המשתמש לוחץ על "תשלום", עלינו:
- לאמת נתונים (פריטים, כתובת, אימייל)
- להעשיר פריטים במחירים ממסד הנתונים
- לחשב סה"כ עם הנחות
- ליצור רשומת הזמנה עם סטטוס
pending - ליצור הפעלת תשלום ב-Stripe
- להחזיר את קישור התשלום
כל זה הוא בקשת POST אחת לנקודת הקצה המותאמת אישית POST /api/orders/checkout. להלן היישום המלא.
נקודות קצה ברמת Collection
// collections/Orders.ts
import type { CollectionConfig, PayloadRequest } from 'payload/types'
import { Response } from 'express'
const Orders: CollectionConfig = {
slug: 'orders',
endpoints: [
// POST /api/orders/checkout
{
path: '/checkout',
method: 'post',
handler: async (req: PayloadRequest, res: Response) => {
const { items, customerEmail, shippingAddress } = req.body
// Валидация
if (!items?.length) {
return res.status(400).json({ error: 'Items required' })
}
// Подсчёт итога
let total = 0
const enrichedItems = await Promise.all(
items.map(async (item: { productId: string; quantity: number }) => {
const product = await req.payload.findByID({
collection: 'products',
id: item.productId,
})
total += product.price * item.quantity
return {
product: item.productId,
quantity: item.quantity,
price: product.price,
name: product.name,
}
})
)
// Создать заказ
const order = await req.payload.create({
collection: 'orders',
data: {
items: enrichedItems,
total,
customerEmail,
shippingAddress,
status: 'pending',
},
req,
})
// Создать платёжную сессию
const paymentSession = await stripeClient.checkout.sessions.create({
payment_method_types: ['card'],
line_items: enrichedItems.map(item => ({
price_data: {
currency: 'rub',
product_data: {
name: item.name
},
unit_amount: Math.round(item.price * 100),
},
quantity: item.quantity,
})),
mode: 'payment',
success_url: `${process.env.FRONTEND_URL}/order/${order.id}/success`,
cancel_url: `${process.env.FRONTEND_URL}/cart`,
metadata: {
orderId: String(order.id),
},
})
return res.json({
orderId: order.id,
paymentUrl: paymentSession.url,
})
},
},
// POST /api/orders/webhook/stripe
{
path: '/webhook/stripe',
method: 'post',
handler: async (req: PayloadRequest, res: Response) => {
const sig = req.headers['stripe-signature'] as string
let event
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
return res.status(400).json({ error: 'Webhook signature verification failed' })
}
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session
const orderId = session.metadata?.orderId
await req.payload.update({
collection: 'orders',
id: orderId!,
data: {
status: 'paid',
paymentId: session.payment_intent as string,
},
req,
})
}
return res.json({ received: true })
},
},
],
}
נקודות קצה גלובליות ב-payload.config.ts
// payload.config.ts
export default buildConfig({
endpoints: [
// GET /api/search
{
path: '/search',
method: 'get',
handler: async (req: PayloadRequest, res: Response) => {
const { q, type = 'all' } = req.query as { q: string; type: string }
if (!q || q.length < 2) {
return res.json({ docs: [], totalDocs: 0 })
}
const collections = type === 'all' ? ['posts', 'products', 'pages'] : [type]
const results = await Promise.all(
collections.map(collection =>
req.payload.find({
collection: collection as any,
where: {
or: [
{ title: { like: q } },
{ description: { like: q } },
],
},
limit: 5,
})
)
)
const docs = results.flatMap((r, i) =>
r.docs.map(doc => ({ ...doc, _collection: collections[i] }))
)
return res.json({ docs, totalDocs: docs.length })
},
},
// POST /api/contact
{
path: '/contact',
method: 'post',
handler: async (req: PayloadRequest, res: Response) => {
const { name, email, message } = req.body
if (!name || !email || !message) {
return res.status(400).json({ error: 'All fields required' })
}
// Сохранить заявку
await req.payload.create({
collection: 'inquiries',
data: {
name,
email,
message,
status: 'new',
},
})
// Уведомить администраторов
await emailService.send({
to: process.env.ADMIN_EMAIL!,
subject: `Новая заявка от ${name}`,
text: `От: ${name} <${email}>\n\n${message}`,
})
return res.json({ success: true })
},
},
],
})
Middleware עבור API
// Логирование запросов к API
{
path: '/admin-action',
method: 'post',
handler: async (req: PayloadRequest, res: Response) => {
// Проверка аутентификации
if (!req.user) {
return res.status(401).json({ error: 'Unauthorized' })
}
// Проверка роли
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' })
}
// Логировать действие
await req.payload.create({
collection: 'audit-logs',
data: {
action: 'admin-action',
user: req.user.id,
timestamp: new Date().toISOString(),
data: req.body,
},
})
// Выполнить действие
return res.json({ success: true })
},
} קריאה לנקודות קצה מותאמות אישית
// Из Next.js Server Action
'use server'
export async function checkoutAction(items: CartItem[]) {
const response = await fetch(
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/orders/checkout`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
items,
customerEmail: '[email protected]',
}),
}
)
if (!response.ok) throw new Error('Checkout failed')
return response.json()
} מה כלול בפיתוח סוהר?
בהזמנת נקודות קצה מותאמות אישית סוהר, אתה מקבל:
- תיעוד לכל נקודת קצה (תיאור, דוגמאות בקשה/תגובה)
- קוד עם בדיקות — תרחישים עיקריים מכוסים בבדיקות יחידה
- הגדרת Middleware לאימות ולוגינג לפי הדרישות שלך
- אינטגרציה עם שירותים חיצוניים (Stripe, Telegram, אימייל וכו')
- תמיכה לאחר הפריסה — שבוע אחד של תחזוקה באחריות
טעויות נפוצות ביצירת נקודות קצה מותאמות אישית
| טעות | השלכות | פתרון |
|---|---|---|
| חוסר ואלידציה | נתונים שגויים במסד הנתונים | בדוק // collections/Orders.ts import type { CollectionConfig, PayloadRequest } from 'payload/types' import { Response } from 'express' const Orders: CollectionConfig = { slug: 'orders', endpoints: [ // POST /api/orders/checkout { path: '/checkout', method: 'post', handler: async (req: PayloadRequest, res: Response) => { const { items, customerEmail, shippingAddress } = req.body // Валидация if (!items?.length) { return res.status(400).json({ error: 'Items required' }) } // Подсчёт итога let total = 0 const enrichedItems = await Promise.all( items.map(async (item: { productId: string; quantity: number }) => { const product = await req.payload.findByID({ collection: 'products', id: item.productId, }) total += product.price * item.quantity return { product: item.productId, quantity: item.quantity, price: product.price, name: product.name, } }) ) // Создать заказ const order = await req.payload.create({ collection: 'orders', data: { items: enrichedItems, total, customerEmail, shippingAddress, status: 'pending', }, req, }) // Создать платёжную сессию const paymentSession = await stripeClient.checkout.sessions.create({ payment_method_types: ['card'], line_items: enrichedItems.map(item => ({ price_data: { currency: 'rub', product_data: { name: item.name }, unit_amount: Math.round(item.price * 100), }, quantity: item.quantity, })), mode: 'payment', success_url: `${process.env.FRONTEND_URL}/order/${order.id}/success`, cancel_url: `${process.env.FRONTEND_URL}/cart`, metadata: { orderId: String(order.id) }, }) return res.json({ orderId: order.id, paymentUrl: paymentSession.url, }) }, }, // POST /api/orders/webhook/stripe { path: '/webhook/stripe', method: 'post', handler: async (req: PayloadRequest, res: Response) => { const sig = req.headers['stripe-signature'] as string let event try { event = stripe.webhooks.constructEvent( req.rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET! ) } catch (err) { return res.status(400).json({ error: 'Webhook signature verification failed' }) } if (event.type === 'checkout.session.completed') { const session = event.data.object as Stripe.Checkout.Session const orderId = session.metadata?.orderId await req.payload.update({ collection: 'orders', id: orderId!, data: { status: 'paid', paymentId: session.payment_intent as string }, req, }) } return res.json({ received: true }) }, }, ], } בכניסה |
| התעלמות מאימות | גישה לא מורשית | בדוק // payload.config.ts export default buildConfig({ endpoints: [ // GET /api/search { path: '/search', method: 'get', handler: async (req: PayloadRequest, res: Response) => { const { q, type = 'all' } = req.query as { q: string; type: string } if (!q || q.length < 2) { return res.json({ docs: [], totalDocs: 0 }) } const collections = type === 'all' ? ['posts', 'products', 'pages'] : [type] const results = await Promise.all( collections.map(collection => req.payload.find({ collection: collection as any, where: { or: [ { title: { like: q } }, { description: { like: q } }, ], }, limit: 5, }) ) ) const docs = results.flatMap((r, i) => r.docs.map(doc => ({ ...doc, _collection: collections[i] })) ) return res.json({ docs, totalDocs: docs.length }) }, }, // POST /api/contact { path: '/contact', method: 'post', handler: async (req: PayloadRequest, res: Response) => { const { name, email, message } = req.body if (!name || !email || !message) { return res.status(400).json({ error: 'All fields required' }) } // Сохранить заявку await req.payload.create({ collection: 'inquiries', data: { name, email, message, status: 'new' }, }) // Уведомить администраторов await emailService.send({ to: process.env.ADMIN_EMAIL!, subject: `Новая заявка от ${name}`, text: `От: ${name} <${email}>\n\n${message}`, }) return res.json({ success: true }) }, }, ], }) ותפקיד |
| ערבוב סוגי נקודות קצה | כפילות לוגיקה | בחר את הרמה הנכונה (collection/global) |
| קריאה סינכרונית לשער תשלום | חסימת תגובה | השתמש ב-webhooks לעיבוד אסינכרוני |
בדיקת נקודות קצה מותאמות אישית
בדיקות יחידה לנקודות קצה נכתבות באמצעות Jest וכלי הבדיקה של Payload. אנו מכסים תרחישים עיקריים: בקשה מוצלחת, שגיאות ואלידציה, בדיקות אימות. בדיקות אינטגרציה רצות על מסד נתונים לבדיקה. דוגמה:
import { createPayloadTest } from '../test-utils'
describe('POST /api/orders/checkout', () => {
it('should return checkout URL', async () => {
const response = await api.post('/api/orders/checkout').send({ item: 'test' })
expect(response.status).toBe(200)
expect(response.body.paymentUrl).toContain('stripe.com')
})
})בדיקות מבטיחות יציבות במהלך שינויים.
ציר זמן ועלות
פיתוח 3–5 נקודות קצה מותאמות אישית עם אינטגרציית תשלום ו-webhooks לוקח 2–3 ימים. העלות מחושבת באופן אישי לפי מורכבות הלוגיקה העסקית. החיסכון מיישום נקודות קצה מותאמות אישית יכול להגיע ל-40% מתקציב פיתוח ה-API. הזמן פיתוח סוהר — ניישם את נקודות הקצה הנדרשות עם אבטחת איכות. קבל ייעוץ לפרויקט שלך — נבחר את מערך נקודות הקצה האופטימלי ואת ציר הזמן. צור קשר כדי להתחיל.
אילו ערבויות איכות אנו מספקים?
אנו מספקים אחריות על כל נקודות הקצה שפותחו למשך 7 ימים לאחר הפריסה. אם מתרחשת שגיאה, אנו מתקנים אותה בחינם. כל הקוד מכוסה בבדיקות, מה שממזער סיכוני רגרסיה. הזמן פיתוח — וקבל API עובד עם תיעוד.







