В Strapi 5 привычный entityService ушёл в прошлое — вместо него основным способом работать с контентом на бэкенде стал Document Service API. Это не косметическое переименование: поменялась сама модель данных. Теперь запись — это «документ» с постоянным строковым идентификатором documentId, а черновик и опубликованная версия перестали быть двумя разными числовыми id. В этой статье разберём на практике, как устроен Document Service API, как мигрировать код с Entity Service и на какие грабли легко наступить при переходе.
Что вообще изменилось: документы вместо сущностей
В Strapi v4 каждая запись имела числовой id, а черновик и публикация фактически были отдельными строками в базе с разными id. Это порождало путаницу: фронтенд получал один идентификатор для черновика и другой — для опубликованной версии одной и той же статьи.
В Strapi 5 ввели концепцию документа. У документа есть стабильный documentId — строка вида ln1gkzs6ojl9d707xn6v86mw, которая не меняется между черновиком и публикацией, между локалями и версиями. Числовой id тоже существует, но теперь это внутренний идентификатор конкретной версии записи в БД, а не то, чем вы оперируете в API. В коде и в REST-ответах основным ключом становится documentId.
Важное следствие: старый параметр publicationState: "preview" | "live" заменён на status: "draft" | "published". А управление публикацией теперь идёт не через запись published_at, а через отдельные методы publish(), unpublish() и discardDraft().
Базовые CRUD-операции
Точка входа — функция strapi.documents(uid), куда передаётся UID типа контента (например, api::article.article). Дальше вызываются методы. Вот чтение одного документа и списка:
// Один документ по documentId
const article = await strapi.documents('api::article.article').findOne({
documentId: 'ln1gkzs6ojl9d707xn6v86mw',
populate: ['author', 'cover'],
});
// Список с фильтрами и статусом
const articles = await strapi.documents('api::article.article').findMany({
filters: { title: { $contains: 'Strapi' } },
status: 'published',
sort: 'createdAt:desc',
populate: ['author'],
});
Создание, обновление и удаление выглядят так. Обратите внимание: в update и delete передаётся именно documentId, а не числовой id:
// Создание (по умолчанию создаётся черновик)
const created = await strapi.documents('api::article.article').create({
data: { title: 'Document Service API на практике', slug: 'ds-api' },
});
// Обновление
const updated = await strapi.documents('api::article.article').update({
documentId: created.documentId,
data: { title: 'Обновлённый заголовок' },
});
// Удаление
await strapi.documents('api::article.article').delete({
documentId: created.documentId,
});
// Подсчёт
const total = await strapi.documents('api::article.article').count();
Есть и удобный findFirst() — он возвращает первый документ, подходящий под условия, без необходимости доставать элемент из массива вручную.
Черновики и публикация
Раз черновик и публикация — это две версии одного документа, публикацией управляют явно. По умолчанию create() и update() работают с черновиком (draft). Чтобы опубликовать документ, вызывают publish():
// Опубликовать документ
await strapi.documents('api::article.article').publish({
documentId: article.documentId,
});
// Снять с публикации (останется только черновик)
await strapi.documents('api::article.article').unpublish({
documentId: article.documentId,
});
// Откатить черновик к опубликованной версии
await strapi.documents('api::article.article').discardDraft({
documentId: article.documentId,
});
Такая модель убирает главную боль v4: теперь можно спокойно редактировать черновик, не трогая «живую» версию на сайте, и публиковать одним вызовом, когда всё готово. Параметр status: 'draft' в findMany() и findOne() позволяет читать именно черновики — например, для страницы предпросмотра.
Локали: работа с i18n
Раньше локализованные версии тоже жили под разными id. В Strapi 5 у всех локалей одного материала — общий documentId, а конкретную языковую версию выбирают параметром locale:
// Прочитать английскую версию документа
const en = await strapi.documents('api::article.article').findOne({
documentId: article.documentId,
locale: 'en',
});
// Создать/обновить французскую локаль того же документа
await strapi.documents('api::article.article').update({
documentId: article.documentId,
locale: 'fr',
data: { title: 'Le titre en français' },
});
// Опубликовать конкретную локаль
await strapi.documents('api::article.article').publish({
documentId: article.documentId,
locale: 'fr',
});
Это заметно упрощает мультиязычные проекты: один стабильный идентификатор материала и явное указание языка вместо жонглирования разрозненными числовыми id.
Миграция с Entity Service: что делает codemod, а что придётся руками
Официальный upgrade-инструмент запускает codemod, который автоматически переписывает вызовы. Структуру кода и замену publicationState на status он берёт на себя. Пример того, как преобразуется findMany:
// Было (Strapi v4)
strapi.entityService.findMany(uid, {
fields: ['id', 'name', 'description'],
populate: ['author', 'comments'],
publicationState: 'preview',
});
// Стало (Strapi 5)
strapi.documents(uid).findMany({
fields: ['id', 'name', 'description'],
populate: ['author', 'comments'],
status: 'draft',
});
Но codemod не всесилен, и есть три момента, которые нужно доработать вручную:
- Идентификаторы. Codemod не может превратить старый числовой
entityIdвdocumentId— он просто вставит заглушкуdocumentId: "__TODO__". Все такие места нужно найти и подставить реальные значения. - Публикация. Любой код, который публиковал запись через установку
published_at, придётся переписать на методыpublish(),unpublish(),discardDraft()— codemod это не трогает. - Single types. В v4
findMany()для одиночного типа возвращал один объект. В Strapi 5findMany()всегда возвращает массив, поэтому для single type нужно брать первый элемент:const home = (await strapi.documents('api::home.home').findMany())[0];
Отдельная тема — плагины. Если вы использовали strapi.entityService.decorate() для перехвата запросов, его заменяют на middlewares Document Service через strapi.documents.use(). Идея та же (вклиниться в запрос и модифицировать ctx.params или ответ), но API другое.
Для тех, кто пока не может переписать фронтенд, Strapi оставил обратную совместимость: заголовок Strapi-Response-Format: v4 для REST заставляет API отвечать в старом формате с data.attributes и числовым id. Это позволяет мигрировать бэкенд и фронтенд независимо, по одному эндпоинту за раз.
Итоги
Document Service API — не просто новое имя для старого сервиса, а более честная модель данных: один документ, один стабильный documentId, явное управление черновиками, публикацией и локалями. При миграции с v4 большую часть механической работы сделает codemod из upgrade-инструмента, но три вещи остаются на вас: подстановка реальных documentId вместо __TODO__, переход на методы publish()/unpublish()/discardDraft() и учёт того, что findMany() теперь всегда возвращает массив. Начните с бэкенда под прикрытием заголовка Strapi-Response-Format: v4, а затем спокойно переводите фронтенд на плоский формат ответов и documentId. Так переход на Strapi 5 пройдёт без простоя и потери данных.