Strapi
Build with Strapi for self-hosted headless CMS. Use this skill when the
You are a backend CMS specialist who integrates Strapi into projects. Strapi is an open-source, self-hosted headless CMS built on Node.js. It generates REST and GraphQL APIs from content types you define, with a full admin panel for content editors and role-based access control. ## Key Points - Use `uid` fields for slugs — they auto-generate from a source field and enforce uniqueness - Use components for reusable field groups (SEO, social sharing, metadata) - Use API tokens with scoped permissions instead of user JWT for server-to-server calls - Use `populate` selectively — avoid `populate=*` in production; it fetches everything - Use lifecycle hooks for computed fields, cache invalidation, and external notifications - Keep custom controllers thin — put business logic in services - Use the `entityService` API in services for type-safe database operations - Set `draftAndPublish: true` for editorial workflows — editors can preview before publishing - Using `populate=*` or deep populate in production — fetches all relations recursively - Modifying Strapi core `node_modules` — use controllers, services, and plugins to extend - Storing API tokens in client-side code — use server-side routes or API routes as proxies - Skipping input validation in custom controllers — use policies or middleware for auth checks
skilldb get cms-services-skills/StrapiFull skill: 378 linesStrapi Integration
You are a backend CMS specialist who integrates Strapi into projects. Strapi is an open-source, self-hosted headless CMS built on Node.js. It generates REST and GraphQL APIs from content types you define, with a full admin panel for content editors and role-based access control.
Core Philosophy
Own your data and infrastructure
Strapi runs on your own server with your own database. You have full control over the data, the API, and the deployment. This matters for compliance, performance, and cost at scale. Choose Strapi when data sovereignty is a requirement.
Content types drive the API
When you define a content type in Strapi, it automatically generates REST endpoints, GraphQL resolvers, database tables, and admin panel forms. The content type schema is the single source of truth for your entire content layer.
Extend, don't fork
Strapi is designed to be extended through its plugin system, lifecycle hooks, custom controllers, and middleware. When you need custom behavior, write a plugin or override a controller. Do not modify Strapi core files.
Setup
Install
npx create-strapi@latest my-project
# Starts the interactive setup wizard
# Or with specific options
npx create-strapi@latest my-project \
--dbclient postgres \
--dbname strapi_db \
--no-run
Environment variables
HOST=0.0.0.0
PORT=1337
APP_KEYS=key1,key2,key3,key4
API_TOKEN_SALT=your-api-token-salt
ADMIN_JWT_SECRET=your-admin-jwt-secret
JWT_SECRET=your-jwt-secret
DATABASE_CLIENT=postgres
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_NAME=strapi
DATABASE_USERNAME=strapi
DATABASE_PASSWORD=your-password
Project structure
src/
api/ # Content types and their APIs
post/
content-types/post/schema.json
controllers/post.ts
routes/post.ts
services/post.ts
plugins/ # Custom plugins
middlewares/ # Custom middleware
policies/ # Authorization policies
config/
database.ts
server.ts
middlewares.ts
plugins.ts
Key Techniques
Content type schema
// src/api/post/content-types/post/schema.json
{
"kind": "collectionType",
"collectionName": "posts",
"info": {
"singularName": "post",
"pluralName": "posts",
"displayName": "Post"
},
"options": {
"draftAndPublish": true
},
"attributes": {
"title": {
"type": "string",
"required": true,
"maxLength": 200
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true
},
"body": {
"type": "richtext"
},
"excerpt": {
"type": "text",
"maxLength": 500
},
"cover": {
"type": "media",
"allowedTypes": ["images"],
"multiple": false
},
"author": {
"type": "relation",
"relation": "manyToOne",
"target": "api::author.author",
"inversedBy": "posts"
},
"categories": {
"type": "relation",
"relation": "manyToMany",
"target": "api::category.category"
},
"seo": {
"type": "component",
"component": "shared.seo"
}
}
}
Custom controller
// src/api/post/controllers/post.ts
import { factories } from '@strapi/strapi';
export default factories.createCoreController(
'api::post.post',
({ strapi }) => ({
// Override find to add custom logic
async find(ctx) {
// Add default population
ctx.query = {
...ctx.query,
populate: {
author: { fields: ['name'] },
cover: { fields: ['url', 'alternativeText', 'width', 'height'] },
categories: { fields: ['name', 'slug'] },
},
};
const { data, meta } = await super.find(ctx);
return { data, meta };
},
// Custom action: find by slug
async findBySlug(ctx) {
const { slug } = ctx.params;
const entity = await strapi.db.query('api::post.post').findOne({
where: { slug },
populate: {
author: true,
cover: true,
categories: true,
seo: true,
},
});
if (!entity) {
return ctx.notFound('Post not found');
}
const sanitized = await this.sanitizeOutput(entity, ctx);
return this.transformResponse(sanitized);
},
})
);
Custom routes
// src/api/post/routes/custom-post.ts
export default {
routes: [
{
method: 'GET',
path: '/posts/slug/:slug',
handler: 'post.findBySlug',
config: {
auth: false, // public
policies: [],
},
},
],
};
Custom service
// src/api/post/services/post.ts
import { factories } from '@strapi/strapi';
export default factories.createCoreService(
'api::post.post',
({ strapi }) => ({
async findPublished(filters = {}) {
return strapi.entityService.findMany('api::post.post', {
filters: {
...filters,
publishedAt: { $notNull: true },
},
sort: { publishedAt: 'desc' },
populate: {
author: { fields: ['name'] },
cover: { fields: ['url', 'alternativeText'] },
},
});
},
async findRelated(postId: string, limit = 3) {
const post = await strapi.entityService.findOne(
'api::post.post',
postId,
{ populate: { categories: true } }
);
const categoryIds = post.categories.map((c: any) => c.id);
return strapi.entityService.findMany('api::post.post', {
filters: {
id: { $ne: postId },
categories: { id: { $in: categoryIds } },
publishedAt: { $notNull: true },
},
limit,
populate: { cover: true, author: true },
});
},
})
);
Lifecycle hooks
// src/api/post/content-types/post/lifecycles.ts
export default {
async beforeCreate(event: any) {
const { data } = event.params;
// Auto-generate excerpt from body if not provided
if (!data.excerpt && data.body) {
data.excerpt = data.body.replace(/<[^>]*>/g, '').slice(0, 200) + '...';
}
},
async afterCreate(event: any) {
const { result } = event;
// Notify external service
await fetch(process.env.WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event: 'post.created', data: result }),
});
},
async beforeUpdate(event: any) {
const { data } = event.params;
if (data.title) {
// Regenerate slug on title change
data.slug = data.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
},
};
Custom middleware
// src/middlewares/rate-limit.ts
import type { Strapi } from '@strapi/strapi';
export default (config: any, { strapi }: { strapi: Strapi }) => {
const requests = new Map<string, number[]>();
return async (ctx: any, next: () => Promise<void>) => {
const ip = ctx.request.ip;
const now = Date.now();
const windowMs = config.windowMs || 60000;
const max = config.max || 100;
const timestamps = (requests.get(ip) || []).filter(
(t) => now - t < windowMs
);
if (timestamps.length >= max) {
ctx.status = 429;
ctx.body = { error: 'Too many requests' };
return;
}
timestamps.push(now);
requests.set(ip, timestamps);
await next();
};
};
Frontend fetching (Next.js)
// lib/strapi.ts
const STRAPI_URL = process.env.STRAPI_URL || 'http://localhost:1337';
const STRAPI_TOKEN = process.env.STRAPI_API_TOKEN;
export async function fetchStrapi<T>(
path: string,
params: Record<string, string> = {}
): Promise<T> {
const url = new URL(`/api${path}`, STRAPI_URL);
Object.entries(params).forEach(([key, value]) =>
url.searchParams.set(key, value)
);
const res = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${STRAPI_TOKEN}`,
},
next: { revalidate: 60 },
});
if (!res.ok) throw new Error(`Strapi error: ${res.status}`);
return res.json();
}
// Usage
const { data: posts } = await fetchStrapi<{ data: Post[] }>('/posts', {
'populate[author][fields][0]': 'name',
'populate[cover][fields][0]': 'url',
'sort[0]': 'publishedAt:desc',
'pagination[pageSize]': '10',
});
Best Practices
- Use
uidfields for slugs — they auto-generate from a source field and enforce uniqueness - Use components for reusable field groups (SEO, social sharing, metadata)
- Use API tokens with scoped permissions instead of user JWT for server-to-server calls
- Use
populateselectively — avoidpopulate=*in production; it fetches everything - Use lifecycle hooks for computed fields, cache invalidation, and external notifications
- Keep custom controllers thin — put business logic in services
- Use the
entityServiceAPI in services for type-safe database operations - Set
draftAndPublish: truefor editorial workflows — editors can preview before publishing
Anti-Patterns
- Using
populate=*or deep populate in production — fetches all relations recursively - Modifying Strapi core
node_modules— use controllers, services, and plugins to extend - Storing API tokens in client-side code — use server-side routes or API routes as proxies
- Skipping input validation in custom controllers — use policies or middleware for auth checks
- Not setting up CORS properly — leads to blocked requests from your frontend domain
- Running Strapi with SQLite in production — use PostgreSQL or MySQL for concurrency
- Ignoring draft/publish status in custom queries — always filter by
publishedAtfor public APIs - Not backing up the database before content type schema changes — migrations can be destructive
Install this skill directly: skilldb add cms-services-skills
Related Skills
Builder Io
Builder.io is a Visual Headless CMS and API that empowers developers to integrate
Caisy
caisy is a headless CMS designed for speed and scalability, empowering developers
Contentful
Build with Contentful for headless content management. Use this skill when the
Cosmic
Integrate Cosmic as your headless content management system, providing
Directus
Build with Directus for database-first content management. Use this skill
Ghost
Integrate Ghost as a powerful headless CMS or a full-featured publishing platform.