Przejdź do treści głównej

Complete Next.js 15 Guide

A practical walkthrough of Next.js 15, from fundamentals to advanced patterns. What you need to know to build fast, SEO-friendly web applications, and when Next.js is (and isn't) the right choice.

Author: Michał Wojciechowski··16 min read
Modern web development workspace with code on screen

What is Next.js and why should you use it?

Next.js is a React framework for building production-ready web applications. Built by Vercel, it adds server-side rendering, static site generation, and API routes on top of React, all with zero configuration.

Where React only handles the UI layer, Next.js takes care of routing, data fetching, rendering strategies, and deployment optimization. It's React with the production plumbing already done. If you're also deciding on a language, check out my TypeScript vs JavaScript comparison.

Why developers pick Next.js:

  • Good performance out of the box -- automatic code splitting, image optimization, and font optimization
  • SEO-friendly by default -- server rendering means search engines can actually crawl your content
  • Solid developer experience -- fast refresh, TypeScript support, and file-based routing that just works
  • Full-stack capabilities -- API routes and Server Actions let you build backend functionality without a separate server
  • Battle-tested -- used by Netflix, TikTok, Twitch, Hulu, and thousands of other companies

What's new in Next.js 15

Next.js 15 shipped in October 2024 with some significant changes. Performance is better, the developer experience is smoother, and the rendering model got a real upgrade. The App Router is now stable and production-ready, replacing the Pages Router as the recommended approach.

React Server Components (RSC)

This is the biggest change to how React works in years. Components render on the server by default, sending only HTML to the client. The result: much smaller JavaScript bundles and faster page loads.

// Server Component (default)
async function BlogPost({ id }: { id: string }) {
  // Fetch data directly in component
  const post = await db.post.findUnique({ where: { id } });

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

Benefits: Zero client JavaScript for static content, direct database access, automatic code splitting, and better security (API keys stay on server).

App Router with Server Actions

The routing system now uses the app/ directory with nested layouts and Server Actions for data mutations. No more writing API routes for basic form submissions.

// app/actions.ts - Server Action
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;

  await db.post.create({
    data: { title, content: formData.get('content') as string }
  });

  revalidatePath('/blog');
  redirect(`/blog/${title}`);
}

// app/blog/new/page.tsx - Client Component
'use client'

import { createPost } from '@/app/actions';

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Publish</button>
    </form>
  );
}

No need for API routes, fetch calls, or state management – forms work progressively even without JavaScript.

Partial Prerendering (experimental)

Mixes static and dynamic rendering on the same page. The static shell loads instantly while dynamic parts stream in. This is the feature I'm most excited about for content-heavy sites with some dynamic elements.

// next.config.ts
export default {
  experimental: {
    ppr: true // Partial Prerendering
  }
}

// Page with mixed rendering
export default async function Dashboard() {
  return (
    <>
      <StaticHeader /> {/* Prerendered */}
      <Suspense fallback={<Skeleton />}>
        <DynamicUserData /> {/* Streams in */}
      </Suspense>
    </>
  );
}

Turbopack (Beta)

A Rust-based bundler claiming 700x faster than Webpack and 10x faster than Vite for large applications. In practice, fast refresh feels nearly instant.

// package.json
{
  "scripts": {
    "dev": "next dev --turbo",
    "build": "next build"
  }
}

Other Next.js 15 improvements: enhanced fetch() caching, better error handling, improved TypeScript support, and native support for React 19 features like useTransition and useFormStatus.

Performance optimization and code efficiency

Rendering strategies: SSR, SSG, ISR, and CSR

Next.js gives you four rendering strategies that can be mixed on the same application. Choose the right strategy for each page based on data freshness requirements and traffic patterns.

Static Site Generation (SSG)

Pages are generated at build time and served as static HTML. Fastest possible performance.

// Generates static HTML at build
export default async function Page() {
  const data = await fetch('...');
  return <div>{data}</div>;
}

Best for: Marketing pages, blogs, documentation, product catalogs

Server-Side Rendering (SSR)

Pages render on every request with fresh data. Always up-to-date but slower than static.

// Force dynamic rendering
export const dynamic = 'force-dynamic';

export default async function Page() {
  const data = await fetch('...');
  return <div>{data}</div>;
}

Best for: Dashboards, user profiles, personalized content, real-time data

Incremental Static Regeneration (ISR)

Static pages that regenerate in the background. Fast like static, fresh like dynamic.

// Revalidate every 60 seconds
export const revalidate = 60;

export default async function Page() {
  const data = await fetch('...');
  return <div>{data}</div>;
}

Best for: E-commerce products, news sites, content platforms, high-traffic pages

Client-Side Rendering (CSR)

Renders in the browser with JavaScript. Use Client Components for interactive UI.

'use client'

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>
    {count}
  </button>;
}

Best for: Interactive components, client state, browser APIs, event handlers

Practical tip: mix strategies

Good Next.js apps use all four strategies on different pages. Static homepage for speed, SSR dashboard for fresh data, ISR product pages for a balance of both, Client Components for interactive bits. Default to Server Components. Use Client Components only when you need interactivity.

Routing and navigation in Next.js 15

Next.js uses file-based routing. Your file structure in the app/ directory automatically maps to URLs. No router configuration, no route tables.

File System Routing Examples:

app/
├── page.tsx              → /
├── about/
│   └── page.tsx          → /about
├── blog/
│   ├── page.tsx          → /blog
│   └── [slug]/
│       └── page.tsx      → /blog/:slug (dynamic)
├── dashboard/
│   ├── layout.tsx        → Shared layout
│   ├── page.tsx          → /dashboard
│   └── settings/
│       └── page.tsx      → /dashboard/settings
└── api/
    └── users/
        └── route.ts      → /api/users (API route)

Dynamic Routes

Use square brackets [param] for dynamic segments:

// app/blog/[slug]/page.tsx
type Props = {
  params: Promise<{ slug: string }>;
};

export default async function BlogPost({ params }: Props) {
  const { slug } = await params;
  const post = await getPost(slug);

  return <article>{post.content}</article>;
}

// Generate static params at build time
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map(post => ({ slug: post.slug }));
}

Layouts and Templates

Layouts persist across route changes and don't re-render. Perfect for navigation, sidebars, and shared UI:

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <Sidebar /> {/* Shared sidebar */}
      <main className="flex-1">
        {children} {/* Page content */}
      </main>
    </div>
  );
}

Navigation with Link and useRouter

Client-side navigation with automatic prefetching for instant page transitions:

import Link from 'next/link';
import { useRouter } from 'next/navigation';

// Declarative navigation
<Link href="/blog/nextjs-guide">Read Guide</Link>

// Programmatic navigation
function NavigateButton() {
  const router = useRouter();

  return (
    <button onClick={() => router.push('/dashboard')}>
      Go to Dashboard
    </button>
  );
}

Data fetching patterns in Next.js 15

Data fetching in Next.js 15 is much simpler than before. With async Server Components, automatic request deduplication, and built-in caching, the old getServerSideProps and getStaticProps patterns are no longer needed.

Server Component Data Fetching

Fetch data directly in components with async/await. Automatic caching and deduplication:

// Server Component - async by default
async function UserProfile({ userId }: { userId: string }) {
  // Fetches are automatically cached
  const user = await fetch(`https://api.example.com/users/${userId}`);

  // Or fetch from database directly
  const posts = await db.post.findMany({
    where: { authorId: userId },
    orderBy: { createdAt: 'desc' },
  });

  return (
    <div>
      <h1>{user.name}</h1>
      <PostList posts={posts} />
    </div>
  );
}

Parallel and Sequential Data Fetching

Optimize performance by fetching data in parallel when possible:

// Parallel fetching - starts at the same time
async function Dashboard() {
  const userPromise = getUser();
  const postsPromise = getPosts();
  const statsPromise = getStats();

  // Wait for all promises
  const [user, posts, stats] = await Promise.all([
    userPromise,
    postsPromise,
    statsPromise,
  ]);

  return <DashboardUI user={user} posts={posts} stats={stats} />;
}

// Sequential fetching - when data depends on previous result
async function UserPosts({ userId }: { userId: string }) {
  const user = await getUser(userId);
  // Fetch posts only after getting user
  const posts = await getPosts(user.region);

  return <PostsList posts={posts} />;
}

Streaming with Suspense

Show instant feedback while data loads. No more loading spinners blocking the entire page:

import { Suspense } from 'react';

export default function Page() {
  return (
    <>
      <Header /> {/* Shows immediately */}

      <Suspense fallback={<Skeleton />}>
        <SlowComponent /> {/* Streams in when ready */}
      </Suspense>

      <Suspense fallback={<Skeleton />}>
        <AnotherSlowComponent /> {/* Also streams independently */}
      </Suspense>

      <Footer /> {/* Shows immediately */}
    </>
  );
}

Revalidation Strategies

Control cache freshness with time-based or on-demand revalidation:

// Time-based revalidation
export const revalidate = 3600; // Revalidate every hour

// On-demand revalidation
import { revalidatePath, revalidateTag } from 'next/cache';

export async function updatePost(id: string) {
  await db.post.update({ where: { id }, data: { ... } });

  // Revalidate specific path
  revalidatePath('/blog');

  // Or revalidate by cache tag
  revalidateTag('posts');
}

// Tagged fetch for granular control
const posts = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'], revalidate: 60 }
});

API routes and Server Actions

Next.js lets you build full-stack apps with backend functionality right in your React project. You have two options: API Routes for REST/GraphQL endpoints, and Server Actions for form submissions and mutations. For real-time features, see my article on SignalR and real-time apps.

API Routes

Create REST endpoints in app/api/ directory:

// app/api/posts/route.ts
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const posts = await db.post.findMany();
  return NextResponse.json(posts);
}

export async function POST(request: Request) {
  const body = await request.json();
  const post = await db.post.create({ data: body });
  return NextResponse.json(post, { status: 201 });
}

// Dynamic route: app/api/posts/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const post = await db.post.findUnique({ where: { id } });

  if (!post) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 });
  }

  return NextResponse.json(post);
}

Server Actions

Server Actions eliminate the need for API routes for most mutations. Works with forms, progressive enhancement, and optimistic updates:

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  // Validate input
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  if (!title || !content) {
    return { error: 'Title and content are required' };
  }

  // Create post
  const post = await db.post.create({
    data: { title, content, authorId: '...' }
  });

  // Revalidate cache
  revalidatePath('/blog');

  // Redirect to new post
  redirect(`/blog/${post.slug}`);
}

// Usage in Client Component
'use client'

import { useFormStatus } from 'react-dom';
import { createPost } from './actions';

export function CreatePostForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <SubmitButton />
    </form>
  );
}

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Creating...' : 'Create Post'}
    </button>
  );
}

Optimistic Updates

Update the UI instantly before the server confirms, giving users a faster perceived experience:

'use client'

import { useOptimistic } from 'react';
import { addTodo } from './actions';

export function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo: string) => [
      ...state,
      { id: Date.now(), text: newTodo, pending: true }
    ]
  );

  async function formAction(formData: FormData) {
    const text = formData.get('todo') as string;
    addOptimisticTodo(text); // Update UI immediately
    await addTodo(text); // Save to server
  }

  return (
    <>
      <form action={formAction}>
        <input name="todo" />
        <button type="submit">Add</button>
      </form>

      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} className={todo.pending ? 'opacity-50' : ''}>
            {todo.text}
          </li>
        ))}
      </ul>
    </>
  );
}

Performance optimization

Next.js optimizes for Core Web Vitals out of the box (the metrics Google uses for search ranking). But there's more you can do to get the best possible performance.

Built-in optimizations:

  • Automatic code splitting -- only loads the JavaScript needed for the current page
  • Image optimization -- automatic WebP/AVIF conversion, lazy loading, responsive images
  • Font optimization -- automatic font subsetting and inlining
  • Route prefetching -- preloads pages visible in viewport before the user clicks
  • Script optimization -- control when third-party scripts load

Image component

Use next/image for automatic optimization:

import Image from 'next/image';

// Automatic optimization and lazy loading
<Image
  src="/hero.jpg"
  alt="Hero image"
  width={1200}
  height={600}
  priority // Load above-the-fold images immediately
  placeholder="blur" // Show blur placeholder while loading
/>

// Responsive images
<Image
  src="/product.jpg"
  alt="Product"
  fill // Fill parent container
  sizes="(max-width: 768px) 100vw, 50vw"
  className="object-cover"
/>

Dynamic imports for code splitting

Load heavy components only when needed:

import dynamic from 'next/dynamic';

// Load component lazily with loading state
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false // Client-side only
});

// Load third-party library only when needed
const DynamicModal = dynamic(() => import('./Modal'));

export default function Dashboard() {
  const [showModal, setShowModal] = useState(false);

  return (
    <>
      <button onClick={() => setShowModal(true)}>
        Open Modal
      </button>

      {/* Modal code only loaded when opened */}
      {showModal && <DynamicModal onClose={() => setShowModal(false)} />}
    </>
  );
}

Metadata and SEO

Type-safe metadata with automatic OpenGraph and Twitter cards:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.coverImage }],
      type: 'article',
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  };
}
Cloud infrastructure and deployment

Deployment options

Next.js runs anywhere Node.js runs. Vercel (the company behind Next.js) gives the smoothest deployment experience, but you can deploy to any cloud platform. For CI/CD setup, see my GitHub Actions vs Azure DevOps comparison.

Vercel (Recommended)

Zero-config deployment with automatic CI/CD, edge network, and analytics:

# Install Vercel CLI
npm i -g vercel

# Deploy to production
vercel --prod

# Or connect GitHub repo for automatic deploys

Features: Edge Functions, Analytics, Image Optimization CDN, Preview Deployments, Team Collaboration

Docker + Cloud Platforms

Deploy to AWS, Azure, GCP, or any container orchestrator:

# Dockerfile
FROM node:20-alpine AS base

# Install dependencies
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Build application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["node", "server.js"]

Static Export

Export as static HTML for traditional hosting (CDN, S3, etc.):

// next.config.ts
export default {
  output: 'export',
  images: {
    unoptimized: true, // Required for static export
  },
}

// Build and export
npm run build
# Output in 'out' directory

# Deploy to any static host
aws s3 sync out/ s3://my-bucket --acl public-read

Self-Hosted with PM2

Run on your own server with process management:

# Install PM2
npm install -g pm2

# Build application
npm run build

# Start with PM2
pm2 start npm --name "nextjs-app" -- start

# Or use custom server
pm2 start npm --name "nextjs-app" -- run start:prod

# Save PM2 configuration
pm2 save
pm2 startup

Production checklist

  • Enable environment variables for API keys and database URLs
  • Configure CDN for static assets and images
  • Set up monitoring and error tracking (Sentry, Datadog)
  • Enable HTTPS and security headers
  • Configure caching headers and revalidation
  • Test Core Web Vitals with Lighthouse and WebPageTest

Next.js vs plain React

Next.js adds complexity on top of React. Sometimes that complexity is worth it, sometimes it isn't. Here's how I think about the choice:

Choose Next.js when:

  • SEO is critical (marketing sites, blogs, e-commerce)
  • Performance matters (Core Web Vitals, page speed)
  • You need server-side rendering or static generation
  • Building full-stack app with API routes
  • Want zero-config setup (routing, optimization, etc.)
  • Need built-in image and font optimization
  • Multi-page application with routing

Choose Plain React when:

  • Building admin dashboards or internal tools (no SEO needed)
  • Single-page application (SPA) behind authentication
  • Component library or design system
  • Embedded widget or small interactive component
  • Learning React (keep it simple first)
  • Need maximum control over build configuration
  • Pure client-side app with separate backend API

The hybrid approach

Many companies use both. Next.js for the public-facing marketing site and blog (where SEO matters), plain React SPA for the application dashboard (behind authentication). Good SEO where you need it, simpler code where you don't.

Real-world use cases

Next.js runs behind some very large websites. Here are the patterns I see working well for common use cases:

E-Commerce Platform

Stack: Next.js + Stripe + Prisma + PostgreSQL

  • Product pages: ISR with 60-second revalidation for inventory updates
  • Homepage: SSG for maximum performance and SEO
  • Search: Client-side with Algolia for instant results
  • Checkout: Server Actions with Stripe integration
  • Admin: SSR dashboard with real-time order updates

Examples: Nike, Walmart, Doordash

SaaS Application

Stack: Next.js + tRPC + Clerk + Vercel + Planetscale

  • Marketing site: SSG for SEO and fast loading
  • Documentation: SSG with MDX for rich content
  • App dashboard: SSR with user-specific data
  • Real-time features: Server Components + WebSockets
  • API: tRPC for type-safe API calls

Examples: Vercel, Linear, Cal.com

Content Platform / Blog

Stack: Next.js + MDX + Contentful + Vercel

  • Articles: SSG with ISR for new content updates
  • Homepage: SSG with latest posts
  • Search: Static index with Fuse.js for client-side search
  • Comments: Server Actions for form submissions
  • Analytics: Edge Functions for view tracking

Examples: TechCrunch, IGN, HashNode

Developer Portfolio

Stack: Next.js + Tailwind + MDX + Vercel (simple and fast)

  • All pages: SSG for instant loading and perfect Lighthouse scores
  • Blog posts: MDX for rich, interactive content
  • Contact form: Server Action with email integration
  • Projects: Static content with dynamic GitHub stats

Perfect starter template for developers

Learning resources

If you want to go deeper, these are the resources I'd start with:

Official Next.js Documentation

The official docs are genuinely well-written, organized, and kept up to date. Start here.

Read the docs

Next.js Learn Tutorial

Free interactive course from the Next.js team: Learn Next.js. Build a full-stack app from scratch.

Start learning

Vercel Blog

Vercel's blog covers new features, practical tips, and real-world case studies.

Read the blog

React Documentation

Since Next.js is React, understanding React deeply is essential: react.dev

Learn React

Frequently asked questions

When should I use Next.js instead of plain React?

React excels at complex interactive UIs and dashboards. Next.js adds structure (routes, data fetching, API layers, deployment) that React alone doesn't provide. Use Next.js if you need: SEO optimization, server-side rendering, built-in API routes, simple deployment. If you find yourself adding routing libraries, authentication layers, and API middleware—you need Next.js.

What's the difference between Server Components and Client Components?

Server Components run only on server and don't ship JavaScript to browser—perfect for database queries. Client Components run in browser, handle interactivity, and require 'use client' directive. Use Server Components by default, switch to Client when you need interactivity. Server Components reduce JavaScript bundle by 30-50% on typical sites.

Which rendering strategy should I use (SSR/SSG/ISR)?

Static Generation (SSG): Blog posts, marketing pages—pre-build at deploy time. Server-Side Rendering (SSR): User-specific content, real-time data (slower, per-request). Incremental Static Regeneration (ISR): E-commerce, news—cache but refresh in background (best of both). Rule of thumb: Use ISR when possible, SSG for stable content, SSR only for dynamic data.

How do Server Actions differ from API routes?

API routes are traditional REST endpoints—useful for third-party integrations. Server Actions are direct function calls from client with less boilerplate and automatic error handling. Use Server Actions for form submissions and data mutations within your app. Use API routes when you need REST endpoints for external services.

Is Next.js suitable for static websites or ecommerce?

Static sites: Yes, extremely well with SSG for fast performance. Ecommerce: Yes—use dynamic product pages with ISR and real-time inventory with Server Components. Marketing sites: Yes, best choice with SSG for content and ISR for blog. Limitation: Not ideal for highly-dynamic, minute-by-minute updating dashboards.

Should you use Next.js?

Next.js 15 is a mature framework that handles server rendering, static generation, and client-side interactivity well. Millions of production sites run on it, from small startups to companies like Netflix and TikTok.

It takes care of routing, optimization, rendering strategies, and deployment so you can focus on your actual product. For most web projects that need SEO and good performance, it's a strong default choice. Check out my Kinetiq case study to see how I used Next.js in a real project.

Need help with your Next.js project?

I build production-ready Next.js applications, from MVP to enterprise scale. I work with Next.js 15, React Server Components, TypeScript, and cloud deployment on Vercel, AWS, and Azure. If you need a hand, let's talk.

Related Articles

Complete Next.js 15 Guide - Modern React Framework Tutorial | Wojciechowski.app