Back

Next.js

frameworkreactfullstack

File Conventions

FilePurpose
page.tsxRoute UI (required for route)
layout.tsxShared layout, wraps children
loading.tsxLoading state (Suspense boundary)
error.tsxError boundary (must be client)
not-found.tsx404 page
route.tsAPI route handler
// error.tsx - must be client component
'use client';
export default function Error({ error, reset }) {
  return <button onClick={reset}>Try again</button>;
}

Routing

PatternSyntaxExample
Dynamic[id]/blog/[id]/blog/123
Catch-all[...slug]/docs/[...slug]/docs/a/b/c
Optional catch-all[[...slug]]Matches /docs and /docs/a
Route groups(name)(auth)/login/login (no URL segment)
Parallel routes@slot@modal + @sidebar in layout
Intercepting(.), (..)(.)photo/[id] intercepts /photo/1
// app/blog/[id]/page.tsx
export default function Page({ params }: { params: { id: string } }) {
  return <h1>Post {params.id}</h1>;
}

// route groups: app/(marketing)/about/page.tsx → /about
// intercepting: app/photo/(.)photo/[id]/page.tsx → modal overlay

Server vs Client Components

TypeDefaultUse
ServerYesData fetch, layout, static content
Client'use client'useState, useEffect, event handlers
// Server (default) - can async, fetch directly
async function ServerPage() {
  const data = await fetch('...');
  return <div>{data}</div>;
}

// Client - add at top of file
'use client';
function ClientButton() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

Data Fetching

OptionBehavior
DefaultCached (static at build)
revalidate: 60ISR, revalidate every 60s
cache: 'no-store'Dynamic, no cache
next: { tags: ['x'] }Tag for on-demand revalidation
// Static (cached)
const res = await fetch('https://api.example.com/data');

// ISR - revalidate every 60 seconds
const res = await fetch('https://...', { next: { revalidate: 60 } });

// Dynamic - always fresh
const res = await fetch('https://...', { cache: 'no-store' });

// On-demand revalidation
import { revalidateTag, revalidatePath } from 'next/cache';
revalidateTag('posts');
revalidatePath('/blog');

Server Actions

// app/actions.ts
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  // mutate DB, revalidate
  revalidatePath('/blog');
  return { success: true };
}

// In component
import { createPost } from './actions';

<form action={createPost}>
  <input name="title" />
  <button type="submit">Submit</button>
</form>

Middleware

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

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/admin')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

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

Metadata API

// Static - layout.tsx or page.tsx
export const metadata = {
  title: 'My App',
  description: 'App description',
  openGraph: { title: '...', images: ['/og.png'] },
};

// Dynamic - page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.id);
  return { title: post.title, description: post.excerpt };
}

Image Optimization

import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Hero"
  width={800}
  height={600}
  priority
  placeholder="blur"
  blurDataURL="data:image/..."
/>

// Remote - add domain to next.config.js
<Image src="https://example.com/img.png" width={500} height={300} />

Link Component

import Link from 'next/link';

<Link href="/blog">Blog</Link>
<Link href={`/blog/${id}`}>Post</Link>
<Link href="/dashboard" prefetch={false}>Dashboard</Link>

Environment Variables

PrefixExposed to
(none)Server only
NEXT_PUBLIC_Server + Client
# .env.local
DATABASE_URL=postgres://...
NEXT_PUBLIC_API_URL=https://api.example.com
// Server
const db = process.env.DATABASE_URL;

// Client (must have NEXT_PUBLIC_)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;