Environment, Setup & Your First Payload Collection
Environment, Setup & Your First Payload Collection
Step 1 — Set Up Your Environment
What VS Code extensions do you need?
For Payload development, you're writing TypeScript in a Next.js project. Install these:
- ESLint — catches errors as you type
- Prettier — auto-formats code on save
- TypeScript Error Lens — inline TypeScript errors in the editor
- Tailwind CSS IntelliSense — if you're styling the frontend with Tailwind
No Payload-specific extension exists — Payload is just TypeScript classes and config objects. VS Code's built-in TypeScript support handles IntelliSense on all Payload types.
Create a Payload project
The fastest path is the official CLI:
npx create-payload-app@latestThe CLI asks:
- Project name — becomes the folder name
- Template — choose "website" (Next.js + Postgres) or "blank"
- Database — choose Postgres (uses Supabase, Neon, Railway, or local)
The CLI installs dependencies and creates a fully working project with example collections. Start it:
cd your-project
npm run devVisit http://localhost:3000/admin — the first load creates the database tables and prompts you to create an admin user.
WHY Payload? — The Headless CMS Problem
Before Payload, teams had two bad choices:
Traditional CMS (WordPress)
- Manages content AND renders the frontend (PHP templates)
- Hard to build modern React frontends on top of it
- Vendor-managed — can't customize the data structure freely
Hosted Headless CMS (Contentful, Sanity)
- API-only — you build the frontend
- But you pay per seat, per API call, per content type
- No code access — customizations happen in their UI
Payload's answer: run the CMS inside your Next.js project. You own the database, the config is TypeScript code in your repo, and the admin UI is auto-generated.
WordPress: CMS owns frontend + data
Contentful: their servers → your frontend
Payload: your code + your DB → admin UI auto-generatedFile Structure After Installation
src/
app/
(payload)/ ← Payload admin route group
admin/
[[...segments]]/
page.tsx ← Admin panel pages
api/
[...slug]/
route.ts ← Payload REST API routes
(frontend)/ ← Your frontend routes
page.tsx
blog/
[slug]/
page.tsx
collections/
Posts.ts ← Post content type
Users.ts ← User content type (with auth)
Media.ts ← File uploads
globals/
SiteSettings.ts ← Singleton config (nav, footer)
payload.config.ts ← Main CMS configuration
payload-types.ts ← Auto-generated TypeScript typesThe key insight: your collections live as TypeScript files in your repo. They are source code — you version them with git, review them in PRs, deploy them with your app.
payload.config.ts — The Central Hub
import { buildConfig } from 'payload'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { Posts } from './collections/Posts'
import { Users } from './collections/Users'
import { Media } from './collections/Media'
export default buildConfig({
secret: process.env.PAYLOAD_SECRET!, // JWT signing key for admin sessions
db: postgresAdapter({
pool: { connectionString: process.env.DATABASE_URL! },
}),
editor: lexicalEditor({}), // rich text editor (Facebook's Lexical)
collections: [Posts, Users, Media],
admin: {
user: 'users', // which collection handles admin authentication
},
cors: [process.env.NEXT_PUBLIC_APP_URL!],
// Allow your frontend origin for REST API calls
})Everything flows from this file. Add a collection here → it appears in the admin panel, REST API, and TypeScript types automatically.
Your First Real Collection — Posts
Let's build a blog posts collection step-by-step:
// src/collections/Posts.ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts', // → /api/posts in REST, payload.find('posts') in Local API
admin: {
useAsTitle: 'title', // shows "My First Post" as the admin list title
defaultColumns: ['title', 'status', 'publishedAt'],
},
fields: [
// Text field — required, shows at top of form
{
name: 'title',
type: 'text',
required: true,
},
// URL-safe identifier, auto-generated from title via a hook (later)
{
name: 'slug',
type: 'text',
required: true,
unique: true,
index: true, // adds a DB index for fast slug lookups
admin: { position: 'sidebar' },
},
// Content state — editors can save drafts without publishing
{
name: 'status',
type: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
defaultValue: 'draft',
required: true,
admin: { position: 'sidebar' },
},
// Relationship — references a document in the 'users' collection
{
name: 'author',
type: 'relationship',
relationTo: 'users',
required: true,
admin: { position: 'sidebar' },
},
// Rich text (Lexical editor — outputs structured JSON)
{
name: 'content',
type: 'richText',
},
// Metadata
{
name: 'publishedAt',
type: 'date',
admin: { position: 'sidebar' },
},
],
}After saving this file and restarting the dev server, visit /admin — the "Posts" menu appears. Click "Create New Post" and you'll see this form rendered automatically.
What Payload just generated from this one file:
- Admin form with all fields
- REST endpoint: GET/POST
/api/posts, GET/PATCH/DELETE/api/posts/:id - GraphQL queries and mutations
- TypeScript type:
import type { Post } from '@/payload-types'
Access the Auto-Generated REST API
# List all published posts
GET http://localhost:3000/api/posts?where[status][equals]=published
# Get one post
GET http://localhost:3000/api/posts/64abc123
# Create (requires auth)
POST http://localhost:3000/api/posts
Authorization: Bearer <your-admin-jwt>
Content-Type: application/json
{
"title": "My First Post",
"slug": "my-first-post",
"status": "draft"
}