Server API: Content-types
Page summary:
The Server API exports a
contentTypesobject from the server entry file to declare plugin content-types. The recommended naming convention is to use the same value for the export key andinfo.singularNameso the runtime UID remains predictable when querying or sanitizing data.
A plugin can declare its own content-types by exporting a contentTypes object from the server entry file. Strapi registers these content-types under the plugin namespace at startup and makes them available through the Document Service API and the content-type registry.
Before diving deeper into the concepts on this page, please ensure you have:
- created a Strapi plugin,
- read and understood the basics of the Server API
Declaration
The contentTypes export is an object where each key registers a content-type under the plugin namespace. To avoid confusion, the key should match the info.singularName field in the schema. The value is an object with a schema property pointing to the schema definition.
- JavaScript
- TypeScript
'use strict';
const article = require('./article');
module.exports = {
article: { schema: article }, // recommended: keep key aligned with info.singularName
};
{
"kind": "collectionType",
"collectionName": "my_plugin_articles",
"info": {
"singularName": "article",
"pluralName": "articles",
"displayName": "Article"
},
"options": {
"draftAndPublish": false
},
"attributes": {
"title": {
"type": "string",
"required": true
},
"body": {
"type": "richtext"
}
}
}
import article from './article';
export default {
article: { schema: article }, // recommended: keep key aligned with info.singularName
};
{
"kind": "collectionType",
"collectionName": "my_plugin_articles",
"info": {
"singularName": "article",
"pluralName": "articles",
"displayName": "Article"
},
"options": {
"draftAndPublish": false
},
"attributes": {
"title": {
"type": "string",
"required": true
},
"body": {
"type": "richtext"
}
}
}
UIDs and naming conventions
When a plugin content-type is registered, Strapi builds its runtime UID from the plugin namespace and the key used in the contentTypes export:
plugin::<plugin-name>.<content-types-key>
The recommended convention is to set content-types-key === info.singularName. Following this convention keeps the schema naming and runtime UID aligned and easier to read.
When the key matches singularName (recommended), the resulting UID follows this format:
plugin::<plugin-name>.<singular-name>
For example, a plugin named my-plugin with a content-type whose singularName is article and export key article has the UID plugin::my-plugin.article.
If the contentTypes key and info.singularName diverge, getters and queries use the UID built from the registered key (not from singularName). This can introduce naming inconsistencies across your plugin code.
This UID is used consistently across all APIs:
| Use case | Example |
|---|---|
| Query via Document Service | strapi.documents('plugin::my-plugin.article').findMany() |
| Access schema via getter | strapi.contentType('plugin::my-plugin.article') |
| Reference in route handler | handler: 'article.find' (short form, resolved via plugin registry) |
| Pass to sanitization API | strapi.contentAPI.sanitize.output(data, schema, { auth }) |
Controllers, services, policies, and middlewares use the same plugin::<plugin-name>.<resource-name> UID format for global getters, but are referenced by their short registry key (e.g., 'article') within plugin-level APIs such as route handler and policies. See Getters & usage for details.
Access at runtime
Querying with the Document Service API
Use the Document Service API to query plugin content-types from controllers, services, or lifecycle hooks:
- JavaScript
- TypeScript
module.exports = ({ strapi }) => ({
async findAll(params = {}) {
return strapi.documents('plugin::my-plugin.article').findMany(params);
},
async create(data) {
return strapi.documents('plugin::my-plugin.article').create({ data });
},
});
import type { Core } from '@strapi/strapi';
export default ({ strapi }: { strapi: Core.Strapi }) => ({
async findAll(params: Record<string, unknown> = {}) {
return strapi.documents('plugin::my-plugin.article').findMany(params);
},
async create(data: Record<string, unknown>) {
return strapi.documents('plugin::my-plugin.article').create({ data });
},
});
For the full list of available methods and parameters, see the Document Service API.
Accessing the schema
Use the content-type getter to retrieve the schema object, for example to pass it to the sanitization API:
- JavaScript
- TypeScript
const schema = strapi.contentType('plugin::my-plugin.article');
const sanitizedOutput = await strapi.contentAPI.sanitize.output(
data,
schema,
{ auth: ctx.state.auth }
);
const schema = strapi.contentType('plugin::my-plugin.article');
const sanitizedOutput = await strapi.contentAPI.sanitize.output(
data,
schema,
{ auth: ctx.state.auth }
);
Best practices
-
Match the export key to
info.singularNameexactly. This keeps naming readable and consistent. At runtime, Strapi derives the plugin content-type UID from the key of thecontentTypesmap under the plugin namespace. A mismatch may create confusing UIDs and maintenance issues, even if registration still succeeds. -
Use
collectionNameto avoid table name conflicts. ThecollectionNamefield sets the database table name. Prefix it with the plugin name (e.g.,my_plugin_articles) to avoid collisions with application content-types or other plugins. -
Keep content-type schemas in their own files. Define each schema in a dedicated
schema.jsonfile inside a subfolder named after thesingularName(e.g.,content-types/article/schema.json). This matches the structure generated by the Plugin SDK and keeps the index file readable. -
Enable
draftAndPublishonly when needed. Draft and Publish adds a publication workflow to the content-type. Enable it only if the plugin's use case requires it, as it adds complexity to queries and content management.