Overview
The App Router is the default in Next.js 15. Routes are folders under app/, and the file names in each folder describe what that segment does. A folder becomes a URL segment, a page.tsx makes it routable, a layout.tsx wraps every child below it, and a handful of other reserved filenames cover loading, errors, and not-found states. The router is server-first; react-server-components is the rendering model underneath.
Put pages in app/, one folder per segment
Each folder under app/ is a URL segment. The folder app/posts/[slug]/page.tsx serves /posts/:slug. Folders without a page.tsx do not produce a route; they only contribute layouts or co-located files.
app/
layout.tsx // root layout (required)
page.tsx // "/"
posts/
page.tsx // "/posts"
[slug]/
page.tsx // "/posts/:slug"
Use [param] for a single dynamic segment, [...slug] for a catch-all, and [[...slug]] for an optional catch-all. Type params in the page signature; Next.js 15 passes them as a Promise that must be awaited.
Use layout.tsx for shells, page.tsx for routes
A layout.tsx wraps every page below it and re-renders only when the layout’s own data changes. Put the nav, providers, theme, and auth context here. A page.tsx is the leaf that owns the route’s main content.
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}The root app/layout.tsx is the only required file in app/. It must render <html> and <body> and is the right place for the <Metadata> defaults (see nextjs-metadata-api).
Group routes without changing the URL
Wrap a folder name in parentheses to create a route group: app/(marketing)/about/page.tsx serves /about, not /marketing/about. Groups exist for organizing layouts, not for URLs.
Use groups to give different sections their own layouts:
app/
(marketing)/
layout.tsx // marketing chrome
about/page.tsx
pricing/page.tsx
(app)/
layout.tsx // authenticated chrome
dashboard/page.tsx
Two segments in different groups can share the same URL prefix without sharing a layout. This replaces most of the layout-juggling that Pages Router needed.
Render parallel routes with named slots
Parallel routes let a single layout render two independent panes that each have their own loading and error states. Mark a folder with @: app/@feed/page.tsx and app/@sidebar/page.tsx are both passed to the layout as props.
// app/layout.tsx
export default function Layout({
children,
feed,
sidebar,
}: {
children: React.ReactNode;
feed: React.ReactNode;
sidebar: React.ReactNode;
}) {
return (
<>
<main>{children}</main>
<aside>{sidebar}</aside>
<section>{feed}</section>
</>
);
}Parallel routes are the right primitive for dashboards with independent panels and for modals layered over a base page. Pair them with intercepting routes for modal navigation.
Intercept routes for modals over a base page
An intercepting route renders one route in the position of another. The convention uses (.), (..), or (...) prefixes that mirror relative imports. Use them when a user clicks a thumbnail and a photo opens as a modal over the gallery, but a direct visit to the same URL shows the full photo page.
app/
photos/
page.tsx // "/photos" gallery
[id]/page.tsx // "/photos/:id" full page
@modal/
(.)photos/[id]/page.tsx // modal over "/photos"
The intercepted view runs inside the parallel @modal slot when the user arrives via client navigation; the canonical page still renders on a hard load or share.
Migrate from Pages Router incrementally
app/ and pages/ can coexist. Move one route at a time and keep the rest on Pages until the migration is done.
getServerSidePropsbecomes anasyncserver component that awaits its data inline.getStaticPropsplusgetStaticPathsbecomes a server component withgenerateStaticParams._app.tsxsplits intoapp/layout.tsx(providers) and per-segment layouts.- API routes in
pages/api/move toroute.tshandlers underapp/; see nextjs-route-handlers. - Mutations move from API routes to server actions; see nextjs-server-actions.
Keep the migration on a branch until every route renders correctly, then delete pages/.