JavaScriptTypeScript

Building a React App with Next.js 14

Introduction

Modern React applications demand fast load times, strong SEO, and a smooth developer experience. Next.js 14 builds on these needs by combining React Server Components, the App Router, and powerful data-fetching patterns into a single framework. Whether you’re building a marketing site, dashboard, or full-stack application, Next.js 14 provides the foundation for production-ready React apps. In this comprehensive guide, you will learn how to build a React app with Next.js 14 from scratch, understand its core concepts including Server Components and the App Router, implement data fetching patterns, and apply best practices for scalable and performant applications.

Why Choose Next.js 14

Next.js extends React with features that solve common production problems. Many teams adopt it as their default React framework because it eliminates the need to configure routing, bundling, and rendering strategies manually.

  • Built-in routing with the App Router and file-system based routes
  • Server-side rendering and static generation out of the box
  • React Server Components by default for smaller bundles
  • Optimized data fetching with automatic caching and deduplication
  • Excellent SEO support with per-route metadata
  • Production-ready performance without additional configuration

Project Setup

# Create a new Next.js 14 app
npx create-next-app@latest my-app

# Options during setup:
# ✔ 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 to use `src/` directory? Yes
# ✔ Would you like to use App Router? Yes
# ✔ Would you like to customize the default import alias? No

cd my-app
npm run dev

Project Structure

my-app/
├── src/
│   ├── app/                    # App Router directory
│   │   ├── layout.tsx          # Root layout
│   │   ├── page.tsx            # Home page (/)
│   │   ├── globals.css         # Global styles
│   │   ├── loading.tsx         # Loading UI
│   │   ├── error.tsx           # Error boundary
│   │   ├── not-found.tsx       # 404 page
│   │   ├── blog/
│   │   │   ├── page.tsx        # /blog
│   │   │   └── [slug]/
│   │   │       └── page.tsx    # /blog/[slug]
│   │   ├── dashboard/
│   │   │   ├── layout.tsx      # Dashboard layout
│   │   │   └── page.tsx        # /dashboard
│   │   └── api/
│   │       └── route.ts        # API route
│   ├── components/             # React components
│   │   ├── ui/                 # UI components
│   │   └── features/           # Feature components
│   └── lib/                    # Utilities and helpers
├── public/                     # Static assets
├── next.config.js              # Next.js configuration
├── tailwind.config.ts          # Tailwind configuration
└── tsconfig.json               # TypeScript configuration

The App Router

The App Router is the foundation of modern Next.js applications. Instead of defining routes manually, the file system becomes the router.

Root Layout

// src/app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: {
    default: 'My App',
    template: '%s | My App',
  },
  description: 'A modern Next.js application',
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://myapp.com',
    siteName: 'My App',
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    
      
        
{children}
{/* Footer */}
); }

Pages and Routes

// src/app/page.tsx - Home page (/)
export default function HomePage() {
  return (
    

Welcome to My App

A modern Next.js 14 application

); } // src/app/blog/page.tsx - Blog listing (/blog) import Link from 'next/link'; import { getPosts } from '@/lib/posts'; export const metadata = { title: 'Blog', description: 'Read our latest articles', }; export default async function BlogPage() { const posts = await getPosts(); return (

Blog

{posts.map((post) => (

{post.title}

{post.excerpt}

))}
); } // src/app/blog/[slug]/page.tsx - Dynamic blog post (/blog/[slug]) import { getPost, getPosts } from '@/lib/posts'; import { notFound } from 'next/navigation'; import type { Metadata } from 'next'; interface Props { params: { slug: string }; } // Generate static params for all posts export async function generateStaticParams() { const posts = await getPosts(); return posts.map((post) => ({ slug: post.slug })); } // Generate dynamic metadata export async function generateMetadata({ params }: Props): Promise { const post = await getPost(params.slug); if (!post) { return { title: 'Post Not Found' }; } return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, type: 'article', publishedTime: post.date, }, }; } export default async function BlogPostPage({ params }: Props) { const post = await getPost(params.slug); if (!post) { notFound(); } return (

{post.title}

); }

Server Components vs Client Components

Next.js 14 uses React Server Components by default. This means most components run on the server unless explicitly marked as client components.

// Server Component (default) - No 'use client' directive
// Can: fetch data, access backend resources, keep secrets
// Cannot: use hooks, browser APIs, event handlers

async function ProductList() {
  // This runs on the server
  const products = await db.products.findMany();

  return (
    
    {products.map((product) => (
  • {product.name}
  • ))}
); } // Client Component - Has 'use client' directive // Can: use hooks, browser APIs, event handlers // Cannot: directly fetch server-only data 'use client'; import { useState } from 'react'; export function Counter() { const [count, setCount] = useState(0); return ( ); } // Mixing Server and Client Components // Server Component can render Client Component // Client Component cannot import Server Component directly import { Counter } from '@/components/Counter'; async function Dashboard() { const stats = await getStats(); // Server-side fetch return (

Dashboard

Total Users: {stats.users}

{/* Client Component */}
); }

Data Fetching Patterns

// src/lib/posts.ts
export interface Post {
  slug: string;
  title: string;
  excerpt: string;
  content: string;
  date: string;
}

const API_URL = process.env.API_URL;

// Static data - cached indefinitely (default)
export async function getPosts(): Promise {
  const res = await fetch(`${API_URL}/posts`, {
    // This is the default - data is cached
    cache: 'force-cache',
  });

  if (!res.ok) throw new Error('Failed to fetch posts');
  return res.json();
}

// Dynamic data - no caching
export async function getPost(slug: string): Promise {
  const res = await fetch(`${API_URL}/posts/${slug}`, {
    // Fetch fresh data on every request
    cache: 'no-store',
  });

  if (!res.ok) return null;
  return res.json();
}

// Revalidated data - ISR pattern
export async function getProducts() {
  const res = await fetch(`${API_URL}/products`, {
    // Revalidate every 60 seconds
    next: { revalidate: 60 },
  });

  return res.json();
}

// Tagged revalidation
export async function getProductById(id: string) {
  const res = await fetch(`${API_URL}/products/${id}`, {
    next: { tags: [`product-${id}`] },
  });

  return res.json();
}

// Trigger revalidation from API route or Server Action
import { revalidateTag, revalidatePath } from 'next/cache';

export async function updateProduct(id: string, data: any) {
  await db.products.update({ where: { id }, data });

  // Revalidate specific tag
  revalidateTag(`product-${id}`);

  // Or revalidate entire path
  revalidatePath('/products');
}

Server Actions

Server Actions allow you to run server-side code directly from components without creating API routes.

// src/app/contact/page.tsx
import { submitContactForm } from './actions';

export default function ContactPage() {
  return (