DEV Community

Alex Spinov
Alex Spinov

Posted on

Payload CMS 3 Has a Free API That Turns Your Next.js App Into a Full CMS

Payload CMS 3 is a headless CMS that lives inside your Next.js app. No separate admin. No external service. Just TypeScript collections and a beautiful auto-generated admin UI.

Define a Collection

// payload.config.ts
import { buildConfig } from "payload";
import { mongooseAdapter } from "@payloadcms/db-mongodb";
import { slateEditor } from "@payloadcms/richtext-slate";

export default buildConfig({
  collections: [
    {
      slug: "products",
      admin: { useAsTitle: "title" },
      fields: [
        { name: "title", type: "text", required: true },
        { name: "price", type: "number", required: true, min: 0 },
        { name: "description", type: "richText" },
        { name: "image", type: "upload", relationTo: "media" },
        { name: "category", type: "select", options: ["electronics", "clothing", "food"] },
        { name: "url", type: "text", unique: true },
        { name: "scrapedAt", type: "date", admin: { readOnly: true } },
        {
          name: "metadata",
          type: "group",
          fields: [
            { name: "source", type: "text" },
            { name: "confidence", type: "number", min: 0, max: 1 },
          ],
        },
      ],
      hooks: {
        beforeChange: [({ data }) => ({ ...data, scrapedAt: new Date() })],
      },
    },
  ],
  db: mongooseAdapter({ url: process.env.DATABASE_URI }),
  editor: slateEditor({}),
});
Enter fullscreen mode Exit fullscreen mode

Auto-Generated REST + GraphQL APIs

# REST — auto-generated from collections
GET    /api/products?where[price][less_than]=50&sort=-scrapedAt&limit=20
POST   /api/products
PATCH  /api/products/:id
DELETE /api/products/:id

# GraphQL — also auto-generated
POST /api/graphql
Enter fullscreen mode Exit fullscreen mode

Local API: Type-Safe Server Queries

import { getPayload } from "payload";
import config from "@payload-config";

const payload = await getPayload({ config });

// Fully typed!
const products = await payload.find({
  collection: "products",
  where: { price: { less_than: 50 }, category: { equals: "electronics" } },
  sort: "-scrapedAt",
  limit: 20,
  depth: 2, // Populate relationships 2 levels deep
});

// Create
const newProduct = await payload.create({
  collection: "products",
  data: { title: "Widget", price: 29.99, category: "electronics" },
});
Enter fullscreen mode Exit fullscreen mode

Access Control

{
  slug: "products",
  access: {
    read: () => true, // Public read
    create: ({ req: { user } }) => user?.role === "admin",
    update: ({ req: { user } }) => user?.role === "admin",
    delete: ({ req: { user } }) => user?.role === "admin",
  },
}
Enter fullscreen mode Exit fullscreen mode

Hooks: Business Logic

{
  hooks: {
    afterChange: [
      async ({ doc, operation }) => {
        if (operation === "create") {
          await sendSlackNotification(`New product: ${doc.title}`);
          await invalidateCache(`products-${doc.category}`);
        }
      },
    ],
  },
}
Enter fullscreen mode Exit fullscreen mode

Feed your CMS with scraped data? My Apify tools export directly into Payload collections.

Custom CMS solution? Email spinov001@gmail.com

Top comments (0)