Next.js Essentials
Next.js is a powerful React framework for building production-ready web applications with features like server-side rendering, static site generation, API routes, and more.
What is Next.js?
Next.js is a full-stack React framework developed by Vercel that provides an excellent developer experience with features like file-based routing, automatic code splitting, and built-in optimization.
Key Features:
Why Use Next.js?
Installation
Create a New Project
# Create with interactive setup
npx create-next-app@latest
# Or specify project name
npx create-next-app@latest my-app
# With TypeScript
npx create-next-app@latest my-app --typescript
# Navigate to project
cd my-app
# Start development server
npm run dev
Open http://localhost:3000 to see your app.
Project Structure
my-app/
├── app/ # App directory (Next.js 13+)
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ └── globals.css # Global styles
├── public/ # Static files
│ └── images/
├── node_modules/
├── package.json
├── next.config.js # Next.js configuration
└── tsconfig.json # TypeScript config
App Router (Next.js 13+)
Pages and Routing
Next.js uses file-based routing in the app directory.
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
└── dashboard/
├── layout.tsx → Dashboard layout
├── page.tsx → /dashboard
└── settings/
└── page.tsx → /dashboard/settings
Basic Page:
// app/page.tsx
export default function Home() {
return (
<main>
<h1>Welcome to Next.js!</h1>
<p>This is the home page.</p>
</main>
);
}
About Page:
// app/about/page.tsx
export default function About() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our company.</p>
</div>
);
}
Dynamic Routes
// app/blog/[slug]/page.tsx
interface PageProps {
params: {
slug: string;
};
}
export default function BlogPost({ params }: PageProps) {
return (
<article>
<h1>Blog Post: {params.slug}</h1>
<p>Content for {params.slug}</p>
</article>
);
}
// Generates static paths at build time
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
Catch-All Routes:
// app/docs/[...slug]/page.tsx
interface PageProps {
params: {
slug: string[];
};
}
export default function Docs({ params }: PageProps) {
// /docs/a → params.slug = ['a']
// /docs/a/b → params.slug = ['a', 'b']
// /docs/a/b/c → params.slug = ['a', 'b', 'c']
return (
<div>
<h1>Documentation</h1>
<p>Path: {params.slug.join('/')}</p>
</div>
);
}
Layouts
Layouts wrap multiple pages and persist across navigation.
// app/layout.tsx (Root Layout)
import './globals.css';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My Next.js App',
description: 'Built with Next.js',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
</nav>
</header>
<main>{children}</main>
<footer>
<p>© 2024 My App</p>
</footer>
</body>
</html>
);
}
Nested Layouts:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<aside className="sidebar">
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/settings">Settings</a>
<a href="/dashboard/profile">Profile</a>
</nav>
</aside>
<div className="content">{children}</div>
</div>
);
}
Navigation
Link Component
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
{/* Basic link */}
<Link href="/">Home</Link>
{/* With custom styling */}
<Link href="/about" className="nav-link">
About
</Link>
{/* Dynamic route */}
<Link href={`/blog/${post.slug}`}>
Read Post
</Link>
{/* With query parameters */}
<Link href="/search?q=nextjs">
Search
</Link>
{/* Open in new tab */}
<Link href="/external" target="_blank">
External Link
</Link>
</nav>
);
}
Programmatic Navigation
'use client';
import { useRouter } from 'next/navigation';
export default function LoginForm() {
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Login logic...
// Navigate programmatically
router.push('/dashboard');
// Other methods
// router.back();
// router.forward();
// router.refresh();
// router.replace('/dashboard');
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit">Login</button>
</form>
);
}
Data Fetching
Server Components (Default)
// app/blog/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
// Optional: revalidate every hour
next: { revalidate: 3600 }
});
if (!res.ok) {
throw new Error('Failed to fetch posts');
}
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<div>
<h1>Blog Posts</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
Parallel Data Fetching
async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
async function getPosts(userId: string) {
const res = await fetch(`/api/posts?userId=${userId}`);
return res.json();
}
export default async function UserProfile({ params }: { params: { id: string } }) {
// Fetch in parallel
const [user, posts] = await Promise.all([
getUser(params.id),
getPosts(params.id)
]);
return (
<div>
<h1>{user.name}</h1>
<h2>Posts by {user.name}</h2>
{posts.map((post) => (
<article key={post.id}>{post.title}</article>
))}
</div>
);
}
Client Components
Use 'use client' directive for interactive components.
'use client';
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Fetching Data in Client Components
'use client';
import { useState, useEffect } from 'react';
interface Post {
id: number;
title: string;
body: string;
}
export default function Posts() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/posts')
.then((res) => res.json())
.then((data) => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
))}
</div>
);
}
API Routes
Create backend endpoints in Next.js.
// app/api/hello/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
return NextResponse.json({ message: 'Hello from API!' });
}
export async function POST(request: NextRequest) {
const body = await request.json();
return NextResponse.json({
message: 'Data received',
data: body
});
}
Dynamic API Routes:
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await getUserById(params.id);
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return NextResponse.json(user);
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const updatedUser = await updateUser(params.id, body);
return NextResponse.json(updatedUser);
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await deleteUser(params.id);
return NextResponse.json({ success: true });
}
Complete CRUD API:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/posts
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = searchParams.get('page') || '1';
const limit = searchParams.get('limit') || '10';
const posts = await getPosts({
page: parseInt(page),
limit: parseInt(limit)
});
return NextResponse.json(posts);
}
// POST /api/posts
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Validate
if (!body.title || !body.content) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
const post = await createPost(body);
return NextResponse.json(post, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Forms and Mutations
Server Actions
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Save to database
await savePost({ title, content });
// Revalidate the page
revalidatePath('/blog');
}
Using Server Actions:
// app/blog/new/page.tsx
import { createPost } from '@/app/actions';
export default function NewPost() {
return (
<form action={createPost}>
<input type="text" name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}
Form with Client-Side Validation
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function ContactForm() {
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
const formData = new FormData(e.currentTarget);
const data = {
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
};
try {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (res.ok) {
router.push('/thank-you');
}
} catch (error) {
console.error('Error:', error);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="name" required />
<input type="email" name="email" required />
<textarea name="message" required />
<button type="submit" disabled={loading}>
{loading ? 'Sending...' : 'Send'}
</button>
</form>
);
}
Image Optimization
import Image from 'next/image';
export default function ProfilePage() {
return (
<div>
{/* Local image */}
<Image
src="/profile.jpg"
alt="Profile"
width={500}
height={500}
priority // Load immediately
/>
{/* Remote image */}
<Image
src="https://example.com/image.jpg"
alt="Remote image"
width={800}
height={600}
quality={90}
/>
{/* Fill container */}
<div style={{ position: 'relative', width: '100%', height: 400 }}>
<Image
src="/banner.jpg"
alt="Banner"
fill
style={{ objectFit: 'cover' }}
/>
</div>
{/* Responsive image */}
<Image
src="/responsive.jpg"
alt="Responsive"
width={0}
height={0}
sizes="100vw"
style={{ width: '100%', height: 'auto' }}
/>
</div>
);
}
Configure Remote Images:
// next.config.js
module.exports = {
images: {
domains: ['example.com', 'cdn.example.com'],
// Or use patterns
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com',
},
],
},
};
Metadata and SEO
Static Metadata
// app/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Home | My App',
description: 'Welcome to my Next.js application',
keywords: ['nextjs', 'react', 'typescript'],
openGraph: {
title: 'Home | My App',
description: 'Welcome to my Next.js application',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: 'Home | My App',
description: 'Welcome to my Next.js application',
images: ['/twitter-image.jpg'],
},
};
export default function Home() {
return <div>Home Page</div>;
}
Dynamic Metadata
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Styling
CSS Modules
// app/components/Button.module.css
.button {
padding: 10px 20px;
background: blue;
color: white;
border: none;
border-radius: 5px;
}
.button:hover {
background: darkblue;
}
// app/components/Button.tsx
import styles from './Button.module.css';
export default function Button({ children }: { children: React.ReactNode }) {
return <button className={styles.button}>{children}</button>;
}
Tailwind CSS
# Install Tailwind
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
};
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
// app/page.tsx
export default function Home() {
return (
<div className="min-h-screen bg-gray-100">
<h1 className="text-4xl font-bold text-blue-600">
Welcome to Next.js
</h1>
<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Click me
</button>
</div>
);
}
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 && 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', 'my-value');
return response;
}
// Configure which paths middleware runs on
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
Environment Variables
# .env.local
DATABASE_URL=postgresql://user:pass@localhost/db
NEXT_PUBLIC_API_URL=https://api.example.com
SECRET_KEY=your-secret-key
// Server-side only
const dbUrl = process.env.DATABASE_URL;
const secretKey = process.env.SECRET_KEY;
// Client-side (must be prefixed with NEXT_PUBLIC_)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
Loading and Error States
Loading UI
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="spinner">Loading...</div>
</div>
);
}
Error Handling
// app/dashboard/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>
);
}
Not Found
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<a href="/">Go back home</a>
</div>
);
}
Authentication Example
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { SignJWT } from 'jose';
export async function POST(request: NextRequest) {
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 secret = new TextEncoder().encode(process.env.JWT_SECRET);
const token = await new SignJWT({ userId: user.id })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('7d')
.sign(secret);
// Set cookie
const response = NextResponse.json({ success: true });
response.cookies.set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
});
return response;
}
Database Integration
With Prisma
npm install prisma @prisma/client
npx prisma init
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: ['query'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
// app/api/posts/route.ts
import { prisma } from '@/lib/prisma';
import { NextResponse } from 'next/server';
export async function GET() {
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: true },
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(posts);
}
Deployment
Build for Production
# Build the application
npm run build
# Start production server
npm run start
Deploy to Vercel
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Deploy to production
vercel --prod
Environment Variables on Vercel
Deploy to Other Platforms
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Best Practices
1. Use Server Components by Default
// Server component (default) - No 'use client'
export default async function Page() {
const data = await fetchData();
return <div>{data}</div>;
}
// Only use client components when needed
'use client';
export default function InteractiveComponent() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
2. Optimize Images
// Always use next/image
import Image from 'next/image';
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
priority={isAboveFold}
/>
3. Implement Loading States
// app/dashboard/loading.tsx
export default function Loading() {
return <Skeleton />;
}
4. Handle Errors Gracefully
// app/error.tsx
'use client';
export default function Error({ error, reset }: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Error: {error.message}</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
5. Use Metadata for SEO
export const metadata = {
title: 'Page Title',
description: 'Page description',
openGraph: {
images: ['/og-image.jpg'],
},
};
6. Organize Your Code
app/
├── (auth)/ # Route group
│ ├── login/
│ └── register/
├── (marketing)/
│ ├── about/
│ └── contact/
├── api/
├── components/
│ ├── ui/
│ └── features/
└── lib/
├── utils.ts
└── db.ts
Performance Optimization
Static Generation
// Generate static pages at build time
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
Revalidation
// Revalidate every hour
export const revalidate = 3600;
// Or per request
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
});
return res.json();
}
Dynamic Rendering
// Force dynamic rendering
export const dynamic = 'force-dynamic';
// Or per request
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
});
return res.json();
}
Key Takeaways
Additional Resources
Official:
Community:
Tools:
Next Steps
Next.js makes building production-ready React applications easy. Start with the basics, then explore advanced features as your application grows!