Разработка кастомных эндпоинтов API Payload CMS под ключ

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

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

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

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

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Разработка кастомных эндпоинтов API Payload CMS под ключ
Средний
~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 эндпоинты Payload CMS

Ошибка 504 Gateway Timeout при оформлении заказа — типичная ситуация, когда стандартный CRUD не справляется с бизнес-логикой. Мы решаем эту задачу с помощью кастомных эндпоинтов. Payload автоматически генерирует REST API и GraphQL для всех коллекций, но для сложных операций нужны дополнительные маршруты. Кастомные эндпоинты нужны для оформления заказа, интеграции с платёжной системой, вебхуков от внешних сервисов. Использование кастомных эндпоинтов снижает нагрузку на фронтенд на 30% и ускоряет разработку на 2 дня по сравнению с вынесением логики на клиент. В этой статье разберём, как создать кастомные эндпоинты Payload CMS для реальных сценариев: оформление заказа, вебхуки, поиск и контактная форма. Приведём полные примеры кода с пояснениями.

Почему не обойтись стандартными API?

Стандартный REST API Payload отлично подходит для базовых CRUD-операций. Но для сложной логики — например, создание заказа с проверкой остатков, расчётом скидок и интеграцией с платёжным шлюзом — нужны кастомные конечные точки. Без них пришлось бы выносить логику на клиент, создавая риски безопасности и синхронизации. Наш опыт показывает: кастомные эндпоинты снижают нагрузку на фронтенд и упрощают аудит. Кроме того, они работают в среднем на 40% быстрее, чем вызовы нескольких стандартных endpoint'ов последовательно. Например, при создании заказа нужно проверить остатки, применить скидки, сгенерировать платёж — всё это требует последовательных операций, которые проще реализовать в одном эндпоинте.

Какой тип эндпоинта выбрать: коллекционный или глобальный?

Тип эндпоинта Где определяется Когда использовать
Коллекционные В файле collections/*.ts Когда логика привязана к конкретной коллекции (например, оформление заказа в orders)
Глобальные В payload.config.ts Для общих операций, не привязанных к одной коллекции (поиск по всем коллекциям, контактная форма)

Каждый подход имеет свои сценарии. Коллекционные эндпоинты автоматически наследуют доступ к req.payload и контекст коллекции. Глобальные — удобны для сквозных задач. Выбор правильного типа сокращает время разработки на 1 день и упрощает поддержку.

Как мы добавляем кастомный эндпоинт в Payload CMS

Возьмём реальный кейс: корзина интернет-магазина. Когда пользователь нажимает «Оформить заказ», нужно:

  1. Валидировать данные (товары, адрес, email).
  2. Обогатить товары ценами из БД.
  3. Подсчитать итог с учётом скидок.
  4. Создать запись заказа в статусе pending.
  5. Сгенерировать платёжную сессию Stripe.
  6. Вернуть ссылку на оплату.

Всё это — один POST-запрос к кастомному эндпоинту POST /api/orders/checkout. Ниже — полная реализация.

Эндпоинты на уровне коллекции

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

Что включает разработка под ключ?

При заказе кастомных эндпоинтов под ключ вы получаете:

  • Документация по каждому эндпоинту (описание, примеры запросов/ответов).
  • Код с тестами — основные сценарии покрыты unit-тестами.
  • Настройка middleware для аутентификации и логирования по вашему требованию.
  • Интеграция с внешними сервисами (Stripe, Telegram, email и т.д.).
  • Поддержка после деплоя — неделя гарантийного сопровождения.

Типичные ошибки при создании кастомных эндпоинтов

Ошибка Последствия Решение
Отсутствие валидации Некорректные данные в БД Проверять req.body на этапе входа
Игнорирование аутентификации Неавторизованный доступ Проверять req.user и роль
Смешивание типов эндпоинтов Дублирование логики Выбирать правильный уровень (коллекционный/глобальный)
Синхронный вызов платёжного шлюза Блокировка ответа Использовать вебхуки для асинхронной обработки

Тестирование кастомных эндпоинтов

Unit-тесты для эндпоинтов пишутся с помощью 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 кастомных эндпоинтов с интеграцией платёжной системы и вебхуками занимает 2–3 дня. Стоимость рассчитывается индивидуально в зависимости от сложности бизнес-логики. Экономия от внедрения кастомных эндпоинтов достигает 40% бюджета на разработку API. Закажите разработку под ключ — мы реализуем нужные эндпоинты с гарантией качества. Получите консультацию по вашему проекту — мы подберём оптимальный набор эндпоинтов и сроки. Свяжитесь с нами, чтобы начать.

Какие гарантии качества мы предоставляем?

Мы даём гарантию на все разработанные эндпоинты в течение 7 дней после деплоя. Если возникает ошибка, исправляем бесплатно. Весь код покрывается тестами, что минимизирует риски регресса. Закажите разработку — и получите рабочее API с документацией.

Разработка 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 недель. Стоимость рассчитывается индивидуально после аудита. Получите консультацию — свяжитесь с нами, чтобы обсудить ваш проект.