Master file-based routing with the App Router for modern Next.js apps.
app/
page.tsx # / route
about/page.tsx # /about route
blog/page.tsx # /blog route// app/about/page.tsx
export default function About() {
return <h1>About Us</h1>;
}// app/blog/page.tsx
export default async function Blog() {
const posts = await getPosts();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}app/
blog/
[slug]/
page.tsx # /blog/:slug routeexport default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
return <h1>Post: {slug}</h1>;
}app/
users/[id]/posts/[postId]/page.tsx
# /users/:id/posts/:postIdexport async function generateStaticParams() {
const posts = await getPosts();
return posts.map(p => ({ slug: p.slug }));
}import { notFound } from "next/navigation";
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return <article>{post.content}</article>;
}// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}// app/blog/layout.tsx
export default function BlogLayout({ children }) {
return (
<div>
<Sidebar />
{children}
</div>
);
}// app/layout.tsx — required at the root
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}export const metadata = {
title: "My Blog",
description: "Posts about web development"
};// app/blog/error.tsx
"use client";
export default function Error({ error, reset }) {
return (
<div>
<h2>Error loading blog</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}// app/blog/loading.tsx
export default function Loading() {
return <p>Loading posts...</p>;
}// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return <h1>Post not found</h1>;
}// app/blog/template.tsx
export default function Template({ children }) {
return <div>{children}</div>; // Remounts on each navigation
}// app/api/hello/route.ts
export async function GET() {
return Response.json({ message: "Hello" });
}app/
(marketing)/
page.tsx # / (still at root)
about/page.tsx # /about
(dashboard)/
layout.tsx
page.tsx # /dashboardapp/
docs/[...slug]/page.tsx # /docs/a/b/capp/
docs/[[...slug]]/page.tsx
# Matches /docs, /docs/a, /docs/a/b/capp/
@analytics/
page.tsx
@team/
page.tsx
layout.tsx # Receives both slots as propsapp/
photos/
[id]/
page.tsx # /photos/42 — full page
(.)photos/
[id]/
page.tsx # intercepted modal viewUse [...slug] folder syntax to match multiple path segments — for example, app/docs/[...slug]/page.tsx matches /docs/a, /docs/a/b, and so on. In Next.js 15, params is a Promise: type it as Promise<{ slug: string[] }> and await it before accessing the slug array.
layout.tsx persists across route changes and does not remount, making it ideal for shared UI like navbars. template.tsx creates a new instance on every navigation, which is useful when you need effects or animations to re-run between pages.
Wrap the routes in a route group by naming the folder with parentheses, like (marketing) or (app). Files inside are grouped under that layout but the folder name is excluded from the URL.
Yes — nest dynamic folders like app/shop/[category]/[productId]/page.tsx to capture multiple params. In Next.js 15, type params as Promise<{ category: string; productId: string }> and use const { category, productId } = await params before accessing the values.
loading.tsx only triggers on the initial load of a segment, not on client-side navigations within the same layout boundary. Wrap slow data-fetching components in individual Suspense boundaries to get streaming loading states on every navigation.