Back to Notes Library
Next.js App Router Notes
App Router Basics
The App Router lives in the /app directory. Key file conventions:
| File | Purpose |
|---|---|
page.tsx | UI for a route segment |
layout.tsx | Shared UI for segment + children |
loading.tsx | Streaming loading UI |
error.tsx | Error boundary UI |
not-found.tsx | 404 page |
route.ts | API endpoint handler |
template.tsx | Like 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 → /registerServer 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+)
paramsin page/layout/route is now a Promise — alwaysawait paramscookies()andheaders()are now async —await cookies()- Default caching behavior changed:
fetchis NOT cached by default (opt-in withrevalidate) - Server Components render on server by default — never expose secrets in Client Components
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.