// Next.js 14 App Router - Server & Client Components
// ============================================
// Server Component (Default) - layout.tsx
// ============================================
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My App',
description: 'Built with Next.js 14',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="min-h-screen bg-gray-50">
<nav className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 py-3">
<a href="/" className="text-xl font-bold text-gray-900">MyApp</a>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 py-8">
{children}
</main>
</body>
</html>
);
}
// ============================================
// Server Component with Data Fetching - page.tsx
// ============================================
interface Post {
id: number;
title: string;
body: string;
}
async function getPosts(): Promise<Post[]> {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10', {
next: { revalidate: 60 } // Revalidate every 60 seconds
});
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export default async function HomePage() {
const posts = await getPosts();
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold">Latest Posts</h1>
<div className="grid gap-4 md:grid-cols-2">
{posts.map((post) => (
<article key={post.id} className="p-6 bg-white rounded-lg shadow">
<h2 className="text-xl font-semibold mb-2">{post.title}</h2>
<p className="text-gray-600">{post.body}</p>
</article>
))}
</div>
</div>
);
}
// ============================================
// Client Component - Counter.tsx
// ============================================
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div className="flex items-center gap-4">
<button
onClick={() => setCount(c => c - 1)}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
-
</button>
<span className="text-2xl font-bold">{count}</span>
<button
onClick={() => setCount(c => c + 1)}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
+
</button>
</div>
);
}
// ============================================
// Server Action - actions.ts
// ============================================
'use server';
import { revalidatePath } from 'next/cache';
interface FormState {
success: boolean;
message: string;
}
export async function createPost(prevState: FormState, formData: FormData): Promise<FormState> {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
// Validate
if (!title || title.length < 3) {
return { success: false, message: 'Title must be at least 3 characters' };
}
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// In real app: save to database
console.log('Creating post:', { title, body });
// Revalidate the posts page
revalidatePath('/');
return { success: true, message: 'Post created successfully!' };
}
// ============================================
// Form with Server Action - CreatePostForm.tsx
// ============================================
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createPost } from './actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{pending ? 'Creating...' : 'Create Post'}
</button>
);
}
export function CreatePostForm() {
const [state, action] = useFormState(createPost, { success: false, message: '' });
return (
<form action={action} className="space-y-4 max-w-md">
{state.message && (
<div className={`p-3 rounded ${state.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{state.message}
</div>
)}
<div>
<label htmlFor="title" className="block text-sm font-medium mb-1">Title</label>
<input
type="text"
id="title"
name="title"
required
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="body" className="block text-sm font-medium mb-1">Content</label>
<textarea
id="body"
name="body"
rows={4}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<SubmitButton />
</form>
);
}
// ============================================
// API Route - app/api/posts/route.ts
// ============================================
import { NextRequest, NextResponse } from 'next/server';
const posts: Post[] = [];
export async function GET() {
return NextResponse.json(posts);
}
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.title) {
return NextResponse.json({ error: 'Title required' }, { status: 400 });
}
const post: Post = {
id: posts.length + 1,
title: body.title,
body: body.body || '',
};
posts.push(post);
return NextResponse.json(post, { status: 201 });
}
// ============================================
// Middleware - middleware.ts
// ============================================
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Add timing header
const response = NextResponse.next();
response.headers.set('x-middleware-timestamp', Date.now().toString());
// Redirect logic example
if (request.nextUrl.pathname === '/old-page') {
return NextResponse.redirect(new URL('/new-page', request.url));
}
// Auth check example
const token = request.cookies.get('token');
if (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return response;
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};