mxHeadless — REST API для MODX 3 без лишнего

MODX как backend для Nuxt, Next.js, мобильного приложения или другого frontend — без написания собственного REST API с нуля. mxHeadless добавляет в MODX 3 REST API для ресурсов, страниц и зарегистрированных объектов с авторизацией, scopes, OpenAPI и Swagger UI.

Последнее время всё чаще приходится сталкиваться с проектами, где MODX используется не как классический PHP-шаблонизатор, а как backend для отдельного frontend-приложения.

Что такое mxHeadless

После установки дополнения у сайта появляется API с базовым префиксом: example.com/api/v1

mxHeadless отдаёт ресурсы MODX, страницы, элементы, контексты и зарегистрированные xPDO-объекты в JSON.

При этом API не предоставляет произвольный доступ к PHP-классам. Объект должен быть зарегистрирован в ObjectRegistry, где определяются доступные поля, фильтры, сортировка и операции.

Идея выглядит примерно так:

┌───────────────┐
│    Nuxt       │
│    Next.js    │
│    SvelteKit  │
│    Mobile App │
└───────┬───────┘
        │
        │ REST / JSON
        ▼
┌─────────────────────┐
│     mxHeadless      │
│                     │
│ Auth                │
│ Registry            │
│ Filters             │
│ OpenAPI             │
│ Swagger             │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│        MODX         │
│ Resources           │
│ Elements            │
│ Contexts            │
│ Registered Objects  │
└─────────────────────┘
А что не так с обычным REST API?

Самый простой путь в MODX обычно выглядит примерно так: frontend → свой PHP endpoint → MODX → JSON

Для небольшого проекта этого вполне достаточно.

Но когда API начинает использоваться серьёзнее, появляются дополнительные задачи:

  • авторизация клиентов;
  • ограничение доступных полей;
  • разрешения на чтение и запись;
  • фильтрация и сортировка;
  • пагинация;
  • работа с контекстами;
  • единый формат ответов и ошибок;
  • документация API;
  • подключение объектов из других Extras.
В какой-то момент оказывается, что мы уже не пишем один endpoint, а фактически собираем собственный REST API.

mxHeadless как раз закрывает этот слой.

Безопасность начинается с registry

Это одна из важных частей mxHeadless.

Мне не хотелось делать API по принципу:

/api/object/modResource
/api/object/modUser
/api/object/какой-нибудь-класс
где внешний клиент потенциально может обращаться к внутренним классам MODX.

В mxHeadless объект сначала должен быть зарегистрирован в ObjectRegistry.

Например, условный объект товара может объявить только необходимые поля:

{
    "fields": [
        "id",
        "pagetitle",
        "price",
        "image"
    ]
}


Имя объекта в URL сопоставляется с зарегистрированным ObjectDefinition, а не с произвольным PHP-классом.

Наружу отдаём только то, что явно зарегистрировано.
Ресурсы MODX уже готовы

Для обычного headless-сайта не нужно начинать с написания собственных endpointов.

Для ресурсов доступны:

  • списки ресурсов;
  • получение ресурса по ID;
  • страницы по URI;
  • фильтрация;
  • сортировка;
  • пагинация;
  • выбор нужных полей;
  • поиск;
  • работа с контекстами;
  • preview неопубликованного контента;
  • создание, изменение и удаление ресурсов.
Например:

GET /api/v1/resources?filter[published]=1

Пример ответа API:

{
    "data": [],
    "meta": {
        "total": 100,
        "count": 20,
        "limit": 20,
        "offset": 0,
        "has_more": true
    },
    "links": {
        "self": "...",
        "next": "..."
    }
}
Формат успешного ответа одинаковый для коллекций: data, meta и links.

Реальные запросы к API

Теперь немного практики. Все примеры ниже можно выполнять обычным curl. Вместо example.com подставляется адрес вашего MODX-сайта.

Важно: все JSON-блоки ниже — примеры ожидаемого формата ответа API. Конкретные данные, ID, количество записей и доступные поля будут зависеть от вашего сайта и настроек ObjectRegistry.
Проверить, что API работает

Начать можно с health check:

curl -s https://example.com/api/v1/health | jq

Пример ответа API:

{
    "data": {
        "status": "ok"
    }
}
Получить список ресурсов

Например, получить пять последних опубликованных страниц:

curl -s \
  'https://example.com/api/v1/resources?limit=5&filter[published]=1&sort=-id&fields=id,pagetitle,uri' \
  | jq

Пример ответа API:

{
    "data": [
        {
            "id": 125,
            "pagetitle": "Новости компании",
            "uri": "news/company"
        },
        {
            "id": 124,
            "pagetitle": "Новая статья",
            "uri": "blog/new-article"
        },
        {
            "id": 123,
            "pagetitle": "Каталог",
            "uri": "catalog"
        }
    ],
    "meta": {
        "total": 127,
        "count": 5,
        "limit": 5,
        "offset": 0,
        "has_more": true
    },
    "links": {
        "self": "...",
        "next": "..."
    }
}

Здесь сразу видно несколько возможностей API:

  • limit=5 — ограничиваем количество результатов;
  • filter[published]=1 — берём только опубликованные ресурсы;
  • sort=-id — сортируем по ID в обратном порядке;
  • fields=... — не отправляем frontend лишние поля.

Получить конкретный ресурс

curl -s \
  'https://example.com/api/v1/resources/5?fields=id,pagetitle,content,publishedon' \
  | jq

Пример ответа API:

{
    "data": {
        "id": 5,
        "pagetitle": "О компании",
        "content": "<p>Текст страницы...</p>",
        "publishedon": "2026-08-20 10:15:00"
    }
}

Получить страницу по URI

curl -s \
  'https://example.com/api/v1/pages/about?fields=id,pagetitle,content' \
  | jq
Пример ответа API:

{
    "data": {
        "id": 5,
        "pagetitle": "О компании",
        "content": "<p>Мы создаём...</p>"
    }
}

Фильтрация

Например, получить опубликованные страницы определённого родителя:

curl -s \
  'https://example.com/api/v1/resources?filter[parent]=2&filter[published]=1&fields=id,pagetitle,uri' \
  | jq

Пример ответа API:

{
    "data": [
        {
            "id": 21,
            "pagetitle": "Первая статья",
            "uri": "blog/first"
        },
        {
            "id": 22,
            "pagetitle": "Вторая статья",
            "uri": "blog/second"
        }
    ],
    "meta": {
        "total": 2,
        "count": 2,
        "limit": 20,
        "offset": 0,
        "has_more": false
    },
    "links": {
        "self": "..."
    }
}

Также можно использовать фильтр по названию:

curl -s \
  'https://example.com/api/v1/resources?filter[pagetitle][like]=%News%' \
  | jq

Пагинация

curl -s \
  'https://example.com/api/v1/resources?limit=20&offset=40&sort=-createdon&fields=id,pagetitle,uri' \
  | jq

Пример ответа API:

{
    "data": [
        {
            "id": 87,
            "pagetitle": "Новость №47",
            "uri": "news/47"
        }
    ],
    "meta": {
        "total": 127,
        "count": 20,
        "limit": 20,
        "offset": 40,
        "has_more": true
    },
    "links": {
        "self": "...",
        "next": "..."
    }
}

Frontend получает общее количество записей и информацию о наличии следующей страницы непосредственно в meta.

Получить зарегистрированный объект

Допустим, дополнение зарегистрировало объект products. Тогда его можно получить через универсальный endpoint:

curl -s \
  'https://example.com/api/v1/objects/products?limit=24&sort=price&fields=id,pagetitle,price,uri' \
  | jq

Пример ответа API:

{
    "data": [
        {
            "id": 101,
            "pagetitle": "iPhone Case",
            "price": 1990,
            "uri": "catalog/iphone-case"
        },
        {
            "id": 102,
            "pagetitle": "USB-C Charger",
            "price": 2490,
            "uri": "catalog/usb-c-charger"
        }
    ],
    "meta": {
        "total": 42,
        "count": 24,
        "limit": 24,
        "offset": 0,
        "has_more": true
    },
    "links": {
        "self": "...",
        "next": "..."
    }
}

При этом frontend работает с публичным именем объекта, а доступные поля, фильтры и операции определяются его регистрацией в mxHeadless.

Запрос с API-ключом

Для защищённых маршрутов используется Bearer-токен.

export MXHEADLESS_API_KEY='mxh_...'
curl -s \
  https://example.com/api/v1/chunks \
  -H "Authorization: Bearer $MXHEADLESS_API_KEY" \
  | jq
Пример ответа API:

{
    "data": [
        {
            "id": 1,
            "name": "header",
            "description": "..."
        }
    ],
    "meta": {
        "total": 1,
        "count": 1,
        "limit": 20,
        "offset": 0,
        "has_more": false
    },
    "links": {
        "self": "..."
    }
}

Конкретные поля зависят от типа объекта и его регистрации.

Создать ресурс через API

Для создания, изменения и удаления ресурсов нужны соответствующие write-scopes.

curl -s -X POST \
  https://example.com/api/v1/resources \
  -H "Authorization: Bearer $MXHEADLESS_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: create-page-001' \
  -d '{
    "pagetitle": "API created page",
    "parent": 2,
    "template": 1,
    "published": 0
  }' \
  | jq

Пример ответа API:

{
    "data": {
        "id": 128,
        "pagetitle": "API created page",
        "parent": 2,
        "template": 1,
        "published": 0
    }
}

Idempotency-Key позволяет безопасно повторять POST при сетевых ошибках: при повторе с тем же ключом и тем же телом API может вернуть сохранённый результат операции.

Ошибка API

Ошибки API возвращаются в формате application/problem+json.

Пример ответа API:

{
    "type": "about:blank",
    "title": "Validation Error",
    "status": 422,
    "detail": "The request data is invalid"
}

Формат позволяет frontend централизованно обрабатывать ошибки API.

OAuth

OAuth client_credentials можно использовать, если он включён в настройках mxHeadless.

TOKEN=$(curl -s -X POST \
  https://example.com/api/v1/auth/token \
  -H 'Content-Type: application/json' \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "...",
    "client_secret": "...",
    "scope": "resources.read"
  }' \
  | jq -r .access_token)

После получения токена:

curl -s \
  https://example.com/api/v1/resources \
  -H "Authorization: Bearer $TOKEN" \
  | jq

Пример ответа API:

{
    "data": [
        {
            "id": 125,
            "pagetitle": "Новости компании",
            "uri": "news/company"
        }
    ],
    "meta": {
        "total": 1,
        "count": 1,
        "limit": 20,
        "offset": 0,
        "has_more": false
    },
    "links": {
        "self": "..."
    }
}

Не только GET

mxHeadless поддерживает не только чтение.

Для ресурсов и зарегистрированных объектов доступны операции:

  • POST — создание;
  • PUT / PATCH — изменение;
  • DELETE — удаление.
Для таких операций требуется identity и соответствующий write-scope.

Удаление ресурсов по умолчанию выполняется как soft delete. Для permanent delete используется ?force=1.

Авторизация

В mxHeadless предусмотрены несколько способов авторизации:

  • API Keys — ключи формата mxh_*;
  • OAuth client_credentials — токены формата mxt_*, если OAuth включён;
  • сессия MODX Manager — для соответствующих сценариев работы.
Доступ к защищённым маршрутам определяется scopes и permissions.

OpenAPI и Swagger уже внутри

Документация API не требует отдельного проекта.

Интерактивный Swagger UI доступен по адресу:

/api/v1/docs

Сырая OpenAPI-спецификация:

/api/v1/meta/openapi.json

Спецификация строится из текущих маршрутов и зарегистрированных объектов. Поэтому зарегистрированный через Extension API объект может появиться в актуальной схеме API.

Это удобно и для генерации клиентов. Например, OpenAPI можно использовать как источник для TypeScript client generation.

CORS и ограничения запросов

Если frontend и MODX находятся на разных origin, в mxHeadless есть настройка CORS.

CORS выключен по умолчанию и включается через настройки компонента.

Можно задать:

  • разрешённые origins;
  • разрешённые HTTP-методы;
  • разрешённые заголовки;
  • credentials.
Также в компоненте есть rate limit. По умолчанию он включён, а глобальный лимит составляет 120 запросов за 60 секунд. Лимиты можно настраивать глобально и отдельно для API key или OAuth client.

Для запросов также предусмотрены ограничения размера тела, URI и параметров выборки.

Webhooks

После успешных create/update/delete mxHeadless может поставить событие в outbox.

Подписки на события можно настроить, например:

  • resources.created;
  • resources.updated;
  • resources.deleted;
  • события зарегистрированных объектов.
Доставка выполняется через CLI worker, который можно запускать по cron.

Для webhook поддерживаются подпись HMAC и повторные попытки доставки.

Это, например, удобно для сценария, когда после изменения страницы frontend должен запустить собственную revalidation.

Extension API для Extras

Одна из важных частей mxHeadless — возможность подключать к API не только стандартные объекты MODX.

Другие дополнения могут зарегистрировать свои объекты через событие:

OnMxHeadlessRegister
После регистрации объект появляется в ObjectRegistry и может быть описан в live schema и OpenAPI.

Например, Extra может предоставить собственный объект с каталогом, ценами или другими данными, не создавая полностью отдельный REST API.

mxHeadless даёт общий транспорт и формат API, а конкретное дополнение отвечает за регистрацию своих объектов и их правила доступа.

Nuxt, Next.js, SvelteKit и другие клиенты

mxHeadless не привязан к конкретному frontend-фреймворку.

API работает через обычный HTTP и JSON, поэтому его можно использовать из:

  • JavaScript;
  • TypeScript;
  • Nuxt;
  • Next.js;
  • SvelteKit;
  • мобильных приложений;
  • других HTTP-клиентов.
Для Nuxt/Next.js при серверной работе с API ключ можно держать на стороне backend/server routes, а frontend получать уже необходимые данные.

Архитектура при этом может выглядеть так:

┌──────────────┐
                 │    Nuxt      │
                 └──────┬───────┘
                        │
                        ▼
                 ┌──────────────┐
                 │ mxHeadless   │
                 └──────┬───────┘
                        │
              ┌─────────┴─────────┐
              ▼                   ▼
        ┌───────────┐       ┌──────────────┐
        │  MODX     │       │ Registered   │
        │ resources │       │ Extra object │
        └───────────┘       └──────────────┘

А если rewrite не работает?

Обычный вариант:

/api/v1/...

Если rewrite настроить невозможно, есть fallback через api.php:

assets/components/mxheadless/api.php?route=/v1/health

Это позволяет проверить работу API даже без стандартного маршрута через index.php.

Кому это действительно нужно?

Я бы не ставил mxHeadless на каждый обычный сайт MODX.

Если у вас классический сайт, где MODX сам генерирует HTML, REST API может вообще не понадобиться.

А вот если вы делаете:

  • headless CMS;
  • Nuxt/Next frontend;
  • мобильное приложение;
  • SPA/PWA;
  • несколько клиентов поверх одного backend;
  • интеграцию MODX с внешними сервисами;
  • собственный Extra, которому нужен REST API;
— здесь mxHeadless уже становится гораздо интереснее.

Что получается в итоге

Если совсем коротко, mxHeadless — это REST API gateway для MODX 3, который позволяет работать с ресурсами, страницами и зарегистрированными объектами через единый JSON API.

В компоненте есть:

  • REST API;
  • Resources и Pages;
  • ObjectRegistry;
  • фильтрация, сортировка и пагинация;
  • API Keys;
  • OAuth client_credentials;
  • scopes и permissions;
  • OpenAPI;
  • Swagger UI;
  • CORS;
  • rate limit;
  • Idempotency-Key;
  • HTTP-кэширование;
  • webhooks;
  • audit log;
  • Extension API через OnMxHeadlessRegister.
При этом часть инфраструктурных возможностей включается и настраивается отдельно. Например, CORS и OAuth имеют собственные настройки, HTTP-кэширование можно включить через соответствующий параметр, а audit log по умолчанию выключен.

Попробовать

mxHeadless доступен бесплатно на ModStore: https://modstore.pro/packages/utilities/mxheadless

Документация: https://docs.modx.pro/components/mxheadless/

Исходный код: https://github.com/Ibochkarev/mxHeadless

Если вы давно хотели попробовать связку MODX + Nuxt или другой headless frontend, но останавливало отсутствие единого API-слоя — mxHeadless как раз рассчитан на такой сценарий.

А отдельный интерес здесь представляет Extension API: можно не заставлять каждый Extra изобретать собственный формат REST API, а постепенно подключать его объекты к общей схеме.

Если хотите поддержать эту работу:

Спасибо всем, кто использует, тестирует, создаёт issues, отправляет pull request'ы, пишет документацию и поддерживает проекты финансово.

Open Source существует благодаря людям, которые не только используют проекты, но и помогают им становиться лучше.
Иван Бочкарев
Иван Бочкарев
50 минут назад
modx.pro
12

Комментарии: 0

Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
0