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({}),
});
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
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" },
});
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",
},
}
Hooks: Business Logic
{
hooks: {
afterChange: [
async ({ doc, operation }) => {
if (operation === "create") {
await sendSlackNotification(`New product: ${doc.title}`);
await invalidateCache(`products-${doc.category}`);
}
},
],
},
}
Feed your CMS with scraped data? My Apify tools export directly into Payload collections.
Custom CMS solution? Email spinov001@gmail.com
Top comments (0)