Next.js has become the go-to framework for building modern web applications. It's not just another React framework — it's a full-stack solution that solves real problems developers face daily: routing, data fetching, caching, rendering, and deployment.
This guide covers everything from project structure to advanced patterns, with practical examples and best practices. Whether you're building a simple blog or a complex dashboard, these concepts will help you build faster, more maintainable applications.
History & Industry Adoption
Next.js was created by Vercel (formerly ZEIT) in 2016 to solve a specific problem: React applications needed better developer experience and production performance. At the time, React was powerful but left developers to figure out routing, code splitting, and server-side rendering on their own.
Why Vercel Created Next.js
The core insight was simple: most web applications share common patterns — routing, layouts, data fetching, optimization. Instead of developers reinventing these for every project, why not build them into the framework?
Next.js introduced several innovations:
- File-system based routing — no more configuring React Router
- Server-side rendering (SSR) out of the box
- Automatic code splitting — only load what's needed for each page
- API routes — build backend endpoints alongside frontend code
- Zero configuration — sensible defaults that work immediately
Why It's Prevalent in the Industry
Next.js now powers thousands of production applications, from startups to enterprises. Here's why:
Enterprise Adoption:
- Companies like Netflix, TikTok, Twitch, Hulu, and Nike use Next.js in production
- Vercel reports over 100,000 production deployments monthly
- The framework has 120k+ GitHub stars and a massive ecosystem
Performance Benefits:
- Automatic static optimization — pages are pre-rendered at build time when possible
- Incremental Static Regeneration (ISR) — update static content without full rebuilds
- Edge runtime support — run code closer to users globally
- Built-in image optimization with
next/image
Developer Experience:
- Hot module replacement with fast refresh
- TypeScript support out of the box
- Built-in ESLint configuration
- Extensive documentation and community resources
Ecosystem & Tooling:
- Vercel's deployment platform integrates seamlessly
- Rich plugin ecosystem for authentication (NextAuth.js), CMS integration, and more
- Strong typing with TypeScript and automatic type checking
- Comprehensive testing support with Jest and React Testing Library
The framework's evolution from Pages Router to App Router (introduced in Next.js 13, stable in 14) shows its commitment to modern React patterns while maintaining backward compatibility. It's not just popular — it's become the default choice for serious React applications.
Project Structure & File Conventions
Next.js uses a file-system based router where folders define routes. Here's the essential structure:
my-app/
├── app/ # App Router (recommended)
│ ├── layout.tsx # Root layout (wraps all pages)
│ ├── page.tsx # Home page (/)
│ ├── dashboard/
│ │ ├── layout.tsx # Dashboard layout
│ │ ├── page.tsx # Dashboard page (/dashboard)
│ │ └── settings/
│ │ └── page.tsx # Settings page (/dashboard/settings)
│ └── blog/
│ ├── page.tsx # Blog listing (/blog)
│ └── [slug]/
│ └── page.tsx # Dynamic blog post (/blog/my-post)
├── public/ # Static assets (images, fonts)
├── components/ # Reusable UI components
├── lib/ # Utility functions, helpers
└── next.config.js # Next.js configuration
Key File Conventions
| File | Purpose | When It's Used |
|---|---|---|
page.tsx | Makes a route segment publicly accessible | Every route needs this to be accessible |
layout.tsx | Shared UI that wraps child segments | Persists across navigation, doesn't re-render |
loading.tsx | Loading UI shown while segment loads | Shows during data fetching |
error.tsx | Error boundary for segment and children | Catches errors in segment |
not-found.tsx | 404 UI for segment | When notFound() is called |
route.ts | Server-side API endpoint | Cannot coexist with page.tsx |
template.tsx | Similar to layout but re-mounts on navigation | When you need fresh state on navigation |
Root Layout Requirements
The root layout.tsx must include <html> and <body> tags:
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
App Router Routing
The App Router is Next.js's recommended routing system. It builds on React Server Components and provides powerful patterns for complex applications.
Basic Routes
Create a folder with a page.tsx file:
// app/about/page.tsx
export default function AboutPage() {
return <h1>About Us</h1>
}
This creates the route /about.
Nested Routes
Folders create nested routes automatically:
app/
└── dashboard/
├── layout.tsx # Wraps all /dashboard/* routes
├── page.tsx # /dashboard
└── settings/
└── page.tsx # /dashboard/settings
Dynamic Routes
Use square brackets for dynamic segments:
// app/blog/[slug]/page.tsx
export default function BlogPost({
params,
}: {
params: { slug: string }
}) {
return <h1>Post: {params.slug}</h1>
}
This handles /blog/hello-world, /blog/nextjs-guide, etc.
Route Groups
Use parentheses to organize routes without affecting the URL:
app/
├── (marketing)/
│ ├── layout.tsx # Marketing layout
│ ├── about/
│ │ └── page.tsx # /about (uses marketing layout)
│ └── blog/
│ └── page.tsx # /blog (uses marketing layout)
└── (shop)/
├── layout.tsx # Shop layout
├── products/
│ └── page.tsx # /products (uses shop layout)
└── cart/
└── page.tsx # /cart (uses shop layout)
Parallel Routes
Use @folder to render multiple pages in the same layout:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<div>
{children}
{analytics}
{team}
</div>
)
}
Parallel routes are perfect for dashboards, modals, and tab groups.
Intercepting Routes
Use (.) to intercept routes and show them in the current context:
app/
├── feed/
│ ├── page.tsx # /feed (shows grid of photos)
│ └── [id]/
│ └── page.tsx # /feed/123 (shows photo detail)
└── @modal/
└── (.)feed/
└── [id]/
└── page.tsx # Intercepts /feed/123 to show in modal
This pattern is powerful for modals and lightboxes.
Data Fetching Patterns
Next.js provides multiple ways to fetch data, each optimized for different use cases.
Server Components (Recommended)
Fetch data directly in Server Components — no API layer needed:
// app/dashboard/page.tsx
async function getMetrics() {
const res = await fetch('https://api.example.com/metrics')
if (!res.ok) throw new Error('Failed to fetch metrics')
return res.json()
}
export default async function DashboardPage() {
const metrics = await getMetrics()
return <Dashboard data={metrics} />
}
Caching Strategies
Next.js caches fetch requests by default. Control caching with options:
// Force dynamic (no caching)
fetch('https://api.example.com/data', { cache: 'no-store' })
// Cache with revalidation
fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // Revalidate every hour
})
// Cache with tags for on-demand revalidation
fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
})
Parallel Data Fetching
Avoid waterfalls by fetching data in parallel:
// Sequential (slow) - each waits for previous
export default async function Page() {
const posts = await fetchPosts() // 1 second
const comments = await fetchComments() // 1 second
const user = await fetchUser() // 1 second
// Total: 3 seconds
}
// Parallel (fast) - all start immediately
export default async function Page() {
const [posts, comments, user] = await Promise.all([
fetchPosts(),
fetchComments(),
fetchUser()
])
// Total: 1 second (limited by slowest)
}
Client-Side Data Fetching
For real-time or user-specific data, fetch on the client:
'use client'
import { useEffect, useState } from 'react'
export default function LiveFeed() {
const [data, setData] = useState(null)
useEffect(() => {
const fetchData = async () => {
const res = await fetch('/api/feed')
setData(await res.json())
}
fetchData()
}, [])
return <div>{/* render data */}</div>
}
Server Actions & Mutations
Server Actions let you mutate data without creating API endpoints. They're functions that run on the server and can be called from components.
Basic Usage
// app/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 content = formData.get('content') as string
await db.posts.create({ data: { title, content } })
revalidatePath('/posts')
redirect('/posts')
}
Using with Forms
// app/new-post/page.tsx
import { createPost } from '../actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<textarea name="content" placeholder="Post content" required />
<button type="submit">Create Post</button>
</form>
)
}
With Event Handlers
'use client'
import { likePost } from './actions'
export default function LikeButton({ postId }: { postId: string }) {
return (
<button onClick={() => likePost(postId)}>
Like
</button>
)
}
Revalidation
Server Actions can invalidate cached data:
'use server'
import { revalidatePath } from 'next/cache'
import { revalidateTag } from 'next/cache'
export async function updatePost(id: string, data: FormData) {
// Update database...
// Revalidate by path
revalidatePath('/posts')
// Or revalidate by tag
revalidateTag('posts')
}
Middleware
Middleware runs before a request is completed. Use it for authentication, redirects, and request modification.
Basic Structure
// 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))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*'
}
Common Use Cases
Authentication:
export function middleware(request: NextRequest) {
const isAuthenticated = checkAuth(request)
if (!isAuthenticated) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
Path Rewriting:
export function middleware(request: NextRequest) {
// Rewrite /blog/:slug to /articles/:slug
if (request.nextUrl.pathname.startsWith('/blog')) {
const slug = request.nextUrl.pathname.split('/')[2]
return NextResponse.rewrite(new URL(`/articles/${slug}`, request.url))
}
}
Bot Detection:
export function middleware(request: NextRequest) {
const userAgent = request.headers.get('user-agent')
if (userAgent?.includes('bot')) {
return NextResponse.next() // Let bots through
}
// Apply other logic for regular users
}
Matcher Configuration
Control which paths trigger middleware:
export const config = {
matcher: [
// Match all paths except static files
'/((?!api|_next/static|_next/image|favicon.ico).*)',
]
}
Error Handling
Next.js provides multiple error handling mechanisms for different scenarios.
Error Boundaries
Create error.tsx files to catch errors in route segments:
// app/dashboard/error.tsx
'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>
)
}
Not Found Errors
Use notFound() for 404 errors:
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
if (!post) {
notFound()
}
return <article>{post.content}</article>
}
Create a corresponding not-found.tsx:
// app/blog/[slug]/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div>
<h2>Post not found</h2>
<Link href="/blog">Back to blog</Link>
</div>
)
}
Global Error Handling
Use global-error.tsx for errors in the root layout:
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
</body>
</html>
)
}
Server-Side Error Handling
Use try/catch in Server Components and Server Actions:
export async function createInvoice(formData: FormData) {
try {
await sql`INSERT INTO invoices ...`
} catch (error) {
return { message: 'Database Error: Failed to Create Invoice.' }
}
revalidatePath('/invoices')
redirect('/invoices')
}
Performance Optimizations
Next.js includes several performance features out of the box.
Code Splitting
Next.js automatically splits code by route. Only the JavaScript needed for the current page is loaded.
Prefetching
The <Link> component prefetches routes when they enter the viewport:
import Link from 'next/link'
// Automatically prefetches /about when this link enters viewport
<Link href="/about">About</Link>
Streaming
Use loading.tsx or <Suspense> to show loading states while data loads:
// app/dashboard/loading.tsx
export default function Loading() {
return <div>Loading dashboard...</div>
}
Or for specific components:
import { Suspense } from 'react'
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading metrics...</div>}>
<Metrics />
</Suspense>
</div>
)
}
Image Optimization
Use next/image for optimized images:
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Loads above-the-fold images immediately
/>
Font Optimization
Use next/font for optimal font loading:
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function Layout({ children }) {
return (
<html className={inter.className}>
<body>{children}</body>
</html>
)
}
Best Practices
Server vs Client Components
Use Server Components by default. Only add 'use client' when you need:
- Event handlers (
onClick,onChange) - State (
useState,useReducer) - Effects (
useEffect) - Browser APIs (
window,document)
// Server Component (default)
async function UserProfile({ userId }) {
const user = await getUser(userId) // Direct database access
return <div>{user.name}</div>
}
// Client Component (when needed)
'use client'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
Data Fetching
- Fetch data in Server Components when possible
- Use
fetchwith appropriate caching strategies - Avoid client-side data fetching for initial page load
- Use
loading.tsxfor better user experience during data fetching
Component Organization
- Keep components small and focused
- Use Server Components for data-heavy components
- Move Client Components down the component tree
- Colocate related files in the same folder
Error Handling
- Create
error.tsxfor each major route segment - Use
notFound()for missing resources - Handle errors in Server Actions with try/catch
- Always call
redirectoutside try/catch blocks
Performance
- Use
<Link>instead of<a>for internal navigation - Enable
priorityfor above-the-fold images - Use dynamic imports for heavy components
- Implement proper caching strategies
Next.js continues to evolve, but these fundamentals remain constant. Master these patterns, and you'll be well-equipped to build fast, scalable applications that leverage the best of React and modern web development.
— Pulkit