JSTAcademy
0 XP
Dashboard
Crash Courses
Environment, Setup & Your First Payload Collection
12 min
Basic+150 XP
Crash Courses · Basic

Environment, Setup & Your First Payload Collection

Install Payload CMS, explore the file structure, and define a real content type from scratch.
12 min read+150 XP on completionCert: Payload CMS Crash Course
Tap any word in the text below to start reading from there.

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:

  1. ESLint — catches errors as you type
  2. Prettier — auto-formats code on save
  3. TypeScript Error Lens — inline TypeScript errors in the editor
  4. 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@latest

The 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 dev

Visit 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-generated

File 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 types

The 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"
}
0%