// Next.js Interview Mastery

Land the Frontend Role You Want

Curated from 15+ years of hiring experience. Every question interviewers actually ask at 1+ YOE level — with real answers, code examples, and industry context.

50+
Questions
10
Topic Areas
2025
Market Aligned
How to Use This Guide
Built from patterns seen across hundreds of frontend interviews at startups, scale-ups, and FAANG-adjacent companies.
What interviewers actually test at 1+ YOE

At 1–3 years of experience, interviewers aren't expecting you to know everything. They're looking for three things: you understand why not just what, you can reason under uncertainty, and you've shipped real things and learned from them.

The biggest mistake candidates at this level make is memorising answers without understanding the tradeoffs. Every question in this guide is structured to help you explain the reasoning — because that's what separates a hire from a pass.

From the hiring side
When I interview 1+ YOE candidates, I'm not looking for someone who knows every Next.js API. I'm looking for someone who can tell me why they chose SSR over SSG on a page, or why they put 'use client' on a leaf component instead of wrapping the whole page. That judgment is what makes a good engineer.
2024–25 market signal
The job market has tightened significantly. Companies are hiring fewer people and expecting more from them. App Router proficiency is now a baseline expectation at most companies using Next.js — not a bonus. If you're still primarily thinking in Pages Router patterns, that's what this guide will help you shift.
Priority order for your prep
Must know cold
Server vs Client Components — the #1 topic at every interview right now
+

In every Next.js interview I've seen in 2024–25, this comes up in the first 10 minutes. It's not a gotcha — it's a genuine signal of whether you understand the modern React/Next.js mental model.

The thing most candidates get wrong: they explain what the difference is but can't explain the consequence on the page they're building. Practice connecting the concept to a real decision: "On our dashboard, I kept the sidebar as a Server Component because it only needs data, but the filter dropdowns became a Client Component because they need useState."

Common mistake
Saying "Client Components run on the client." This is incomplete. Client Components also SSR on the server for the initial HTML. The distinction is: they also run in the browser and are included in the JS bundle. Server Components never touch the browser.
Must know cold
Data fetching and caching semantics in App Router
+

The three fetch cache options (force-cache, no-store, next: { revalidate }) are asked in almost every technical screen. Know them and know which renders SSG, SSR, and ISR behaviour.

Bonus points for mentioning revalidateTag vs revalidatePath — most 1 YOE candidates don't get this deep and it signals genuine production experience.

Know well
Performance — specifically Core Web Vitals and what causes them to fail
+

Product-minded companies care deeply about performance metrics because they tie directly to conversion and SEO. If you can say "we had a CLS issue because we were loading fonts without swap, and I fixed it by switching to next/font which self-hosts them at build time" — that's a story that lands.

Learn LCP, INP, and CLS. Know what Next.js features address each one. Be ready to describe a real performance improvement you made.

App Router & Routing
The structural foundation. Almost every interview starts here — expect 2–3 questions minimum.
Interviewers probe this to understand if you actually work with App Router day-to-day or just know it theoretically. Be ready to talk about the folder structure, special files, and route segments — and ideally give examples from projects you've built.
Very commonly asked
Walk me through the App Router folder structure. What does each special file do?
+

The app/ directory is the root of App Router. Each folder represents a route segment. A route becomes publicly accessible only when it contains a page.tsx file — other files in the folder (components, utils, tests) are colocated but not exposed as routes.

The special files that add behaviour to a segment:

  • page.tsx — the UI for that route. The only file that makes a route accessible.
  • layout.tsx — wraps the page and all children. Persists across navigations without re-mounting. Great for navbars, sidebars.
  • loading.tsx — automatically wraps the page in a Suspense boundary. Shows while the page is loading. Renders instantly — no waiting for data.
  • error.tsx — a React error boundary for that segment. Must be a Client Component ('use client'). Catches errors in the page and children.
  • not-found.tsx — rendered when notFound() is called from the page, or when no route matches.
  • template.tsx — like layout but re-mounts on every navigation. Use when you need fresh state or effects per navigation (rare).
// A typical App Router structure
app/
  layout.tsx          ← root layout (required)
  page.tsx            ← / route
  (marketing)/
    about/page.tsx    ← /about (route group)
  (dashboard)/
    layout.tsx        ← dashboard-specific layout
    home/page.tsx     ← /home
    settings/
      page.tsx        ← /settings
      loading.tsx     ← settings skeleton
      error.tsx       ← settings error boundary
What I listen for
Candidates who mention colocation — that you can put your component files right next to your page without them becoming routes — shows you're actually using it, not just reading docs.
Very commonly asked — trap question
What's the difference between useRouter from next/navigation vs next/router?
+

next/router — the Pages Router API. If you import this in App Router, you'll get a runtime error or undefined behaviour.

next/navigation — the App Router API. This is what you use in App Router. It provides separate hooks for each concern:

  • useRouter() — only navigation methods: push, replace, refresh, back, forward. Does NOT expose pathname or query.
  • usePathname() — the current pathname as a string
  • useSearchParams() — the current URL search params (must be wrapped in Suspense if used in a Server Component tree)
  • useParams() — dynamic route params like { slug: 'hello-world' }
// WRONG in App Router
import { useRouter } from 'next/router'

// CORRECT in App Router
import { useRouter, usePathname, useSearchParams } from 'next/navigation'

export default function MyComponent() {
  const router = useRouter()       // push, replace, back
  const pathname = usePathname()   // '/dashboard/settings'
  const params = useParams()       // { id: '123' }
}
This is a live coding trap
Interviewers often give you a component to write and watch which package you import from. Getting this wrong is a highly visible mistake. Burn next/navigation into your memory.
Mid-level question
What are route groups and when would you actually use them?
+

Route groups use (folderName) syntax. The parentheses are stripped from the URL — they only exist to organize your file system and share layouts.

The two main use cases:

1. Different layouts for different sections without URL nesting:

app/
  (auth)/
    layout.tsx   ← centered card layout, no navbar
    login/page.tsx    → /login
    signup/page.tsx   → /signup
  (dashboard)/
    layout.tsx   ← full sidebar layout
    home/page.tsx     → /home
    settings/page.tsx → /settings

2. Organizing a large codebase without affecting URLs: You might group all e-commerce routes in (shop)/ and all content routes in (content)/ just for developer experience.

Real world usage
A very common pattern: one root layout for the whole app, a (marketing) group with a minimal public layout, and an (app) group with the authenticated dashboard layout. Both share the root layout but have their own middle layouts.
Deep dive question
Explain parallel routes and intercepting routes. Give a real use case for each.
+

Parallel Routes use @slotName folder naming. They render multiple independent pages in the same layout simultaneously. Each slot can have its own loading and error states.

app/
  @analytics/
    page.tsx    ← renders independently
  @team/
    page.tsx    ← renders independently
  layout.tsx    ← receives { analytics, team, children } props

Real use case: a dashboard where a team activity feed and analytics charts load independently. If the analytics API is slow, the team feed still shows — they don't block each other.

Intercepting Routes use (..) notation. They let you show a different UI when navigating to a URL from within the app, while the direct URL still works normally.

Real use case: Instagram-style photo modal. When you click a photo in the feed, the URL changes to /photos/123 but a modal opens over the feed. If you share that link, the recipient gets a full page. This is the "modal with shareable URL" pattern.

Server vs Client Components
The most important mental model shift in modern React. Get this deeply right.
This is the single most tested area for App Router experience. Interviewers want to see that you have a genuine mental model — not just that you've memorised the rules.
Always asked
What's the fundamental difference between Server and Client Components? How do you decide which to use?
+

Server Components run only on the server. Their source code never ships to the browser. They can access databases, secrets, environment variables, and the filesystem directly. They cannot use browser APIs, event handlers, useState, useEffect, or useContext.

Client Components run in the browser (and also SSR on the server for the initial HTML). They are included in the JS bundle sent to the browser. They can use all React hooks and browser APIs.

In App Router, everything is a Server Component by default. You opt into Client behaviour by adding 'use client' at the top of a file.

Decision framework — use a Client Component when you need:

  • Event handlers (onClick, onChange, onSubmit)
  • React hooks (useState, useEffect, useContext, useRef)
  • Browser-only APIs (localStorage, window, navigator)
  • Real-time updates or subscriptions

If none of those apply, keep it as a Server Component. The goal is to push 'use client' as deep (as close to leaves) in the component tree as possible.

// Server Component — no 'use client', runs on server only
export default async function ProductList() {
  const products = await db.product.findMany()  // direct DB access
  return (
    <ul>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </ul>
  )
}

// Client Component — needs state for the cart button
'use client'
import { useState } from 'react'
export function AddToCartButton({ productId }) {
  const [added, setAdded] = useState(false)
  return <button onClick={() => setAdded(true)}>{added ? 'Added!' : 'Add to Cart'}</button>
}
What separates good answers from great ones
Great candidates connect this to bundle size. "By keeping ProductList as a Server Component, all the data fetching logic stays on the server. The only thing that ships to the browser is the AddToCartButton — which is a tiny component. The product data, the DB query logic, none of that touches the browser bundle."
Commonly asked
What does 'use client' actually do? Does it mean the component only runs in the browser?
+

'use client' marks a bundle boundary. It tells Next.js: "include this file and everything it imports in the client JavaScript bundle."

It does NOT mean "only runs in the browser." Client Components still server-side render for the initial HTML — they produce HTML on the server just like Server Components. The difference is they also hydrate and run interactively in the browser after the initial load.

Think of it this way: 'use client' is a border declaration. Below this border, everything is in client-land. Server Components can import Client Components (crossing from server to client land), but Client Components cannot import Server Components.

// This whole file and its import tree goes into the browser bundle
'use client'

import { HeavyChart } from './HeavyChart'  // ⚠️ also goes into bundle
import { useState } from 'react'

export function Dashboard() {
  const [tab, setTab] = useState('overview')
  return <HeavyChart tab={tab} />
}
Key insight
Because 'use client' pulls in everything it imports, be very conscious of what you import in Client Components. A single large library imported in a top-level Client Component can inflate your bundle significantly. This is why keeping 'use client' on leaf components matters.
Mid-level question
How do you solve the Context provider problem in App Router? Why is it tricky?
+

Context providers need useState or useReducer to hold state, which means they must be Client Components. The naive approach is to wrap your entire app in a provider:

// ❌ Naive — entire app becomes Client-land
'use client'
export default function RootLayout({ children }) {
  return <ThemeProvider>{children}</ThemeProvider>
}

The problem: once a layout or page has 'use client', all components rendered by it become Client Components too — you lose Server Component benefits for all children.

The solution: extract just the provider into a tiny Client Component file, then use it in your Server Component layout. Children passed as props to a Client Component remain Server Components:

// ✅ providers.tsx — Client Component (just the wrapper)
'use client'
import { ThemeContext } from './context'
import { useState } from 'react'
export function Providers({ children }) {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}  // ← children are still Server Components!
    </ThemeContext.Provider>
  )
}

// ✅ layout.tsx — Server Component
import { Providers } from './providers'
export default function RootLayout({ children }) {
  return <Providers>{children}</Providers>
}
Rendering Strategies
SSG, SSR, ISR, streaming — and the practical judgment of when to use each.
Interviewers at product companies ask this because rendering strategy has a direct impact on performance, SEO, and infrastructure cost. They want to see that you think about it in terms of data freshness, user personalization, and load characteristics — not just as abstract concepts.
Always asked
Explain SSG, SSR, ISR, and CSR. How do they map to App Router APIs?
+

SSG (Static Site Generation) — HTML built at deploy time. Served from CDN. Fastest possible delivery, cheapest to host. Use for content that's the same for all users and changes rarely.

SSR (Server-Side Rendering) — HTML generated on the server for every request. Always fresh. Higher server cost, slower than CDN but data is always current. Use for personalized or highly dynamic pages.

ISR (Incremental Static Regeneration) — Starts as SSG. Regenerates in the background after a set interval. The best of both worlds for content that changes occasionally. The old page is served while the new one generates.

CSR (Client-Side Rendering) — No server HTML (or minimal shell). Data fetched in the browser. Bad for SEO, good for highly interactive, authenticated dashboards where SEO doesn't matter.

// SSG — default behaviour, fetch result cached indefinitely
const res = await fetch('https://api.example.com/posts')
// or explicitly:
const res = await fetch(url, { cache: 'force-cache' })

// SSR — no caching, fresh on every request
const res = await fetch(url, { cache: 'no-store' })
// or at the route level:
export const dynamic = 'force-dynamic'

// ISR — revalidate every 60 seconds
const res = await fetch(url, { next: { revalidate: 60 } })
// or at the route level:
export const revalidate = 60
The answer that impresses
Map each strategy to a real page type. "Our marketing homepage is SSG — it's the same for everyone and we deploy to update it. Our product listing is ISR with 5-minute revalidation — inventory changes but not every second. Our user dashboard is SSR because it's personalized and behind auth."
Mid-level question
What is streaming in Next.js? How does Suspense enable it and why does it improve performance?
+

Without streaming, SSR waits for all data to be fetched before sending any HTML to the browser. If one data source is slow, the whole page is delayed. The user sees nothing until everything is ready.

Streaming sends HTML in chunks as they become ready. The browser can start rendering and displaying content immediately — the shell of the page appears almost instantly, and slower parts stream in as they resolve.

Next.js App Router enables this via two mechanisms:

  • loading.tsx — automatically wraps the page in a Suspense boundary. Shows instantly while the page's async data loads.
  • Manual Suspense boundaries — wrap individual slow components to stream them independently from the rest of the page.
// page.tsx — the shell renders immediately
import { Suspense } from 'react'
import { ProductList } from './ProductList'
import { ReviewsSkeleton } from './ReviewsSkeleton'
import { Reviews } from './Reviews'

export default function ProductPage() {
  return (
    <div>
      <ProductList />  // fast, streams immediately
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews />  // slow API, streams when ready
      </Suspense>
    </div>
  )
}
Real world impact
Streaming dramatically improves Time To First Byte (TTFB) and Largest Contentful Paint (LCP) because the browser gets something to render immediately. Instead of a blank screen for 800ms, the user sees the page shell in 50ms and content streams in progressively.
Data Fetching & Caching
Where most candidates have gaps. The cache semantics are specific and commonly tested.
Commonly asked
How do Server Actions work? When would you use them instead of API routes?
+

Server Actions are async functions with 'use server' at the top that run on the server but can be called directly from Client Components or HTML forms. They're the recommended mutation pattern in App Router.

// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  await db.post.create({ data: { title } })
  revalidatePath('/posts')        // purge cache for this path
  redirect('/posts')              // navigate after mutation
}

// In a Client Component
'use client'
import { createPost } from './actions'
export function CreatePostForm() {
  return <form action={createPost}>
    <input name="title" />
    <button>Create</button>
  </form>
}

Server Actions vs API Routes:

  • Use Server Actions for mutations triggered from your own UI — form submissions, button clicks, data updates
  • Use API Routes (Route Handlers) when you need an endpoint that third parties can call, or when you're building a proper REST API consumed by mobile apps or external services
  • Server Actions are tightly coupled to your app; Route Handlers are a public interface
Common interview mistake
Saying Server Actions replace all API routes. They don't. Server Actions are for your own UI mutations. Route Handlers are for when you need an actual HTTP endpoint that can be called from anywhere.
Mid-level question
What is the difference between revalidatePath and revalidateTag? When would you use each?
+

revalidatePath('/posts') — purges all cached data for a specific URL path. Simple and blunt. Use it when you want to refresh a specific page after a mutation.

revalidateTag('posts') — purges all fetch requests tagged with that label, across all paths. Surgical. Use it when the same data is displayed on multiple pages and you want to invalidate it everywhere.

// Tag your fetch requests
const posts = await fetch('/api/posts', {
  next: { tags: ['posts'] }
})

// When a post is created, invalidate anywhere 'posts' is used
export async function createPost(data) {
  await db.post.create({ data })
  revalidateTag('posts')  // updates /posts, /home, /feed, anywhere
}

// vs revalidatePath — only updates /posts
revalidatePath('/posts')
Real world pattern
Use tags when you have a blog where posts appear on the homepage, in category pages, and in a posts list. One revalidateTag('posts') clears all of them. With revalidatePath you'd have to list every affected path.
Deep dive
What is request memoization in Next.js? How does it help with component architecture?
+

Request memoization means that if multiple Server Components in the same render pass call fetch() with the exact same URL and options, Next.js makes only one network request. All callers get the same cached result.

This changes how you architect components. Instead of prop-drilling data from a parent who fetches it once, each component can declare its own data needs:

// Before: prop drilling to avoid duplicate fetches
async function Page() {
  const user = await getUser()  // fetch once at top
  return <><Header user={user} /><Profile user={user} /></>
}

// After: each component fetches what it needs, memoization handles dedup
async function Header() {
  const user = await getUser()  // ← same URL = deduplicated
  return <nav>Hello {user.name}</nav>
}
async function Profile() {
  const user = await getUser()  // ← same fetch, no extra network call
  return <div>{user.bio}</div>
}
Industry context
This enables a much cleaner component architecture — data colocation instead of data hoisting. The component that needs data fetches it. The framework handles efficiency. This is similar to how React Query deduplicates client-side fetches, but it's built into the server rendering model.
Performance & Optimization
Product companies care deeply about this. Tie everything to real metrics.
Always asked at product companies
What are Core Web Vitals? How does Next.js help you optimize each one?
+

Core Web Vitals are Google's metrics for page experience. They directly affect SEO ranking and user satisfaction.

LCP — Largest Contentful Paint (target: under 2.5s): How fast the main content loads.

  • Use SSG/ISR so the HTML comes from CDN, not a server doing work
  • Use next/image with priority prop on hero images — preloads them
  • Use next/font to eliminate render-blocking font requests
  • Use streaming to send the shell immediately

INP — Interaction to Next Paint (target: under 200ms): How responsive the page feels to interactions.

  • Minimize client-side JS — Server Components reduce bundle size
  • Use dynamic imports for heavy libraries not needed on load
  • Avoid long-running synchronous JS in event handlers

CLS — Cumulative Layout Shift (target: under 0.1): Visual stability — does the page jump around?

  • Always specify width and height on images (or use fill with a sized container)
  • next/font eliminates font-swap layout shift
  • Don't inject content above the fold after the initial render
  • Reserve space for ads and dynamic content
High-impact answer
If you can say "we had a CLS score of 0.25, I identified it was our hero image loading without dimensions, added width/height to next/image and it dropped to 0.04" — that's a story that lands. Real numbers, real fix, real outcome.
Mid-level question
How do you analyse and reduce your JavaScript bundle size in a Next.js app?
+

Step 1: Measure first. Install @next/bundle-analyzer to visualise what's in your bundle and where the weight is coming from. Don't optimise blindly.

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true'
})
module.exports = withBundleAnalyzer({})

// Run: ANALYZE=true npm run build

Step 2: Move logic to Server Components. Any utility, data transformation, or library that doesn't need to run in the browser should be in a Server Component. It won't ship to the client at all.

Step 3: Dynamic imports for heavy UI.

import dynamic from 'next/dynamic'
const RichTextEditor = dynamic(() => import('./RichTextEditor'), {
  loading: () => <p>Loading editor...</p>,
  ssr: false  // skip SSR for browser-only components
})

Step 4: Named imports, not default imports. Import only what you use from libraries. Many libraries (like lodash) have poor tree-shaking with default imports.

// ❌ Imports entire library
import _ from 'lodash'
const result = _.debounce(fn, 300)

// ✅ Import only what you need
import debounce from 'lodash/debounce'
API Routes & Middleware
Route Handlers, middleware edge cases, and when to use each.
Commonly asked
How do Route Handlers work in App Router? What changed from Pages Router API routes?
+

Route Handlers replace pages/api/ routes. Create a route.ts file inside app/. Export named async functions matching HTTP methods.

// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const user = await db.user.findUnique({ where: { id: params.id } })
  if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 })
  return NextResponse.json(user)
}

export async function PATCH(request: NextRequest, ...) {
  const body = await request.json()
  const updated = await db.user.update({ where: { id: params.id }, data: body })
  return NextResponse.json(updated)
}

Key differences from Pages Router: uses Web standard Request/Response APIs instead of Node.js req/res. Can run at the edge. A folder cannot have both page.tsx and route.ts.

Mid-level question
What is middleware in Next.js? What can it do — and what can't it do?
+

Middleware runs at the edge before any request is processed. It can inspect and modify incoming requests before they reach your pages or API routes.

What it CAN do:

  • Redirect: send users to a different URL
  • Rewrite: change what URL is actually served (URL stays the same for the user)
  • Set or read request and response headers
  • Read cookies (for auth checks)
  • Return a response directly (for rate limiting)

What it CANNOT do:

  • Access a database directly (edge runtime has no Node.js APIs)
  • Use most Node.js built-ins
  • Run heavy computation (it runs on every matched request)
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*']
}
Important nuance
For JWT-based auth, you can verify a token in middleware because it's CPU-only. For session-based auth (where you'd need to look up a session in a database), you can only verify the existence of a cookie in middleware — the actual session validation happens in the page/layout as a Server Component check.
State Management
How state management changes with App Router and what patterns work well.
Mid-level question
How do you approach global state management in an App Router application?
+

App Router changes the state management picture significantly. The goal is to keep as much state on the server as possible and minimize client-side state.

URL state (recommended first choice): Use search params for shareable, bookmarkable state — filters, pagination, selected tab. Server Components can read search params directly. Client Components use useSearchParams().

Server state: Data from the server lives in Server Components and the cache. Don't duplicate it in client stores — that creates sync issues. After mutations, use revalidatePath or revalidateTag to refresh it.

UI state (ephemeral): Modals open/closed, dropdown state, hover state. useState is fine here — it's transient and doesn't need to survive navigation.

Cross-component client state: When multiple Client Components share state that doesn't belong in the URL, use Zustand or Jotai. Both work well in App Router without the Redux boilerplate.

// Zustand store (client-side only)
import { create } from 'zustand'
const useCartStore = create((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) }))
}))
Common mistake
Using React Query or SWR to fetch data that could be fetched in a Server Component. If you're already in App Router, fetch it in a Server Component with async/await. React Query is still valuable for client-side realtime, optimistic updates, or polling — not for initial data loads that a Server Component handles better.
Authentication Patterns
Protecting routes, session management, and Auth.js v5 integration.
Mid-level question
What are the different approaches to protecting routes in App Router? When would you use each?
+

There are three layers where you can enforce authentication:

1. Middleware (recommended for broad protection): Runs at the edge before anything renders. Fastest. Best for protecting entire sections of the app.

export function middleware(request: NextRequest) {
  const session = request.cookies.get('session')
  if (!session) return NextResponse.redirect(new URL('/login', request.url))
}
export const config = { matcher: ['/dashboard/:path*'] }

2. Layout Server Component: Verify the session in a layout and redirect if not authenticated. Useful for role-based access within an authenticated section.

export default async function AdminLayout({ children }) {
  const session = await auth()
  if (session?.user?.role !== 'admin') redirect('/unauthorized')
  return children
}

3. Page level: For granular page-specific checks, verify in the page itself.

Which to use when:

  • Middleware for "is the user logged in at all" checks across a whole section
  • Layout for "does the user have the right role/permissions for this section"
  • Page for very specific data-level checks ("does this user own this resource")
Important security note
Never rely solely on middleware for security-critical checks. Middleware can only verify a cookie exists — not that the session is valid in the database. Always verify the actual session in Server Components for sensitive operations.
Deep dive
How does Auth.js (NextAuth v5) integrate with App Router? What changed from v4?
+

Auth.js v5 was rewritten specifically for App Router. The main differences from v4:

Route Handler instead of catch-all API route:

// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth'
export const { GET, POST } = handlers

Universal auth() function:

// In Server Components
import { auth } from '@/auth'
const session = await auth()

// In Client Components
import { useSession } from 'next-auth/react'
const { data: session } = useSession()

// In middleware
export { auth as middleware } from '@/auth'
Testing Next.js Applications
A differentiator at 1+ YOE. Most candidates can't speak to this confidently.
Testing Next.js App Router apps is genuinely harder than testing Pages Router apps. Interviewers ask this because it's a real pain point — knowing the patterns signals you've actually wrestled with it in production.
Good differentiator
What is your testing strategy for a Next.js App Router application?
+

A layered approach works best for App Router apps:

Unit tests (Vitest + React Testing Library): Test Client Components in isolation. These are the easiest to test — they're just React components.

// Testing a Client Component
import { render, screen, fireEvent } from '@testing-library/react'
import { AddToCartButton } from './AddToCartButton'

it('shows "Added!" after clicking', async () => {
  render(<AddToCartButton productId="123" />)
  fireEvent.click(screen.getByRole('button'))
  await screen.findByText('Added!')
})

Server Component testing: Server Components are async functions — you can test them directly:

// Testing a Server Component
import { ProductList } from './ProductList'
vi.mock('../db', () => ({ findProducts: vi.fn().mockResolvedValue([...]) }))

it('renders products', async () => {
  const component = await ProductList()
  const { getByText } = render(component)
  getByText('Product Name')
})

Integration / E2E tests (Playwright): Test full user flows — authentication, form submissions, navigation. This is where you catch App Router-specific issues like caching, streaming, and redirect behaviour.

Industry reality
Many teams test Server Components indirectly through E2E tests and focus unit tests on Client Components and pure utilities. The tooling for direct Server Component testing is still maturing. Knowing this nuance signals real experience.
Interview Tips from the Hiring Side
Patterns that consistently separate hired candidates from those who don't make it.
01
Lead with shipping, not studying
Start every answer by grounding it in something you built. "On the TDS landing page I worked on, we had X problem and I solved it by Y." Interviewers remember stories, not textbook answers. Your experience refactoring AddCTC into a modular structure? That's exactly the kind of thing to mention when architecture questions come up.
02
Always say the tradeoff
Never say "I used X." Always say "I used X because Y, even though it means Z." "I used SSR because the dashboard is personalized, even though it means higher server costs than SSG." Tradeoff thinking is what separates junior from mid-level in interviewers' minds.
03
Pages vs App Router — be explicit
This trips up more candidates than any other topic. When you answer a data fetching question, be explicit: "In Pages Router, this was getServerSideProps. In App Router, I'd use an async Server Component with cache: 'no-store'." Never leave the interviewer guessing which router you're describing.
04
Handling "I don't know"
Say what you do know, then reason forward. "I haven't used that specific API, but given that Next.js handles fetch caching at the request level, I'd expect it to work similarly to how revalidateTag works — purging cached entries by label." Shows thinking. Silence doesn't.
05
The live coding trap: import source
If you're asked to write any component using routing in App Router, you must import from next/navigation, not next/router. Interviewers notice this immediately. It's one of the highest-signal mistakes in a live coding session.
06
Performance answers need numbers
Vague: "I improved performance using next/image." Strong: "Our LCP was 3.2s. The biggest contributor was our hero image loading without dimensions, causing layout shift and a full re-render. I added explicit dimensions to next/image and LCP dropped to 1.4s." Specificity signals production experience.
07
Show curiosity, not just competence
At the end of every interview, ask thoughtful questions. "What rendering strategies are you using today? Are you evaluating App Router migration? What's the biggest frontend challenge the team is facing?" These questions show you think at a product level, not just an implementation level.
08
On system design questions
At 1+ YOE, you might get light system design: "how would you architect a product listing page?" Think out loud: routing (how to handle slugs), rendering (SSG + ISR for product pages), data fetching (Server Components + fetch with revalidate), and Client Components (add to cart, filters). Structure your answer around these pillars.
Questions to ask them — these signal depth
Tech stack
Which Next.js version are you on? App Router or Pages Router?
Infrastructure
Are you on Vercel or self-hosted? How does that affect your caching strategy?
Quality
What does your testing strategy look like for the frontend?
Performance
Do you track Core Web Vitals in production? What's your biggest performance challenge?
Team
What does the deploy process look like? How often does the team ship?
Growth
What does the onboarding process look like for a new frontend engineer?
Work type
What's the ratio of greenfield work to maintaining existing code?
Pain points
What's the biggest frontend challenge the team is trying to solve right now?
Pre-Interview Readiness Checklist
Tick off each item. Focus your remaining prep on the gaps.
Overall readiness
0 / 24 complete
Click each item to mark it as confident. Focus on unchecked items first.
App Router Fundamentals
App Router folder conventions: page, layout, loading, error, not-found, template
Know what each file does and when to use it
Route groups (folderName) — organize without affecting URL
Common pattern: (auth) and (dashboard) with different layouts
Dynamic segments [slug] and catch-all [...slug] routes
Know how params are accessed in App Router (not useRouter)
useRouter from next/navigation (NOT next/router)
This is a live coding trap — burn this into memory
Parallel routes @slot and intercepting routes (...) — basics
Know one real use case for each
Server & Client Components
Server vs Client Components — when to use each, and why
Be able to explain the bundle size consequence
'use client' marks a bundle boundary — not "browser only"
Client Components still SSR — know the nuance
No hooks in Server Components — extract to Client Components
Context providers — wrap only the provider in 'use client', pass children as props
This is the most commonly missed pattern
Data Fetching & Caching
async/await data fetching directly in Server Components
No useEffect, no loading state — just async/await
Three cache options: force-cache, no-store, next: { revalidate: N }
Map each to SSG, SSR, ISR — always asked
Server Actions — 'use server', mutation pattern, revalidation after
When to use vs Route Handlers
revalidatePath vs revalidateTag — difference and when to use each
Tags = cross-route; path = specific URL
Request memoization — duplicate fetch() calls are deduplicated
Performance
Core Web Vitals: LCP, INP, CLS — targets and what Next.js features address each
Have a story about a real performance improvement
next/image — formats, lazy loading, priority prop, preventing CLS
next/font — self-hosted at build time, no runtime Google request
Dynamic imports with next/dynamic for code splitting
API, Auth & Testing
Route Handlers — route.ts, named HTTP method exports, Web standard Request/Response
Middleware — what it can and can't do, keep it fast, use matcher config
Can't access DB directly — edge runtime limitations
Auth patterns — middleware vs layout vs page level, and when to use each
Testing strategy — Vitest for Client Components, Playwright for E2E, Server Component testing nuances