Data Fetching Patterns
Server-side data fetching patterns using lib functions that call API routes for clean separation of concerns.
Plainform uses a three-layer architecture: Server Components → Lib Functions → API Routes.
Data Fetching Architecture
Plainform uses lib functions (e.g., getProducts(), getEvent()) that call API routes. Server components never call APIs directly - they use these helper functions for clean, reusable data fetching.
Three-Layer Pattern
Layer 1: Server Component
import { getEvent } from '@/lib/events/getEvent';
export default async function HomePage() {
const { event } = await getEvent();
return (
<main>
{event && <div>{event.text}</div>}
</main>
);
}Layer 2: Lib Function
import { env } from '@/env';
export async function getEvent() {
try {
const res = await fetch(`${env.SITE_URL}/api/events`, {
method: 'GET',
next: { tags: ['event'] },
cache: 'force-cache',
});
if (!res.ok) {
throw new Error('Failed to fetch event');
}
return res.json();
} catch (error) {
return error;
}
}Layer 3: API Route
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma/prisma';
export async function GET() {
try {
const event = await prisma.event.findFirst({
where: { active: true },
orderBy: { createdAt: 'desc' },
});
return NextResponse.json({ event });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch event' },
{ status: 500 }
);
}
}Benefits
- Separation of Concerns: Database logic in API routes, fetching in lib functions, rendering in components
- Reusability: Lib functions work across multiple components
- Type Safety: Centralized return types
- Caching: Configure caching in lib functions
- Error Handling: Consistent error handling
Basic Query Pattern
import { env } from '@/env';
export async function getUsers() {
try {
const res = await fetch(`${env.SITE_URL}/api/users`, {
cache: 'force-cache',
next: { tags: ['users'] },
});
if (!res.ok) {
throw new Error('Failed to fetch users');
}
return res.json();
} catch (error) {
return { users: [] };
}
}import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma/prisma';
export async function GET() {
const users = await prisma.user.findMany();
return NextResponse.json({ users });
}Stripe Integration
import { env } from '@/env';
export async function getProducts() {
const res = await fetch(`${env.SITE_URL}/api/stripe/products`, {
cache: 'force-cache',
next: { tags: ['stripe/products'] },
});
return res.json();
}import { stripe } from '@/lib/stripe/stripe';
export async function GET() {
const products = await stripe.products.list({ active: true });
return NextResponse.json({ products: products.data });
}Parallel Data Fetching
import { getUser } from '@/lib/users/getUser';
import { getPosts } from '@/lib/blog/getPosts';
export default async function DashboardPage() {
const [userData, postsData] = await Promise.all([
getUser('123'),
getPosts(),
]);
return (
<div>
<UserProfile user={userData.user} />
<PostList posts={postsData.posts} />
</div>
);
}Caching Strategies
// Force cache (default)
fetch(`${env.SITE_URL}/api/data`, {
cache: 'force-cache',
next: { tags: ['data'] },
});
// No cache (always fresh)
fetch(`${env.SITE_URL}/api/user`, {
cache: 'no-store',
});
// Revalidate by time
fetch(`${env.SITE_URL}/api/stats`, {
next: { revalidate: 3600 }, // 1 hour
});Cache Invalidation
'use server';
import { revalidateTag } from 'next/cache';
export async function createPost(data: FormData) {
await fetch(`${env.SITE_URL}/api/posts`, {
method: 'POST',
body: JSON.stringify(data),
});
revalidateTag('posts');
}Why This Pattern?
Plainform uses the three-layer pattern for clean separation of concerns and better organization. The alternative (direct database access in components) is faster but mixes concerns.
Related
- Next.js Data Fetching - Official docs
- Prisma ORM - Database queries
- Stripe Integration - Payment data
How is this guide ?
Last updated on