Next.js 14 Tutorial: Complete Guide with App Router
Master Next.js 14 with App Router. Learn server components, data fetching, routing, server actions, and build full-stack React applications.
Moshiour Rahman
Advertisement
What is Next.js 14?
Next.js 14 is a React framework for building full-stack web applications. It features the App Router with React Server Components, server actions, and powerful data fetching patterns.
Key Features
| Feature | Benefit |
|---|---|
| App Router | File-based routing with layouts |
| Server Components | Better performance, smaller bundles |
| Server Actions | Form handling without API routes |
| Streaming | Progressive page loading |
| Partial Prerendering | Static + dynamic in one route |
Installation
npx create-next-app@latest my-app
cd my-app
npm run dev
Project Structure
my-app/
├── app/
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page (/)
│ ├── loading.tsx # Loading UI
│ ├── error.tsx # Error UI
│ ├── not-found.tsx # 404 page
│ ├── globals.css
│ ├── about/
│ │ └── page.tsx # /about
│ ├── blog/
│ │ ├── page.tsx # /blog
│ │ └── [slug]/
│ │ └── page.tsx # /blog/[slug]
│ └── api/
│ └── route.ts # API routes
├── components/
├── lib/
├── public/
└── next.config.js
Routing
Basic Pages
// app/page.tsx - Home page
export default function Home() {
return (
<main>
<h1>Welcome to Next.js 14</h1>
</main>
);
}
// app/about/page.tsx - About page
export default function About() {
return <h1>About Us</h1>;
}
Dynamic Routes
// app/blog/[slug]/page.tsx
interface Props {
params: { slug: string };
}
export default function BlogPost({ params }: Props) {
return <h1>Blog Post: {params.slug}</h1>;
}
// Generate static params for SSG
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
Catch-All Routes
// app/docs/[...slug]/page.tsx
// Matches /docs/a, /docs/a/b, /docs/a/b/c
interface Props {
params: { slug: string[] };
}
export default function Docs({ params }: Props) {
return <div>Docs: {params.slug.join('/')}</div>;
}
Route Groups
// app/(marketing)/about/page.tsx
// app/(marketing)/contact/page.tsx
// app/(shop)/products/page.tsx
// Route groups don't affect URL structure
// Use for organizing and sharing layouts
Layouts
Root Layout
// app/layout.tsx
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'My App',
description: 'Built with Next.js 14',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<nav>Navigation</nav>
{children}
<footer>Footer</footer>
</body>
</html>
);
}
Nested Layouts
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<aside>Sidebar</aside>
<main>{children}</main>
</div>
);
}
Server Components
Default Behavior
// Server Component by default
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function Posts() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Client Components
// Add 'use client' directive
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Data Fetching
Server-Side Fetching
// Fetch in Server Component
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache', // Default - SSG
// cache: 'no-store', // SSR
// next: { revalidate: 60 }, // ISR
});
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{JSON.stringify(data)}</div>;
}
Parallel Data Fetching
export default async function Page() {
// Fetch in parallel
const [posts, users] = await Promise.all([
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/users').then(r => r.json()),
]);
return (
<div>
<Posts posts={posts} />
<Users users={users} />
</div>
);
}
Revalidation
// Time-based revalidation
export const revalidate = 3600; // Revalidate every hour
// On-demand revalidation
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(request: Request) {
const { path, tag } = await request.json();
if (path) revalidatePath(path);
if (tag) revalidateTag(tag);
return Response.json({ revalidated: true });
}
Server Actions
Form Handling
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.create({
data: { title, content },
});
revalidatePath('/posts');
}
// app/posts/new/page.tsx
import { createPost } from '../actions';
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<button type="submit">Create Post</button>
</form>
);
}
With Validation
'use server';
import { z } from 'zod';
const PostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(10),
});
export async function createPost(formData: FormData) {
const validatedFields = PostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
});
if (!validatedFields.success) {
return { error: validatedFields.error.flatten().fieldErrors };
}
// Create post...
}
Client-Side Usage
'use client';
import { useTransition } from 'react';
import { createPost } from './actions';
export default function PostForm() {
const [isPending, startTransition] = useTransition();
const handleSubmit = (formData: FormData) => {
startTransition(async () => {
await createPost(formData);
});
};
return (
<form action={handleSubmit}>
<input name="title" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create'}
</button>
</form>
);
}
Loading and Error States
Loading UI
// app/posts/loading.tsx
export default function Loading() {
return <div>Loading posts...</div>;
}
// Or with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<Posts />
</Suspense>
);
}
Error Handling
// app/posts/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>
);
}
API Routes
Route Handlers
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const posts = await db.post.findMany();
return NextResponse.json(posts);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await db.post.create({ data: body });
return NextResponse.json(post, { status: 201 });
}
Dynamic API Routes
// app/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await db.post.findUnique({
where: { id: params.id },
});
if (!post) {
return NextResponse.json(
{ error: 'Not found' },
{ status: 404 }
);
}
return NextResponse.json(post);
}
Middleware
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Check authentication
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Add headers
const response = NextResponse.next();
response.headers.set('x-custom-header', 'value');
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
Metadata and SEO
// Static metadata
export const metadata = {
title: 'My Page',
description: 'Page description',
openGraph: {
title: 'My Page',
description: 'Page description',
images: ['/og-image.jpg'],
},
};
// Dynamic metadata
export async function generateMetadata({ params }: Props) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
}
Image Optimization
import Image from 'next/image';
export default function Page() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Load immediately
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
);
}
Summary
| Feature | Description |
|---|---|
| App Router | File-based routing with layouts |
| Server Components | Default, better performance |
| Server Actions | Form handling without API |
| Streaming | Progressive loading |
| Caching | Built-in with revalidation |
Next.js 14 provides the best developer experience for building modern React applications.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
React Hooks Complete Guide: useState to Custom Hooks
Master all React hooks from basics to advanced. Learn useState, useEffect, useContext, useReducer, useMemo, useCallback, and create custom hooks.
JavaScriptTypeScript Advanced Guide: Types, Generics, and Patterns
Master advanced TypeScript concepts. Learn generics, utility types, conditional types, mapped types, and professional patterns for type-safe code.
JavaScripttRPC: End-to-End Type-Safe APIs for TypeScript
Master tRPC for full-stack TypeScript applications. Learn procedures, routers, React Query integration, and build type-safe APIs without schemas.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.