Next.js - Kompletny przewodnik po frameworku React, który zdominował rynek
Czym jest Next.js i dlaczego jest tak popularny?
Next.js to framework React stworzony przez Vercel, który zmienił sposób w jaki budujemy aplikacje webowe. W przeciwieństwie do czystego Reacta, który jest biblioteką do budowania interfejsów, Next.js dostarcza kompletne rozwiązanie z routingiem, renderowaniem po stronie serwera, optymalizacją i wieloma innymi funkcjami out of the box.
Next.js używają giganci technologiczni: Netflix, TikTok, Twitch, Nike, Hulu, Target, i tysiące innych firm. Jest to de facto standard dla nowoczesnych aplikacji React w 2025 roku.
Dlaczego Next.js wygrał z alternatywami?
Zero-config setup
# Stwórz nowy projekt - działa od razu
npx create-next-app@latest my-app
cd my-app
npm run devBez konfiguracji Webpack, bez setup Babel, bez konfiguracji routera. Wszystko działa od pierwszej minuty.
Full-stack w jednym miejscu
Next.js pozwala budować zarówno frontend jak i backend w jednym projekcie:
// Frontend - app/page.tsx
export default function Home() {
return <h1>Witaj!</h1>
}
// Backend - app/api/users/route.ts
export async function GET() {
const users = await db.users.findMany()
return Response.json(users)
}Performance out of the box
Next.js automatycznie optymalizuje:
- Code splitting - Ładuje tylko potrzebny JavaScript
- Image optimization - Lazy loading, responsive sizes, WebP/AVIF
- Font optimization - Zero layout shift z Google Fonts
- Prefetching - Preloaduje strony linkowane na ekranie
App Router - Nowa era routingu
Od Next.js 13 wprowadzono App Router - nowy system routingu oparty na React Server Components.
Podstawy routingu
Routing w App Router oparty jest na strukturze folderów:
app/
├── page.tsx # / (strona główna)
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/any-slug
├── dashboard/
│ ├── layout.tsx # Layout dla wszystkich podstron
│ ├── page.tsx # /dashboard
│ ├── settings/
│ │ └── page.tsx # /dashboard/settings
│ └── [...catchAll]/
│ └── page.tsx # /dashboard/anything/else
└── (marketing)/ # Route group - nie wpływa na URL
├── pricing/
│ └── page.tsx # /pricing
└── features/
└── page.tsx # /featuresSpecjalne pliki
// page.tsx - Strona
export default function Page() {
return <main>Treść strony</main>
}
// layout.tsx - Layout współdzielony
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<nav>Navigation</nav>
{children}
<footer>Footer</footer>
</body>
</html>
)
}
// loading.tsx - Suspense fallback
export default function Loading() {
return <div className="animate-pulse">Ładowanie...</div>
}
// error.tsx - Error boundary
'use client'
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Coś poszło nie tak!</h2>
<button onClick={() => reset()}>Spróbuj ponownie</button>
</div>
)
}
// not-found.tsx - 404
export default function NotFound() {
return <h1>Nie znaleziono strony</h1>
}
// template.tsx - Jak layout ale re-renderuje się przy nawigacji
export default function Template({ children }: { children: React.ReactNode }) {
return <div className="fade-in">{children}</div>
}Server Components - Rewolucja w React
Server Components to domyślny tryb komponentów w App Router. Renderują się wyłącznie na serwerze.
Zalety Server Components
// app/posts/page.tsx - Server Component (domyślnie)
import { db } from '@/lib/db'
export default async function PostsPage() {
// Możesz bezpośrednio odpytywać bazę danych!
const posts = await db.posts.findMany({
include: { author: true }
})
// Możesz używać fs, env variables itp.
const secret = process.env.API_SECRET
return (
<ul>
{posts.map(post => (
<li key={post.id}>
{post.title} by {post.author.name}
</li>
))}
</ul>
)
}Korzyści:
- Zero JavaScript wysyłanego do klienta dla logiki server-side
- Bezpośredni dostęp do bazy danych, systemu plików, sekretów
- Automatyczny code splitting
- Lepsze SEO - treść widoczna w źródle strony
Client Components
Kiedy potrzebujesz interaktywności (useState, useEffect, event handlers), użyj 'use client':
// components/Counter.tsx
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Kliknięto {count} razy
</button>
)
}Kompozycja Server i Client Components
// app/page.tsx - Server Component
import { db } from '@/lib/db'
import { Counter } from '@/components/Counter'
import { LikeButton } from '@/components/LikeButton'
export default async function Page() {
const posts = await db.posts.findMany()
return (
<main>
{/* Server-rendered content */}
<h1>Blog</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
{/* Client Component wewnątrz Server Component */}
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
))}
{/* Inny Client Component */}
<Counter />
</main>
)
}Server Actions - Mutacje bez API
Server Actions to funkcje serwera wywoływane bezpośrednio z komponentów:
// app/posts/new/page.tsx
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export default function NewPostPage() {
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title') as string
const content = formData.get('content') as string
// Walidacja
if (!title || title.length < 3) {
throw new Error('Tytuł musi mieć minimum 3 znaki')
}
// Zapis do bazy
const post = await db.posts.create({
data: { title, content }
})
// Rewalidacja cache
revalidatePath('/posts')
// Przekierowanie
redirect(`/posts/${post.id}`)
}
return (
<form action={createPost}>
<input name="title" placeholder="Tytuł" required />
<textarea name="content" placeholder="Treść" />
<button type="submit">Opublikuj</button>
</form>
)
}Server Actions w Client Components
// actions/posts.ts
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export async function likePost(postId: string) {
await db.posts.update({
where: { id: postId },
data: { likes: { increment: 1 } }
})
revalidatePath('/posts')
}
export async function deletePost(postId: string) {
await db.posts.delete({ where: { id: postId } })
revalidatePath('/posts')
}// components/LikeButton.tsx
'use client'
import { useTransition } from 'react'
import { likePost } from '@/actions/posts'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [isPending, startTransition] = useTransition()
return (
<button
disabled={isPending}
onClick={() => startTransition(() => likePost(postId))}
>
{isPending ? '...' : `❤️ ${initialLikes}`}
</button>
)
}Data Fetching - Różne strategie
Static Generation (SSG)
Strony generowane w build time - najszybsze możliwe:
// app/blog/[slug]/page.tsx
import { db } from '@/lib/db'
// Generuj statyczne strony dla wszystkich postów
export async function generateStaticParams() {
const posts = await db.posts.findMany({ select: { slug: true } })
return posts.map((post) => ({ slug: post.slug }))
}
// Strona jest generowana statycznie
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await db.posts.findUnique({
where: { slug: params.slug }
})
return <article>{post.content}</article>
}Incremental Static Regeneration (ISR)
Statyczne strony z automatyczną rewalidacją:
// app/products/page.tsx
export const revalidate = 3600 // Rewaliduj co godzinę
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products')
return <ProductList products={await products.json()} />
}Server-Side Rendering (SSR)
Strona renderowana przy każdym żądaniu:
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic' // Wymusza SSR
export default async function DashboardPage() {
const stats = await fetch('https://api.example.com/stats', {
cache: 'no-store' // Zawsze świeże dane
})
return <Dashboard stats={await stats.json()} />
}Streaming z Suspense
Progresywne ładowanie części strony:
// app/page.tsx
import { Suspense } from 'react'
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
{/* Ten komponent ładuje się natychmiast */}
<QuickStats />
{/* Ten komponent streamuje się gdy dane są gotowe */}
<Suspense fallback={<ChartSkeleton />}>
<SlowChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<DataTable />
</Suspense>
</main>
)
}
async function SlowChart() {
const data = await fetchChartData() // Wolne zapytanie
return <Chart data={data} />
}Metadata i SEO
Static Metadata
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | Moja Aplikacja',
default: 'Moja Aplikacja',
},
description: 'Najlepsza aplikacja na świecie',
metadataBase: new URL('https://example.com'),
openGraph: {
title: 'Moja Aplikacja',
description: 'Najlepsza aplikacja na świecie',
images: ['/og-image.png'],
},
twitter: {
card: 'summary_large_image',
title: 'Moja Aplikacja',
description: 'Najlepsza aplikacja na świecie',
images: ['/twitter-image.png'],
},
}Dynamic Metadata
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
type Props = {
params: { slug: string }
}
export async function generateMetadata({ params }: Props): 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],
},
}
}Middleware
Middleware uruchamia się przed każdym requestem:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Sprawdź token auth
const token = request.cookies.get('token')?.value
// Przekieruj niezalogowanych z /dashboard
if (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Dodaj header do response
const response = NextResponse.next()
response.headers.set('x-custom-header', 'hello')
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*']
}Image Optimization
import Image from 'next/image'
// Lokalne obrazy - automatycznie optymalizowane
import heroImage from '@/public/hero.jpg'
export default function Page() {
return (
<div>
{/* Lokalne obrazy */}
<Image
src={heroImage}
alt="Hero"
placeholder="blur" // Wbudowany blur placeholder
priority // Ładuj jako priorytet
/>
{/* Zdalne obrazy */}
<Image
src="https://example.com/photo.jpg"
alt="Photo"
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 50vw"
quality={85}
/>
{/* Fill container */}
<div className="relative h-64">
<Image
src="/background.jpg"
alt="Background"
fill
style={{ objectFit: 'cover' }}
/>
</div>
</div>
)
}Font Optimization
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin', 'latin-ext'],
display: 'swap',
variable: '--font-inter',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pl" className={`${inter.variable} ${robotoMono.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}/* Użycie w CSS */
body {
font-family: var(--font-inter);
}
code {
font-family: var(--font-roboto-mono);
}API Routes
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = 10
const posts = await db.posts.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' }
})
return NextResponse.json({
posts,
page,
hasMore: posts.length === limit
})
}
export async function POST(request: Request) {
const body = await request.json()
const post = await db.posts.create({
data: body
})
return NextResponse.json(post, { status: 201 })
}
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await db.posts.findUnique({
where: { id: params.id }
})
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json(post)
}Edge Runtime
Uruchom kod na edge (blisko użytkownika):
// app/api/geo/route.ts
export const runtime = 'edge'
export async function GET(request: Request) {
const country = request.headers.get('x-vercel-ip-country')
const city = request.headers.get('x-vercel-ip-city')
return Response.json({
country,
city,
message: `Cześć z ${city}, ${country}!`
})
}Parallel Routes
Jednoczesne renderowanie wielu stron:
app/
├── @dashboard/
│ └── page.tsx
├── @notifications/
│ └── page.tsx
├── layout.tsx
└── page.tsx// app/layout.tsx
export default function Layout({
children,
dashboard,
notifications,
}: {
children: React.ReactNode
dashboard: React.ReactNode
notifications: React.ReactNode
}) {
return (
<div className="grid grid-cols-3">
<div className="col-span-2">{dashboard}</div>
<div>{notifications}</div>
<div className="col-span-3">{children}</div>
</div>
)
}Intercepting Routes
Modal bez zmiany URL lub z zachowaniem kontekstu:
app/
├── @modal/
│ └── (.)photo/[id]/
│ └── page.tsx # Otwiera jako modal
├── photo/[id]/
│ └── page.tsx # Pełna strona (direct access)
├── feed/
│ └── page.tsx
└── layout.tsxKonfiguracja next.config.js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Strict mode React
reactStrictMode: true,
// Zdalne obrazy
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: '*.cloudinary.com',
},
],
},
// Przekierowania
async redirects() {
return [
{
source: '/old-blog/:slug',
destination: '/blog/:slug',
permanent: true,
},
]
},
// Rewrites (proxy)
async rewrites() {
return [
{
source: '/api/external/:path*',
destination: 'https://external-api.com/:path*',
},
]
},
// Headers
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
],
},
]
},
// Environment variables
env: {
CUSTOM_KEY: 'value',
},
// Experimental features
experimental: {
serverActions: {
bodySizeLimit: '2mb',
},
},
}
module.exports = nextConfigBest Practices
1. Struktura projektu
src/
├── app/ # App Router
│ ├── (auth)/ # Auth route group
│ ├── (marketing)/ # Marketing route group
│ ├── api/ # API routes
│ └── layout.tsx
├── components/
│ ├── ui/ # Reusable UI components
│ └── features/ # Feature-specific components
├── lib/
│ ├── db.ts # Database client
│ ├── auth.ts # Auth utilities
│ └── utils.ts # Helper functions
├── hooks/ # Custom React hooks
├── actions/ # Server Actions
└── types/ # TypeScript types2. Error Handling
// lib/errors.ts
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code?: string
) {
super(message)
this.name = 'AppError'
}
}
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
import { AppError } from '@/lib/errors'
export async function POST(request: Request) {
try {
const body = await request.json()
if (!body.title) {
throw new AppError('Title is required', 400, 'VALIDATION_ERROR')
}
const post = await createPost(body)
return NextResponse.json(post, { status: 201 })
} catch (error) {
if (error instanceof AppError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode }
)
}
console.error('Unexpected error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}3. Type Safety z TypeScript
// types/posts.ts
export interface Post {
id: string
title: string
content: string
authorId: string
createdAt: Date
updatedAt: Date
}
export interface CreatePostInput {
title: string
content: string
}
// Server Action z typami
export async function createPost(input: CreatePostInput): Promise<Post> {
'use server'
// ...
}Next.js vs Alternatywy
| Aspekt | Next.js | Remix | Astro |
|---|---|---|---|
| Rendering | SSR, SSG, ISR, CSR | SSR, CSR | SSG, SSR |
| Framework | React | React | Multi-framework |
| Routing | File-based | File-based | File-based |
| Data fetching | Server Components | Loaders | Islands |
| Deployment | Vercel, self-host | Any | Any |
| Best for | Full-stack apps | Data-heavy apps | Content sites |
Cennik (Vercel deployment)
| Plan | Cena | Bandwidth | Functions |
|---|---|---|---|
| Hobby | Free | 100GB | 100GB-hrs |
| Pro | $20/mo | 1TB | 1000GB-hrs |
| Enterprise | Custom | Unlimited | Unlimited |
Możesz też hostować Next.js na dowolnej platformie: AWS, Railway, Render, self-hosted.
FAQ
Czy powinienem używać App Router czy Pages Router?
App Router dla nowych projektów. Oferuje Server Components, lepsze performance i jest przyszłością Next.js. Pages Router jest nadal wspierany ale nie otrzymuje nowych funkcji.
Jak migrować z Pages do App Router?
Możesz migrować stopniowo - oba routery mogą współistnieć. Zacznij od prostych stron i stopniowo przenoś bardziej złożone.
Server Components vs Client Components - kiedy używać?
- Server Components (domyślnie): Fetch data, dostęp do backendu, duże zależności
- Client Components: Interaktywność, hooks (useState, useEffect), event handlers, browser APIs
Czy Next.js jest za wolny przez JavaScript?
Next.js jest jednym z najszybszych frameworków dzięki:
- Server Components (zero JS dla statycznego contentu)
- Automatyczny code splitting
- Image/Font optimization
- Edge runtime
Podsumowanie
Next.js to kompletny framework do budowania nowoczesnych aplikacji webowych. Łączy moc Reacta z funkcjami serwerowymi, optymalizacją performance i świetnym developer experience.
Kluczowe zalety:
- Server Components dla lepszego performance
- Server Actions dla prostszych mutacji
- File-based routing dla intuicyjnej struktury
- Full-stack w jednym projekcie
- Vercel integration dla łatwego deployment
Czy to dla Ciebie? Jeśli budujesz cokolwiek większego niż prosty landing page z Reactem, Next.js to oczywisty wybór.
Next.js - Complete guide to the React framework that dominated the market
What is Next.js and why is it so popular?
Next.js is a React framework created by Vercel that changed the way we build web applications. Unlike pure React, which is a library for building interfaces, Next.js provides a complete solution with routing, server-side rendering, optimization, and many other features out of the box.
Next.js is used by tech giants: Netflix, TikTok, Twitch, Nike, Hulu, Target, and thousands of other companies. It is the de facto standard for modern React applications in 2025.
Why did Next.js win over alternatives?
Zero-config setup
# Create a new project - works right away
npx create-next-app@latest my-app
cd my-app
npm run devNo Webpack configuration, no Babel setup, no router configuration. Everything works from the very first minute.
Full-stack in one place
Next.js lets you build both frontend and backend in a single project:
// Frontend - app/page.tsx
export default function Home() {
return <h1>Welcome!</h1>
}
// Backend - app/api/users/route.ts
export async function GET() {
const users = await db.users.findMany()
return Response.json(users)
}Performance out of the box
Next.js automatically optimizes:
- Code splitting - Loads only the necessary JavaScript
- Image optimization - Lazy loading, responsive sizes, WebP/AVIF
- Font optimization - Zero layout shift with Google Fonts
- Prefetching - Preloads pages linked on screen
App Router - The new era of routing
Since Next.js 13, App Router was introduced - a new routing system based on React Server Components.
Routing basics
Routing in App Router is based on folder structure:
app/
├── page.tsx # / (home page)
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/any-slug
├── dashboard/
│ ├── layout.tsx # Layout for all subpages
│ ├── page.tsx # /dashboard
│ ├── settings/
│ │ └── page.tsx # /dashboard/settings
│ └── [...catchAll]/
│ └── page.tsx # /dashboard/anything/else
└── (marketing)/ # Route group - doesn't affect the URL
├── pricing/
│ └── page.tsx # /pricing
└── features/
└── page.tsx # /featuresSpecial files
// page.tsx - Page
export default function Page() {
return <main>Page content</main>
}
// layout.tsx - Shared layout
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<nav>Navigation</nav>
{children}
<footer>Footer</footer>
</body>
</html>
)
}
// loading.tsx - Suspense fallback
export default function Loading() {
return <div className="animate-pulse">Loading...</div>
}
// error.tsx - Error boundary
'use client'
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
// not-found.tsx - 404
export default function NotFound() {
return <h1>Page not found</h1>
}
// template.tsx - Like layout but re-renders on navigation
export default function Template({ children }: { children: React.ReactNode }) {
return <div className="fade-in">{children}</div>
}Server Components - A revolution in React
Server Components are the default component mode in App Router. They render exclusively on the server.
Advantages of Server Components
// app/posts/page.tsx - Server Component (default)
import { db } from '@/lib/db'
export default async function PostsPage() {
// You can query the database directly!
const posts = await db.posts.findMany({
include: { author: true }
})
// You can use fs, env variables, etc.
const secret = process.env.API_SECRET
return (
<ul>
{posts.map(post => (
<li key={post.id}>
{post.title} by {post.author.name}
</li>
))}
</ul>
)
}Benefits:
- Zero JavaScript sent to the client for server-side logic
- Direct access to the database, file system, and secrets
- Automatic code splitting
- Better SEO - content is visible in the page source
Client Components
When you need interactivity (useState, useEffect, event handlers), use 'use client':
// components/Counter.tsx
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
)
}Composition of Server and Client Components
// app/page.tsx - Server Component
import { db } from '@/lib/db'
import { Counter } from '@/components/Counter'
import { LikeButton } from '@/components/LikeButton'
export default async function Page() {
const posts = await db.posts.findMany()
return (
<main>
{/* Server-rendered content */}
<h1>Blog</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
{/* Client Component inside a Server Component */}
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
))}
{/* Another Client Component */}
<Counter />
</main>
)
}Server Actions - Mutations without an API
Server Actions are server functions called directly from components:
// app/posts/new/page.tsx
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export default function NewPostPage() {
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title') as string
const content = formData.get('content') as string
// Validation
if (!title || title.length < 3) {
throw new Error('Title must be at least 3 characters long')
}
// Save to database
const post = await db.posts.create({
data: { title, content }
})
// Cache revalidation
revalidatePath('/posts')
// Redirect
redirect(`/posts/${post.id}`)
}
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" />
<button type="submit">Publish</button>
</form>
)
}Server Actions in Client Components
// actions/posts.ts
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export async function likePost(postId: string) {
await db.posts.update({
where: { id: postId },
data: { likes: { increment: 1 } }
})
revalidatePath('/posts')
}
export async function deletePost(postId: string) {
await db.posts.delete({ where: { id: postId } })
revalidatePath('/posts')
}// components/LikeButton.tsx
'use client'
import { useTransition } from 'react'
import { likePost } from '@/actions/posts'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [isPending, startTransition] = useTransition()
return (
<button
disabled={isPending}
onClick={() => startTransition(() => likePost(postId))}
>
{isPending ? '...' : `❤️ ${initialLikes}`}
</button>
)
}Data fetching - Different strategies
Static Generation (SSG)
Pages generated at build time - the fastest possible approach:
// app/blog/[slug]/page.tsx
import { db } from '@/lib/db'
// Generate static pages for all posts
export async function generateStaticParams() {
const posts = await db.posts.findMany({ select: { slug: true } })
return posts.map((post) => ({ slug: post.slug }))
}
// Page is generated statically
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await db.posts.findUnique({
where: { slug: params.slug }
})
return <article>{post.content}</article>
}Incremental Static Regeneration (ISR)
Static pages with automatic revalidation:
// app/products/page.tsx
export const revalidate = 3600 // Revalidate every hour
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products')
return <ProductList products={await products.json()} />
}Server-Side Rendering (SSR)
Page rendered on every request:
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic' // Forces SSR
export default async function DashboardPage() {
const stats = await fetch('https://api.example.com/stats', {
cache: 'no-store' // Always fresh data
})
return <Dashboard stats={await stats.json()} />
}Streaming with Suspense
Progressive loading of page sections:
// app/page.tsx
import { Suspense } from 'react'
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
{/* This component loads immediately */}
<QuickStats />
{/* This component streams in when data is ready */}
<Suspense fallback={<ChartSkeleton />}>
<SlowChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<DataTable />
</Suspense>
</main>
)
}
async function SlowChart() {
const data = await fetchChartData() // Slow query
return <Chart data={data} />
}Metadata and SEO
Static Metadata
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | My Application',
default: 'My Application',
},
description: 'The best application in the world',
metadataBase: new URL('https://example.com'),
openGraph: {
title: 'My Application',
description: 'The best application in the world',
images: ['/og-image.png'],
},
twitter: {
card: 'summary_large_image',
title: 'My Application',
description: 'The best application in the world',
images: ['/twitter-image.png'],
},
}Dynamic Metadata
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
type Props = {
params: { slug: string }
}
export async function generateMetadata({ params }: Props): 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],
},
}
}Middleware
Middleware runs before every request:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Check auth token
const token = request.cookies.get('token')?.value
// Redirect unauthenticated users from /dashboard
if (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Add header to response
const response = NextResponse.next()
response.headers.set('x-custom-header', 'hello')
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*']
}Image Optimization
import Image from 'next/image'
// Local images - automatically optimized
import heroImage from '@/public/hero.jpg'
export default function Page() {
return (
<div>
{/* Local images */}
<Image
src={heroImage}
alt="Hero"
placeholder="blur" // Built-in blur placeholder
priority // Load as priority
/>
{/* Remote images */}
<Image
src="https://example.com/photo.jpg"
alt="Photo"
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 50vw"
quality={85}
/>
{/* Fill container */}
<div className="relative h-64">
<Image
src="/background.jpg"
alt="Background"
fill
style={{ objectFit: 'cover' }}
/>
</div>
</div>
)
}Font Optimization
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin', 'latin-ext'],
display: 'swap',
variable: '--font-inter',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}/* Usage in CSS */
body {
font-family: var(--font-inter);
}
code {
font-family: var(--font-roboto-mono);
}API Routes
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = 10
const posts = await db.posts.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' }
})
return NextResponse.json({
posts,
page,
hasMore: posts.length === limit
})
}
export async function POST(request: Request) {
const body = await request.json()
const post = await db.posts.create({
data: body
})
return NextResponse.json(post, { status: 201 })
}
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await db.posts.findUnique({
where: { id: params.id }
})
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json(post)
}Edge Runtime
Run code on the edge (close to the user):
// app/api/geo/route.ts
export const runtime = 'edge'
export async function GET(request: Request) {
const country = request.headers.get('x-vercel-ip-country')
const city = request.headers.get('x-vercel-ip-city')
return Response.json({
country,
city,
message: `Hello from ${city}, ${country}!`
})
}Parallel Routes
Simultaneous rendering of multiple pages:
app/
├── @dashboard/
│ └── page.tsx
├── @notifications/
│ └── page.tsx
├── layout.tsx
└── page.tsx// app/layout.tsx
export default function Layout({
children,
dashboard,
notifications,
}: {
children: React.ReactNode
dashboard: React.ReactNode
notifications: React.ReactNode
}) {
return (
<div className="grid grid-cols-3">
<div className="col-span-2">{dashboard}</div>
<div>{notifications}</div>
<div className="col-span-3">{children}</div>
</div>
)
}Intercepting Routes
Modal without changing the URL or with preserved context:
app/
├── @modal/
│ └── (.)photo/[id]/
│ └── page.tsx # Opens as a modal
├── photo/[id]/
│ └── page.tsx # Full page (direct access)
├── feed/
│ └── page.tsx
└── layout.tsxnext.config.js configuration
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// React strict mode
reactStrictMode: true,
// Remote images
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: '*.cloudinary.com',
},
],
},
// Redirects
async redirects() {
return [
{
source: '/old-blog/:slug',
destination: '/blog/:slug',
permanent: true,
},
]
},
// Rewrites (proxy)
async rewrites() {
return [
{
source: '/api/external/:path*',
destination: 'https://external-api.com/:path*',
},
]
},
// Headers
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
],
},
]
},
// Environment variables
env: {
CUSTOM_KEY: 'value',
},
// Experimental features
experimental: {
serverActions: {
bodySizeLimit: '2mb',
},
},
}
module.exports = nextConfigBest practices
1. Project structure
src/
├── app/ # App Router
│ ├── (auth)/ # Auth route group
│ ├── (marketing)/ # Marketing route group
│ ├── api/ # API routes
│ └── layout.tsx
├── components/
│ ├── ui/ # Reusable UI components
│ └── features/ # Feature-specific components
├── lib/
│ ├── db.ts # Database client
│ ├── auth.ts # Auth utilities
│ └── utils.ts # Helper functions
├── hooks/ # Custom React hooks
├── actions/ # Server Actions
└── types/ # TypeScript types2. Error handling
// lib/errors.ts
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code?: string
) {
super(message)
this.name = 'AppError'
}
}
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
import { AppError } from '@/lib/errors'
export async function POST(request: Request) {
try {
const body = await request.json()
if (!body.title) {
throw new AppError('Title is required', 400, 'VALIDATION_ERROR')
}
const post = await createPost(body)
return NextResponse.json(post, { status: 201 })
} catch (error) {
if (error instanceof AppError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode }
)
}
console.error('Unexpected error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}3. Type safety with TypeScript
// types/posts.ts
export interface Post {
id: string
title: string
content: string
authorId: string
createdAt: Date
updatedAt: Date
}
export interface CreatePostInput {
title: string
content: string
}
// Server Action with types
export async function createPost(input: CreatePostInput): Promise<Post> {
'use server'
// ...
}Next.js vs alternatives
| Aspect | Next.js | Remix | Astro |
|---|---|---|---|
| Rendering | SSR, SSG, ISR, CSR | SSR, CSR | SSG, SSR |
| Framework | React | React | Multi-framework |
| Routing | File-based | File-based | File-based |
| Data fetching | Server Components | Loaders | Islands |
| Deployment | Vercel, self-host | Any | Any |
| Best for | Full-stack apps | Data-heavy apps | Content sites |
Pricing (Vercel deployment)
| Plan | Price | Bandwidth | Functions |
|---|---|---|---|
| Hobby | Free | 100GB | 100GB-hrs |
| Pro | $20/mo | 1TB | 1000GB-hrs |
| Enterprise | Custom | Unlimited | Unlimited |
You can also host Next.js on any platform: AWS, Railway, Render, or self-hosted.
FAQ
Should I use App Router or Pages Router?
App Router for new projects. It offers Server Components, better performance, and is the future of Next.js. Pages Router is still supported but no longer receives new features.
How do I migrate from Pages to App Router?
You can migrate gradually - both routers can coexist. Start with simple pages and gradually move the more complex ones.
Server Components vs Client Components - when to use which?
- Server Components (default): Data fetching, backend access, large dependencies
- Client Components: Interactivity, hooks (useState, useEffect), event handlers, browser APIs
Is Next.js too slow because of JavaScript?
Next.js is one of the fastest frameworks thanks to:
- Server Components (zero JS for static content)
- Automatic code splitting
- Image/Font optimization
- Edge runtime
Summary
Next.js is a complete framework for building modern web applications. It combines the power of React with server-side features, performance optimization, and excellent developer experience.
Key advantages:
- Server Components for better performance
- Server Actions for simpler mutations
- File-based routing for intuitive structure
- Full-stack in one project
- Vercel integration for easy deployment
Is it for you? If you are building anything larger than a simple landing page with React, Next.js is the obvious choice.