DEV Community

arenasbob2024-cell
arenasbob2024-cell

Posted on • Originally published at viadreams.cc

Next.js App Router Complete Guide 2026: Server Components, Layouts, and Data Fetching

The Next.js App Router changed everything. If you're still on Pages Router, it's time to migrate. Here's everything you need to know.

App Router vs Pages Router

The fundamental difference: App Router uses React Server Components by default.

app/
├── layout.tsx      # Root layout (always server component)
├── page.tsx        # Home page
├── about/
│   └── page.tsx    # /about
├── blog/
│   ├── layout.tsx  # Blog-specific layout
│   ├── page.tsx    # /blog
│   └── [slug]/
│       └── page.tsx # /blog/[slug]
└── api/
    └── users/
        └── route.ts # API endpoint
Enter fullscreen mode Exit fullscreen mode

Server Components (Default)

Server components run on the server. They can fetch data directly:

// app/blog/page.tsx — Server Component (no 'use client' needed)
async function BlogPage() {
  // Direct database query — no useEffect needed!
  const posts = await db.query('SELECT * FROM posts ORDER BY created_at DESC');

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

export default BlogPage;
Enter fullscreen mode Exit fullscreen mode

Benefits: no client-side JavaScript, direct DB access, smaller bundle.

Client Components

Add 'use client' only when you need interactivity:

'use client';
import { useState } from 'react';

export function LikeButton({ postId }: { postId: string }) {
  const [likes, setLikes] = useState(0);
  const [liked, setLiked] = useState(false);

  const handleLike = async () => {
    if (liked) return;
    setLiked(true);
    setLikes(l => l + 1);
    await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
  };

  return (
    <button onClick={handleLike} disabled={liked}>
      ❤️ {likes}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Layouts

Layouts wrap pages and persist across navigation:

// app/layout.tsx — Root layout
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: { template: '%s | My Site', default: 'My Site' },
  description: 'Your awesome site',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Header />
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Nested Layouts

// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex">
      <Sidebar />
      <div className="flex-1">{children}</div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Data Fetching

Server Components (Recommended)

// Parallel fetching
async function ProductPage({ params }: { params: { id: string } }) {
  const [product, reviews, recommendations] = await Promise.all([
    fetch(`/api/products/${params.id}`).then(r => r.json()),
    fetch(`/api/products/${params.id}/reviews`).then(r => r.json()),
    fetch(`/api/recommendations?productId=${params.id}`).then(r => r.json()),
  ]);

  return (
    <div>
      <ProductDetails product={product} />
      <Reviews reviews={reviews} />
      <Recommendations items={recommendations} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Caching

// Cached for 60 seconds
const data = await fetch('/api/data', {
  next: { revalidate: 60 }
});

// Never cache (always fresh)
const data = await fetch('/api/live', {
  cache: 'no-store'
});

// Cache until explicitly revalidated
const data = await fetch('/api/static', {
  next: { tags: ['product'] }
});
Enter fullscreen mode Exit fullscreen mode

On-demand Revalidation

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { tag } = await request.json();
  revalidateTag(tag);
  return NextResponse.json({ revalidated: true });
}
Enter fullscreen mode Exit fullscreen mode

Route Handlers (API Routes)

// app/api/users/route.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = parseInt(searchParams.get('page') || '1');

  const users = await db.users.findMany({
    skip: (page - 1) * 20,
    take: 20,
  });

  return NextResponse.json({ users, page });
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const user = await db.users.create({ data: body });
  return NextResponse.json(user, { status: 201 });
}
Enter fullscreen mode Exit fullscreen mode

Loading and Error States

// app/blog/loading.tsx — Shown while page loads
export default function Loading() {
  return <div className="animate-pulse">Loading posts...</div>;
}

// app/blog/error.tsx — Shown on error
'use client';
export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Metadata API

// Static metadata
export const metadata: Metadata = {
  title: 'About',
  description: 'Learn about our company',
  openGraph: {
    images: ['/og-image.jpg'],
  },
};

// Dynamic metadata
export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      images: [post.coverImage],
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Server Actions

// Directly call server functions from forms
async function addTodo(formData: FormData) {
  'use server';
  const title = formData.get('title') as string;
  await db.todos.create({ data: { title } });
  revalidatePath('/todos');
}

export default function TodoForm() {
  return (
    <form action={addTodo}>
      <input name="title" />
      <button type="submit">Add Todo</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Middleware

// middleware.ts (root of project)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value;

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/dashboard/:path*',
};
Enter fullscreen mode Exit fullscreen mode

The App Router is the future of Next.js. Start migrating your Pages Router apps today. Use DevToolBox's Next.js tools to help with your development workflow.

Top comments (0)