⏱️6 min read · 1,120 words
Next.js 15 solidifies React’s full-stack future. With the App Router stable, React Server Components as the default, and Turbopack replacing Webpack, building production Next.js apps in 2026 is faster and more powerful than ever.
📋 Table of Contents
What’s New in Next.js 15
- Turbopack stable — 10x faster builds, 700x faster HMR vs Webpack
- React 19 first-class support — Server Components, Actions, use() hook
- Partial Prerendering (PPR) — static shell + dynamic islands on same page
- Improved caching — no more surprising cache-by-default behavior
- Dynamic APIs — cookies(), headers(), searchParams are now async
Project Setup
# Create new Next.js 15 project
npx create-next-app@latest my-app --typescript --tailwind --app --turbopack
cd my-app
# Project structure
# app/
# layout.tsx — root layout
# page.tsx — home page
# globals.css
# (dashboard)/ — route group (no URL segment)
# page.tsx
# blog/
# [slug]/
# page.tsx — dynamic route
# api/
# users/
# route.ts — API route handler
App Router Fundamentals
// app/layout.tsx — Root Layout (required)
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: { template: "%s | My App", default: "My App" },
description: "Built with Next.js 15",
openGraph: {
type: "website",
locale: "en_US",
url: "https://myapp.com",
siteName: "My App",
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
Server Components vs Client Components
// Server Component (default) — runs on server, no JavaScript sent to client
// app/blog/page.tsx
async function BlogPage() {
// Direct database/API access — no useEffect needed
const posts = await db.query("SELECT * FROM posts ORDER BY created_at DESC");
return (
<main>
<h1>Blog</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</main>
);
}
// Client Component — add "use client" directive
"use client";
import { useState } from "react";
function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
onKeyDown={e => e.key === "Enter" && onSearch(query)}
placeholder="Search..."
/>
);
}
Data Fetching with Server Components
// Parallel data fetching — no waterfalls
async function Dashboard() {
// These run in parallel
const [user, posts, stats] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchStats(),
]);
return (
<div>
<UserCard user={user} />
<PostList posts={posts} />
<StatsPanel stats={stats} />
</div>
);
}
// Dynamic rendering with searchParams (Next.js 15: async)
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string }>;
}) {
const { q = "", page = "1" } = await searchParams;
const results = await searchPosts(q, parseInt(page));
return <SearchResults results={results} query={q} />;
}
Server Actions
Server Actions let you run server code from forms and event handlers:
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
// Validate
if (!title || title.length < 3) {
return { error: "Title must be at least 3 characters" };
}
// Save to database
await db.posts.create({ title, content });
// Revalidate and redirect
revalidatePath("/blog");
redirect("/blog");
}
// Use in a form
export default function NewPostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" />
<button type="submit">Publish</button>
</form>
);
}
Route Handlers (API Routes)
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const page = parseInt(searchParams.get("page") ?? "1");
const users = await db.users.findMany({
skip: (page - 1) * 10,
take: 10,
});
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 });
}
// app/api/users/[id]/route.ts
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
await db.users.delete({ where: { id } });
return new NextResponse(null, { status: 204 });
}
Partial Prerendering (PPR)
PPR lets you combine static and dynamic content on a single page:
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
experimental: {
ppr: true,
},
};
export default config;
// page.tsx — static shell + dynamic island
import { Suspense } from "react";
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
{/* Static — generated at build time */}
<ProductDetails id={params.id} />
{/* Dynamic — rendered per request */}
<Suspense fallback={<PriceSkeleton />}>
<DynamicPrice id={params.id} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<UserReviews id={params.id} />
</Suspense>
</div>
);
}
Middleware
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("auth-token")?.value;
const isProtected = request.nextUrl.pathname.startsWith("/dashboard");
if (isProtected && !token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Add security headers
const response = NextResponse.next();
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
return response;
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
Image Optimization
import Image from "next/image";
// Optimized image — auto WebP/AVIF, lazy loading, size hints
export function HeroImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={1200}
height={630}
priority // LCP image — eager load
quality={85}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j..." // low-quality placeholder
/>
);
}
// Fill container
<div style={{ position: "relative", aspectRatio: "16/9" }}>
<Image src="/hero.jpg" alt="Hero" fill sizes="100vw" />
</div>
Deployment and Production
- Vercel — zero-config, edge network, automatic PPR
- Self-hosted —
next build && next startwith Node.js - Docker — official Next.js Dockerfile with standalone output
Next.js 15 is the most complete React framework for 2026. Server Components eliminate over-fetching, Server Actions simplify mutations, and PPR delivers the best of static and dynamic rendering on the same page.
📚 You might also like
🔗 Share this article




✍️ Leave a Comment