Production Next.js App Router & Server Actions
Structuring high-performance full-stack applications with Next.js App Router, streaming SSR, parallel routes, and type-safe server actions.
Overview
The Next.js App Router fundamentally changed how we build full-stack React applications. Instead of a filesystem-based page router, the App Router gives us a composable, server-first model with React Server Components at its core.
File System Conventions
app/
├── layout.tsx ← Root layout (always server)
├── page.tsx ← / route
├── loading.tsx ← Streaming suspense boundary
├── error.tsx ← Error boundary
├── (marketing)/ ← Route group — no URL segment
│ └── about/page.tsx
└── dashboard/
├── @analytics/ ← Parallel route slot
├── @team/
└── layout.tsx
Server vs Client Components
By default every component in app/ is a React Server Component (RSC). They run only on the server, can await data, and ship zero JavaScript to the client.
// app/blog/[slug]/page.tsx — Server Component
import { getPost } from "@/lib/db";
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug); // direct DB access — no API needed
return <article>{post.content}</article>;
}
Add "use client" only when you need hooks, browser APIs, or event handlers.
Type-Safe Server Actions
Server Actions replace form-handling APIs. They run on the server but can be called directly from the client.
// app/actions.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
const Schema = z.object({ title: z.string().min(3) });
export async function createPost(formData: FormData) {
const { title } = Schema.parse({ title: formData.get("title") });
await db.post.create({ data: { title } });
revalidatePath("/blog");
}
// components/PostForm.tsx — Client Component
"use client";
import { createPost } from "@/app/actions";
export function PostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" />
<button type="submit">Create</button>
</form>
);
}
Streaming & Suspense
Wrap slow data-fetching segments in <Suspense> to stream HTML progressively:
import { Suspense } from "react";
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<Skeleton />}>
<SlowAnalytics /> {/* streams in when ready */}
</Suspense>
</main>
);
}
Parallel Routes
Parallel routes render multiple pages simultaneously in the same layout — perfect for dashboards and modals:
// app/dashboard/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="grid grid-cols-3">
{children}
{analytics}
{team}
</div>
);
}
Caching Strategy
Next.js 14+ gives you granular cache control:
// Opt out of caching entirely
export const revalidate = 0;
// Revalidate every 60s (ISR)
export const revalidate = 60;
// Tagged revalidation
fetch("/api/posts", { next: { tags: ["posts"] } });
revalidateTag("posts"); // called inside a Server Action
Key Takeaways
- Default to Server Components; add
"use client"only where needed - Use Server Actions instead of API routes for mutations
- Wrap slow segments in
<Suspense>for streaming - Prefer tagged revalidation over full-page ISR for granular control
- Route groups (
(folder)) for layout sharing without URL impact