Data Fetching Patterns

Server-side data fetching patterns in Plainform 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

@/types/StripeInterfaces.ts
import type Stripe from 'stripe';

export interface IStripeProductWithPrice
  extends Omit<Stripe.Product, 'default_price'> {
  default_price: Stripe.Price & {
    unit_amount: number;
  };
}
@/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';
import type { IStripeProductWithPrice } from '@/types/StripeInterfaces';
import { NextResponse } from 'next/server';

export async function GET() {
  const products = await stripe.products.list({
    active: true,
    expand: ['data.default_price'],
  });

  const sortedProducts = products.data.sort((a, b) => a.created - b.created);

  return NextResponse.json({
    products: sortedProducts as IStripeProductWithPrice[],
  });
}

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

On this page