DEV Community

Alex Spinov
Alex Spinov

Posted on

Payload CMS Has a Free API — The TypeScript Headless CMS That Replaces Strapi

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
Enter fullscreen mode Exit fullscreen mode

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' },
  ],
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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' },
});
Enter fullscreen mode Exit fullscreen mode

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: [...],
};
Enter fullscreen mode Exit fullscreen mode

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)