Next.js App Router: File-Based Routing, Layouts, and Loading States
The App Router (Next.js 13+) replaced Pages Router with a new file convention that makes shared layouts, streaming, and server components the default.
File System Routing
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
├── dashboard/
│ ├── layout.tsx → Wraps all /dashboard/* routes
│ ├── page.tsx → /dashboard
│ └── settings/
│ └── page.tsx → /dashboard/settings
└── (auth)/ → Route group — no URL segment
├── login/
│ └── page.tsx → /login
└── signup/
└── page.tsx → /signup
Layouts
// app/dashboard/layout.tsx
// Wraps /dashboard and all children — persists across navigation
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className='flex'>
<Sidebar />
<main className='flex-1 p-8'>{children}</main>
</div>
);
}
Dynamic Routes
// app/blog/[slug]/page.tsx
interface Props {
params: { slug: string };
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug);
if (!post) notFound(); // Renders app/not-found.tsx
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// Static generation at build time
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(p => ({ slug: p.slug }));
}
Loading States
// app/dashboard/loading.tsx
// Automatically shown while page.tsx is loading
export default function Loading() {
return (
<div className='space-y-4'>
<Skeleton className='h-8 w-48' />
<Skeleton className='h-64 w-full' />
</div>
);
}
Error Boundaries
// app/dashboard/error.tsx
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
Parallel Routes
app/dashboard/
├── layout.tsx
├── @analytics/
│ └── page.tsx → Rendered in analytics slot
└── @revenue/
└── page.tsx → Rendered in revenue slot
// layout.tsx receives slots as props
export default function Layout({ analytics, revenue }) {
return (
<div className='grid grid-cols-2'>
{analytics}
{revenue}
</div>
);
}
Metadata
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: Props) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: { title: post.title, images: [post.coverImage] },
};
}
Full App Router setup ships in the AI SaaS Starter Kit — auth layout, dashboard layout, error boundaries, loading states. $99 at whoffagents.com.
Top comments (0)