Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
Web Dev

Next.js App Router Notes

App Router, layouts, loading states, server components, generateMetadata — all documented.

Back to Notes Library

Next.js App Router Notes

App Router Basics

The App Router lives in the /app directory. Key file conventions:

FilePurpose
page.tsxUI for a route segment
layout.tsxShared UI for segment + children
loading.tsxStreaming loading UI
error.tsxError boundary UI
not-found.tsx404 page
route.tsAPI endpoint handler
template.tsxLike layout but re-renders on navigate

File-Based Routing

text
app/
  page.tsx                → /
  about/page.tsx          → /about
  blog/
    page.tsx              → /blog
    [slug]/page.tsx       → /blog/:slug
  shop/
    [...all]/page.tsx     → /shop/a, /shop/a/b (catch-all)
    [[...all]]/page.tsx   → /shop, /shop/a, /shop/a/b (optional)
  (auth)/                 → Route group (not in URL)
    login/page.tsx        → /login
    register/page.tsx     → /register

Server vs Client Components

tsx
// Server Component (default — no 'use client')
// Can access databases, files, environment variables
// Zero bundle size impact
// Cannot use useState, useEffect, browser APIs

async function ServerPage() {
  const data = await fetch("https://api.example.com/data");
  const json = await data.json();
  return <div>{json.title}</div>;
}

// Client Component — add 'use client' at top
'use client';
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Rule: Server components are default. Add 'use client' only when you need interactivity or browser APIs.


Data Fetching

tsx
// In Server Components — fetch with auto deduplication
async function Page() {
  // Cached by default
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
  return <PostList posts={posts} />;
}

// No caching (always fresh)
const res = await fetch(url, { cache: 'no-store' });

// ISR — revalidate every 60 seconds
const res = await fetch(url, { next: { revalidate: 60 } });

// Tag-based revalidation
const res = await fetch(url, { next: { tags: ['posts'] } });

// Direct database query (no fetch needed)
import { db } from '@/lib/db';
const posts = await db.posts.findMany();

Route Handlers (API Routes)

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

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const id = searchParams.get('id');
  const users = await db.users.findMany();
  return NextResponse.json({ users });
}

export async function POST(req: NextRequest) {
  const body = await req.json();
  const user = await db.users.create({ data: body });
  return NextResponse.json({ user }, { status: 201 });
}

// app/api/users/[id]/route.ts
export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const user = await db.users.findUnique({ where: { id } });
  if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 });
  return NextResponse.json({ user });
}

generateMetadata

tsx
import type { Metadata } from 'next';

// Static metadata
export const metadata: Metadata = {
  title: 'My Site',
  description: 'Welcome to my site',
};

// Dynamic metadata
export async function generateMetadata(
  { params }: { params: Promise<{ slug: string }> }
): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);

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

generateStaticParams

tsx
// Pre-generate static pages at build time
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);
  return <article>{post.content}</article>;
}

Loading & Error UI

tsx
// app/blog/loading.tsx — shown during page load
export default function Loading() {
  return (
    <div className="animate-pulse space-y-4">
      {[1,2,3].map(i => (
        <div key={i} className="h-24 bg-gray-200 rounded-lg" />
      ))}
    </div>
  );
}

// app/blog/error.tsx — error boundary
'use client';
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Server Actions

tsx
// actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const post = await db.posts.create({ data: { title } });
  revalidatePath('/blog');
  redirect(`/blog/${post.slug}`);
}

// Use in form
<form action={createPost}>
  <input name="title" type="text" />
  <button type="submit">Create</button>
</form>

Middleware

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

export function middleware(request: NextRequest) {
  const isAuthenticated = request.cookies.has('session');

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

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

Important Notes (Next.js 15+)

  • params in page/layout/route is now a Promise — always await params
  • cookies() and headers() are now async — await cookies()
  • Default caching behavior changed: fetch is NOT cached by default (opt-in with revalidate)
  • Server Components render on server by default — never expose secrets in Client Components
📱

Get more notes like this daily on Telegram!

Free study notes, cheat sheets & AI tips

Join Free →
10K+ Members Growing Daily

Get Free AI Notes Daily

Join AiTechWorlds on Telegram and get daily AI tips, prompt engineering templates, coding resources, and exclusive content — 100% free!

📚 Free Study Notes🤖 AI Tips Daily⚡ Prompt Templates💻 Coding Resources
Join Free Channel

No spam. Leave anytime.

!