DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Next.js App Router: File-Based Routing, Layouts, and Loading States

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 }));
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Parallel Routes

app/dashboard/
├── layout.tsx
├── @analytics/
│   └── page.tsx   → Rendered in analytics slot
└── @revenue/
    └── page.tsx   → Rendered in revenue slot
Enter fullscreen mode Exit fullscreen mode
// layout.tsx receives slots as props
export default function Layout({ analytics, revenue }) {
  return (
    <div className='grid grid-cols-2'>
      {analytics}
      {revenue}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

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] },
  };
}
Enter fullscreen mode Exit fullscreen mode

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)