Home/Blog/Article
Web Development

Next.js 15: App Router, Server Components, and Streaming SSR

Explore Next.js 15's powerful features including the App Router, React Server Components, and streaming SSR. Build faster, more efficient web applications with modern React patterns.

Nov 27, 2025
13 min read

About the Author

S
Sarah Mitchell
Lead Frontend Architect

Sarah is a frontend architect with 10+ years of experience building scalable React applications. She's passionate about developer experience and modern web standards.

Need Expert Help?

Let's discuss how we can help bring your project to life with our web development expertise.


Introduction


Next.js 15 represents a major leap forward in React application development, introducing revolutionary features like the App Router, React Server Components, and enhanced streaming capabilities. This guide explores these features and how they transform modern web development.


The App Router: A New Paradigm


The App Router is Next.js's new routing system built on React Server Components, providing enhanced performance and developer experience.


File-based Routing Structure


app/
├── layout.tsx          # Root layout
├── page.tsx           # Homepage route: /
├── about/
│   └── page.tsx       # Route: /about
├── blog/
│   ├── page.tsx       # Route: /blog
│   └── [id]/
│       └── page.tsx   # Dynamic route: /blog/:id
└── api/
    └── users/
        └── route.ts   # API route: /api/users

Creating Routes


// app/dashboard/page.tsx
export default function DashboardPage() {
  return <h1>Dashboard</h1>;
}

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <div className="dashboard">
      <nav>{/* Dashboard navigation */}</nav>
      <main>{children}</main>
    </div>
  );
}

React Server Components


Server Components render on the server, reducing client-side JavaScript and improving performance.


Server vs Client Components


// app/components/ServerComponent.tsx (Default)
// Runs on server, no interactivity
async function ServerComponent() {
  const data = await fetch('https://api.example.com/data');
  return <div>{/* Render data */}</div>;
}

// app/components/ClientComponent.tsx
'use client'; // Mark as client component

import { useState } from 'react';

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

Data Fetching in Server Components


// Server Component with async/await
async function ProductList() {
  // Fetch happens on server
  const products = await db.product.findMany();

  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// With caching
async function CachedData() {
  const data = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 } // Cache for 1 hour
  });

  return <div>{/* Use data */}</div>;
}

Streaming and Suspense


Stream content as it's ready instead of waiting for everything.


Implementing Streaming


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

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* Fast content renders immediately */}
      <UserInfo />

      {/* Slow content streams in */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <Analytics />
      </Suspense>

      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>
    </div>
  );
}

// Slow component (Server Component)
async function Analytics() {
  // Slow database query
  const analytics = await getAnalytics();
  return <div>{/* Render analytics */}</div>;
}

Loading and Error Handling


loading.tsx


// app/dashboard/loading.tsx
export default function Loading() {
  return <LoadingSpinner />;
}

// Automatically wraps page in Suspense

error.tsx


// app/dashboard/error.tsx
'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>
  );
}

Metadata and SEO


// Static metadata
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'My Page',
  description: 'Page description',
};

// Dynamic metadata
export async function generateMetadata({ params }: {
  params: { id: string }
}): Promise<Metadata> {
  const product = await getProduct(params.id);

  return {
    title: product.name,
    description: product.description,
    openGraph: {
      images: [product.image],
    },
  };
}

Route Handlers (API Routes)


// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const users = await db.user.findMany();
  return NextResponse.json(users);
}

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

// Dynamic routes
// app/api/users/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const user = await db.user.findUnique({
    where: { id: params.id }
  });
  return NextResponse.json(user);
}

Parallel and Sequential Data Fetching


// Sequential (waterfall)
async function Sequential() {
  const user = await getUser();
  const posts = await getPosts(user.id); // Waits for user
  return <div>{/* Render */}</div>;
}

// Parallel (faster)
async function Parallel() {
  const [user, posts] = await Promise.all([
    getUser(),
    getPosts(),
  ]);
  return <div>{/* Render */}</div>;
}

Middleware


// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Authentication
  const token = request.cookies.get('token');

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // Add custom header
  const response = NextResponse.next();
  response.headers.set('x-custom-header', 'value');
  return response;
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

Best Practices


1. Composition Pattern


// Server Component
async function Page() {
  const data = await getData();

  return (
    <div>
      {/* Pass data to client component */}
      <ClientComponent data={data} />
    </div>
  );
}

// Client Component
'use client';
function ClientComponent({ data }: { data: Data }) {
  const [selected, setSelected] = useState(data[0]);
  return <div>{/* Interactive UI */}</div>;
}

2. Colocate Fetch Requests


// In the component that needs the data
async function ProductDetail({ id }: { id: string }) {
  const product = await getProduct(id);
  return <div>{product.name}</div>;
}

// Not at the top level

3. Use Streaming for Long Tasks


<Suspense fallback={<Skeleton />}>
  <SlowComponent />
</Suspense>

Resources


  • [Next.js Documentation](https://nextjs.org/docs)
  • [React Server Components](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#react-server-components)
  • [App Router Migration](https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration)

  • Stay Updated

    Subscribe to Our Newsletter

    Get the latest articles, insights, and updates delivered directly to your inbox. Join our community of developers and tech enthusiasts.

    We respect your privacy. Unsubscribe at any time.