Build production-ready Next.js applications — App Router, Server Components, API routes, and deployment.
app/
layout.tsx # Root layout (wraps all pages)
page.tsx # Home page (/)
globals.css # Global styles
dashboard/
layout.tsx # Dashboard layout
page.tsx # /dashboard
settings/
page.tsx # /dashboard/settings
api/
users/
route.ts # API: /api/users
Components in the App Router are Server Components by default. They run on the server and can directly access databases, env vars, and file systems.
// This runs on the server — no "use client" needed
export default async function UsersPage() {
const users = await db.query("SELECT * FROM users");
return (
<main>
<h1>Users</h1>
<ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
</main>
);
}
Add "use client" only when you need browser APIs, state, or event handlers:
"use client";
import { useState } from "react";
export function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && onSearch(query)}
placeholder="Search..."
/>
);
}
// app/api/users/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const users = await db.query("SELECT * FROM users");
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.insert("users", body);
return NextResponse.json(user, { status: 201 });
}
// actions.ts
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await db.insert("posts", { title });
revalidatePath("/posts");
}
export const metadata = {
title: "My App",
description: "Production-ready Next.js application",
openGraph: { title: "My App", description: "..." },
};
// middleware.ts (at project root)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("session");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = { matcher: ["/dashboard/:path*"] };
"use client" when necessary.loading.tsx for loading states and error.tsx for error boundaries.generateStaticParams() for static paths.revalidatePath() or revalidateTag() for cache invalidation.layout.tsx, not in individual pages.