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, где определяются доступные поля, фильтры, сортировка и операции.
Идея выглядит примерно так:
Самый простой путь в MODX обычно выглядит примерно так: frontend → свой PHP endpoint → MODX → JSON
Для небольшого проекта этого вполне достаточно.
Но когда API начинает использоваться серьёзнее, появляются дополнительные задачи:
mxHeadless как раз закрывает этот слой.
Безопасность начинается с registry
Это одна из важных частей mxHeadless.
Мне не хотелось делать API по принципу:
В mxHeadless объект сначала должен быть зарегистрирован в ObjectRegistry.
Например, условный объект товара может объявить только необходимые поля:
Имя объекта в URL сопоставляется с зарегистрированным ObjectDefinition, а не с произвольным PHP-классом.
Для обычного headless-сайта не нужно начинать с написания собственных endpointов.
Для ресурсов доступны:
Пример ответа API:
Реальные запросы к API
Теперь немного практики. Все примеры ниже можно выполнять обычным curl. Вместо example.com подставляется адрес вашего MODX-сайта.
Начать можно с health check:
Пример ответа API:
Например, получить пять последних опубликованных страниц:
Пример ответа API:
Здесь сразу видно несколько возможностей API:
Пример ответа API:
Получить страницу по URI
Фильтрация
Например, получить опубликованные страницы определённого родителя:
Пример ответа API:
Также можно использовать фильтр по названию:
Пример ответа API:
Frontend получает общее количество записей и информацию о наличии следующей страницы непосредственно в meta.
Получить зарегистрированный объект
Допустим, дополнение зарегистрировало объект products. Тогда его можно получить через универсальный endpoint:
Пример ответа API:
При этом frontend работает с публичным именем объекта, а доступные поля, фильтры и операции определяются его регистрацией в mxHeadless.
Запрос с API-ключом
Для защищённых маршрутов используется Bearer-токен.
Конкретные поля зависят от типа объекта и его регистрации.
Создать ресурс через API
Для создания, изменения и удаления ресурсов нужны соответствующие write-scopes.
Пример ответа API:
Idempotency-Key позволяет безопасно повторять POST при сетевых ошибках: при повторе с тем же ключом и тем же телом API может вернуть сохранённый результат операции.
Пример ответа API:
Формат позволяет frontend централизованно обрабатывать ошибки API.
После получения токена:
Пример ответа API:
Не только GET
mxHeadless поддерживает не только чтение.
Для ресурсов и зарегистрированных объектов доступны операции:
Удаление ресурсов по умолчанию выполняется как soft delete. Для permanent delete используется ?force=1.
Авторизация
В mxHeadless предусмотрены несколько способов авторизации:
OpenAPI и Swagger уже внутри
Документация API не требует отдельного проекта.
Интерактивный Swagger UI доступен по адресу:
Сырая OpenAPI-спецификация:
Спецификация строится из текущих маршрутов и зарегистрированных объектов. Поэтому зарегистрированный через Extension API объект может появиться в актуальной схеме API.
Это удобно и для генерации клиентов. Например, OpenAPI можно использовать как источник для TypeScript client generation.
CORS и ограничения запросов
Если frontend и MODX находятся на разных origin, в mxHeadless есть настройка CORS.
CORS выключен по умолчанию и включается через настройки компонента.
Можно задать:
Для запросов также предусмотрены ограничения размера тела, URI и параметров выборки.
Webhooks
После успешных create/update/delete mxHeadless может поставить событие в outbox.
Подписки на события можно настроить, например:
Для webhook поддерживаются подпись HMAC и повторные попытки доставки.
Это, например, удобно для сценария, когда после изменения страницы frontend должен запустить собственную revalidation.
Extension API для Extras
Одна из важных частей mxHeadless — возможность подключать к API не только стандартные объекты MODX.
Другие дополнения могут зарегистрировать свои объекты через событие:
Например, Extra может предоставить собственный объект с каталогом, ценами или другими данными, не создавая полностью отдельный REST API.
mxHeadless даёт общий транспорт и формат API, а конкретное дополнение отвечает за регистрацию своих объектов и их правила доступа.
Nuxt, Next.js, SvelteKit и другие клиенты
mxHeadless не привязан к конкретному frontend-фреймворку.
API работает через обычный HTTP и JSON, поэтому его можно использовать из:
Архитектура при этом может выглядеть так:
А если rewrite не работает?
Обычный вариант:
Если rewrite настроить невозможно, есть fallback через api.php:
Это позволяет проверить работу API даже без стандартного маршрута через index.php.
Кому это действительно нужно?
Я бы не ставил mxHeadless на каждый обычный сайт MODX.
Если у вас классический сайт, где MODX сам генерирует HTML, REST API может вообще не понадобиться.
А вот если вы делаете:
Что получается в итоге
Если совсем коротко, mxHeadless — это REST API gateway для MODX 3, который позволяет работать с ресурсами, страницами и зарегистрированными объектами через единый JSON API.
В компоненте есть:
Попробовать
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 существует благодаря людям, которые не только используют проекты, но и помогают им становиться лучше.
Последнее время всё чаще приходится сталкиваться с проектами, где 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.
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 — удаление.
Удаление ресурсов по умолчанию выполняется как soft delete. Для permanent delete используется ?force=1.
Авторизация
В mxHeadless предусмотрены несколько способов авторизации:
- API Keys — ключи формата mxh_*;
- OAuth client_credentials — токены формата mxt_*, если OAuth включён;
- сессия MODX Manager — для соответствующих сценариев работы.
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.
Для запросов также предусмотрены ограничения размера тела, URI и параметров выборки.
Webhooks
После успешных create/update/delete mxHeadless может поставить событие в outbox.
Подписки на события можно настроить, например:
- resources.created;
- resources.updated;
- resources.deleted;
- события зарегистрированных объектов.
Для 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 │
└──────┬───────┘
│
▼
┌──────────────┐
│ 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 — это 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.
Попробовать
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 существует благодаря людям, которые не только используют проекты, но и помогают им становиться лучше.
Техническая поддержка MODX
Сайт лежит, тормозит или остался без разработчика?
Переезд с MODX 2 на 3, PHP 7 на 8, скорость и безопасность. Поддержка со сроками и ответственностью, а не совет в чате.
Подробнее
Реклама
0
Комментарии: 0