Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
mkshahzad77 avatar

Strapi Expert

  • 1.1k installs
  • 3 repo stars
  • Updated June 8, 2026
  • mkshahzad77/claude-skill-strapi-expert

strapi-expert provides documented workflows for Strapi v5 plugin development expert. Use for building, refactoring, or revamping plugins, custom APIs, admin panel extensions, Document Service API usage, conte

About

The strapi-expert skill strapi v5 plugin development expert Use for building refactoring or revamping plugins custom APIs admin panel extensions Document Service API usage content-type creation and CMS architecture Invoke when working with Strapi v5 backend development troubleshooting plugin issues implementing Strapi best practices or following Strapi plugin design guidelines Also use when the user mentions Strapi-specific terms like content-types controllers services routes or plugin structure 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 dat.

  • **Factory Pattern**: Use Strapi's `factories.createCoreService()`, `factories.createCoreController()`, and `factories.cr
  • **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.

Strapi Expert by the numbers

  • 1,061 all-time installs (skills.sh)
  • Ranked #226 of 1,039 Mobile Development skills by installs in the Skillselion catalog
  • Security screen: CRITICAL risk (skills.sh audit)
  • Data as of Aug 4, 2026 (Skillselion catalog sync)
At a glance

strapi-expert capabilities & compatibility

Capabilities
**factory pattern**: use strapi's `factories.cre · **service layer pattern**: business logic lives · **admin/content api separation**: routes are spl · **content manager integration**: use injection z · **react query for data**: use `@tanstack/react q
Use cases
documentation
From the docs

What strapi-expert says it does

# Strapi v5 Expert You are an expert Strapi v5 developer specializing in plugin development, custom APIs, and CMS architecture.
SKILL.md
Your mission is to write production-grade Strapi v5 code following official conventions and best practices.
SKILL.md
npx skills add https://github.com/mkshahzad77/claude-skill-strapi-expert --skill strapi-expert

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs1.1k
repo stars3
Security audit1 / 3 scanners passed
Last updatedJune 8, 2026
Repositorymkshahzad77/claude-skill-strapi-expert

How do I use strapi-expert for the task described in its SKILL.md triggers?

Strapi v5 plugin development expert. Use for building, refactoring, or revamping plugins, custom APIs, admin panel extensions, Document Service API usage, content-type creation, and CMS architecture.

Who is it for?

Teams invoking strapi-expert when the user request matches documented triggers and prerequisites.

Skip if: Skip when cached docs are missing, the request is a negative trigger, or another sibling skill owns the workflow.

When should I use this skill?

Strapi v5 plugin development expert. Use for building, refactoring, or revamping plugins, custom APIs, admin panel extensions, Document Service API usage, content-type creation, and CMS architecture. Invoke when working

What you get

Step-by-step guidance grounded in strapi-expert documentation and reference files.

  • Strapi v5 plugins
  • content-type schemas
  • custom API routes and services

Files

SKILL.mdMarkdownGitHub ↗

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

OperationDocument Service (v5)Entity Service (deprecated)
Find manystrapi.documents('api::article.article').findMany()strapi.entityService.findMany()
Find onestrapi.documents(uid).findOne({ documentId })strapi.entityService.findOne()
Createstrapi.documents(uid).create({ data })strapi.entityService.create()
Updatestrapi.documents(uid).update({ documentId, data })strapi.entityService.update()
Deletestrapi.documents(uid).delete({ documentId })strapi.entityService.delete()
Publishstrapi.documents(uid).publish({ documentId })N/A
Unpublishstrapi.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:

TypeFormatExample
API content-typeapi::singular.singularapi::article.article
Plugin content-typeplugin::plugin-name.typeplugin::my-plugin.item
Userplugin::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-PatternCorrect Approach
Using Entity ServiceUse Document Service API
strapi.query() for CRUDUse strapi.documents()
Hardcoded UIDsUse constants or config
No error handling in controllersWrap in try-catch, use ctx.throw
Direct database queriesUse Document Service with filters
Skipping policiesAlways implement authorization

Troubleshooting Guide

IssueSolution
Plugin not loadingCheck package.json has strapi.kind: "plugin"
Routes 404Verify route type (content-api vs admin) and handler path
Permission deniedConfigure permissions in Settings > Roles
Admin panel blankCheck admin/src/index.tsx exports and React errors
TypeScript errorsRun strapi ts:generate-types
Build failuresRun 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

1. Factory Pattern: Use Strapi's factories.createCoreService(), factories.createCoreController(), and factories.createCoreRouter() for standard CRUD operations. 2. Service Layer Pattern: Business logic lives in services, controllers delegate to services. 3. Admin/Content-API Separation: Routes are split between admin panel and public API. 4. Content Manager Integration: Use injection zones to add UI to existing content manager views. 5. 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 && (
        <>
          <TodoModal open={modalOpen} setOpen={setModalOpen} />
          <TaskList />
        </>
      )}
    </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.

Related skills

How it compares

Pick strapi-expert over generic Node.js backend skills when the task involves Strapi v5 plugin structure, content-types, or Document Service API patterns specific to the headless CMS platform.

FAQ

What does strapi-expert do?

Strapi v5 plugin development expert. Use for building, refactoring, or revamping plugins, custom APIs, admin panel extensions, Document Service API usage, content-type creation, and CMS architecture. Invoke when working

When should I use strapi-expert?

Strapi v5 plugin development expert. Use for building, refactoring, or revamping plugins, custom APIs, admin panel extensions, Document Service API usage, content-type creation, and CMS architecture. Invoke when working

What are common prerequisites?

--- name: strapi-expert description: Strapi v5 plugin development expert.

Is Strapi Expert safe to install?

skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.