Beta Acid logo

Next.js 15: A Deep Dive into Modern Web Development

Development

Otavio Barros

December 13, 2024

12 min read

Choosing the right web framework can significantly impact your project's success. Next.js has emerged as a powerhouse in the React ecosystem, offering a robust solution for building modern web applications. As we explore Next.js 15, let's understand why developers and businesses increasingly rely on this framework.

In this post we'll explore significant improvements in Next’s routing, server components, caching behavior, and performance optimizations. These updates continue to reshape how we approach modern web development, making it easier to build faster, more reliable applications.

Let's start by examining the revolutionary App Router, followed by deep dives into server components, caching strategies, and solutions to common performance challenges.

The Power of App Router

Next.js 15's App Router represents a new approach to application structure, building upon the stable foundation introduced in version 13. At its core, the App Router uses a file-system based routing that transforms how we organize and build web applications.

Root Layout Structure

The foundation of any Next.js application starts with a root layout:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <header>Global Navigation</header>
        <main>{children}</main>
        <footer>Global Footer</footer>
      </body>
    </html>
  );
}

This root layout wraps every page in your application, providing consistent UI elements across all routes.

Intuitive Folder Structure

The App Router follows a predictable folder structure where each folder represents a route segment:

app/
├── layout.tsx
├── page.tsx
├── dashboard/
│   ├── layout.tsx
│   ├── page.tsx
│   └── settings/
│       └── page.tsx
└── blog/
    ├── layout.tsx
    └── [slug]/
        └── page.tsx

Each page.tsx file automatically becomes a route, while layout.tsx files create a shared UI for their respective segments.

Nested Layouts

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="dashboard-container">
      <aside>
        <nav>Dashboard Navigation</nav>
      </aside>
      <section className="content">{children}</section>
    </div>
  );
}

This layout will wrap all pages within the dashboard section while preserving its state during navigation between dashboard routes.

Special Files

The App Router introduces special files that handle common scenarios:

  • loading.tsx for suspense boundaries
  • error.tsx for error handling
  • not-found.tsx for 404 cases
  • page.tsx for route UI
// app/dashboard/loading.tsx
export default function Loading() {
  return <div className="dashboard-loader">Loading...</div>;
}

These files automatically create optimal loading and error states for your application.

The App Router's architecture naturally leads us to Server Components, which form the backbone of Next.js 15's performance optimizations. These components, rendered entirely on the server, work seamlessly with the App Router to deliver exceptional performance and developer experience.

Server Components and Actions in Next.js 15

Next.js 15 enhances both Server Components and Server Actions, providing developers with powerful tools for building efficient, secure applications. Let's explore these features and their practical applications.

Server Components

Server Components fundamentally change how we build React applications by moving component rendering to the server. Here's a practical example:

// app/products/page.tsx
async function ProductList() {
  const products = await fetchProducts(); // Server-side data fetching

  return (
    <div className="grid">
      {products.map((product) => (
        <ProductCard
          key={product.id}
          product={product}
          // Sensitive data can be safely passed
          internalMetrics={product.metrics}
        />
      ))}
    </div>
  );
}

Key Benefits:

  • Data fetching occurs closer to the data source
  • Sensitive information remains server-side
  • Reduced client-side JavaScript bundle
  • Automatic caching and optimization

Advanced Server Component Patterns

Streaming with Suspense:

// app/dashboard/page.tsx
import { Suspense } from "react";

export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<UserProfileSkeleton />}>
        <UserProfile />
      </Suspense>
      <Suspense fallback={<AnalyticsSkeleton />}>
        <AnalyticsChart />
      </Suspense>
    </div>
  );
}

Parallel Data Fetching:

// app/profile/page.tsx
async function ProfilePage() {
  // These requests run in parallel
  const [userData, userPosts, userAnalytics] = await Promise.all([
    fetchUserData(),
    fetchUserPosts(),
    fetchUserAnalytics(),
  ]);

  return (
    <div>
      <UserInfo data={userData} />
      <UserPosts posts={userPosts} />
      <UserStats analytics={userAnalytics} />
    </div>
  );
}

Server Actions

Server Actions provide a secure way to handle server-side mutations. Next.js 15 introduces enhanced security features and better integration with forms.

Basic Form Handling:

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

export async function updateProfile(formData: FormData) {
  const name = formData.get("name");
  const email = formData.get("email");

  await db.user.update({
    where: { id: session.user.id },
    data: { name, email },
  });
}

// app/profile/page.tsx
export default function ProfileForm() {
  return (
    <form action={updateProfile}>
      <input name="name" type="text" />
      <input name="email" type="email" />
      <button type="submit">Update Profile</button>
    </form>
  );
}

Advanced Server Action Pattern:

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

export async function handleOrder(formData: FormData) {
  const items = JSON.parse(formData.get("items") as string);

  try {
    // Start transaction
    const order = await db.$transaction(async (tx) => {
      // Check inventory
      await validateInventory(items);
      // Process payment
      const payment = await processPayment(items);
      // Create order
      return await createOrder(items, payment);
    });

    revalidatePath("/orders");
    return { success: true, orderId: order.id };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

Client-Side Integration:

"use client";

import { handleOrder } from "./actions";
import { useTransition } from "react";

export function CheckoutButton({ items }) {
  const [isPending, startTransition] = useTransition();

  const processCheckout = () => {
    startTransition(async () => {
      const formData = new FormData();
      formData.append("items", JSON.stringify(items));

      const result = await handleOrder(formData);
      if (result.success) {
        // Handle success
      }
    });
  };

  return (
    <button onClick={processCheckout} disabled={isPending}>
      {isPending ? "Processing..." : "Checkout"}
    </button>
  );
}

Server Components and Actions in Next.js 15 provide a robust foundation for building modern web applications. They enable developers to create performant, secure, and maintainable applications while keeping sensitive operations server-side. The next section will explore how Next.js 15 handles caching to further optimize application performance.

Understanding Caching in Next.js 15

Next.js 15 introduces significant changes to its caching behavior, moving from an opinionated cached-by-default approach to giving developers more explicit control over caching strategies.

Default Behavior

In version 15, requests are no longer cached by default. This provides more predictable behavior and better control over data freshness. Here's how to implement different caching strategies:

Time-based Caching

// Basic fetch with cache
async function getProducts() {
  const products = await fetch("https://api.store.com/products", {
    cache: "force-cache",
    next: { revalidate: 3600 }, // Revalidate every hour
  });

  return products.json();
}

// Using route segment config
export const revalidate = 3600; // Page-level revalidation

export default async function ProductsPage() {
  const products = await getProducts();
  return (
    <div className="products-grid">
      {products.map((product) => (
        <ProductCard key={product.id} {...product} />
      ))}
    </div>
  );
}

On-Demand Revalidation:

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

import { revalidatePath } from "next/cache";

export async function updateProduct(formData: FormData) {
  await saveProduct(formData);

  // Revalidate the products page and all its subpages
  revalidatePath("/products");
}

Using Tag-based Revalidation:

// Fetching with tags
async function getProduct(id: string) {
  const product = await fetch(`https://api.store.com/products/${id}`, {
    next: { tags: ["products"] },
  });
  return product.json();
}

// Revalidating tagged data
export async function updateProducts() {
  "use server";
  await updateProductInDatabase();
  revalidateTag("products");
}

Request Deduplication

Next.js automatically deduplicates identical requests during the same render pass. Here's how to leverage this:


// This component can be used multiple times, but only one request will be made
async function ProductInfo({ id }: { id: string }) {
  const product = await fetch(`https://api.store.com/products/${id}`, {
    cache: "force-cache",
  });

  return product.json();
}

// Multiple instances will trigger only one request
export default async function Page() {
  return (
    <>
      <ProductInfo id="1" />
      <ProductInfo id="1" /> {/* Reuses cached data */}
      <OtherComponent>
        <ProductInfo id="1" /> {/* Still reuses cached data */}
      </OtherComponent>
    </>
  );
}

Advanced Caching Patterns


// Combining multiple caching strategies
async function getDashboardData() {
  const [
    products = await fetch("/api/products", { cache: "force-cache" }),
    recentOrders = await fetch("/api/orders/recent", { cache: "no-store" }),
    analytics = await fetch("/api/analytics", { next: { revalidate: 300 } }),
  ] = await Promise.all([getProducts(), getRecentOrders(), getAnalytics()]);

  return {
    products: await products.json(),
    recentOrders: await recentOrders.json(),
    analytics: await analytics.json(),
  };
}

This caching system in Next.js 15 provides developers with fine-grained control over data freshness while maintaining optimal performance through automatic request deduplication and efficient cache invalidation strategies.

What's New in Next.js 15

Next.js 15 introduces several significant improvements that enhance both development experience and application performance.

React 19 Support

The framework now fully supports React 19 RC, bringing access to new React features and improved hydration error handling. Hydration errors now display detailed source code information with actionable suggestions for resolution.

Turbopack Stability

Turbopack has reached stability in development mode, delivering impressive performance gains:

  • 76.7% faster local server startup
  • 96.3% faster code updates with Fast Refresh
  • 45.8% faster initial route compilation

Enhanced Security

Server Actions received significant security improvements:

  • Automatic elimination of unused Server Actions
  • Non-deterministic, unguessable IDs for client-side references
  • Improved runtime configuration inheritance

Development Experience

  • New Static Route Indicator helps identify static routes during development
  • TypeScript support for next.config.ts
  • ESLint 9 support while maintaining compatibility with ESLint 8

Self-hosting Improvements

  • Better control over Cache-Control directives
  • Configurable expireTime value in next.config
  • Automatic sharp integration for image optimization

These updates collectively make Next.js 15 a more robust, secure, and developer-friendly framework.

Final Thoughts

Next.js 15 marks a significant evolution in modern web development, introducing crucial improvements in routing, server-side rendering, and caching mechanisms. The framework maintains its position as a leading choice for building performant web applications while providing developers with more explicit control over their applications' behavior.

Whether you're building a new project or considering an upgrade, Next.js 15's enhanced features and improved developer experience make it a compelling choice for teams focused on building fast, reliable, and maintainable web applications.

The future of web development continues to evolve, and Next.js remains at the forefront of this evolution, balancing innovation with practical development needs.

SHARE THIS STORY

Get. Shit. Done. 👊

Whether your ideas are big or small, we know you want it built yesterday. With decades of experience working at, or with, startups, we know how to get things built fast, without compromising scalability and quality.

Get in touch

Whether your plans are big or small, together, we'll get it done.

Let's get a conversation going. Shoot an email over to projects@betaacid.co, or do things the old fashioned way and fill out the handy dandy form below.

Beta Acid is a software development agency based in New York City and Barcelona.


hi@betaacid.co

About us
locations

New York City

77 Sands St, Brooklyn, NY

Barcelona

C/ Calàbria 149, Entresòl, 1ª, Barcelona, Spain

London

90 York Way, London, UK

© 2025 Beta Acid, Inc. All rights reserved.