Strapi 5 Document Service API: Drafts, Locales, and Queries
Learn how the Strapi 5 Document Service API replaces the Entity Service, handles documentId, and manages drafts, publishing, and localized content.
By the Revisionary team · · 6 min read
The Strapi 5 Document Service API replaces the legacy Entity Service to provide a unified, document-centric way to query and manipulate content. Instead of managing separate database rows for draft, published, and translated versions of a record, you now interact with a single documentId that encapsulates all states and locales of an entry. Understanding how to query, mutate, publish, and filter through strapi.documents is essential for writing custom controllers, services, and backend lifecycles in Strapi 5.
Why Strapi 5 Replaced the Entity Service
In Strapi 4, content entries relied strictly on auto-incrementing integer database IDs. When Draft & Publish was enabled, Strapi stored both draft and published states as individual rows in the same table, linking them behind the scenes. Adding the Internationalization (i18n) plugin further complicated this pattern: each locale created another distinct row with its own ID, connected through relational join tables.
This architecture caused common headaches for developers:
- Querying a record by ID could return a draft or a published entry depending on internal flags.
- Managing relations across locales often led to broken links or unexpected cascade updates.
- Third-party integrations and front ends had to track multiple IDs for what editors consider a single piece of content.
Strapi 5 resolves this with the Document Service. A document represents an individual piece of content across all its variants. Every document receives a unique, persistent string identifier called documentId. The numerical id still exists at the database row level for raw SQL references, but your application code, custom controllers, and Content APIs now operate on documentId.
The Core Concept: documentId vs. id
To work effectively with Strapi 5, you must separate database rows from content documents.
| Concept | Identifier Type | Scope | Usage |
|---|---|---|---|
| Document | documentId (string) | Universal across drafts, published versions, and locales | Business logic, Document Service queries, REST/GraphQL APIs |
| Entry Row | id (integer) | Specific to an exact database record (a single state in a single locale) | Low-level Knex queries, legacy database migrations |
When writing business logic, always use documentId. If you update a document using its documentId, Strapi automatically directs the modification to the correct draft or published record depending on the parameters you supply.
Accessing the Document Service
You access the Document Service using the global helper strapi.documents('api::contentType.contentType'). This method accepts the Content Type UID and returns an API client tailored for that schema.
// Example: Fetching the document service for an Article collection
const articleService = strapi.documents('api::article.article');The Document Service supports standard CRUD operations alongside document-specific actions:
findMany(): Query multiple documents matching specific criteria.findOne(): Query a single document by itsdocumentId.findFirst(): Retrieve the first document matching a set of filters.create(): Create a new document in a draft or published state.update(): Modify an existing document bydocumentId.delete(): Remove an entire document, a single locale, or a specific state.publish(): Promote a draft version to published status.unpublish(): Remove the published version while keeping the draft intact.discardDraft(): Revert a draft to the contents of the current published version.count(): Count documents matching filter parameters.
Querying Content: findMany and findOne
By default, queries made with the Document Service target the draft state unless you explicitly request published entries. This differs from the public Content API (REST and GraphQL), which returns published data to unauthenticated requests by default.
Basic Queries and Filtering
When fetching documents, you can filter fields, sort records, populate relations, and select specific attributes:
// controllers/article.ts
export default {
async findFeatured(ctx) {
const articles = await strapi.documents('api::article.article').findMany({
fields: ['title', 'slug', 'publishedAt'],
filters: {
isFeatured: { $eq: true },
},
populate: {
author: {
fields: ['name', 'email'],
},
coverImage: true,
},
sort: { createdAt: 'desc' },
limit: 10,
});
ctx.body = articles;
},
};Querying a Document by documentId
To find a specific entry, pass documentId directly to findOne():
const article = await strapi.documents('api::article.article').findOne({
documentId: 'a1b2c3d4e5f6g7h8i9j0',
populate: ['category'],
});If you omit the status parameter, Strapi fetches the draft version of the document if it exists, or the published version if no draft exists.
Managing Draft and Publish States
Draft & Publish is a native capability in Strapi 5 managed directly through query parameters rather than separate endpoints or complicated database flags.
Specifying the Status Parameter
The status option accepts two values:
'draft': Targets draft versions. If an entry has unpublished changes, this returns the pending updates.'published': Targets only the active, published version. If an entry has never been published, a query for its published state returnsnull.
// Fetch only the published version for a public-facing page
const publishedArticle = await strapi.documents('api::article.article').findOne({
documentId: 'a1b2c3d4e5f6g7h8i9j0',
status: 'published',
});
// Fetch the draft version for a preview endpoint
const draftArticle = await strapi.documents('api::article.article').findOne({
documentId: 'a1b2c3d4e5f6g7h8i9j0',
status: 'draft',
});Publishing, Unpublishing, and Discarding
Strapi 5 provides dedicated methods to transition documents across workflow stages without requiring manual updates to timestamp fields like publishedAt.
const docService = strapi.documents('api::article.article');
const targetId = 'a1b2c3d4e5f6g7h8i9j0';
// 1. Publish a draft
const published = await docService.publish({
documentId: targetId,
});
// 2. Unpublish (moves content back to draft-only, unsetting published state)
const unpublished = await docService.unpublish({
documentId: targetId,
});
// 3. Discard Draft (discards uncommitted edits, restoring the published state into draft)
const reverted = await docService.discardDraft({
documentId: targetId,
});When discardDraft() is called, any edits made since the last publication event are erased, aligning the draft content with the currently live document.
Working with Internationalization (i18n)
In Strapi 5, locales are sub-records of the same document. A single documentId groups all translations of an entry together. This eliminates the need to query translation link tables or match alternate entries manually.
Querying Specific Locales
To query a document in a specific locale, pass the locale parameter:
// Fetch the French translation of an article
const frenchArticle = await strapi.documents('api::article.article').findOne({
documentId: 'a1b2c3d4e5f6g7h8i9j0',
locale: 'fr',
status: 'published',
});If you want to fetch all available locales for a set of documents, use locale: '*' in findMany():
const allTranslations = await strapi.documents('api::article.article').findMany({
locale: '*',
filters: {
category: { slug: 'technology' },
},
});Creating and Updating Localized Content
When creating a localized version of an existing document, you do not invoke a separate i18n service. Instead, you supply the existing documentId alongside the new locale code using update() or create():
// Adding a German version to an existing document
const germanEntry = await strapi.documents('api::article.article').create({
documentId: 'a1b2c3d4e5f6g7h8i9j0',
locale: 'de',
data: {
title: 'Einführung in Strapi 5',
content: 'Hier ist der Inhalt auf Deutsch...',
},
});If you call update() with a specific locale, Strapi only modifies that locale's data, leaving other languages untouched:
await strapi.documents('api::article.article').update({
documentId: 'a1b2c3d4e5f6g7h8i9j0',
locale: 'es',
data: {
title: 'Título Actualizado',
},
});Creating and Updating Content
When creating new content via the Document Service, you can decide whether the document should begin as a draft or publish immediately.
Creating Drafts vs. Published Documents
By default, calling create() without workflow flags creates a draft record:
// Creates a draft document
const newDraft = await strapi.documents('api::article.article').create({
data: {
title: 'Upcoming Product Launch',
slug: 'upcoming-product-launch',
body: 'Draft content undergoing review.',
},
});To create a document and make it live in a single call, set status: 'published' in the parameters:
// Creates and immediately publishes the document
const liveArticle = await strapi.documents('api::article.article').create({
data: {
title: 'Breaking News Announcement',
slug: 'breaking-news',
body: 'This content is live immediately.',
},
status: 'published',
});Updating Specific Fields
When updating a document, you only need to provide the fields you wish to change. Relational fields accept documentId values or arrays of IDs depending on whether the relation is single or polymorphic:
await strapi.documents('api::article.article').update({
documentId: 'a1b2c3d4e5f6g7h8i9j0',
data: {
title: 'Revised Headline',
category: 'cat_987654321',
},
});Document Service Middlewares
Strapi 5 introduces Document Service Middlewares. These act as interceptors around Document Service calls, replacing many use cases where developers previously relied on brittle lifecycle hooks (beforeCreate, afterUpdate).
Middlewares wrap operations in a predictable chain, allowing you to validate data, mutate parameters before they reach the database, or trigger side effects after mutations complete.
Registering a Document Middleware
You register Document Service Middlewares inside your application's register or bootstrap functions in src/index.ts:
// src/index.ts
import type { Core } from '@strapi/strapi';
export default {
register({ strapi }: { strapi: Core.Strapi }) {
strapi.documents.use(async (context, next) => {
// Check the targeted collection and action
if (context.uid === 'api::article.article' && context.action === 'create') {
// Inspect or modify incoming parameters
if (context.params.data && !context.params.data.slug) {
context.params.data.slug = context.params.data.title
.toLowerCase()
.replace(/\s+/g, '-');
}
}
// Execute the next middleware or database operation
const result = await next();
// Post-operation logic
if (context.uid === 'api::article.article' && context.action === 'publish') {
strapi.log.info(`Document published: ${result.documentId}`);
}
return result;
});
},
};Unlike traditional lifecycles, middlewares receive the full context of the Document Service query, including requested fields, populate options, locale, and workflow status.
Safe Deletions: Document vs. Locale
Deleting content in Strapi 5 provides granular control over whether you are clearing a single translation or deleting the entire entity.
Deleting a Specific Locale
If you pass both documentId and a locale, only that translation is removed. Other locales for the same document remain intact:
// Removes only the Spanish translation
await strapi.documents('api::article.article').delete({
documentId: 'a1b2c3d4e5f6g7h8i9j0',
locale: 'es',
});Deleting the Entire Document
Omitting the locale parameter or deleting the primary locale deletes the document across all locales and states:
// Removes all locales, drafts, and published versions of this document
await strapi.documents('api::article.article').delete({
documentId: 'a1b2c3d4e5f6g7h8i9j0',
});When managing complex editorial workflows with multiple authors across locales, teams often need auditability for deletions and revisions. If your team requires full rollback capabilities, plugins like Revisionary keep every version of your Strapi content, providing field-level differences, selective restores, and a trash bin where deleted document states can be recovered per locale.
Migrating from Strapi 4 Entity Service
When upgrading backend code from Strapi 4 to Strapi 5, audit your codebase for strapi.entityService calls and convert them to strapi.documents.
Key Migration Differences
- Method Calls: Replace
strapi.entityService.findMany(uid, params)withstrapi.documents(uid).findMany(params). - ID References: Replace integer lookups (
findOne(uid, id)) with document IDs (findOne({ documentId })). - Publishing Logic: Remove manual updates to
publishedAt: new Date(). Usestrapi.documents(uid).publish({ documentId })instead. - Populate Syntax: Populate structures in Strapi 5 follow similar object syntax, but relations now return
documentIdalongside the record. - Draft Defaults: In Strapi 4, Entity Service queries bypassed Draft & Publish unless configured with publication state parameters. In Strapi 5,
strapi.documentsdefaults tostatus: 'draft'. If you need live data, always specifystatus: 'published'explicitly.
Here is a side-by-side comparison for a common lookup:
// --- Strapi 4 (Entity Service) ---
const strapi4Result = await strapi.entityService.findOne('api::article.article', 42, {
populate: { author: true },
publicationState: 'live',
});
// --- Strapi 5 (Document Service) ---
const strapi5Result = await strapi.documents('api::article.article').findOne({
documentId: 'c1d2e3f4g5h6i7j8k9l0',
status: 'published',
populate: { author: true },
});Conclusion
The Document Service API in Strapi 5 unifies drafts, publications, and translations under a single, consistent documentId. By transitioning your custom controllers, cron jobs, and service logic to strapi.documents(), you gain predictable querying behavior, cleaner data structures, and native support for complex localization workflows. Ensure your queries explicitly define the desired status and locale parameters to retrieve exactly the content version your application expects.
The Revisionary team
Developers who build Revisionary, a version history plugin for Strapi 5. We write about Strapi, web development, AI and application security.
This article was written with the help of AI.