Skip to content
Learni
View all tutorials
CMS Headless

How to Master Payload CMS with Next.js in 2026

Lire en français

Introduction

Payload CMS is a TypeScript-first headless CMS that integrates natively with Next.js. Unlike no-code solutions, it provides full control through code. This expert tutorial guides you through a production-ready architecture including complex collections, access middlewares, and performance optimizations. You will learn to structure a scalable project while maintaining security and maintainability.

Prerequisites

  • Node.js 20+ and npm 10+
  • Next.js 15 with strict TypeScript
  • Advanced knowledge of PostgreSQL
  • Docker for production environments

Project Initialization

terminal
npx create-next-app@latest . --yes
npm install payload @payloadcms/next @payloadcms/db-postgres @payloadcms/richtext-lexical

This command initializes a Next.js 15 project and installs the essential Payload CMS dependencies for App Router integration.

Basic Payload Configuration

payload.config.ts
import { buildConfig } from 'payload/config'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { lexicalEditor } from '@payloadcms/richtext-lexical'

export default buildConfig({
  serverURL: process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000',
  admin: { user: 'users' },
  collections: [],
  editor: lexicalEditor({}),
  db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL } }),
  typescript: { outputFile: './payload-types.ts' },
})

This file defines the central configuration. The PostgreSQL adapter and Lexical editor are set up for production with strict typing.

Creating an Advanced Posts Collection

collections/Posts.ts
import { CollectionConfig } from 'payload/types'

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: { useAsTitle: 'title' },
  access: { read: () => true, create: ({ req }) => !!req.user },
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'content', type: 'richText' },
    { name: 'author', type: 'relationship', relationTo: 'users' }
  ],
  hooks: { beforeChange: [({ data }) => { data.slug = data.title.toLowerCase().replace(/\s+/g, '-'); return data }] }
}

Complete collection with access control, relationship, and slug generation hook. The hook runs before each modification to maintain data consistency.

Implementing Granular Access Control

access/isAdmin.ts
import { Access } from 'payload/config'
export const isAdmin: Access = ({ req: { user } }) => {
  if (!user) return false
  return user.roles?.includes('admin') || false
}

Reusable access function that checks for the admin role. It protects sensitive operations and integrates into all collections.

Payload API Route in Next.js

app/api/[...payload]/route.ts
import { GET, POST, PATCH, DELETE } from '@payloadcms/next'
import config from '@/payload.config'

export { GET, POST, PATCH, DELETE }
export const runtime = 'nodejs'

Catch-all route that exposes Payload's REST and GraphQL API. The Node runtime is required for database operations.

Best Practices

  • Always type collections with the generated payload-types.ts
  • Use beforeChange hooks for business validation
  • Separate access rules into reusable files
  • Enable PostgreSQL indexes on frequently filtered fields
  • Version schema migrations in production

Common Mistakes to Avoid

  • Forgetting to regenerate types after modifying collections
  • Using relations without indexes, causing slowdowns
  • Neglecting async hooks that can block requests
  • Exposing sensitive fields without precise access control

Go Further

Deepen your skills with our advanced Payload CMS training.