We use tracking cookies to understand how you use the product and help us improve it. For more information on how we store cookies, read our  privacy policy.

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

@/app/(base)/page.tsx
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

@/lib/events/getEvent.ts
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

@/app/api/events/route.ts
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

@/lib/data/getUsers.ts
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: [] };
  }
}
@/app/api/users/route.ts
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

@/lib/stripe/getProducts.ts
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();
}
@/app/api/stripe/products/route.ts
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

@/app/(base)/dashboard/page.tsx
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

Cache Options
// 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

Server Action
'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.

How is this guide ?

Last updated on