// 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).*)"],
};