| name | nextjs-app-router |
| user-invocable | false |
| description | Use when next.js App Router with layouts, loading states, and streaming. Use when building modern Next.js 13+ applications. |
| allowed-tools | ["Bash","Read"] |
Next.js App Router
Master the Next.js App Router for building modern, performant web
applications with server components and advanced routing.
App Directory Structure
The app directory uses file-system based routing with special files:
app/
layout.tsx # Root layout (required)
page.tsx # Home page
loading.tsx # Loading UI
error.tsx # Error UI
not-found.tsx # 404 UI
template.tsx # Re-rendered layout
about/
page.tsx # /about
blog/
layout.tsx # Blog-specific layout
page.tsx # /blog
loading.tsx # Blog loading state
[slug]/
page.tsx # /blog/[slug]
dashboard/
(auth)/ # Route group (doesn't affect URL)
layout.tsx # Layout for auth routes
settings/
page.tsx # /dashboard/settings
profile/
page.tsx # /dashboard/profile
// app/layout.tsx - Root layout (required)
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<header>
<Navigation />
</header>
<main>{children}</main>
<footer>
<Footer />
</footer>
</body>
</html>
);
}
Layouts: Root, Nested, and Templates
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'My App',
description: 'Built with Next.js App Router'
};
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>
<Navigation />
{children}
</Providers>
</body>
</html>
);
}
export default function DashboardLayout({
children
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard">
{children}
);
}
() {
;
}
Dynamic Routes and generateStaticParams
interface PageProps {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default async function BlogPost({ params }: PageProps) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug
}));
}
export async () {
post = (params.);
{
: post.,
: post.,
: {
: post.,
: post.,
: [{ : post. }]
}
};
}
() {
product = (params., params.);
;
}
() {
products = ();
products.( ({
: product.,
: product.
}));
}
() {
path = params..();
;
}
Loading UI and Streaming
export default function Loading() {
return (
<div className="loading">
<Skeleton />
<Skeleton />
<Skeleton />
</div>
);
}
import { Suspense } from 'react';
export default function BlogPage() {
return (
<div>
<h1>Blog</h1>
{/* Stream this component independently */}
<Suspense fallback={<PostsSkeleton />}>
<BlogPosts />
</Suspense>
{/* Stream this separately */}
<Suspense fallback={<CommentsSkeleton />}>
<RecentComments />
</Suspense>
</>
);
}
() {
posts = ();
(
);
}
() {
comments = ();
(
);
}
Error Boundaries and Error Handling
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error('Blog error:', error);
}, [error]);
return (
<div className="error-container">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
'use client';
export default function GlobalError({
error,
reset
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
(
);
}
;
() {
(
);
}
{ notFound } ;
() {
post = (params.);
(!post) {
();
}
;
}
Route Groups for Organization
app/
(marketing)/ # Group routes without affecting URLs
layout.tsx # Marketing layout
page.tsx # / (homepage)
about/
page.tsx # /about
contact/
page.tsx # /contact
(shop)/
layout.tsx # Shop layout
products/
page.tsx # /products
cart/
page.tsx # /cart
(auth)/
layout.tsx # Auth layout
login/
page.tsx # /login
register/
page.tsx # /register
export default function MarketingLayout({
children
}: {
children: React.ReactNode
}) {
return (
<>
<MarketingHeader />
{children}
<MarketingFooter />
</>
);
}
export default function ShopLayout({
children
}: {
children: React.ReactNode
}) {
return (
<>
<ShopHeader />
<ShopNav />
{children}
</>
);
}
Parallel Routes
app/
dashboard/
@analytics/
page.tsx
@team/
page.tsx
@user/
page.tsx
layout.tsx
page.tsx
export default function DashboardLayout({
children,
analytics,
team,
user
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
user: React.ReactNode;
}) {
return (
<div className="dashboard-grid">
<div className="main">{children}</div>
<div className="analytics">{analytics}</div>
<div className="team">{team}</div>
<div className="user">{user}</div>
</div>
);
}
export default function Layout({ user, admin }: {
user: React.ReactNode;
admin: React.ReactNode;
}) {
session = ();
session. ? admin : user;
}
Intercepting Routes
app/
feed/
page.tsx
@modal/
(.)photo/
[id]/
page.tsx
photo/
[id]/
page.tsx
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<Modal>
<Photo id={params.id} />
</Modal>
);
}
export default function PhotoPage({ params }: { params: { id: string } }) {
return (
<div className="photo-page">
<Photo id={params.id} />
</div>
);
}
Metadata API for SEO
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: {
default: 'My App',
template: '%s | My App'
},
description: 'My awesome Next.js app',
keywords: ['nextjs', 'react', 'typescript'],
authors: [{ name: 'John Doe' }],
openGraph: {
title: 'My App',
description: 'My awesome Next.js app',
url: 'https://myapp.com',
siteName: 'My App',
images: [
{
url: 'https://myapp.com/og.png',
width: 1200,
height: 630
}
],
locale: 'en_US',
type: 'website'
},
twitter: {
card: 'summary_large_image',
title: 'My App',
description: 'My awesome Next.js app',
: []
},
: {
: ,
: ,
: {
: ,
: ,
: -,
: ,
: -
}
}
};
(): <> {
post = (params.);
{
: post.,
: post.,
: [{ : post. }],
: {
: post.,
: post.,
: [post.],
: post.,
: [post.]
}
};
}
() {
post = (params.);
jsonLd = {
: ,
: ,
: post.,
: post.,
: {
: ,
: post.
}
};
(
);
}
Route Handlers (API Routes)
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('query');
const users = await getUsers(query);
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await createUser(body);
return NextResponse.json(user, { status: 201 });
}
export async function GET(
: ,
{ params }: { params: { id: } }
) {
user = (params.);
(!user) {
.({ : }, { : });
}
.(user);
}
() {
(params.);
(, { : });
}
() {
session = (request);
(!session) {
.({ : }, { : });
}
data = (session.);
.(data);
}
Middleware
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
const response = NextResponse.next();
response.headers.set('x-custom-header', 'value');
return response;
}
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*',
'/((?!_next/static|_next/image|favicon.ico).*)'
]
};
() {
bucket = .() < ? : ;
request..(, bucket);
(bucket === ) {
.( (, request.));
}
.();
}
When to Use This Skill
Use nextjs-app-router when you need to:
- Build modern Next.js 13+ applications
- Implement complex routing with layouts
- Use server and client components effectively
- Create loading and error boundaries
- Optimize performance with streaming
- Build SEO-friendly applications
- Implement dynamic and static routes
- Use parallel and intercepting routes
- Build scalable Next.js applications
- Implement advanced routing patterns
- Create type-safe API routes
- Optimize metadata for social sharing
Best Practices
-
Use server components by default - Only mark components with 'use client'
when they need interactivity or browser APIs.
-
Implement proper loading states - Use loading.tsx files and Suspense
boundaries for better UX during data fetching.
-
Create granular error boundaries - Place error.tsx files at appropriate
levels to handle errors gracefully.
-
Leverage static generation - Use generateStaticParams for dynamic routes
that can be pre-rendered at build time.
-
Organize with route groups - Use route groups to organize code without
affecting URL structure.
-
Optimize metadata - Implement both static and dynamic metadata for better
SEO and social sharing.
-
Stream content strategically - Use Suspense to stream independent UI
sections as they load.
-
Keep client-side JavaScript minimal - Maximize server components to reduce
bundle size and improve performance.
-
Use middleware wisely - Apply middleware for authentication, redirects,
and request modifications.
-
Test routing behavior - Verify navigation, loading states, and error
handling across different routes.
Common Pitfalls
-
Using client components unnecessarily - Marking components with 'use client'
when they don't need browser APIs increases bundle size.
-
Not implementing loading states - Missing loading.tsx files lead to poor UX
during navigation and data fetching.
-
Forgetting error boundaries - Without error.tsx files, errors crash the
entire application instead of failing gracefully.
-
Mixing server and client code incorrectly - Importing server-only code in
client components or vice versa causes errors.
-
Not optimizing for static generation - Missing generateStaticParams means
pages render on-demand instead of at build time.
-
Overusing dynamic routes - Too many dynamic segments can make routing
complex and hard to maintain.
-
Not handling route parameters properly - Failing to validate or sanitize
route parameters can cause errors or security issues.
-
Ignoring SEO considerations - Missing or incomplete metadata hurts search
engine rankings and social sharing.
-
Not testing edge cases - Skipping tests for 404s, errors, and loading
states leads to poor user experience.
-
Misunderstanding file conventions - Naming files incorrectly (e.g., using
Loading.tsx instead of loading.tsx) breaks conventions.
Resources