MyApp

Getting Started

Introduction

User Interface

Button.tsxCard.tsxDialog.tsxMarquee.tsxBentoGrid.tsx

Feature Components

PricingCard.tsxTestimonials.tsxTotalBuyers.tsxFaq.tsxEvent.tsx

Utility Components

SvgFinder.tsxCustomIcons.tsImageWithFallback.tsx

Patterns

Layout PatternsForm PatternsData Fetching PatternsError Handling Patterns
MyApp

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.

Related

  • Next.js Data Fetching - Official docs
  • Prisma ORM - Database queries
  • Stripe Integration - Payment data

How is this guide ?

Last updated on

Form Patterns

Common form patterns using React Hook Form, Zod validation, and Clerk authentication.

Error Handling Patterns

Error handling strategies for forms, API routes, server components, and client interactions.

On this page

Three-Layer Pattern
Layer 1: Server Component
Layer 2: Lib Function
Layer 3: API Route
Benefits
Basic Query Pattern
Stripe Integration
Parallel Data Fetching
Caching Strategies
Cache Invalidation
Why This Pattern?
Related