Overview

Next.js 15 is App Router by default. Server components do the heavy lifting, server actions replace most ad-hoc API routes, and the caching model is explicit instead of magical. This page covers the conventions that hold for a Next.js 15 app on React 19.

Server components by default, client islands at the edges

A file in app/ is a server component unless its first line is "use client". Server components render on the server, fetch data directly, ship no JS to the browser, and cannot use hooks or event handlers.

Push "use client" as far down the tree as possible. A page can be a server component that renders a server-rendered list and a small client island for the filter dropdown. Wrapping the whole page in "use client" defeats the model. See react for the broader rule on the boundary.

Mutate with server actions, not ad-hoc routes

Server actions are functions marked "use server" that the framework wires up as RPC endpoints. They are the default for any mutation a form submits.

// app/notes/actions.ts
"use server";
import { revalidatePath } from "next/cache";
 
export async function createNote(formData: FormData) {
  const title = String(formData.get("title"));
  await db.note.create({ data: { title } });
  revalidatePath("/notes");
}
 
// app/notes/new/page.tsx
import { createNote } from "../actions";
export default function NewNote() {
  return (
    <form action={createNote}>
      <input name="title" />
      <button type="submit">Save</button>
    </form>
  );
}

Reach for a route.ts handler only for true HTTP endpoints (webhooks, public APIs, file uploads with custom headers).

Cache deliberately, revalidate by tag or path

Next.js 15 caches less by default than Next.js 14. Mark what you want cached.

  • cache(fn) from React: dedupe a function within a single render pass.
  • unstable_cache(fn, keys, { tags, revalidate }): persist across requests with tagging.
  • fetch(url, { next: { revalidate: 60, tags: ["notes"] } }): tag a fetch.

Invalidate from server actions:

revalidatePath("/notes");
revalidateTag("notes");

Tags are usually right for collections; paths are right for one-off pages. Pick one strategy per resource and document it.

Drive metadata from the route

Export metadata or generateMetadata from a page or layout. Do not write <head> tags by hand in App Router.

// app/posts/[slug]/page.tsx
import type { Metadata } from "next";
 
export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.summary,
    alternates: { canonical: `/posts/${post.slug}` },
    openGraph: { title: post.title, description: post.summary, images: [post.cover] },
  };
}

Layouts contribute defaults; child segments override. Open Graph and Twitter Card data are first-class. For structured data, render JSON-LD in the page body. See technical and structured-data.

Use route segments and layouts for shared shells

A layout.tsx wraps every page below it. Use layouts for the chrome that does not change between routes: the nav, the auth check, the providers. Use loading.tsx and error.tsx next to a route for the Suspense fallback and the error boundary.

Group routes without changing the URL using (group) folders. Use @slot parallel routes for sidebars and modals that live next to the main content. Use [param] for dynamic segments and [...slug] for catch-alls.

Pick the runtime per route

Each route segment can run on nodejs (default) or edge.

  • Node runtime: full Node APIs, Prisma, native modules, longer cold start.
  • Edge runtime: Web APIs only, fast cold start, regional and global execution, no native modules.
export const runtime = "edge"; // or "nodejs"

Pick edge for short, network-bound work that benefits from low latency (auth checks, redirects, A/B routing). Pick nodejs for anything that touches Prisma, the filesystem, or a Node-only library. Mixing is fine; choose per segment.