LearnThatStack Ace your next interview

Next.js.
Interview cheat sheet.

Quick reference for Next.js - sectioned for fast scanning. Skim the part you're shaky on, walk in confident.

Frontend Development 14-section reference ~8 min read

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:

  1. Text content mismatch: Server/client render different content
  2. Invalid nesting: Wrong HTML structure
  3. Missing keys: Dynamic lists without keys
  4. 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

  1. Use Server Components by default - Only add 'use client' when needed
  2. Colocate related files - Keep components near their routes
  3. Optimize images always - Use next/image
  4. Prefetch critical routes - Better UX
  5. Monitor bundle size - Use analyzer tools
  6. Cache strategically - Balance freshness vs performance
  7. Handle errors gracefully - Use error boundaries
  8. Test on production build - Dev mode differs
  9. Use TypeScript - Better DX and fewer bugs
  10. 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.

Found this useful? Pass it on.
Pro · $10/mo

The sheet is free. Pro goes deeper.

Pro opens the full question library behind every sheet, every refresher and a monthly AI allowance. One subscription, all formats.

Full question library All refreshers Cancel anytime