Перейти к содержанию

REST API и идентификатор документа

Оглавление серии

REST API позволяет запросить содержание без административной панели. Клиент обращается к адресу коллекции, передаёт нужные параметры и получает структурированные данные. В этом уроке мы разберём ответ Strapi 5 так, чтобы дальнейший сайт не зависел от привычек старой версии API. Главный результат — чтение опубликованных статей и различение documentId, служебного id и редакционного slug.

До первого запроса опубликуйте одну учебную статью в локальной панели. Остальные оставьте черновиками. Для чтения создайте временный Custom API token с операциями Article find/findOne; точную матрицу прав расширим в девятом уроке. Public role оставьте закрытой. Token находится в серверном окружении, а не в браузерном скрипте.

Ответ принадлежит документу

Strapi 5 возвращает поля Article непосредственно внутри объектов data. Для списка data является массивом; для отдельного документа — объектом. Не обращайтесь к data.attributes.title: это характерная структура прежней версии. В выбранном контракте название списка читается как result.data[0].title. Различие форматов зафиксировано в REST API.

У документа есть documentId, объединяющий его представления: опубликованную и черновую версии, а при включённой локализации — языковые варианты. Числовой id может встречаться в ответе, но для адреса отдельного документа выбираем documentId. Редакционный slug нужен маршруту сайта. Это три разных назначения, хотя все значения могут выглядеть как способы «найти статью».

Ниже приведён учебный образец формы ответа, а не полученный лог. Строка documentId вымышлена и не должна копироваться в запрос к вашей базе.

{
  "data": [
    {
      "id": 3,
      "documentId": "a1b2c3d4e5f6g7h8i9j0klm",
      "title": "Сетка карточек CSS Grid",
      "slug": "css-grid-cards",
      "summary": "Размещаем карточки в адаптивной сетке.",
      "publishedAt": "2026-10-11T09:00:00.000Z"
    }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "pageSize": 10,
      "pageCount": 1,
      "total": 1
    }
  }
}

Сохраните реальный documentId из собственного ответа, если будете запрашивать /api/articles/{documentId}. Не используйте вместо него число 3 из образца. Также не сравнивайте текущие служебные IDs с чужим скриншотом: другой порядок создания записей естественно даёт другие значения.

Slug удобен для ссылки /ru/articles/css-grid-cards, но сервер должен преобразовать его в фильтр или поиск документа. Наличие строки в URL не доказывает существование опубликованной записи. В одиннадцатом уроке отсутствие совпадения приведёт к 404. Таким образом, внутренний идентификатор помогает API, а читаемый адрес помогает человеку; клиент связывает эти уровни явно.

Минимальный серверный запрос

Файл requests/read.mjs выполняет одно чтение и выводит только идентификаторы и заголовки. Он не открывает сервер и не меняет документы. Перед будущим запуском читатель передаст CMS_TOKEN локально. Мы его не запускали при подготовке курса.

const response = await fetch('http://127.0.0.1:1337/api/articles?status=published&locale=ru', {
  headers: { Authorization: `Bearer ${process.env.CMS_TOKEN}` },
  signal: AbortSignal.timeout(5000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
for (const article of result.data) {
  console.log(article.documentId, article.title);
}

fetch получает Authorization с Bearer token. Метод не указан, поэтому используется GET. Проверка response.ok нужна до чтения ожидаемой структуры: ответ об ошибке не обязан содержать массив Article. Timeout ограничивает ожидание, но не является доказательством, что CMS перестала обрабатывать запрос; для чтения это обычно не создаёт побочного эффекта.

В файле уже указан locale=ru, хотя локализацию Article добавим в десятом уроке. До включения i18n параметр не создаёт русскую версию сам по себе. В первоначальной модели можно убрать его для первого чтения; после настройки ru верните явное значение. Этот переход описан в snapshots и не должен скрываться под предположением, что язык автоматически берётся из текста заголовка.

Ожидаемый вывод после вашей публикации — строка с реальным documentId и «Сетка карточек CSS Grid». Черновики «Форма обратной связи» и «Структура адресов библиотеки» не должны появиться в выборке published. Если data пуст, сначала сопоставьте состояние публикации и локаль, затем права token. Пустой список не равен ошибке сети и не исправляется увеличением pageSize.

Поля отношений запрашиваются отдельно

Author, Category, media и components могут отсутствовать в обычном ответе. Для получения связанных значений применяется populate. Запрашивайте только нужные части: имя автора для подписи, название категории для карточки, SEO для metadata. Связанный тип также должен быть доступен token. Пустая связь при недостаточных permissions не означает, что её следует создать повторно.

Для первой проверки добавьте populate[author][fields][0]=name. В ответе ожидается объект author с именем Анны, если связь существует и разрешено чтение Author. Сравните ответ без populate и с ним. Меняется представление данных, а не сама статья в базе. Это полезный эксперимент: он показывает, что API отвечает на конкретный запрос, а не всегда отдаёт всю редакционную запись.

Не расширяйте populate до всего графа без причины. Автор может иметь обратную связь articles, у статей — category и resources. Неограниченное вложение делает ответ тяжёлым и затрудняет понимание, какие сведения нужны странице. Для минимального потребителя мы позже перечислим только несколько явных полей и ограничим размер списка.

Особое внимание уделите публичным запросам. Без token наш контракт должен запрещать /api/articles, поскольку Public find выключен. Серверный успешный ответ не означает, что те же права появились у браузера. Если анонимный запрос неожиданно возвращает статьи, проверьте Public role и настройки маршрутов. Значение status=published по умолчанию не является самостоятельной защитой черновиков: клиент с правом чтения может запросить draft, если вы не создали дополнительную границу. Поэтому наш проект передаёт token только доверенному серверу.

Разделите также формат transport и модель содержания. JSON определяет, как значения передаются, но не сообщает, что title является заголовком статьи, а body — обычным текстом. Эту семантику задаёт наша схема и договорённость клиента. Поэтому успешный JSON.parse ещё не означает пригодный Article. В более устойчивом потребителе добавляют проверку ожидаемых полей ответа перед rendering. Например, data списка должна быть массивом, а title — строкой. Такая проверка полезна при неожиданном изменении schema или версии API и отличается от проверки HTTP-кода.

Если запрос вернул 200, но полезные поля отсутствуют, сохраните безопасное описание проблемы на сервере и сопоставьте контракт. Не показывайте token и не пытайтесь «починить» ответ случайной подстановкой undefined. Первое чтение учит разбирать данные, а не только добиваться отсутствия exception в консоли.

В результате урока вы сможете объяснить каждую часть первого ответа: data содержит документы, meta описывает страницу выборки, documentId используется для адресного API, slug — для сайта, populate — для отношения. Следующий шаг добавит условия отбора и покажет, почему предсказуемый порядок важнее случайного расположения записей при создании.