Skip to main content
Back to Blog
Web Development15 min read

Getting Started with Next.js 15: The Complete 2025 Developer Guide

Kunal Chheda
Next.jsReactJavaScriptTypeScriptTutorialWeb Development2025
Getting Started with Next.js 15: The Complete 2025 Developer Guide

Getting Started with Next.js 15: The Complete 2025 Developer Guide

Next.js 15 represents the most significant evolution of the React framework since its inception. Released October 2024 and now stable as of December 2025, it brings React 19 support, a production-ready Turbopack, revolutionary Partial Prerendering, and fundamental changes to how we think about building web applications.

I've been using Next.js since version 9, and this version feels like the framework finally achieving what it always promised: the best developer experience with the best user experience. Let me show you everything you need to know.


What's Actually New in Next.js 15

Not just marketing bullet points - here's what genuinely matters:

The Big Changes (December 2025 Status)

FeatureStatusWhat It Means
React 19 Support✅ StableActions, use() hook, improved hydration
Turbopack✅ Stable (dev)700% faster cold starts, 96% faster HMR
Partial Prerendering✅ StableStatic + dynamic in same request
Async Request APIs✅ Requiredcookies(), headers() are now async
Caching Changes✅ Default offfetch(), Route Handlers no longer cached by default
Enhanced Security✅ StableServer Actions protection, improved CSRF handling

Breaking Changes You Need to Know

  1. cookies(), headers(), params, searchParams are now async - this will break existing code
  2. Default caching is OFF - explicit opt-in required
  3. Minimum React version is 19 - no backwards compatibility
  4. Node.js 18.18+ required - older versions won't work

Installation and Setup (December 2025)

Creating a New Project

# Recommended: Use create-next-app with all defaults
npx create-next-app@latest my-project

# Or with specific options
npx create-next-app@latest my-project --typescript --tailwind --eslint --app --src-dir --use-pnpm

The CLI will ask you:

✔ Would you like to use TypeScript? Yes
✔ Would you like to use ESLint? Yes
✔ Would you like to use Tailwind CSS? Yes
✔ Would you like your code inside a `src/` directory? Yes
✔ Would you like to use App Router? (recommended) Yes
✔ Would you like to use Turbopack for `next dev`? Yes
✔ Would you like to customize the import alias? No

#OPINION: Always say yes to TypeScript and the src/ directory. You'll thank yourself in 6 months when the project grows.

Upgrading from Next.js 14

# Automatic upgrade with codemods
npx @next/codemod@latest upgrade latest

# Or manual upgrade
npm install next@latest react@latest react-dom@latest

The codemod handles:

  • Async request API transformations
  • Deprecated API removals
  • Import path updates
  • Configuration migrations

Project Structure Deep Dive

my-project/
├── src/
│   ├── app/                    # App Router (routes)
│   │   ├── (auth)/            # Route groups (no URL impact)
│   │   │   ├── login/
│   │   │   │   └── page.tsx
│   │   │   └── register/
│   │   │       └── page.tsx
│   │   ├── api/               # API routes
│   │   │   └── webhooks/
│   │   │       └── route.ts
│   │   ├── blog/
│   │   │   ├── [slug]/        # Dynamic route
│   │   │   │   └── page.tsx
│   │   │   └── page.tsx
│   │   ├── layout.tsx         # Root layout (required)
│   │   ├── page.tsx           # Home page
│   │   ├── error.tsx          # Error boundary
│   │   ├── loading.tsx        # Loading UI
│   │   ├── not-found.tsx      # 404 page
│   │   └── globals.css
│   ├── components/            # Reusable components
│   │   ├── ui/               # shadcn/ui or similar
│   │   └── features/         # Feature-specific
│   ├── lib/                  # Utilities, configs
│   ├── hooks/                # Custom React hooks
│   └── types/                # TypeScript types
├── public/                   # Static assets
├── next.config.ts            # Next.js configuration
├── tailwind.config.ts        # Tailwind CSS
├── tsconfig.json             # TypeScript
└── package.json

Understanding the App Router

Every folder in app/ can have these special files:

FilePurpose
page.tsxUnique UI for this route (makes it publicly accessible)
layout.tsxShared layout wrapping children
template.tsxLike layout but re-mounts on navigation
loading.tsxLoading UI (Suspense boundary)
error.tsxError boundary (client-side)
not-found.tsx404 UI for this segment
route.tsAPI endpoint (GET, POST, etc.)
default.tsxParallel route fallback

Server Components vs Client Components

This is the mental model shift that trips up most developers.

Server Components (Default)

// app/blog/[slug]/page.tsx
// This runs ONLY on the server - no JS sent to client

import { db } from '@/lib/db';

export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  // Note: params is now a Promise in Next.js 15!
  const { slug } = await params;
  
  // Direct database access - no API needed
  const post = await db.post.findUnique({ where: { slug } });
  
  if (!post) notFound();
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Benefits:

  • Zero JavaScript for static content
  • Direct database/filesystem access
  • Better SEO (complete HTML)
  • Faster initial page load

Client Components

// components/like-button.tsx
'use client'; // This directive is required

import { useState, useTransition } from 'react';
import { likePost } from '@/app/actions';

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes);
  const [isPending, startTransition] = useTransition();
  
  const handleLike = () => {
    startTransition(async () => {
      const newLikes = await likePost(postId);
      setLikes(newLikes);
    });
  };
  
  return (
    <button onClick={handleLike} disabled={isPending}>
      {isPending ? 'Liking...' : `❤️ ${likes}`}
    </button>
  );
}

Use Client Components when you need:

  • useState, useEffect, useReducer
  • Browser APIs (localStorage, geolocation)
  • Event handlers (onClick, onChange)
  • Third-party libraries that use hooks

The Composition Pattern

// app/blog/[slug]/page.tsx (Server Component)
import { LikeButton } from '@/components/like-button';

export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
      {/* Client component embedded in Server component */}
      <LikeButton postId={post.id} initialLikes={post.likes} />
    </article>
  );
}

Server Actions (The Game Changer)

Server Actions eliminate the need for API routes in most cases. They're functions that run on the server but can be called from the client.

Defining Server Actions

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
import { z } from 'zod';

const CreatePostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
});

export async function createPost(formData: FormData) {
  // Validate input
  const validatedFields = CreatePostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });
  
  if (!validatedFields.success) {
    return { error: validatedFields.error.flatten().fieldErrors };
  }
  
  const { title, content } = validatedFields.data;
  
  // Insert into database
  const post = await db.post.create({
    data: { title, content }
  });
  
  // Revalidate cache and redirect
  revalidatePath('/blog');
  redirect(`/blog/${post.slug}`);
}

Using Server Actions in Forms

// app/blog/new/page.tsx
import { createPost } from '@/app/actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Post title" required />
      <textarea name="content" placeholder="Write your post..." required />
      <button type="submit">Publish</button>
    </form>
  );
}

Progressive Enhancement

Forms with Server Actions work without JavaScript:

  • Form submits as normal POST request
  • Server processes and redirects
  • If JS loads, enhances to prevent full page reload

#OPINION: Server Actions are the biggest quality-of-life improvement in Next.js. They make full-stack development feel seamless.


The New Async Request APIs

This is the breaking change that will affect most codebases.

Before (Next.js 14)

// ❌ This no longer works
import { cookies } from 'next/headers';

export default function Page() {
  const cookieStore = cookies();
  const theme = cookieStore.get('theme');
  // ...
}

After (Next.js 15)

// ✅ Must await the APIs
import { cookies, headers } from 'next/headers';

export default async function Page() {
  const cookieStore = await cookies();
  const headersList = await headers();
  
  const theme = cookieStore.get('theme');
  const userAgent = headersList.get('user-agent');
  // ...
}

Params and SearchParams

// ❌ Before
type Props = { params: { slug: string } };

// ✅ After - params is a Promise
type Props = { params: Promise<{ slug: string }> };

export default async function Page({ params, searchParams }: {
  params: Promise<{ slug: string }>;
  searchParams: Promise<{ sort?: string }>;
}) {
  const { slug } = await params;
  const { sort } = await searchParams;
  // ...
}

Why This Change?

Vercel explains this enables:

  1. Better static analysis
  2. Improved prerendering
  3. Foundation for Partial Prerendering

Caching: The New Defaults

Major shift: Next.js 15 defaults to NO caching. You must explicitly opt-in.

fetch() Caching

// ❌ Before: cached by default
fetch('https://api.example.com/data');

// ✅ After: NOT cached by default
fetch('https://api.example.com/data');

// ✅ Explicit caching
fetch('https://api.example.com/data', { cache: 'force-cache' });

// ✅ With revalidation
fetch('https://api.example.com/data', { next: { revalidate: 3600 } });

Route Handlers

// app/api/data/route.ts

// ❌ Before: GET was cached by default
// ✅ After: NOT cached by default

export async function GET() {
  const data = await fetchData();
  return Response.json(data);
}

// To cache, export config
export const dynamic = 'force-static';
// Or with revalidation
export const revalidate = 3600;

When to Cache What

Content TypeCache Strategy
Static pagesforce-static
API data that changes rarelyrevalidate: 3600 (1 hour)
User-specific dataNo cache (default)
Real-time dataNo cache (default)
Product listingsrevalidate: 60

#OPINION: The new defaults are correct. Implicit caching caused too many "why isn't my data updating?" bugs.


Turbopack: The Speed Revolution

Turbopack is now stable for next dev as of October 2024.

Enabling Turbopack

# In development
next dev --turbo

# Or in package.json
"scripts": {
  "dev": "next dev --turbo"
}

Performance Benchmarks (Real-World)

From the Vercel documentation:

MetricWebpackTurbopackImprovement
Cold start11.2s1.6s700% faster
Route compile800ms35ms96% faster
HMR (small change)450ms15ms97% faster
Memory usage1.8GB600MB66% less

Current Limitations (December 2025)

  • Production builds still use Webpack
  • Some plugins not yet compatible
  • Custom Webpack configs may need adjustment

#OPINION: Turbopack for dev is a game-changer. The HMR feels instant even on large codebases.


Partial Prerendering (PPR)

The future of Next.js rendering. Combines static and dynamic in the same request.

How It Works

// app/product/[id]/page.tsx
import { Suspense } from 'react';
import { ProductDetails } from '@/components/product-details';
import { ProductReviews } from '@/components/product-reviews';
import { AddToCartButton } from '@/components/add-to-cart';

// Static shell renders at build time
// Dynamic parts stream in at request time

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const product = await getProduct(id);
  
  return (
    <main>
      {/* Static - prerendered */}
      <ProductDetails product={product} />
      
      {/* Dynamic - streamed */}
      <Suspense fallback={<div>Loading reviews...</div>}>
        <ProductReviews productId={id} />
      </Suspense>
      
      {/* Dynamic - user-specific */}
      <Suspense fallback={<div>Loading cart...</div>}>
        <AddToCartButton productId={id} />
      </Suspense>
    </main>
  );
}

Enabling PPR

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  experimental: {
    ppr: true,
  },
};

export default nextConfig;

Per-Route Opt-In

// app/product/[id]/page.tsx
export const experimental_ppr = true;

TypeScript Configuration (2025 Best Practices)

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["dom", "dom.iterable", "ES2022"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Data Fetching Patterns

Direct Database Access (Recommended)

// lib/db.ts
import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const db = globalForPrisma.prisma || new PrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
// app/blog/page.tsx
import { db } from '@/lib/db';

export default async function BlogPage() {
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10,
  });
  
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

External API Fetching

// lib/api.ts
export async function getProducts() {
  const res = await fetch('https://api.store.com/products', {
    next: { revalidate: 60 }, // Cache for 60 seconds
  });
  
  if (!res.ok) throw new Error('Failed to fetch products');
  
  return res.json();
}

Parallel Data Fetching

export default async function DashboardPage() {
  // Fetch in parallel - don't await each sequentially!
  const [user, orders, notifications] = await Promise.all([
    getUser(),
    getOrders(),
    getNotifications(),
  ]);
  
  return (
    <Dashboard user={user} orders={orders} notifications={notifications} />
  );
}

Metadata and SEO

Static Metadata

// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: {
    default: 'My Website',
    template: '%s | My Website',
  },
  description: 'Building the future of the web',
  metadataBase: new URL('https://mywebsite.com'),
  openGraph: {
    type: 'website',
    siteName: 'My Website',
    images: ['/og-image.png'],
  },
  twitter: {
    card: 'summary_large_image',
    creator: '@myhandle',
  },
  robots: {
    index: true,
    follow: true,
  },
};

Dynamic Metadata

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): 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: [post.coverImage],
      type: 'article',
      publishedTime: post.createdAt,
      authors: [post.author.name],
    },
  };
}

Deployment to Vercel

Environment Variables

# .env.local (development)
DATABASE_URL="postgresql://..."
NEXTAUTH_SECRET="your-secret"

# Vercel dashboard (production)
# Add these in Settings > Environment Variables

next.config.ts for Production

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
    ],
  },
  experimental: {
    ppr: true,
  },
  logging: {
    fetches: {
      fullUrl: true,
    },
  },
};

export default nextConfig;

Build and Deploy

# Local build test
npm run build
npm start

# Deploy to Vercel
vercel

# Or connect to GitHub for automatic deploys

Common Mistakes to Avoid

1. Forgetting async on Request APIs

// ❌ Wrong
const cookieStore = cookies(); // Error!

// ✅ Correct
const cookieStore = await cookies();

2. Using Client-Side Hooks in Server Components

// ❌ Wrong - server component can't use useState
export default function Page() {
  const [count, setCount] = useState(0); // Error!
}

// ✅ Correct - mark as client component
'use client';
export default function Counter() {
  const [count, setCount] = useState(0);
}

3. Not Awaiting Params

// ❌ Wrong
export default function Page({ params }) {
  const slug = params.slug; // params is a Promise!
}

// ✅ Correct
export default async function Page({ params }) {
  const { slug } = await params;
}

4. Expecting Cached fetch by Default

// ❌ Expecting cache (won't work in Next.js 15)
const data = await fetch('/api/data');

// ✅ Explicit cache
const data = await fetch('/api/data', { cache: 'force-cache' });

Performance Checklist

  • Use Server Components for static content
  • Implement Suspense boundaries for streaming
  • Enable Turbopack for development
  • Configure proper caching strategies
  • Optimize images with next/image
  • Use dynamic imports for heavy components
  • Implement proper error boundaries
  • Add loading states for better UX
  • Use React 19's use() hook for async resources
  • Consider Partial Prerendering for mixed content

Resources and Next Steps

Official Documentation

Community Resources

Recommended Libraries (December 2025)

CategoryLibraryWhy
DatabasePrisma / DrizzleType-safe ORM
AuthNextAuth.js v5Built for App Router
FormsReact Hook Form + ZodType-safe validation
UIshadcn/uiComposable components
StateZustandLightweight, React 19 ready
APItRPCEnd-to-end type safety

Bibliography and Sources

  1. Next.js 15 Release Notes - Vercel, October 2024 - https://nextjs.org/blog/next-15
  2. React 19 Release - React Team, December 2024 - https://react.dev/blog/2024/12/05/react-19
  3. Turbopack Announcement - Vercel, October 2024 - https://turbo.build/pack/docs
  4. Partial Prerendering RFC - Next.js GitHub - https://github.com/vercel/next.js/discussions/58443
  5. App Router Documentation - Next.js - https://nextjs.org/docs/app
  6. Server Components RFC - React Working Group - https://github.com/reactwg/server-components

Last updated: December 2025. This guide is maintained to reflect the latest stable releases and best practices.