nextjs-app-router

Master the Next.js App Router for building modern, performant web applications with server components and advanced routing.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "nextjs-app-router" with this command: npx skills add thebushidocollective/han/thebushidocollective-han-nextjs-app-router

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

// app/layout.tsx - Root Layout (wraps entire app) 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> ); }

// app/dashboard/layout.tsx - Nested Layout export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( <div className="dashboard"> <aside> <DashboardNav /> </aside> <section>{children}</section> </div> ); }

// app/dashboard/template.tsx - Template (re-renders on navigation) // Use when you need fresh state on each navigation export default function DashboardTemplate({ children }: { children: React.ReactNode }) { // This re-renders and resets state on navigation return <div className="animate-fade-in">{children}</div>; }

Dynamic Routes and generateStaticParams

// app/blog/[slug]/page.tsx 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> ); }

// Generate static paths at build time (SSG) export async function generateStaticParams() { const posts = await getPosts();

return posts.map((post) => ({ slug: post.slug })); }

// Generate metadata dynamically export async function generateMetadata({ params }: PageProps) { const post = await getPost(params.slug);

return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [{ url: post.image }] } }; }

// Multiple dynamic segments: app/shop/[category]/[product]/page.tsx export default async function Product({ params }: { params: { category: string; product: string } }) { const product = await getProduct(params.category, params.product); return <div>{product.name}</div>; }

export async function generateStaticParams() { const products = await getProducts();

return products.map((product) => ({ category: product.category, product: product.slug })); }

// Catch-all routes: app/docs/[...slug]/page.tsx export default function Docs({ params }: { params: { slug: string[] } }) { // /docs/a/b/c -> params.slug = ['a', 'b', 'c'] const path = params.slug.join('/'); return <div>Documentation: {path}</div>; }

// Optional catch-all: app/blog/[[...slug]]/page.tsx // Matches both /blog and /blog/a/b/c

Loading UI and Streaming

// app/blog/loading.tsx - Automatic loading UI export default function Loading() { return ( <div className="loading"> <Skeleton /> <Skeleton /> <Skeleton /> </div> ); }

// app/blog/page.tsx - Server component with streaming import { Suspense } from 'react';

export default function BlogPage() { return ( <div> <h1>Blog</h1>

  {/* Stream this component independently */}
  &#x3C;Suspense fallback={&#x3C;PostsSkeleton />}>
    &#x3C;BlogPosts />
  &#x3C;/Suspense>

  {/* Stream this separately */}
  &#x3C;Suspense fallback={&#x3C;CommentsSkeleton />}>
    &#x3C;RecentComments />
  &#x3C;/Suspense>
&#x3C;/div>

); }

// Components can stream as they load async function BlogPosts() { const posts = await getPosts(); // Server-side data fetch

return ( <div> {posts.map(post => ( <PostCard key={post.id} post={post} /> ))} </div> ); }

async function RecentComments() { const comments = await getComments();

return ( <div> {comments.map(comment => ( <CommentCard key={comment.id} comment={comment} /> ))} </div> ); }

Error Boundaries and Error Handling

// app/blog/error.tsx - Error boundary 'use client'; // Error components must be Client Components

import { useEffect } from 'react';

export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void; }) { useEffect(() => { // Log error to error reporting service 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> ); }

// app/global-error.tsx - Global error boundary 'use client';

export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void; }) { return ( <html> <body> <h2>Application Error</h2> <button onClick={() => reset()}>Try again</button> </body> </html> ); }

// app/blog/not-found.tsx - Custom 404 page import Link from 'next/link';

export default function NotFound() { return ( <div> <h2>Post Not Found</h2> <p>Could not find the requested blog post.</p> <Link href="/blog">View all posts</Link> </div> ); }

// Programmatically trigger 404 import { notFound } from 'next/navigation';

export default async function Post({ params }: { params: { id: string } }) { const post = await getPost(params.id);

if (!post) { notFound(); // Renders closest not-found.tsx }

return <article>{post.content}</article>; }

Route Groups for Organization

// Route groups don't affect URL structure 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

// app/(marketing)/layout.tsx export default function MarketingLayout({ children }: { children: React.ReactNode }) { return ( <> <MarketingHeader /> {children} <MarketingFooter /> </> ); }

// app/(shop)/layout.tsx export default function ShopLayout({ children }: { children: React.ReactNode }) { return ( <> <ShopHeader /> <ShopNav /> {children} </> ); }

Parallel Routes

// Use parallel routes to render multiple pages in the same layout app/ dashboard/ @analytics/ page.tsx @team/ page.tsx @user/ page.tsx layout.tsx page.tsx

// app/dashboard/layout.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> ); }

// Conditional rendering with parallel routes export default function Layout({ user, admin }: { user: React.ReactNode; admin: React.ReactNode; }) { const session = await getSession();

return session.isAdmin ? admin : user; }

Intercepting Routes

// Intercept routes to show modals or overlays app/ feed/ page.tsx @modal/ (.)photo/ [id]/ page.tsx photo/ [id]/ page.tsx

// app/feed/@modal/(.)photo/[id]/page.tsx // Intercepts /photo/[id] when navigating from /feed export default function PhotoModal({ params }: { params: { id: string } }) { return ( <Modal> <Photo id={params.id} /> </Modal> ); }

// app/photo/[id]/page.tsx // Direct navigation to /photo/[id] shows full page export default function PhotoPage({ params }: { params: { id: string } }) { return ( <div className="photo-page"> <Photo id={params.id} /> </div> ); }

// Intercepting patterns: // (.) matches same level // (..) matches one level up // (..)(..) matches two levels up // (...) matches from root

Metadata API for SEO

// app/layout.tsx - Static metadata import type { Metadata } from 'next';

export const metadata: Metadata = { title: { default: 'My App', template: '%s | My App' // Used by child pages }, 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', images: ['https://myapp.com/twitter.png'] }, robots: { index: true, follow: true, googleBot: { index: true, follow: true, 'max-video-preview': -1, 'max-image-preview': 'large', 'max-snippet': -1 } } };

// app/blog/[slug]/page.tsx - Dynamic metadata export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> { const post = await getPost(params.slug);

return { title: post.title, description: post.excerpt, authors: [{ name: post.author }], openGraph: { title: post.title, description: post.excerpt, images: [post.image], publishedTime: post.publishedAt, authors: [post.author] } }; }

// JSON-LD structured data export default function Article({ params }: { params: { slug: string } }) { const post = getPost(params.slug);

const jsonLd = { '@context': 'https://schema.org', '@type': 'Article', headline: post.title, datePublished: post.publishedAt, author: { '@type': 'Person', name: post.author } };

return ( <> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> <article>{post.content}</article> </> ); }

Route Handlers (API Routes)

// app/api/users/route.ts import { NextRequest, NextResponse } from 'next/server';

// GET /api/users 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); }

// POST /api/users export async function POST(request: NextRequest) { const body = await request.json();

const user = await createUser(body);

return NextResponse.json(user, { status: 201 }); }

// app/api/users/[id]/route.ts // GET /api/users/:id export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { const user = await getUser(params.id);

if (!user) { return NextResponse.json({ error: 'User not found' }, { status: 404 }); }

return NextResponse.json(user); }

// DELETE /api/users/:id export async function DELETE( request: NextRequest, { params }: { params: { id: string } } ) { await deleteUser(params.id);

return new NextResponse(null, { status: 204 }); }

// With middleware export async function GET(request: NextRequest) { const session = await getSession(request);

if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); }

const data = await getData(session.userId);

return NextResponse.json(data); }

Middleware

// middleware.ts (at root level) import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) { // Check authentication const token = request.cookies.get('token');

if (!token && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)); }

// Add custom header const response = NextResponse.next(); response.headers.set('x-custom-header', 'value');

return response; }

// Configure which routes use middleware export const config = { matcher: [ '/dashboard/:path*', '/api/:path*', '/((?!_next/static|_next/image|favicon.ico).*)' ] };

// Advanced middleware with rewrites export function middleware(request: NextRequest) { // A/B testing const bucket = Math.random() < 0.5 ? 'a' : 'b'; request.cookies.set('bucket', bucket);

if (bucket === 'a') { return NextResponse.rewrite(new URL('/experiment-a', request.url)); }

return NextResponse.next(); }

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

  • Next.js App Router Documentation

  • Next.js Routing Fundamentals

  • Server and Client Components

  • Data Fetching Patterns

  • Metadata API Reference

  • Next.js Examples

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

android-jetpack-compose

No summary provided by upstream source.

Repository SourceNeeds Review
General

fastapi-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

storybook-story-writing

No summary provided by upstream source.

Repository SourceNeeds Review
General

atomic-design-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review