Building Production Apps with Next.js 15

Reverend Philip Nov 27, 2025 5 min read

Learn to build production-ready applications with Next.js 15 using App Router, Server Components, and modern React patterns.

Next.js has become the go-to framework for building production React applications. With version 15, it's more capable than ever. This guide covers what you need to know to build and deploy production-ready Next.js applications.

Why Next.js for Production

Next.js solves problems you'll inevitably face when building production React apps: server-side rendering, routing, code splitting, and deployment. Rather than assembling these pieces yourself, Next.js provides an integrated solution that works out of the box.

The framework's opinionated approach means less configuration and more consistency across projects. When you hire a developer who knows Next.js, they can contribute immediately because the project structure is predictable.

App Router vs Pages Router

Next.js 15 supports both the newer App Router and the legacy Pages Router. For new projects, use the App Router;it's the future of the framework and enables features like React Server Components.

The App Router uses a file-system based routing inside the app directory:

app/
├── page.tsx           # /
├── about/
│   └── page.tsx       # /about
├── blog/
│   ├── page.tsx       # /blog
│   └── [slug]/
│       └── page.tsx   # /blog/my-post
└── layout.tsx         # Shared layout

Each page.tsx file becomes a route. Folders with brackets like [slug] create dynamic segments. The layout.tsx file wraps child pages and persists across navigation.

Server Components and When to Use Them

React Server Components (RSC) are the headline feature of modern Next.js. They render on the server, send HTML to the client, and never ship their JavaScript to the browser.

Use Server Components by default. They're perfect for:

  • Fetching data from your database or API
  • Displaying static content
  • Components that don't need interactivity

Add the 'use client' directive only when you need:

  • Event handlers (onClick, onChange)
  • Browser APIs (localStorage, window)
  • React hooks (useState, useEffect)
// Server Component (default)
async function ProductList() {
  const products = await db.products.findMany();
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

// Client Component
'use client'
function AddToCartButton({ productId }) {
  const [loading, setLoading] = useState(false);

  async function handleClick() {
    setLoading(true);
    await addToCart(productId);
    setLoading(false);
  }

  return <button onClick={handleClick}>{loading ? 'Adding...' : 'Add to Cart'}</button>;
}

Data Fetching Patterns

In the App Router, data fetching happens directly in Server Components using async/await. No special hooks required:

async function BlogPost({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`);
  return <article>{post.content}</article>;
}

Next.js extends the native fetch API with caching and revalidation:

// Cache forever (static)
fetch(url, { cache: 'force-cache' });

// Never cache (dynamic)
fetch(url, { cache: 'no-store' });

// Revalidate every hour
fetch(url, { next: { revalidate: 3600 } });

For database queries without fetch, use the unstable_cache function or route segment configuration to control caching behavior.

API Routes and Server Actions

API routes live in the app/api directory. Each route.ts file exports functions for HTTP methods:

// app/api/users/route.ts
export async function GET(request: Request) {
  const users = await db.users.findMany();
  return Response.json(users);
}

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

Server Actions let you call server-side functions directly from client components without creating API routes:

// actions.ts
'use server'
export async function createPost(formData: FormData) {
  const title = formData.get('title');
  await db.posts.create({ data: { title } });
  revalidatePath('/posts');
}

// Component
<form action={createPost}>
  <input name="title" />
  <button type="submit">Create</button>
</form>

Performance Optimization

Next.js includes several built-in optimizations. Use them:

Image Component: Automatically optimizes images, serves modern formats, and handles lazy loading.

import Image from 'next/image';
<Image src="/hero.jpg" width={1200} height={600} alt="Hero" />

Font Optimization: Self-hosts fonts and eliminates layout shift.

import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });

Route Prefetching: Links are automatically prefetched when they enter the viewport. Users experience instant navigation.

Static Generation: Pages without dynamic data are pre-rendered at build time. Add generateStaticParams for dynamic routes you want to pre-render.

Deployment Options

Vercel is the path of least resistance. Push to GitHub and your app deploys automatically with edge functions, image optimization, and analytics included.

Self-hosted works too. Run next build and next start on any Node.js server. You'll need to handle CDN, SSL, and scaling yourself.

Docker deployment:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
CMD ["node", "server.js"]

Set output: 'standalone' in your Next.js config to enable this optimized Docker build.

Common Pitfalls

Over-using 'use client': Start with Server Components and only add the client directive when needed. It's easy to accidentally make your entire tree client-rendered.

Not understanding caching: Next.js caches aggressively. Learn the caching layers (request memoization, data cache, full route cache) and how to invalidate them.

Ignoring the build output: Check next build output regularly. It tells you which routes are static vs dynamic and warns about common issues.

Forgetting environment variables: Client-side environment variables must be prefixed with NEXT_PUBLIC_. Server-only variables stay secret.

Conclusion

Next.js 15 provides everything you need to build production React applications. The App Router and Server Components represent a significant shift in how we build React apps, moving more work to the server and reducing client-side JavaScript.

Start with Server Components by default, add interactivity where needed, and leverage the built-in optimizations. The framework handles the complexity so you can focus on building your product.

Share this article

Related Articles

Need help with your project?

Let's discuss how we can help you build reliable software.