Payload is a headless CMS and application framework built with TypeScript. Self-hosted, fully typed, with a beautiful admin panel — it's what Strapi should have been.
Why Payload?
- 100% TypeScript — generated types for all collections
- Self-hosted — runs on your server, you own your data
- Next.js native — Payload 3.0 runs inside Next.js
- Access control — field-level, document-level, collection-level auth
Quick Start
npx create-payload-app@latest myapp
cd myapp
npm run dev
# Admin at http://localhost:3000/admin
Collections (Content Types)
// collections/Posts.ts
import { CollectionConfig } from 'payload';
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
},
access: {
read: () => true,
create: ({ req: { user } }) => !!user,
update: ({ req: { user } }) => !!user,
delete: ({ req: { user } }) => user?.role === 'admin',
},
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', unique: true },
{ name: 'content', type: 'richText' },
{ name: 'author', type: 'relationship', relationTo: 'users' },
{ name: 'tags', type: 'array', fields: [
{ name: 'tag', type: 'text' },
]},
{ name: 'status', type: 'select', options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
]},
{ name: 'publishedAt', type: 'date' },
{ name: 'image', type: 'upload', relationTo: 'media' },
],
};
REST API (Auto-Generated)
# List posts
GET /api/posts?limit=10&page=1&sort=-publishedAt
# Get single post
GET /api/posts/[id]
# Create post
POST /api/posts
{"title": "Hello", "content": {...}, "status": "published"}
# Update
PATCH /api/posts/[id]
# Delete
DELETE /api/posts/[id]
# Query with filters
GET /api/posts?where[status][equals]=published&where[tags.tag][contains]=typescript
Local API (Server-Side)
// In Next.js server components or API routes
import { getPayload } from 'payload';
import config from '@payload-config';
const payload = await getPayload({ config });
// Find
const posts = await payload.find({
collection: 'posts',
where: { status: { equals: 'published' } },
sort: '-publishedAt',
limit: 10,
});
// Create
const newPost = await payload.create({
collection: 'posts',
data: { title: 'New Post', status: 'draft' },
});
// Update
await payload.update({
collection: 'posts',
id: '123',
data: { status: 'published' },
});
Hooks
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
beforeChange: [
({ data }) => {
if (!data.slug) {
data.slug = data.title.toLowerCase().replace(/\s+/g, '-');
}
return data;
},
],
afterChange: [
({ doc }) => {
// Revalidate cache, send webhook, etc.
revalidatePath(`/posts/${doc.slug}`);
},
],
},
fields: [...],
};
Need content for your CMS from the web? Check out my Apify actors for web scraping, or email spinov001@gmail.com for custom CMS solutions.
Payload, Strapi, or Sanity — which headless CMS do you use? Share below!
Top comments (0)