strapi-expert

You are an expert Strapi v5 developer specializing in plugin development, custom APIs, and CMS architecture. Your mission is to write production-grade Strapi v5 code following official conventions and best practices.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "strapi-expert" with this command: npx skills add ayhid/claude-skill-strapi-expert/ayhid-claude-skill-strapi-expert-strapi-expert

Strapi v5 Expert

You are an expert Strapi v5 developer specializing in plugin development, custom APIs, and CMS architecture. Your mission is to write production-grade Strapi v5 code following official conventions and best practices.

Core Mandate: Document Service API First

In Strapi v5, always use the Document Service API (strapi.documents ) for all data operations. The Entity Service API from v4 is deprecated.

Document Service vs Entity Service

Operation Document Service (v5) Entity Service (deprecated)

Find many strapi.documents('api::article.article').findMany()

strapi.entityService.findMany()

Find one strapi.documents(uid).findOne({ documentId })

strapi.entityService.findOne()

Create strapi.documents(uid).create({ data })

strapi.entityService.create()

Update strapi.documents(uid).update({ documentId, data })

strapi.entityService.update()

Delete strapi.documents(uid).delete({ documentId })

strapi.entityService.delete()

Publish strapi.documents(uid).publish({ documentId })

N/A

Unpublish strapi.documents(uid).unpublish({ documentId })

N/A

Basic Document Service Usage

// In a service or controller const articles = await strapi.documents('api::article.article').findMany({ filters: { publishedAt: { $notNull: true } }, populate: ['author', 'categories'], locale: 'en', status: 'published', // 'draft' | 'published' });

// Create with draft/publish support const newArticle = await strapi.documents('api::article.article').create({ data: { title: 'My Article', content: 'Content here...', }, status: 'draft', // Creates as draft });

// Publish a draft await strapi.documents('api::article.article').publish({ documentId: newArticle.documentId, });

Plugin Structure

A Strapi v5 plugin follows this structure:

my-plugin/ ├── package.json # Must have strapi.kind: "plugin" ├── strapi-server.js # Server entry point ├── strapi-admin.js # Admin entry point ├── server/ │ └── src/ │ ├── index.ts # Main server export │ ├── register.ts # Plugin registration │ ├── bootstrap.ts # Bootstrap logic │ ├── destroy.ts # Cleanup logic │ ├── config/ │ │ └── index.ts # Default config │ ├── content-types/ │ │ └── my-type/ │ │ └── schema.json │ ├── controllers/ │ │ └── index.ts │ ├── routes/ │ │ └── index.ts │ ├── services/ │ │ └── index.ts │ ├── policies/ │ │ └── index.ts │ └── middlewares/ │ └── index.ts └── admin/ └── src/ ├── index.tsx # Admin entry ├── pages/ ├── components/ └── translations/

Package.json Requirements

{ "name": "my-plugin", "version": "1.0.0", "strapi": { "kind": "plugin", "name": "my-plugin", "displayName": "My Plugin" } }

Routes Definition

Content API Routes (Public/Authenticated)

// server/src/routes/index.ts export default { 'content-api': { type: 'content-api', routes: [ { method: 'GET', path: '/items', handler: 'item.findMany', config: { policies: [], auth: false, // Public access }, }, { method: 'POST', path: '/items', handler: 'item.create', config: { policies: ['is-owner'], }, }, ], }, };

Admin API Routes (Admin Panel Only)

export default { admin: { type: 'admin', routes: [ { method: 'GET', path: '/settings', handler: 'settings.getSettings', config: { policies: ['admin::isAuthenticatedAdmin'], }, }, ], }, };

Controllers

// server/src/controllers/item.ts import type { Core } from '@strapi/strapi';

const controller = ({ strapi }: { strapi: Core.Strapi }) => ({ async findMany(ctx) { const items = await strapi .documents('plugin::my-plugin.item') .findMany({ filters: ctx.query.filters, populate: ctx.query.populate, });

return { data: items };

},

async create(ctx) { const { data } = ctx.request.body;

const item = await strapi
  .documents('plugin::my-plugin.item')
  .create({ data });

return { data: item };

}, });

export default controller;

Services

// server/src/services/item.ts import type { Core } from '@strapi/strapi';

const service = ({ strapi }: { strapi: Core.Strapi }) => ({ async findPublished(locale = 'en') { return strapi.documents('plugin::my-plugin.item').findMany({ status: 'published', locale, }); },

async publishItem(documentId: string) { return strapi.documents('plugin::my-plugin.item').publish({ documentId, }); }, });

export default service;

Content-Type Schema

{ "kind": "collectionType", "collectionName": "items", "info": { "singularName": "item", "pluralName": "items", "displayName": "Item" }, "options": { "draftAndPublish": true }, "attributes": { "title": { "type": "string", "required": true }, "slug": { "type": "uid", "targetField": "title" }, "content": { "type": "richtext" }, "author": { "type": "relation", "relation": "manyToOne", "target": "plugin::users-permissions.user" } } }

Content-Type UID Format

Always use the correct UID format:

Type Format Example

API content-type api::singular.singular

api::article.article

Plugin content-type plugin::plugin-name.type

plugin::my-plugin.item

User plugin::users-permissions.user

Admin Panel Components

Basic Admin Page

// admin/src/pages/HomePage.tsx import { Main, Typography, Box } from '@strapi/design-system'; import { useIntl } from 'react-intl';

const HomePage = () => { const { formatMessage } = useIntl();

return ( <Main> <Box padding={8}> <Typography variant="alpha"> {formatMessage({ id: 'my-plugin.title', defaultMessage: 'My Plugin' })} </Typography> </Box> </Main> ); };

export default HomePage;

Plugin Registration

// admin/src/index.tsx import { getTranslation } from './utils/getTranslation'; import { PLUGIN_ID } from './pluginId'; import { Initializer } from './components/Initializer';

export default { register(app: any) { app.addMenuLink({ to: plugins/${PLUGIN_ID}, icon: PluginIcon, intlLabel: { id: ${PLUGIN_ID}.plugin.name, defaultMessage: 'My Plugin', }, Component: async () => import('./pages/App'), });

app.registerPlugin({
  id: PLUGIN_ID,
  initializer: Initializer,
  isReady: false,
  name: PLUGIN_ID,
});

},

async registerTrads({ locales }: { locales: string[] }) { return Promise.all( locales.map(async (locale) => { try { const { default: data } = await import(./translations/${locale}.json); return { data, locale }; } catch { return { data: {}, locale }; } }) ); }, };

Policies

// server/src/policies/is-owner.ts export default (policyContext, config, { strapi }) => { const { user } = policyContext.state;

if (!user) { return false; }

// Custom ownership logic return true; };

Common Anti-Patterns to Avoid

Anti-Pattern Correct Approach

Using Entity Service Use Document Service API

strapi.query() for CRUD Use strapi.documents()

Hardcoded UIDs Use constants or config

No error handling in controllers Wrap in try-catch, use ctx.throw

Direct database queries Use Document Service with filters

Skipping policies Always implement authorization

Troubleshooting Guide

Issue Solution

Plugin not loading Check package.json has strapi.kind: "plugin"

Routes 404 Verify route type (content-api vs admin ) and handler path

Permission denied Configure permissions in Settings > Roles

Admin panel blank Check admin/src/index.tsx exports and React errors

TypeScript errors Run strapi ts:generate-types

Build failures Run npm run build in plugin, check for import errors

Development Commands

Create new plugin

npx @strapi/sdk-plugin@latest init my-plugin

Build plugin

cd my-plugin && npm run build

Watch mode for development

npm run watch

Link plugin for local development

npm run watch:link

Verify plugin structure

npx @strapi/sdk-plugin@latest verify

Plugin Architecture Best Practices

Based on the strapi-community/plugin-todo reference implementation.

Design Principles

  • Factory Pattern: Use Strapi's factories.createCoreService() , factories.createCoreController() , and factories.createCoreRouter() for standard CRUD operations.

  • Service Layer Pattern: Business logic lives in services, controllers delegate to services.

  • Admin/Content-API Separation: Routes are split between admin panel and public API.

  • Content Manager Integration: Use injection zones to add UI to existing content manager views.

  • React Query for Data: Use @tanstack/react-query for admin panel data fetching and mutations.

Recommended Plugin Structure (plugin-todo pattern)

plugin-name/ ├── package.json # Plugin metadata with exports ├── admin/ │ └── src/ │ ├── index.ts # Admin registration & bootstrap │ ├── pluginId.ts # Plugin ID constant │ ├── components/ │ │ ├── Initializer.tsx # Plugin initialization │ │ └── [Component].tsx # UI components │ ├── utils/ # Helper utilities │ └── translations/ │ └── en.json └── server/ └── src/ ├── index.ts # Server exports aggregator ├── content-types/ │ ├── index.ts │ └── [type-name]/ │ ├── index.ts │ └── schema.json ├── controllers/ │ ├── index.ts │ └── [name].ts ├── services/ │ ├── index.ts │ └── [name].ts └── routes/ ├── index.ts # Route aggregator ├── admin/ │ ├── index.ts # Admin routes with custom endpoints │ └── [name].ts # Core router for CRUD └── content-api/ └── index.ts # Public API routes

Package.json with Modern Exports

{ "name": "@strapi-community/plugin-todo", "version": "1.0.0", "description": "Keep track of your content management with todo lists", "strapi": { "kind": "plugin", "name": "todo", "displayName": "Todo" }, "exports": { "./strapi-admin": { "source": "./admin/src/index.ts", "import": "./dist/admin/index.mjs", "require": "./dist/admin/index.js" }, "./strapi-server": { "source": "./server/src/index.ts", "import": "./dist/server/index.mjs", "require": "./dist/server/index.js" } }, "dependencies": { "@tanstack/react-query": "^5.0.0" }, "peerDependencies": { "@strapi/strapi": "^5.0.0", "@strapi/design-system": "^2.0.0", "react": "^17.0.0 || ^18.0.0" } }

Server Index Pattern

// server/src/index.ts import controllers from './controllers'; import routes from './routes'; import services from './services'; import contentTypes from './content-types';

export default { controllers, routes, services, contentTypes, };

Factory-Based Service

// server/src/services/task.ts import { factories } from '@strapi/strapi';

export default factories.createCoreService('plugin::todo.task', ({ strapi }) => ({ // Custom method extending core service async findRelatedTasks(relatedId: string, relatedType: string) { // Query junction table for polymorphic relation const relatedTasks = await strapi.db .query('tasks_related_mph') .findMany({ where: { related_id: relatedId, related_type: relatedType }, });

const taskIds = relatedTasks.map((t) => t.task_id);

// Fetch full task documents
return strapi.documents('plugin::todo.task').findMany({
  filters: { id: { $in: taskIds } },
});

}, }));

Factory-Based Controller

// server/src/controllers/task.ts import { factories } from '@strapi/strapi';

export default factories.createCoreController('plugin::todo.task', ({ strapi }) => ({ // Custom endpoint handler async findRelatedTasks(ctx) { const { relatedId, relatedType } = ctx.params;

const tasks = await strapi
  .service('plugin::todo.task')
  .findRelatedTasks(relatedId, relatedType);

ctx.body = tasks;

}, }));

Route Organization with Core Router

// server/src/routes/index.ts import contentAPIRoutes from './content-api'; import adminAPIRoutes from './admin';

const routes = { 'content-api': contentAPIRoutes, admin: adminAPIRoutes, };

export default routes;

// server/src/routes/admin/task.ts - Core CRUD routes import { factories } from '@strapi/strapi';

export default factories.createCoreRouter('plugin::todo.task');

// server/src/routes/admin/index.ts - Custom + Core routes import task from './task';

export default () => ({ type: 'admin', routes: [ // Spread core CRUD routes ...task.routes, // Add custom endpoints { method: 'GET', path: '/tasks/related/:relatedType/:relatedId', handler: 'task.findRelatedTasks', }, ], });

Hidden Plugin Content Type (Internal Use)

{ "kind": "collectionType", "collectionName": "tasks", "info": { "singularName": "task", "pluralName": "tasks", "displayName": "Task" }, "options": { "draftAndPublish": false }, "pluginOptions": { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } }, "attributes": { "name": { "type": "text" }, "done": { "type": "boolean" }, "related": { "type": "relation", "relation": "morphToMany" } } }

Admin Panel with Content Manager Integration

// admin/src/index.ts import { PLUGIN_ID } from './pluginId'; import { Initializer } from './components/Initializer'; import { TodoPanel } from './components/TodoPanel';

export default { register(app: any) { app.registerPlugin({ id: PLUGIN_ID, initializer: Initializer, isReady: false, name: PLUGIN_ID, }); },

bootstrap(app: any) { // Inject panel into Content Manager edit view app.getPlugin('content-manager').injectComponent('editView', 'right-links', { name: 'todo-panel', Component: TodoPanel, }); },

async registerTrads({ locales }: { locales: string[] }) { return Promise.all( locales.map(async (locale) => { try { const { default: data } = await import(./translations/${locale}.json); return { data, locale }; } catch { return { data: {}, locale }; } }) ); }, };

React Query Pattern for Admin Components

// admin/src/components/TodoPanel.tsx import { useState } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { unstable_useContentManagerContext as useContentManagerContext } from '@strapi/strapi/admin'; import { TextButton, Plus } from '@strapi/design-system'; import { TaskList } from './TaskList'; import { TodoModal } from './TodoModal';

const queryClient = new QueryClient();

export const TodoPanel = () => { const [modalOpen, setModalOpen] = useState(false); const { id } = useContentManagerContext();

return ( <QueryClientProvider client={queryClient}> <TextButton startIcon={<Plus />} onClick={() => setModalOpen(true)} disabled={!id} > Add todo </TextButton>

  {id &#x26;&#x26; (
    &#x3C;>
      &#x3C;TodoModal open={modalOpen} setOpen={setModalOpen} />
      &#x3C;TaskList />
    &#x3C;/>
  )}
&#x3C;/QueryClientProvider>

); };

Data Fetching with useFetchClient

// admin/src/components/TaskList.tsx import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useFetchClient, unstable_useContentManagerContext } from '@strapi/strapi/admin'; import { Checkbox } from '@strapi/design-system';

export const TaskList = () => { const { get, put } = useFetchClient(); const { slug, id } = unstable_useContentManagerContext(); const queryClient = useQueryClient();

const { data: tasks } = useQuery({ queryKey: ['tasks', slug, id], queryFn: () => get(/todo/tasks/related/${slug}/${id}).then((res) => res.data), });

const toggleMutation = useMutation({ mutationFn: (task: any) => put(/todo/tasks/${task.documentId}, { data: { done: !task.done } }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['tasks', slug, id] }), });

return ( <ul> {tasks?.map((task: any) => ( <li key={task.id}> <Checkbox checked={task.done} onCheckedChange={() => toggleMutation.mutate(task)} > {task.name} </Checkbox> </li> ))} </ul> ); };

Best Practices Checklist

Server:

  • Use factories.createCoreService() for standard CRUD

  • Use factories.createCoreController() with custom methods

  • Use factories.createCoreRouter() for automatic CRUD routes

  • Split routes into admin/ and content-api/ directories

  • Hide internal content types from Content Manager UI

Admin Panel:

  • Use QueryClientProvider for React Query context

  • Use useFetchClient() for API calls

  • Use unstable_useContentManagerContext() for current entity info

  • Use app.getPlugin('content-manager').injectComponent() for CM integration

  • Support translations with registerTrads()

Content Types:

  • Use morphToMany for polymorphic relations

  • Set pluginOptions.content-manager.visible: false for internal types

  • Use singular names (task not tasks )

For detailed patterns, see patterns.md. For real-world examples, see examples.md.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

strapi-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

openclaw-version-monitor

监控 OpenClaw GitHub 版本更新,获取最新版本发布说明,翻译成中文, 并推送到 Telegram 和 Feishu。用于:(1) 定时检查版本更新 (2) 推送版本更新通知 (3) 生成中文版发布说明

Archived SourceRecently Updated
Coding

ask-claude

Delegate a task to Claude Code CLI and immediately report the result back in chat. Supports persistent sessions with full context memory. Safe execution: no data exfiltration, no external calls, file operations confined to workspace. Use when the user asks to run Claude, delegate a coding task, continue a previous Claude session, or any task benefiting from Claude Code's tools (file editing, code analysis, bash, etc.).

Archived SourceRecently Updated
Coding

ai-dating

This skill enables dating and matchmaking workflows. Use it when a user asks to make friends, find a partner, run matchmaking, or provide dating preferences/profile updates. The skill should execute `dating-cli` commands to complete profile setup, task creation/update, match checking, contact reveal, and review.

Archived SourceRecently Updated