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
npx create-next-app@latest . --yes
npm install payload @payloadcms/next @payloadcms/db-postgres @payloadcms/richtext-lexicalThis command initializes a Next.js 15 project and installs the essential Payload CMS dependencies for App Router integration.
Basic Payload Configuration
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
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
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
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.