What is Next.js?
Next.js is a React framework built by Vercel that adds:
- Routing — file-system based, no configuration
- Rendering strategies — SSR, SSG, ISR, client-side
- Full-stack — API routes co-located with frontend code
- Performance — automatic code splitting, image optimization, font loading
- Deployment — production-ready from day one
Analogy: React is a powerful engine; Next.js is the complete car —
engine + chassis + steering + safety features, ready to drive.
React alone gives you components. You still need a router, a build system, API handling, and rendering strategy. Next.js makes all of these decisions for you — sensibly.
App Router vs Pages Router
Next.js 13+ introduced the App Router (the current default). The older Pages Router still exists and works, but new projects should use App Router.
src/
app/ ← App Router (new)
layout.tsx ← Root layout (shared UI wrapper)
page.tsx ← Home route "/"
about/
page.tsx ← "/about" route
blog/
[slug]/
page.tsx ← "/blog/:slug" dynamic route
api/
users/
route.ts ← API endpoint "/api/users"
Every file named page.tsx becomes a route. Every layout.tsx wraps its children.
Rendering Strategies
The core decision in Next.js: when is this page's HTML generated?
┌─────────────────┬──────────────────┬────────────────────────────────────┐
│ Strategy │ HTML generated │ Best for │
├─────────────────┼──────────────────┼────────────────────────────────────┤
│ SSG │ At build time │ Blogs, docs, marketing pages │
│ ISR │ On demand, cache │ Product pages, large catalogs │
│ SSR │ Per request │ Dashboards, personalized content │
│ Client-side │ In the browser │ Interactive widgets, private data │
└─────────────────┴──────────────────┴────────────────────────────────────┘
Server Components (Default in App Router)
Every component in the App Router is a Server Component by default — it runs only on the server, never in the browser. Benefits:
- Access databases, file system, secrets directly — no API layer needed
- Smaller bundle (zero JS sent for the component)
- Faster first load (HTML streamed from server)
// app/users/page.tsx — Server Component (no "use client" directive)
// Runs on the server. Can use fetch, database, fs — never in the browser.
interface User { id: number; name: string; email: string; }
async function getUsers(): Promise<User[]> {
const res = await fetch("https://api.example.com/users", {
next: { revalidate: 60 } // ISR: revalidate every 60 seconds
});
if (!res.ok) throw new Error("Failed to fetch users");
return res.json();
}
export default async function UsersPage() {
const users = await getUsers(); // await directly in the component!
return (
<main>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>
<strong>{user.name}</strong> — {user.email}
</li>
))}
</ul>
</main>
);
}
Client Components — Interactive UI
Add "use client" at the top when you need browser APIs, event handlers, or hooks:
// components/SearchBox.tsx
"use client";
import { useState } from "react";
export function SearchBox({ 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..."
/>
);
}
Composing both: Server Components can render Client Components as children. Client Components cannot directly render Server Components (pass them as props/children instead).
// app/page.tsx (Server Component)
import { SearchBox } from "@/components/SearchBox";
import { UserList } from "@/components/UserList";
export default function Home() {
return (
<div>
<SearchBox onSearch={/* ... */} /> {/* Client Component */}
<UserList /> {/* Server Component */}
</div>
);
}
The Layout System
Layouts wrap route segments and persist across navigations:
// app/layout.tsx — Root layout, wraps every page
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: { default: "PrepDeck", template: "%s | PrepDeck" },
description: "The ultimate FAANG prep platform",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<nav>/* Navigation */</nav>
<main>{children}</main>
<footer>/* Footer */</footer>
</body>
</html>
);
}
// app/dashboard/layout.tsx — nested layout for /dashboard/*
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard">
<Sidebar />
<div className="content">{children}</div>
</div>
);
}
Dynamic Routes
// app/blog/[slug]/page.tsx
interface Props { params: { slug: string } }
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug);
if (!post) notFound(); // renders the nearest not-found.tsx
return <article>{post.content}</article>;
}
// Generate static paths at build time (SSG)
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
// SEO metadata per page
export async function generateMetadata({ params }: Props) {
const post = await getPost(params.slug);
return {
title: post?.title,
description: post?.excerpt,
openGraph: { title: post?.title, images: [post?.coverImage] },
};
}
Catch-all routes: [...slug] matches /a, /a/b, /a/b/c. Optional: [[...slug]].
API Routes
API routes turn Next.js into a full-stack framework:
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
// GET /api/users
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = Number(searchParams.get("page") ?? 1);
const users = await db.user.findMany({
skip: (page - 1) * 20,
take: 20,
select: { id: true, name: true, email: true },
});
return NextResponse.json({ users, page });
}
// POST /api/users
export async function POST(request: NextRequest) {
const body = await request.json();
// Validate input
if (!body.name || !body.email) {
return NextResponse.json({ error: "Name and email required" }, { status: 400 });
}
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}
// app/api/users/[id]/route.ts
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
const user = await db.user.findUnique({ where: { id: Number(params.id) } });
if (!user) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json(user);
}
Middleware
Middleware runs before every request — authentication, redirects, A/B testing, geolocation:
// middleware.ts (root of project)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("auth-token")?.value;
// Protect /dashboard routes
if (request.nextUrl.pathname.startsWith("/dashboard")) {
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/api/admin/:path*"],
};
Image Optimization
The next/image component automatically:
- Converts to WebP/AVIF
- Lazy loads
- Prevents layout shift (reserves space)
- Generates responsive sizes
import Image from "next/image";
// Local image (TypeScript knows width/height from import)
import heroImage from "@/public/hero.jpg";
<Image src={heroImage} alt="Hero" priority />
// Remote image (must configure domain in next.config.ts)
<Image
src="https://example.com/user.jpg"
alt="User avatar"
width={64}
height={64}
className="rounded-full"
/>
Data Fetching Patterns
// 1. Server Component fetch — simplest
async function Page() {
const data = await fetch("/api/data").then(r => r.json());
return <div>{data.value}</div>;
}
// 2. Parallel data fetching with Promise.all
async function Dashboard() {
const [user, stats, posts] = await Promise.all([
getUser(1),
getStats(),
getRecentPosts(),
]);
return <DashboardView user={user} stats={stats} posts={posts} />;
}
// 3. Streaming with Suspense (show UI as data loads progressively)
import { Suspense } from "react";
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading stats...</p>}>
<Stats /> {/* Streams in when ready */}
</Suspense>
<Suspense fallback={<p>Loading posts...</p>}>
<RecentPosts /> {/* Streams in independently */}
</Suspense>
</div>
);
}
Error Handling
// app/error.tsx — catches errors in its segment (must be Client Component)
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/not-found.tsx
export default function NotFound() {
return <h2>404 — Page Not Found</h2>;
}
Common Interview Questions
Practice
- Basic: Create a blog with a home page listing posts (SSG from a JSON file) and dynamic
/blog/[slug]pages for each post. - API Routes: Build a
/api/todosendpoint with GET (list), POST (create), and DELETE (by id). Persist to an in-memory array. - Auth: Add cookie-based authentication — a
/api/loginroute that sets a cookie, and middleware that protects/dashboard/*routes. - Full-stack: Build a notes app — Server Component fetches notes from a DB (SQLite + Prisma), a Client Component form creates new notes via a Server Action (no API route needed).
This section covers Level 8 — Frontend Development. Next: Databases for Level 9.