We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide27 min read

Next.js

Next.js is a React framework for building production applications. Learn App Router, Server Components, Server Actions, SSR, SSG and everything you need to build modern web applications.

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

Code
Bash
# Stwórz nowy projekt - działa od razu
npx create-next-app@latest my-app
cd my-app
npm run dev

Bez 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:

Code
TypeScript
// 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:

Code
TEXT
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   # /features

Specjalne pliki

TSpage.tsx
TypeScript
// 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

TSapp/posts/page.tsx
TypeScript
// 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':

TScomponents/Counter.tsx
TypeScript
// 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

TSapp/page.tsx
TypeScript
// 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:

TSapp/posts/new/page.tsx
TypeScript
// 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

TSactions/posts.ts
TypeScript
// 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')
}
TScomponents/LikeButton.tsx
TypeScript
// 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:

TSapp/blog/[slug]/page.tsx
TypeScript
// 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ą:

TSapp/products/page.tsx
TypeScript
// 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:

TSapp/dashboard/page.tsx
TypeScript
// 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:

TSapp/page.tsx
TypeScript
// 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

TSapp/layout.tsx
TypeScript
// 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

TSapp/blog/[slug]/page.tsx
TypeScript
// 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:

TSmiddleware.ts
TypeScript
// 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

Code
TypeScript
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

TSapp/layout.tsx
TypeScript
// 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>
  )
}
Code
CSS
/* Użycie w CSS */
body {
  font-family: var(--font-inter);
}

code {
  font-family: var(--font-roboto-mono);
}

API Routes

TSapp/api/posts/route.ts
TypeScript
// 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):

TSapp/api/geo/route.ts
TypeScript
// 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:

Code
TEXT
app/
├── @dashboard/
│   └── page.tsx
├── @notifications/
│   └── page.tsx
├── layout.tsx
└── page.tsx
TSapp/layout.tsx
TypeScript
// 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:

Code
TEXT
app/
├── @modal/
│   └── (.)photo/[id]/
│       └── page.tsx    # Otwiera jako modal
├── photo/[id]/
│   └── page.tsx        # Pełna strona (direct access)
├── feed/
│   └── page.tsx
└── layout.tsx

Konfiguracja next.config.js

JSnext.config.js
JavaScript
// 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 = nextConfig

Best Practices

1. Struktura projektu

Code
TEXT
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 types

2. Error Handling

TSlib/errors.ts
TypeScript
// 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

TStypes/posts.ts
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

AspektNext.jsRemixAstro
RenderingSSR, SSG, ISR, CSRSSR, CSRSSG, SSR
FrameworkReactReactMulti-framework
RoutingFile-basedFile-basedFile-based
Data fetchingServer ComponentsLoadersIslands
DeploymentVercel, self-hostAnyAny
Best forFull-stack appsData-heavy appsContent sites

Cennik (Vercel deployment)

PlanCenaBandwidthFunctions
HobbyFree100GB100GB-hrs
Pro$20/mo1TB1000GB-hrs
EnterpriseCustomUnlimitedUnlimited

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

Code
Bash
# Create a new project - works right away
npx create-next-app@latest my-app
cd my-app
npm run dev

No 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:

Code
TypeScript
// 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:

Code
TEXT
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   # /features

Special files

TSpage.tsx
TypeScript
// 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

TSapp/posts/page.tsx
TypeScript
// 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':

TScomponents/Counter.tsx
TypeScript
// 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

TSapp/page.tsx
TypeScript
// 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:

TSapp/posts/new/page.tsx
TypeScript
// 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

TSactions/posts.ts
TypeScript
// 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')
}
TScomponents/LikeButton.tsx
TypeScript
// 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:

TSapp/blog/[slug]/page.tsx
TypeScript
// 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:

TSapp/products/page.tsx
TypeScript
// 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:

TSapp/dashboard/page.tsx
TypeScript
// 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:

TSapp/page.tsx
TypeScript
// 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

TSapp/layout.tsx
TypeScript
// 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

TSapp/blog/[slug]/page.tsx
TypeScript
// 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:

TSmiddleware.ts
TypeScript
// 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

Code
TypeScript
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

TSapp/layout.tsx
TypeScript
// 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>
  )
}
Code
CSS
/* Usage in CSS */
body {
  font-family: var(--font-inter);
}

code {
  font-family: var(--font-roboto-mono);
}

API Routes

TSapp/api/posts/route.ts
TypeScript
// 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):

TSapp/api/geo/route.ts
TypeScript
// 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:

Code
TEXT
app/
├── @dashboard/
│   └── page.tsx
├── @notifications/
│   └── page.tsx
├── layout.tsx
└── page.tsx
TSapp/layout.tsx
TypeScript
// 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:

Code
TEXT
app/
├── @modal/
│   └── (.)photo/[id]/
│       └── page.tsx    # Opens as a modal
├── photo/[id]/
│   └── page.tsx        # Full page (direct access)
├── feed/
│   └── page.tsx
└── layout.tsx

next.config.js configuration

JSnext.config.js
JavaScript
// 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 = nextConfig

Best practices

1. Project structure

Code
TEXT
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 types

2. Error handling

TSlib/errors.ts
TypeScript
// 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

TStypes/posts.ts
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

AspectNext.jsRemixAstro
RenderingSSR, SSG, ISR, CSRSSR, CSRSSG, SSR
FrameworkReactReactMulti-framework
RoutingFile-basedFile-basedFile-based
Data fetchingServer ComponentsLoadersIslands
DeploymentVercel, self-hostAnyAny
Best forFull-stack appsData-heavy appsContent sites

Pricing (Vercel deployment)

PlanPriceBandwidthFunctions
HobbyFree100GB100GB-hrs
Pro$20/mo1TB1000GB-hrs
EnterpriseCustomUnlimitedUnlimited

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.