DEV Community

ZNY
ZNY

Posted on

The Complete Guide to Building with Next.js App Router in 2026: Server Components, Streaming, and More

The Complete Guide to Building with Next.js App Router in 2026: Server Components, Streaming, and More

Next.js App Router became the stable default in late 2025, with Server Components, Server Actions, and Streaming changing how we build React applications. The shift from client-side rendering to server-first is now the standard approach.

Here's the practical guide.

App Router Basics

app/
├── layout.tsx        # Root layout (persists across pages)
├── page.tsx          # Home page (/)
├── about/
│   └── page.tsx     # /about
├── posts/
│   ├── page.tsx     # /posts
│   └── [id]/
│       └── page.tsx # /posts/:id
└── api/
    └── posts/
        └── route.ts # /api/posts
Enter fullscreen mode Exit fullscreen mode

Server Components

// app/users/page.tsx — Server Component by default
import { prisma } from "@/lib/prisma";

export default async function UsersPage() {
  // Direct database access, no API needed
  const users = await prisma.user.findMany({
    orderBy: { createdAt: "desc" },
  });

  return (
    <main>
      <h1>Users</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Client Components

// app/counter.tsx — Add "use client" for interactivity
"use client";

import { useState } from "react";

export function Counter({ initial = 0 }) {
  const [count, setCount] = useState(initial);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Dynamic Routes

// app/posts/[id]/page.tsx
import { prisma } from "@/lib/prisma";

interface Props {
  params: { id: string };
}

export default async function PostPage({ params }: Props) {
  const post = await prisma.post.findUnique({
    where: { id: params.id },
    include: { author: true },
  });

  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <p>By {post.author.name}</p>
      <div>{post.content}</div>
    </article>
  );
}
Enter fullscreen mode Exit fullscreen mode

Server Actions

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

import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
  const title = formData.get("title") as string;
  const content = formData.get("content") as string;

  await prisma.post.create({
    data: { title, content },
  });

  revalidatePath("/posts");
}

// In a component:
import { createPost } from "@/app/actions";

export function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" type="text" required />
      <textarea name="content" required />
      <button type="submit">Create Post</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Streaming with Suspense

import { Suspense } from "react";

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>

      <Suspense fallback={<div>Loading metrics...</div>}>
        <Metrics />
      </Suspense>

      <Suspense fallback={<div>Loading users...</div>}>
        <UserList />
      </Suspense>
    </div>
  );
}

async function Metrics() {
  // Slow query
  const metrics = await fetchMetrics();
  return <div>Metrics: {JSON.stringify(metrics)}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Middleware

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // Redirect unauthenticated users
  const token = request.cookies.get("auth-token");

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

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*", "/profile/:path*"],
};
Enter fullscreen mode Exit fullscreen mode

This article contains affiliate links. If you sign up through the links above, I may earn a commission at no additional cost to you.

Ready to Build Your Online Business?

Get started with Systeme.io for free — All-in-one platform for building your online business with AI tools.

Top comments (0)