In the App Router, data fetching happens primarily in Server Components using native fetch, database clients, or ORMs directly.

Fetching in Server Components

Any Server Component can be async and await data:

  // app/posts/page.tsx
async function getPosts() {
    const res = await fetch('https://jsonplaceholder.typicode.com/posts');
    if (!res.ok) throw new Error('Failed to fetch posts');
    return res.json();
}

export default async function PostsPage() {
    const posts = await getPosts();

    return (
        <ul>
            {posts.map((post: { id: number; title: string }) => (
                <li key={post.id}>{post.title}</li>
            ))}
        </ul>
    );
}
  

No useEffect, no loading state boilerplate — the page waits for data before rendering.

Fetch Caching

Next.js extends fetch with caching options:

  fetch('https://api.example.com/data');                              // Cached (default)
fetch('https://api.example.com/data', { next: { revalidate: 60 } }); // ISR — refresh every 60s
fetch('https://api.example.com/data', { cache: 'no-store' });        // Always fresh
  
Option Behavior
Default Cache until manually invalidated
{ next: { revalidate: N } } Revalidate every N seconds
{ cache: 'no-store' } Fetch on every request

Segment-Level Revalidation

Set revalidation for an entire route segment:

  export const revalidate = 3600; // Revalidate every hour

export default async function ProductsPage() {
    const products = await getProducts();
    return <ProductList products={products} />;
}
  

Force dynamic rendering: export const dynamic = 'force-dynamic';

Parallel Data Fetching

Fetch multiple resources concurrently:

  export default async function DashboardPage() {
    const [users, stats] = await Promise.all([getUsers(), getStats()]);
    return (
        <div>
            <Stats data={stats} />
            <UserList users={users} />
        </div>
    );
}
  

Database Queries

Call your ORM directly in Server Components — no API layer required:

  import { db } from '@/lib/db';

export default async function UsersPage() {
    const users = await db.user.findMany({ orderBy: { createdAt: 'desc' } });
    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>{user.name} — {user.email}</li>
            ))}
        </ul>
    );
}
  

Server Actions

Server Actions handle form submissions and mutations on the server:

  // app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
    const title = formData.get('title') as string;
    await db.post.create({ data: { title } });
    revalidatePath('/posts');
}
  
  // app/posts/new/page.tsx
import { createPost } from '@/app/actions';

export default function NewPostPage() {
    return (
        <form action={createPost}>
            <input name="title" placeholder="Post title" required />
            <button type="submit">Create</button>
        </form>
    );
}
  

On-Demand Revalidation

Invalidate cached data after mutations with revalidatePath('/posts') or revalidateTag('posts'). Tag fetches with { next: { tags: ['posts'] } } for targeted invalidation.

Add loading.tsx next to page.tsx for automatic Suspense loading UI.

Next: understand SSR, SSG, and ISR rendering strategies.