Next.js Essentials
Quick reference guide for Next.js - the React framework for production.
What is Next.js?
Next.js is a React framework that enables:
Installation
Create New Project
# ========== Using create-next-app ==========
npx create-next-app@latest my-app
# With TypeScript
npx create-next-app@latest my-app --typescript
# With specific options
npx create-next-app@latest my-app \
--typescript \
--tailwind \
--app \
--src-dir
# ========== Start Development Server ==========
cd my-app
npm run dev
# Open http://localhost:3000
Manual Installation
# ========== Install Dependencies ==========
npm install next@latest react@latest react-dom@latest
# ========== Add Scripts to package.json ==========
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}
# ========== Create pages Directory ==========
mkdir pages
Project Structure
App Router (Next.js 13+)
my-app/
├── app/
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page (/)
│ ├── loading.tsx # Loading UI
│ ├── error.tsx # Error UI
│ ├── not-found.tsx # 404 page
│ ├── about/
│ │ └── page.tsx # About page (/about)
│ ├── blog/
│ │ ├── page.tsx # Blog list (/blog)
│ │ ├── [slug]/
│ │ │ └── page.tsx # Blog post (/blog/[slug])
│ └── api/
│ └── hello/
│ └── route.ts # API route
├── components/
│ └── Button.tsx
├── public/
│ └── images/
├── styles/
│ └── globals.css
├── next.config.js
├── package.json
└── tsconfig.json
Pages Router (Classic)
my-app/
├── pages/
│ ├── _app.tsx # Custom App
│ ├── _document.tsx # Custom Document
│ ├── index.tsx # Home page (/)
│ ├── about.tsx # About page (/about)
│ ├── blog/
│ │ ├── index.tsx # Blog list (/blog)
│ │ └── [slug].tsx # Blog post (/blog/[slug])
│ └── api/
│ └── hello.ts # API route
├── components/
├── public/
├── styles/
├── next.config.js
└── package.json
Routing (App Router)
Pages
// ========== app/page.tsx (Home Page) ==========
export default function HomePage() {
return (
<main>
<h1>Welcome to Next.js</h1>
<p>This is the home page</p>
</main>
);
}
// ========== app/about/page.tsx ==========
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our company</p>
</div>
);
}
// ========== app/blog/[slug]/page.tsx (Dynamic Route) ==========
export default function BlogPost({ params }: { params: { slug: string } }) {
return (
<article>
<h1>Blog Post: {params.slug}</h1>
<p>Content for {params.slug}</p>
</article>
);
}
// ========== app/shop/[...slug]/page.tsx (Catch-all Route) ==========
// Matches /shop/a, /shop/a/b, /shop/a/b/c, etc.
export default function ShopPage({ params }: { params: { slug: string[] } }) {
return (
<div>
<h1>Shop</h1>
<p>Path: {params.slug.join('/')}</p>
</div>
);
}
Layouts
// ========== app/layout.tsx (Root Layout) ==========
import './globals.css';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'My App',
description: 'Built with Next.js',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<header>
<nav>Navigation</nav>
</header>
{children}
<footer>Footer</footer>
</body>
</html>
);
}
// ========== app/dashboard/layout.tsx (Nested Layout) ==========
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<aside>
<nav>Dashboard Navigation</nav>
</aside>
<main>{children}</main>
</div>
);
}
Navigation
// ========== Using Link ==========
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
{/* With dynamic route */}
<Link href={`/blog/${slug}`}>Read Post</Link>
{/* With query parameters */}
<Link href="/search?q=nextjs">Search</Link>
{/* With object */}
<Link href={{ pathname: '/blog', query: { page: 1 } }}>
Blog Page 1
</Link>
</nav>
);
}
// ========== Programmatic Navigation ==========
'use client';
import { useRouter } from 'next/navigation';
export default function MyComponent() {
const router = useRouter();
const handleClick = () => {
router.push('/dashboard');
// router.replace('/dashboard'); // Without adding to history
// router.back(); // Go back
// router.forward(); // Go forward
// router.refresh(); // Refresh current route
};
return <button onClick={handleClick}>Go to Dashboard</button>;
}
Data Fetching (App Router)
Server Components (Default)
// ========== Static Data Fetching ==========
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // Default: cache forever
});
if (!res.ok) {
throw new Error('Failed to fetch posts');
}
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
// ========== Dynamic Data Fetching ==========
async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
cache: 'no-store' // Always fetch fresh data
});
return res.json();
}
// ========== Revalidation ==========
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Revalidate every hour
});
return res.json();
}
// ========== Multiple Data Sources ==========
export default async function Page() {
// Fetch in parallel
const [posts, users] = await Promise.all([
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/users').then(r => r.json())
]);
return (
<div>
<Posts data={posts} />
<Users data={users} />
</div>
);
}
Client Components
// ========== Use Client Directive ==========
'use client';
import { useState, useEffect } from 'react';
export default function ClientComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return <div>{JSON.stringify(data)}</div>;
}
// ========== With SWR ==========
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
export default function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
return <div>Hello {data.name}!</div>;
}
Static Generation
// ========== Generate Static Params ==========
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function Post({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// ========== Generate Metadata ==========
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(r => r.json());
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
},
};
}
API Routes
App Router API Routes
// ========== app/api/hello/route.ts ==========
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
return NextResponse.json({ message: 'Hello World' });
}
export async function POST(request: Request) {
const body = await request.json();
return NextResponse.json({
message: 'Data received',
data: body
});
}
// ========== With Dynamic Route ==========
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const id = params.id;
const post = await db.posts.findById(id);
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
return NextResponse.json(post);
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const updated = await db.posts.update(params.id, body);
return NextResponse.json(updated);
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.posts.delete(params.id);
return NextResponse.json({ success: true });
}
// ========== With Query Parameters ==========
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');
const page = searchParams.get('page') || '1';
const results = await search(query, parseInt(page));
return NextResponse.json(results);
}
// ========== With Headers ==========
export async function GET(request: Request) {
const token = request.headers.get('authorization');
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const data = await fetchProtectedData(token);
return NextResponse.json(data, {
headers: {
'Cache-Control': 'no-store',
'X-Custom-Header': 'value'
}
});
}
// ========== With Cookies ==========
import { cookies } from 'next/headers';
export async function GET() {
const cookieStore = cookies();
const token = cookieStore.get('token');
return NextResponse.json({ token: token?.value });
}
export async function POST(request: Request) {
const response = NextResponse.json({ success: true });
response.cookies.set('token', 'abc123', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 3600
});
return response;
}
Middleware
// ========== middleware.ts ==========
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) {
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 Matcher ==========
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*',
]
};
// ========== Rewrite ==========
export function middleware(request: NextRequest) {
// Rewrite /old-path to /new-path
if (request.nextUrl.pathname === '/old-path') {
return NextResponse.rewrite(new URL('/new-path', request.url));
}
}
// ========== Logging ==========
export function middleware(request: NextRequest) {
console.log('Request:', request.method, request.url);
return NextResponse.next();
}
Image Optimization
// ========== Next.js Image Component ==========
import Image from 'next/image';
export default function MyComponent() {
return (
<div>
{/* Static import */}
<Image
src="/images/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Load immediately
/>
{/* External image */}
<Image
src="https://example.com/image.jpg"
alt="External image"
width={800}
height={400}
loading="lazy"
/>
{/* Fill container */}
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/images/background.jpg"
alt="Background"
fill
style={{ objectFit: 'cover' }}
/>
</div>
{/* Responsive */}
<Image
src="/images/responsive.jpg"
alt="Responsive image"
width={1200}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
);
}
// ========== Configure External Images ==========
// next.config.js
module.exports = {
images: {
domains: ['example.com', 'cdn.example.com'],
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com',
},
],
},
};
Loading and Error States
// ========== app/loading.tsx ==========
export default function Loading() {
return (
<div className="spinner">
<p>Loading...</p>
</div>
);
}
// ========== app/error.tsx ==========
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// ========== app/not-found.tsx ==========
export default function NotFound() {
return (
<div>
<h2>404 - Page Not Found</h2>
<p>The page you're looking for doesn't exist.</p>
</div>
);
}
// ========== Custom 404 in Page ==========
import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: { id: string } }) {
const data = await fetchData(params.id);
if (!data) {
notFound();
}
return <div>{data.title}</div>;
}
Streaming and Suspense
// ========== app/page.tsx ==========
import { Suspense } from 'react';
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 3000));
return <div>Slow content loaded!</div>;
}
export default function Page() {
return (
<div>
<h1>Fast content</h1>
<Suspense fallback={<div>Loading slow content...</div>}>
<SlowComponent />
</Suspense>
<h2>More fast content</h2>
</div>
);
}
// ========== Multiple Suspense Boundaries ==========
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Skeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<Skeleton />}>
<RecentOrders />
</Suspense>
<Suspense fallback={<Skeleton />}>
<TopProducts />
</Suspense>
</div>
);
}
Server Actions
// ========== Server Action ==========
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title');
const content = formData.get('content');
const post = await db.posts.create({
title,
content,
});
revalidatePath('/posts');
return { success: true, id: post.id };
}
// ========== Use in Client Component ==========
'use client';
import { createPost } from './actions';
export default function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" type="text" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}
// ========== With useFormState ==========
'use client';
import { useFormState } from 'react-dom';
import { createPost } from './actions';
export default function Form() {
const [state, formAction] = useFormState(createPost, null);
return (
<form action={formAction}>
<input name="title" />
<button type="submit">Submit</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
Environment Variables
# ========== .env.local ==========
# Public variables (exposed to browser)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_GA_ID=UA-123456789-1
# Private variables (server-side only)
DATABASE_URL=postgresql://localhost/mydb
API_SECRET_KEY=secret123
// ========== Usage ==========
// Client-side
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// Server-side only
const dbUrl = process.env.DATABASE_URL;
// ========== Type-safe Environment Variables ==========
// env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_API_URL: string;
DATABASE_URL: string;
API_SECRET_KEY: string;
}
}
Configuration
// ========== next.config.js ==========
/** @type {import('next').NextConfig} */
const nextConfig = {
// React strict mode
reactStrictMode: true,
// Image optimization
images: {
domains: ['example.com'],
formats: ['image/avif', 'image/webp'],
},
// Redirects
async redirects() {
return [
{
source: '/old-path',
destination: '/new-path',
permanent: true,
},
];
},
// Rewrites
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
];
},
// Headers
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
],
},
];
},
// Environment variables
env: {
CUSTOM_KEY: 'value',
},
// Webpack configuration
webpack: (config, { isServer }) => {
// Custom webpack config
return config;
},
};
module.exports = nextConfig;
Metadata and SEO
// ========== Static Metadata ==========
// app/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My Page',
description: 'Page description',
keywords: ['nextjs', 'react', 'ssr'],
authors: [{ name: 'John Doe' }],
openGraph: {
title: 'My Page',
description: 'Page description',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: 'My Page',
description: 'Page description',
images: ['/twitter-image.jpg'],
},
};
// ========== Dynamic Metadata ==========
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await fetchPost(params.id);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
// ========== Template Metadata ==========
// app/layout.tsx
export const metadata: Metadata = {
title: {
template: '%s | My Site',
default: 'My Site',
},
};
// app/about/page.tsx
export const metadata: Metadata = {
title: 'About', // Becomes "About | My Site"
};
Authentication Example
// ========== lib/auth.ts ==========
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { jwtVerify } from 'jose';
export async function getSession() {
const token = cookies().get('session')?.value;
if (!token) return null;
try {
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(process.env.JWT_SECRET)
);
return payload;
} catch {
return null;
}
}
export async function requireAuth() {
const session = await getSession();
if (!session) {
redirect('/login');
}
return session;
}
// ========== app/dashboard/page.tsx ==========
import { requireAuth } from '@/lib/auth';
export default async function DashboardPage() {
const session = await requireAuth();
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {session.email}</p>
</div>
);
}
// ========== app/api/auth/login/route.ts ==========
import { SignJWT } from 'jose';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { email, password } = await request.json();
// Verify credentials
const user = await verifyCredentials(email, password);
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Create JWT
const token = await new SignJWT({ userId: user.id, email: user.email })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('24h')
.sign(new TextEncoder().encode(process.env.JWT_SECRET));
const response = NextResponse.json({ success: true });
response.cookies.set('session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 86400, // 24 hours
});
return response;
}
Performance Optimization
Code Splitting
// ========== Dynamic Import ==========
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('@/components/Heavy'), {
loading: () => <p>Loading...</p>,
ssr: false, // Disable SSR for this component
});
export default function Page() {
return (
<div>
<DynamicComponent />
</div>
);
}
// ========== Lazy Load with Suspense ==========
import { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('@/components/Heavy'));
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
Font Optimization
// ========== Google Fonts ==========
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
weight: ['400', '700'],
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}
// ========== Local Fonts ==========
import localFont from 'next/font/local';
const myFont = localFont({
src: './my-font.woff2',
display: 'swap',
});
Deployment
Build and Export
# ========== Build for Production ==========
npm run build
# ========== Start Production Server ==========
npm start
# ========== Static Export ==========
# next.config.js
module.exports = {
output: 'export',
};
npm run build
# Output in 'out' directory
Vercel Deployment
# ========== Install Vercel CLI ==========
npm i -g vercel
# ========== Deploy ==========
vercel
# ========== Production Deployment ==========
vercel --prod
Docker Deployment
# ========== Dockerfile ==========
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
Best Practices
Common Patterns
Protected Routes
// ========== HOC for Auth ==========
import { redirect } from 'next/navigation';
import { getSession } from '@/lib/auth';
export default async function ProtectedPage() {
const session = await getSession();
if (!session) {
redirect('/login');
}
return <div>Protected content</div>;
}
Pagination
// ========== app/posts/page.tsx ==========
export default async function PostsPage({
searchParams,
}: {
searchParams: { page?: string };
}) {
const page = parseInt(searchParams.page || '1');
const limit = 10;
const posts = await fetchPosts(page, limit);
const total = await getPostCount();
const totalPages = Math.ceil(total / limit);
return (
<div>
<PostList posts={posts} />
<Pagination currentPage={page} totalPages={totalPages} />
</div>
);
}
Search
// ========== app/search/page.tsx ==========
export default async function SearchPage({
searchParams,
}: {
searchParams: { q?: string };
}) {
const query = searchParams.q || '';
const results = query ? await search(query) : [];
return (
<div>
<SearchForm defaultValue={query} />
<SearchResults results={results} />
</div>
);
}