Migrate to Payload CMS: Seamless Swap from Dummy Data
How to replace example data with Payload CMS queries, preserve frontend types, and add getPage/getBlock utilities for…
📚 Comprehensive Payload CMS Guides
Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.
Part 7 of the Design to Code series — Following Quick Reference
You've built your entire frontend with custom types and example data. Now it's time to integrate Payload CMS. Here's the good news: your code barely changes. You're just swapping the data source.
This guide shows how smooth the transition is.
Why Your Code Doesn't Change
The magic of design-driven development is that your types are your specification. When you integrated with Payload, your types became the source of truth for Payload's field configuration.
Result: Your frontend types match Payload's output exactly. When you switch from example data to Payload data, the component sees the same type structure. No refactoring needed.
The Big Picture
BEFORE (Dummy Data)
├─ Types: src/types/blocks/, src/types/collections/
├─ Example Data: example.ts files
├─ Components: Use example data
└─ Page: Imports example data
↓↓↓ ONE CHANGE ↓↓↓
AFTER (Payload Integration)
├─ Types: @payload-types (Payload generates them)
├─ Data: Payload CMS queries
├─ Components: Use Payload data (same structure!)
└─ Page: Queries Payload
The component code stays identical.
Step 1: Verify Your Types Match Payload
Before switching to Payload, make sure your frontend types match what Payload will generate.
Your manual type:
export interface FeaturedIndustriesBlock {
blockType: 'featuredIndustries';
template: 'default';
title?: string;
selectedIndustries?: Industry[];
bgColor?: 'white' | 'light' | 'dark';
}
export interface Industry {
id: number;
title: string;
slug?: string;
description?: string;
icon?: string;
link?: CTA;
}
Should match your Payload collection/block config:
// payload/blocks/featured-industries.ts
{
slug: 'featured-industries',
fields: [
{ name: 'title', type: 'text' },
{ name: 'selectedIndustries', type: 'relationship', relationTo: 'industries' },
{ name: 'bgColor', type: 'select', options: ['white', 'light', 'dark'] },
]
}
// payload/collections/industries.ts
{
slug: 'industries',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text' },
{ name: 'description', type: 'textarea' },
{ name: 'icon', type: 'text' },
{ name: 'link', type: 'relationship', relationTo: 'ctas' },
]
}
The structure should be identical. If Payload has extra fields, that's fine—your components just won't use them. If Payload is missing fields, you have a mismatch to fix.
Step 2: Prepare Your Component
Your component doesn't change. It already expects the exact type Payload will provide.
// This component works with BOTH example data and Payload data
// No changes needed!
export function FeaturedIndustriesTemplate1({ data }: { data: FeaturedIndustriesBlock }) {
const { selectedIndustries = [] } = data;
return (
<section>
{selectedIndustries.map(industry => (
<Card key={industry.id}>
<h3>{industry.title}</h3>
{/* ... render industry data ... */}
</Card>
))}
</section>
);
}
This component works with:
// Before: example data
const data = {
blockType: 'featuredIndustries',
selectedIndustries: industriesData,
};
// After: Payload data
const data = await getBlock('featured-industries', id);
// Same type, same component, works in both cases!
Step 3: Update Data Fetching
Replace your example data with Payload queries.
Before:
// src/app/page.tsx
import homePageData from './data';
export default function HomePage() {
return (
<main>
{homePageData.layout.map((block, i) => (
<BlockRenderer key={i} block={block} />
))}
</main>
);
}
After:
// src/app/page.tsx
import { getPage } from '@/lib/payload';
import { BlockRenderer } from '@/components/block-renderer';
export default async function HomePage() {
// Query Payload CMS instead of importing example data
const homePageData = await getPage('home');
return (
<main>
{homePageData.layout.map((block, i) => (
<BlockRenderer key={i} block={block} />
))}
</main>
);
}
That's it. Everything else stays the same.
Step 4: Create Payload Utilities
Build helper functions to query Payload data:
File: src/lib/payload.ts
import type { Page, FeaturedIndustriesBlock } from '@payload-types';
const PAYLOAD_API = process.env.NEXT_PUBLIC_PAYLOAD_API_URL;
/**
* Get a complete page with all blocks
*/
export async function getPage(slug: string): Promise<Page> {
const response = await fetch(
`${PAYLOAD_API}/api/pages?where[slug][equals]=${slug}`,
{ next: { revalidate: 60 } }
);
if (!response.ok) throw new Error(`Failed to fetch page: ${slug}`);
const data = await response.json();
return data.docs[0];
}
/**
* Get a single block by ID
*/
export async function getBlock(blockType: string, id: string) {
const response = await fetch(
`${PAYLOAD_API}/api/blocks?where[blockType][equals]=${blockType}&where[id][equals]=${id}`,
{ next: { revalidate: 60 } }
);
if (!response.ok) throw new Error(`Failed to fetch block: ${blockType}/${id}`);
const data = await response.json();
return data.docs[0];
}
/**
* Get all industries (collection)
*/
export async function getIndustries(): Promise<Industry[]> {
const response = await fetch(
`${PAYLOAD_API}/api/industries`,
{ next: { revalidate: 60 } }
);
if (!response.ok) throw new Error('Failed to fetch industries');
const data = await response.json();
return data.docs;
}
/**
* Get single industry by slug
*/
export async function getIndustry(slug: string): Promise<Industry> {
const response = await fetch(
`${PAYLOAD_API}/api/industries?where[slug][equals]=${slug}`,
{ next: { revalidate: 60 } }
);
if (!response.ok) throw new Error(`Failed to fetch industry: ${slug}`);
const data = await response.json();
return data.docs[0];
}
These utilities handle Payload API calls consistently. Your components don't know about Payload—they just get data.
Step 5: Update Page Data Functions
For pages that reference specific industries or other collections:
Before:
// src/app/industries/[slug]/page.tsx
import { industriesData } from '@/types/collections';
export default function IndustryPage({ params }) {
const industry = industriesData.find(i => i.slug === params.slug);
if (!industry) return <div>Not found</div>;
return <IndustryDetail industry={industry} />;
}
After:
// src/app/industries/[slug]/page.tsx
import { getIndustry } from '@/lib/payload';
export default async function IndustryPage({ params }) {
try {
const industry = await getIndustry(params.slug);
return <IndustryDetail industry={industry} />;
} catch (error) {
return <div>Not found</div>;
}
}
Same component (IndustryDetail), different data source.
Step 6: Update BlockRenderer (Optional)
If you're using Payload's block system, BlockRenderer might query blocks from Payload instead of receiving them as props:
Before:
export function BlockRenderer({ block }: { block: Block }) {
if (block.blockType === 'hero') {
return <HeroTemplate1 data={block} />;
}
// ... etc
}
After (no change):
The BlockRenderer stays exactly the same. It doesn't know or care where blocks come from. It just renders them.
Step 7: Handle Dynamic Imports (If Needed)
If Payload has new block types you haven't built yet, handle them gracefully:
export function BlockRenderer({ block }: { data: Block }) {
// Map blockType to component
const component = blockComponentMap[block.blockType]?.[block.template];
if (!component) {
return (
<div className="p-6 bg-yellow-50 border border-yellow-200">
<p className="text-yellow-800">
Block type "{block.blockType}" / "{block.template}" not yet implemented
</p>
</div>
);
}
return component(block);
}
This prevents the app from breaking if Payload has a block type your frontend doesn't support yet.
Migration Checklist
PAYLOAD MIGRATION CHECKLIST:
SETUP
□ Payload CMS installed and configured
□ Types exported from Payload (@payload-types)
□ Environment variables set (API URL, tokens)
□ Test Payload API connection with curl/postman
VERIFICATION
□ Frontend types match Payload schema
□ All required fields exist in Payload
□ Example data still works (verify nothing broke)
□ BlockRenderer still routes correctly
UTILITIES
□ Created src/lib/payload.ts with helper functions
□ getPage() function works
□ getBlock() function works
□ Collection query functions (getIndustries(), etc.) work
MIGRATION
□ Updated src/app/page.tsx to use getPage()
□ Updated collection pages to use get() functions
□ Removed imports of example data
□ Verified all pages render with Payload data
□ No console errors
□ No TypeScript errors
TESTING
□ Home page loads from Payload
□ All blocks render correctly
□ Detail pages load from Payload
□ Images and media display correctly
□ Links and CTAs work
□ No 404s or missing data
□ Performance is acceptable
FALLBACKS
□ Graceful error handling for missing data
□ 404 pages for not-found content
□ Empty states for no data
□ Development mode still works with example data
Keeping Development with Examples
While integrating Payload, you might want to keep development mode using example data. You can add a flag:
// src/lib/payload.ts
const USE_EXAMPLES = process.env.NEXT_PUBLIC_USE_EXAMPLES === 'true';
export async function getPage(slug: string) {
if (USE_EXAMPLES) {
// Return example data in development
return homePageData;
}
// Query Payload in production
const response = await fetch(`${PAYLOAD_API}/api/pages?where[slug][equals]=${slug}`);
// ... etc
}
Then set NEXT_PUBLIC_USE_EXAMPLES=true in .env.local for local development.
Rollback Strategy
If something goes wrong, you can quickly rollback:
// src/lib/payload.ts
export async function getPage(slug: string) {
try {
// Query Payload
const response = await fetch(`${PAYLOAD_API}/api/pages?where[slug][equals]=${slug}`);
if (!response.ok) throw new Error('Payload error');
return await response.json();
} catch (error) {
// Fallback to example data
console.warn('Fallback to example data:', error);
return homePageData;
}
}
This way, if Payload is down, your site still works with example data.
Performance Considerations
Payload queries should be efficient:
Revalidation:
// Revalidate every 60 seconds (good for mostly-static sites)
{ next: { revalidate: 60 } }
// Revalidate every hour
{ next: { revalidate: 3600 } }
// Never revalidate (manually trigger with revalidatePath)
{ cache: 'no-store' }
Selective Fields:
// Only query fields you need (not everything)
await fetch(`${PAYLOAD_API}/api/pages?select=slug,title,layout&where[slug][equals]=home`)
Pagination:
// Limit results if querying collections
await fetch(`${PAYLOAD_API}/api/industries?limit=100&page=1`)
Key Points to Remember
- Your components don't change - They expect the exact type Payload provides
- Your types guide Payload schema - Payload should match your frontend types
- Data source changes, not the code - Just swap example data for Payload queries
- BlockRenderer stays the same - It doesn't know or care where blocks come from
- Fallbacks prevent broken sites - Always have a graceful fallback
The Real Magic
This is where design-driven development truly shines. By letting design determine your data structure before you even built Payload, you ensured that when Payload was added, it matched perfectly.
No refactoring. No "we need to add a field." No "the CMS output doesn't match our frontend types." Just seamless integration.
You've Completed the Series!
Congratulations! You now have the complete path: design-first development → custom types → components → example data → Payload integration.
Series Summary:
- Part 1: Design-Driven Development — Philosophy & Mindset
- Part 2: Use Existing Payload Block Types — Working with existing definitions
- Part 3: Create Custom Block Types — Building from scratch
- Part 4: Creating Collections — Reusable data entities
- Part 5: Icons, Components & Best Practices — Consistency patterns
- Part 6: Quick Reference — Templates & checklists
- Back to Hub: Design to Code — Full series overview
Next Resources:
- Need help with specific Payload config? Check Payload docs at payloadcms.com
- Questions about queries? See your Payload API docs
- Performance optimization? Review Next.js caching docs
Each step builds on the previous, and switching to Payload requires almost no code changes. This is the efficiency of design-driven development.