Summary
Next.js is a powerful React framework developed by Vercel that enables developers to build production-ready web applications with features like server-side rendering (SSR), static site generation (SSG), and incremental static regeneration (ISR). It provides an opinionated structure with file-based routing, automatic code splitting, built-in CSS support, API routes, and optimized performance out of the box. Next.js 13+ introduced the App Router with React Server Components, offering improved performance and developer experience. This cheatsheet covers Next.js fundamentals, rendering strategies, data fetching patterns, and best practices essential for technical interviews.
Core Concepts
What is Next.js?
- React framework for production with built-in SSR/SSG
- File-based routing
- Automatic code splitting
- Built-in CSS support
- API routes
- TypeScript support
Key Features
- ✅ Server-Side Rendering (SSR)
- ✅ Static Site Generation (SSG)
- ✅ Incremental Static Regeneration (ISR)
- ✅ Client-Side Rendering (CSR)
- ✅ API Routes
- ✅ Built-in Image Optimization
- ✅ Automatic Code Splitting
- ✅ Fast Refresh
Routing
App Router (Next.js 13+)
// app/page.js - Home route (/)
export default function HomePage() {
return <h1>Home</h1>
}
// app/about/page.js - About route (/about)
export default function AboutPage() {
return <h1>About</h1>
}
// app/blog/[slug]/page.js - Dynamic route
export default function BlogPost({ params }) {
return <h1>Post: {params.slug}</h1>
}
Pages Router (Legacy)
// pages/index.js - Home route (/)
export default function Home() {
return <h1>Home</h1>
}
// pages/blog/[id].js - Dynamic route
export default function Post({ post }) {
return <h1>{post.title}</h1>
}
Dynamic Routes
// Catch-all routes: [...slug].js
// Optional catch-all: [[...slug]].js
// app/shop/[...categories]/page.js
export default function Categories({ params }) {
// /shop/clothes/mens/shirts
// params.categories = ['clothes', 'mens', 'shirts']
return <div>{params.categories.join(' > ')}</div>
}
Route Groups
// app/(marketing)/about/page.js
// app/(marketing)/contact/page.js
// app/(shop)/products/page.js
// Groups don't affect URL structure
Rendering Methods
Server Components (Default in App Router)
// Runs on server, zero JS to client
async function ServerComponent() {
const data = await fetch('https://api.example.com/data')
return <div>{data}</div>
}
Client Components
'use client' // Required directive
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
Static Generation (SSG)
// App Router
export default async function Page() {
const data = await fetch('https://api.example.com/data')
return <div>{data}</div>
}
// Pages Router
export async function getStaticProps() {
const data = await fetchData()
return { props: { data } }
}
Server-Side Rendering (SSR)
// App Router - Dynamic rendering
export const dynamic = 'force-dynamic'
// Pages Router
export async function getServerSideProps(context) {
const data = await fetchData()
return { props: { data } }
}
Incremental Static Regeneration (ISR)
// App Router
export const revalidate = 60 // revalidate every 60 seconds
// Pages Router
export async function getStaticProps() {
return {
props: { data },
revalidate: 60
}
}
Data Fetching
App Router Fetching
// Server Component (default)
async function Page() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache' // Default
// cache: 'no-store' // Dynamic
// next: { revalidate: 60 } // ISR
})
const data = await res.json()
return <div>{data}</div>
}
Parallel Data Fetching
async function Page() {
// Parallel fetching
const [users, posts] = await Promise.all([
fetch('/api/users').then(res => res.json()),
fetch('/api/posts').then(res => res.json())
])
return <div>{/* render data */}</div>
}
Client-Side Fetching
'use client'
import { useEffect, useState } from 'react'
function ClientComponent() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData)
}, [])
return <div>{data}</div>
}
API Routes
Basic API Route
// app/api/hello/route.js (App Router)
export async function GET(request) {
return Response.json({ message: 'Hello' })
}
export async function POST(request) {
const body = await request.json()
return Response.json({ received: body })
}
Dynamic API Routes
// app/api/users/[id]/route.js
export async function GET(request, { params }) {
const { id } = params
return Response.json({ userId: id })
}
Middleware
// middleware.js (root level)
import { NextResponse } from 'next/server'
export function middleware(request) {
// Clone the request headers
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-custom-header', 'value')
return NextResponse.next({
request: { headers: requestHeaders }
})
}
export const config = {
matcher: '/api/:path*'
}
Styling
CSS Modules
// styles/Home.module.css
.container { padding: 20px; }
// Component
import styles from './Home.module.css'
export default function Home() {
return <div className={styles.container}>Home</div>
}
Tailwind CSS
// tailwind.config.js
module.exports = {
content: ['./app/**/*.{js,jsx}', './components/**/*.{js,jsx}'],
theme: { extend: {} },
plugins: []
}
// Usage
export default function Button() {
return <button className="bg-blue-500 text-white p-4">Click</button>
}
CSS-in-JS
// styled-components example
import styled from 'styled-components'
const Button = styled.button`
background: blue;
color: white;
padding: 10px;
`
Image Optimization
Next.js Image Component
import Image from 'next/image'
export default function Avatar() {
return (
<Image
src="/avatar.jpg"
alt="Avatar"
width={200}
height={200}
priority // Load eagerly
placeholder="blur" // With static import
quality={90}
/>
)
}
External Images
// next.config.js
module.exports = {
images: {
domains: ['example.com', 'cdn.example.com']
}
}
Performance Optimization
Code Splitting
// Automatic for pages
// Dynamic imports for components
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false // Disable SSR if needed
})
Font Optimization
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter'
})
export default function Layout({ children }) {
return (
<html className={inter.variable}>
<body>{children}</body>
</html>
)
}
Prefetching
import Link from 'next/link'
// Automatic prefetching in viewport
<Link href="/about">About</Link>
// Disable prefetching
<Link href="/heavy-page" prefetch={false}>Heavy Page</Link>
Authentication
JWT Example
// app/api/auth/login/route.js
import jwt from 'jsonwebtoken'
export async function POST(request) {
const { email, password } = await request.json()
// Validate credentials
if (isValid(email, password)) {
const token = jwt.sign(
{ userId: user.id, email },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
)
return Response.json({ token })
}
return Response.json({ error: 'Invalid credentials' }, { status: 401 })
}
Protected Routes
// middleware.js
import { NextResponse } from 'next/server'
import { verifyToken } from './lib/auth'
export async function middleware(request) {
const token = request.cookies.get('token')?.value
if (!token || !await verifyToken(token)) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: '/dashboard/:path*'
}
Deployment
Environment Variables
// .env.local (development)
NEXT_PUBLIC_API_URL=http://localhost:3000
SECRET_KEY=secret
// Usage
const apiUrl = process.env.NEXT_PUBLIC_API_URL // Client & Server
const secret = process.env.SECRET_KEY // Server only
Build Optimization
// package.json
{
"scripts": {
"build": "next build",
"start": "next start",
"export": "next export" // Static export
}
}
next.config.js
module.exports = {
reactStrictMode: true,
swcMinify: true,
output: 'standalone', // Docker optimization
experimental: {
serverActions: true
}
}
Key Interview Concepts
Rendering Methods Comparison
| Method | When Rendered | Use Case | Data Freshness | Performance |
|---|---|---|---|---|
| SSG | Build time | Static content, blogs | Stale until rebuild | Fastest |
| SSR | Each request | Dynamic, user-specific | Always fresh | Slower |
| ISR | Build + Revalidation | E-commerce, news | Configurable staleness | Fast + Fresh |
| CSR | Client-side | Interactive dashboards | Real-time | Initial load slower |
Server vs Client Components Decision Matrix
| Feature | Server Component | Client Component |
|---|---|---|
| Data Fetching | ✅ Direct DB/API access | ❌ Requires API endpoint |
| Large Dependencies | ✅ Not sent to client | ❌ Increases bundle |
| Sensitive Logic | ✅ Stays on server | ❌ Exposed to client |
| Interactivity | ❌ No hooks/events | ✅ Full React features |
| Browser APIs | ❌ No access | ✅ Full access |
| State Management | ❌ No useState/useEffect | ✅ All hooks available |
SEO Optimization in Next.js
// Metadata API (App Router)
export const metadata = {
title: 'Page Title',
description: 'Page description',
openGraph: {
title: 'OG Title',
description: 'OG Description',
images: ['/og-image.jpg']
}
}
// Dynamic metadata
export async function generateMetadata({ params }) {
const product = await getProduct(params.id)
return {
title: product.name,
description: product.description
}
}
Hydration Process & Issues
Hydration: React taking over server-rendered HTML
Common Hydration Errors:
- Text content mismatch: Server/client render different content
- Invalid nesting: Wrong HTML structure
- Missing keys: Dynamic lists without keys
- Date/time differences: Server vs client timestamps
Solutions:
// Suppress hydration warning
<div suppressHydrationWarning>{new Date().toString()}</div>
// Use useEffect for client-only content
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
Bundle Optimization Strategies
// 1. Dynamic imports
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <Skeleton />,
ssr: false
})
// 2. Bundle analyzer setup
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true'
})
module.exports = withBundleAnalyzer({})
// 3. Tree shaking with barrel exports
export { Button } from './Button' // Good
export * from './components' // Bad - imports everything
Data Fetching Patterns
// ❌ Waterfall (Sequential) - Avoid
async function Page() {
const user = await getUser()
const posts = await getPosts(user.id) // Waits for user
return <>{/* render */}</>
}
// ✅ Parallel Fetching - Preferred
async function Page() {
const [user, posts, comments] = await Promise.all([
getUser(),
getPosts(),
getComments()
])
return <>{/* render */}</>
}
// ✅ Streaming with Suspense
import { Suspense } from 'react'
export default function Page() {
return (
<>
<Suspense fallback={<UserSkeleton />}>
<UserData />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<PostsData />
</Suspense>
</>
)
}
Error Handling Strategies
// app/error.js
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
8. Loading States?
// app/loading.js
export default function Loading() {
return <div>Loading...</div>
}
9. Caching Strategies?
fetch()cache options- Route segment config
revalidatePath()/revalidateTag()- Client-side: SWR, React Query
Security Best Practices
Environment Variables:
// ❌ Exposed to client
process.env.SECRET_KEY // undefined on client
// ✅ Available everywhere
process.env.NEXT_PUBLIC_API_URL
API Route Protection:
// middleware.js
export function middleware(request) {
const token = request.cookies.get('token')
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
}
CORS Configuration:
// app/api/route.js
export async function GET(request) {
return new Response('Hello', {
headers: {
'Access-Control-Allow-Origin': 'https://trusted-domain.com',
'Access-Control-Allow-Methods': 'GET, POST',
}
})
}
Caching Strategies in Next.js
| Strategy | Configuration | Use Case | Revalidation |
|---|---|---|---|
| Static | cache: 'force-cache' |
Unchanging data | Manual/Build |
| Dynamic | cache: 'no-store' |
Real-time data | Every request |
| ISR | revalidate: 60 |
Periodic updates | Time-based |
| On-demand | revalidatePath() |
Event-triggered | API triggered |
Interview Preparation Checklist
- ✅ Routing: File-based, dynamic routes, route groups
- ✅ Rendering: SSG, SSR, ISR, CSR differences
- ✅ Components: Server vs Client components
- ✅ Data Fetching: Patterns, caching, revalidation
- ✅ Performance: Code splitting, image optimization, fonts
- ✅ SEO: Metadata API, sitemaps, structured data
- ✅ Deployment: Environment variables, build optimization
- ✅ App vs Pages Router: Migration, differences
- ✅ Middleware: Authentication, redirects, headers
- ✅ Error Handling: Error boundaries, error.js, not-found.js
Quick Tips
- Use Server Components by default - Only add 'use client' when needed
- Colocate related files - Keep components near their routes
- Optimize images always - Use next/image
- Prefetch critical routes - Better UX
- Monitor bundle size - Use analyzer tools
- Cache strategically - Balance freshness vs performance
- Handle errors gracefully - Use error boundaries
- Test on production build - Dev mode differs
- Use TypeScript - Better DX and fewer bugs
- Keep dependencies updated - Security and features
Key Takeaway: Next.js excels at providing a full-stack React framework with built-in optimizations. Master the rendering methods (SSG/SSR/ISR), understand when to use Server vs Client Components, and leverage the built-in features like Image optimization and API routes for production-ready applications.