FREE PREVIEW

You're viewing a free preview

This is a sample of 15 questions from our full collection of 43 interview questions.

Unlock all 43 questions with detailed explanations and code examples

Get Full Access

Data Fetching

What is the difference between fetching data in Server Components vs Client Components?

The 30-Second Answer: Server Components fetch data on the server during render using async/await, with automatic caching and deduplication. Client Components fetch data in the browser using hooks like useEffect, requiring client-side JavaScript and causing loading states visible to users.

The 2-Minute Answer (If They Want More): Server Components fundamentally change where and when data fetching happens. They execute only on the server, allowing direct access to databases, file systems, and backend services without exposing credentials. Data is fetched during the render process, and only the resulting HTML is sent to the client, reducing JavaScript bundle size and improving initial page load.

Server Components support async/await natively, making the component function itself async. There's no loading state for the initial render because the server waits for data before sending the response. You can use Suspense boundaries to stream parts of the UI while other parts are still loading, providing instant feedback without client-side JavaScript.

Client Components fetch data in the browser after hydration. They use React hooks like useEffect, useState, or libraries like SWR and React Query. This means users see loading spinners, the fetch happens over the network from the client's location, and the fetching logic is included in the JavaScript bundle.

The recommended pattern is to fetch in Server Components and pass data as props to Client Components. This minimizes client-side JavaScript, improves performance, and keeps sensitive logic on the server. Use Client Components for data fetching only when you need interactivity-driven refetching, such as search-as-you-type or user-triggered refreshes.

Code Example:

// Server Component - Recommended approach
// app/dashboard/page.tsx
async function getUser(id: string) {
  // Direct database access - credentials never exposed to client
  const user = await db.user.findUnique({
    where: { id },
    include: { posts: true, profile: true },
  });

  return user;
}

export default async function DashboardPage() {
  // Fetch happens on server, no loading state needed
  const user = await getUser('123');

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <UserProfile user={user} /> {/* Pass data as props */}
      <PostList posts={user.posts} />
    </div>
  );
}

// Client Component - When interactivity is needed
// app/components/SearchResults.tsx
'use client';

import { useState, useEffect } from 'react';

export default function SearchResults() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!query) return;

    setLoading(true);
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(data => {
        setResults(data);
        setLoading(false);
      });
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {loading && <p>Loading...</p>}
      {results.map(result => (
        <div key={result.id}>{result.title}</div>
      ))}
    </div>
  );
}

// Hybrid approach - Server Component wraps Client Component
// app/products/page.tsx
async function getProducts() {
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 },
  }).then(res => res.json());

  return products;
}

export default async function ProductsPage() {
  // Fetch initial data on server
  const initialProducts = await getProducts();

  return (
    <div>
      <h1>Products</h1>
      {/* Pass server-fetched data to client component */}
      <ProductFilter initialData={initialProducts} />
    </div>
  );
}

Mermaid Diagram:

flowchart LR
    subgraph Server Component
        A[Request] --> B[Server Execution]
        B --> C[Direct DB Access]
        C --> D[Async Await]
        D --> E[Render HTML]
        E --> F[Send HTML to Client]
    end

    subgraph Client Component
        G[HTML Received] --> H[JavaScript Hydration]
        H --> I[useEffect Triggered]
        I --> J[Browser Fetch API]
        J --> K[Network Request]
        K --> L[Update State]
        L --> M[Re-render UI]
    end

    F -.Initial HTML.-> G

References:

↑ Back to top

Next.js Fundamentals

What is the difference between Next.js and Create React App?

The 30-Second Answer: Create React App (CRA) is a client-side only React tool with no built-in routing or SSR, while Next.js is a full-featured framework with server-side rendering, static generation, file-based routing, and API routes built-in. Next.js is production-ready out of the box, whereas CRA requires additional libraries for most real-world applications.

The 2-Minute Answer (If They Want More): Create React App and Next.js serve different purposes and are suited for different types of projects, though Next.js has largely superseded CRA for most use cases.

Rendering Approach: CRA creates purely client-side applications where all rendering happens in the browser. The server sends a minimal HTML shell, and JavaScript builds the entire UI after loading. This leads to poor SEO and slower initial page loads. Next.js supports multiple rendering strategies including SSR, SSG, and ISR, allowing you to choose the optimal approach for each page and delivering pre-rendered HTML for better performance and SEO.

Routing: CRA has no built-in router; you must install and configure React Router or another routing library yourself. Next.js provides file-based routing out of the box—simply create a file in pages/ or app/, and it becomes a route automatically. This includes support for dynamic routes, nested layouts, and route groups without any configuration.

Backend Capabilities: CRA is frontend-only; you need a separate backend for API endpoints. Next.js includes API Routes, allowing you to create backend endpoints within the same application. This is perfect for serverless functions, authentication handlers, or simple CRUD operations.

Build Output and Deployment: CRA produces static files that can be hosted anywhere but always runs as a SPA. Next.js can generate fully static sites (like CRA), server-rendered applications requiring a Node.js server, or hybrid apps with both. This flexibility allows you to optimize each route differently.

Performance and Optimization: CRA requires manual setup for image optimization, code splitting by route, and other performance features. Next.js includes these optimizations automatically, along with the Image component, automatic font optimization, and granular code splitting.

Use Cases: CRA is suitable for simple admin dashboards, internal tools, or prototypes where SEO doesn't matter. Next.js is better for marketing sites, e-commerce, blogs, documentation sites, or any public-facing application where performance and SEO are priorities. Note that CRA is no longer actively maintained, and the React team now recommends using frameworks like Next.js instead.

Code Example:

// Create React App - Client-Side Only
// App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { useState, useEffect } from 'react';

function ProductPage() {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    // Fetch happens in browser, after page loads
    fetch('/api/product/123')
      .then(res => res.json())
      .then(setProduct);
  }, []);

  if (!product) return <div>Loading...</div>;
  return <div>{product.name}</div>;
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/product/:id" element={<ProductPage />} />
      </Routes>
    </BrowserRouter>
  );
}

// Next.js - Server-Side Rendering
// pages/product/[id].js
export async function getServerSideProps({ params }) {
  // Fetch happens on server, HTML is pre-rendered
  const res = await fetch(`https://api.example.com/product/${params.id}`);
  const product = await res.json();

  return {
    props: { product }
  };
}

export default function ProductPage({ product }) {
  // Product data is already here when page loads
  return <div>{product.name}</div>;
}

Comparison Table:

Feature Create React App Next.js
Rendering Client-side only (CSR) SSR, SSG, ISR, CSR
Routing Manual (React Router) File-based, built-in
SEO Poor (requires workarounds) Excellent (pre-rendered HTML)
API Endpoints Requires separate backend Built-in API Routes
Image Optimization Manual Built-in Image component
Code Splitting By route (manual) Automatic, per-page
Initial Page Load Slow (JS must execute) Fast (pre-rendered HTML)
Deployment Static files anywhere Static or Node.js server
TypeScript Requires setup Built-in support
Maintenance No longer actively maintained Actively developed

References:

↑ Back to top

What is the difference between the Pages Router and the App Router in Next.js?

The 30-Second Answer: The Pages Router is Next.js's original routing system using the pages/ directory with client-side React components, while the App Router is the newer system using the app/ directory with React Server Components, streaming, and more powerful layout capabilities. The App Router is now recommended for new projects.

The 2-Minute Answer (If They Want More): Next.js offers two routing systems that represent different eras of the framework's evolution, and they can coexist in the same project during migration.

Architecture and Components: The Pages Router uses traditional React components where everything renders on the client by default (with opt-in SSR/SSG via data fetching functions). The App Router introduces React Server Components as the default, meaning components render on the server unless marked with 'use client'. This fundamental shift allows for better performance, smaller JavaScript bundles, and direct database access from components.

File Structure and Routing: In the Pages Router, each file in pages/ becomes a route (e.g., pages/blog/[slug].js). The App Router uses folders to define routes, with special files like page.js, layout.js, loading.js, and error.js inside each route segment (e.g., app/blog/[slug]/page.js). This folder-based approach provides more granular control over route segments.

Layouts and Nested Routes: The Pages Router has a single _app.js for global layouts and requires custom solutions for nested layouts. The App Router has native support for nested layouts that persist across navigation, don't re-render unnecessarily, and can be nested at any level. Each route segment can have its own layout, loading state, and error boundary.

Data Fetching: The Pages Router uses getStaticProps, getServerSideProps, and getStaticPaths functions. The App Router uses async Server Components that can fetch data directly, with built-in request deduplication and caching. The new fetch API in the App Router extends the Web Fetch API with caching and revalidation options.

Streaming and Suspense: The App Router supports streaming server-side rendering with React Suspense, allowing you to progressively send HTML to the client as it's ready. This enables instant loading states and better perceived performance. The Pages Router doesn't support streaming.

Performance Characteristics: The App Router generally produces smaller JavaScript bundles because Server Components don't ship to the client. It also enables better code organization with colocation of components, styles, and tests. The Pages Router sends all component code to the client, even if it could be server-only.

Both routers are fully supported, and you can use them side-by-side during migration. However, the App Router is recommended for new projects as it represents the future of Next.js and unlocks React's latest features.

Code Example:

// Pages Router Approach
// pages/blog/[slug].js
import { useRouter } from 'next/router';

export async function getStaticProps({ params }) {
  const post = await fetchPost(params.slug);
  return { props: { post } };
}

export async function getStaticPaths() {
  return {
    paths: [{ params: { slug: 'hello-world' } }],
    fallback: true
  };
}

export default function BlogPost({ post }) {
  const router = useRouter();
  if (router.isFallback) return <div>Loading...</div>;
  return <article>{post.title}</article>;
}

// App Router Approach
// app/blog/[slug]/page.js
import { Suspense } from 'react';

async function getPost(slug) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 } // Cache for 1 hour
  });
  return res.json();
}

export async function generateStaticParams() {
  return [{ slug: 'hello-world' }];
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);
  return <article>{post.title}</article>;
}

// app/blog/layout.js
export default function BlogLayout({ children }) {
  return (
    <div className="blog-container">
      <nav>Blog Navigation</nav>
      <Suspense fallback={<div>Loading...</div>}>
        {children}
      </Suspense>
    </div>
  );
}

Mermaid Diagram:

graph TB
    subgraph "Pages Router"
        P1[pages/] --> P2[_app.js<br/>Global Layout]
        P1 --> P3[index.js<br/>Client Component]
        P1 --> P4[blog/[slug].js<br/>Client Component]
        P4 --> P5[getStaticProps<br/>Server Function]
    end

    subgraph "App Router"
        A1[app/] --> A2[layout.js<br/>Root Layout]
        A1 --> A3[page.js<br/>Server Component]
        A1 --> A4[blog/]
        A4 --> A5[layout.js<br/>Blog Layout]
        A4 --> A6[[slug]/]
        A6 --> A7[page.js<br/>Server Component]
        A6 --> A8[loading.js<br/>Loading UI]
        A6 --> A9[error.js<br/>Error Boundary]
    end

    style P2 fill:#61dafb
    style A2 fill:#0070f3
    style A7 fill:#0070f3

Comparison Table:

Feature Pages Router App Router
Directory pages/ app/
Components Client Components (default) Server Components (default)
Layouts Single _app.js Nested layout.js files
Data Fetching getStaticProps, getServerSideProps Async Server Components
Loading States Manual implementation Built-in loading.js
Error Handling Custom error pages error.js boundaries
Streaming Not supported Supported with Suspense
Route Structure File = Route Folder = Route segment
Code Colocation Limited Full (components, tests, styles)
JavaScript Bundle Larger (all components) Smaller (Server Components)
Stability Stable, mature Stable as of Next.js 13.4+

References:

↑ Back to top

What is Next.js and what problems does it solve?

The 30-Second Answer: Next.js is a React framework that provides server-side rendering, static site generation, and built-in routing out of the box. It solves the problems of SEO optimization, initial load performance, and complex configuration that pure client-side React applications face.

The 2-Minute Answer (If They Want More): Next.js is a production-ready React framework built by Vercel that extends React's capabilities by adding server-side features and developer-friendly tooling. While React is just a UI library, Next.js is a full-featured framework that handles routing, data fetching, and various rendering strategies.

The framework solves several critical problems in modern web development. First, it addresses SEO challenges by enabling server-side rendering (SSR) and static site generation (SSG), ensuring search engines can crawl and index content properly. Second, it dramatically improves initial page load performance by sending pre-rendered HTML to the browser instead of waiting for JavaScript to execute. Third, it eliminates the configuration overhead of setting up Webpack, Babel, and routing libraries that would otherwise require significant setup time.

Next.js also provides automatic code splitting, which means each page only loads the JavaScript it needs, reducing bundle sizes. The framework includes built-in optimizations for images, fonts, and third-party scripts, as well as API routes that let you build backend endpoints without a separate server. With features like Incremental Static Regeneration (ISR), you can update static content without rebuilding your entire site.

For development teams, Next.js offers a streamlined experience with fast refresh, TypeScript support out of the box, and a file-system based router that makes creating new pages as simple as adding a file to your project.

Mermaid Diagram:

flowchart TD
    A[Traditional React SPA] --> B[Client-Side Only]
    A --> C[Poor SEO]
    A --> D[Slow Initial Load]
    A --> E[Manual Routing Setup]

    F[Next.js] --> G[SSR/SSG/ISR]
    F --> H[Excellent SEO]
    F --> I[Fast Initial Load]
    F --> J[File-Based Routing]
    F --> K[Built-in Optimizations]

    style F fill:#0070f3,color:#fff
    style A fill:#61dafb,color:#000

References:

↑ Back to top

Rendering Strategies

What is the difference between SSR, SSG, and ISR in Next.js?

The 30-Second Answer: SSG (Static Site Generation) builds pages at build time, SSR (Server-Side Rendering) generates pages on each request, and ISR (Incremental Static Regeneration) combines both by serving static pages that can be regenerated in the background. SSG is fastest, SSR is most dynamic, and ISR balances performance with fresh content.

The 2-Minute Answer (If They Want More):

Static Site Generation (SSG) pre-renders pages at build time, creating HTML files that are served to all users. This is the fastest option since pages are cached on a CDN, but requires rebuilding the entire site to update content. It's ideal for content that doesn't change frequently, like blog posts or documentation.

Server-Side Rendering (SSR) generates HTML on each request, ensuring users always see the latest data. While slower than SSG due to server processing on every request, it's necessary for personalized content or frequently changing data. The server fetches data and renders the page before sending it to the client.

Incremental Static Regeneration (ISR) combines the best of both worlds. Pages are statically generated but can be regenerated in the background after a specified time interval. When a user visits an ISR page, they get the cached version immediately, and if the revalidation period has passed, Next.js triggers a background regeneration. The next visitor gets the updated page.

The choice depends on your use case: SSG for maximum performance with stable content, SSR for real-time personalization, and ISR for content that needs periodic updates without full rebuilds.

Code Example:

// SSG - Static Site Generation
export async function getStaticProps() {
  const data = await fetchData();

  return {
    props: { data }
  };
}

// SSR - Server-Side Rendering
export async function getServerSideProps(context) {
  const data = await fetchData();

  return {
    props: { data }
  };
}

// ISR - Incremental Static Regeneration
export async function getStaticProps() {
  const data = await fetchData();

  return {
    props: { data },
    revalidate: 60 // Regenerate page every 60 seconds
  };
}

Comparison Table:

Feature SSG SSR ISR
Render Time Build time Request time Build time + Background
Performance Fastest (CDN cached) Slower (per-request) Fast (mostly cached)
Data Freshness Static until rebuild Always fresh Periodically fresh
Server Load Minimal High Low
Best For Blogs, marketing pages Dashboards, user profiles E-commerce, news sites
Build Time Can be long None Incremental

Mermaid Diagram:

flowchart TD
    A[User Request] --> B{Rendering Strategy?}
    B -->|SSG| C[Serve Pre-built HTML from CDN]
    B -->|SSR| D[Server fetches data]
    B -->|ISR| E{Cache expired?}

    D --> F[Server renders HTML]
    F --> G[Send HTML to client]

    E -->|No| H[Serve cached HTML]
    E -->|Yes| I[Serve cached HTML]
    I --> J[Trigger background regeneration]
    J --> K[Update cache for next request]

    C --> L[Fast Response]
    G --> M[Fresh but Slower Response]
    H --> L

References:

↑ Back to top

What is Incremental Static Regeneration (ISR) and how do you implement it?

The 30-Second Answer: ISR allows you to update static pages after build time without rebuilding the entire site. By setting a revalidate value in getStaticProps, pages are regenerated in the background when the timer expires, giving you the performance of SSG with the freshness of SSR.

The 2-Minute Answer (If They Want More):

Incremental Static Regeneration (ISR) is a Next.js feature that combines the speed of static generation with the flexibility of server-side rendering. Instead of regenerating all pages at build time or on every request, ISR allows you to specify a revalidation period for each page. When a user visits a page after the revalidation period has passed, they receive the cached version immediately, and Next.js triggers a background regeneration.

The process works in three stages: First, pages are statically generated at build time just like SSG. Second, when the revalidation timer expires and a user visits the page, they get the stale cached version (ensuring fast response times). Third, Next.js regenerates the page in the background and updates the cache for subsequent visitors.

ISR also supports on-demand revalidation, allowing you to manually trigger page regeneration via API routes. This is perfect for when you publish new content and want to update specific pages immediately without waiting for the revalidation timer. You can call res.revalidate('/path') from an API route to regenerate any page.

For dynamic routes with fallback options, ISR enables you to generate pages on-demand. Set fallback: 'blocking' or fallback: true in getStaticPaths, and Next.js will generate pages that weren't pre-rendered at build time when users first request them. These newly generated pages are then cached and served statically to future visitors.

Code Example:

// pages/blog/[slug].js - Basic ISR implementation
export async function getStaticProps({ params }) {
  const post = await fetchPost(params.slug);

  if (!post) {
    return { notFound: true };
  }

  return {
    props: { post },
    // Regenerate page every 60 seconds
    revalidate: 60
  };
}

export async function getStaticPaths() {
  const posts = await fetchRecentPosts(10);

  return {
    paths: posts.map(post => ({ params: { slug: post.slug } })),
    // Generate other pages on-demand and cache them
    fallback: 'blocking'
  };
}

// pages/api/revalidate.js - On-demand revalidation
export default async function handler(req, res) {
  // Check for secret to confirm valid request
  if (req.query.secret !== process.env.REVALIDATE_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  try {
    // Get path to revalidate from request
    const path = req.query.path || req.body.path;

    if (!path) {
      return res.status(400).json({ message: 'Path is required' });
    }

    // Trigger revalidation
    await res.revalidate(path);

    return res.json({
      revalidated: true,
      path,
      timestamp: new Date().toISOString()
    });
  } catch (err) {
    return res.status(500).json({
      message: 'Error revalidating',
      error: err.message
    });
  }
}

// Advanced ISR with multiple revalidation strategies
export async function getStaticProps({ params, preview = false }) {
  const product = await fetchProduct(params.id);
  const reviews = await fetchReviews(params.id);
  const relatedProducts = await fetchRelatedProducts(params.id);

  // Different content may need different revalidation times
  return {
    props: {
      product,
      reviews,
      relatedProducts,
      buildTime: new Date().toISOString()
    },
    // Product details: revalidate every 10 minutes
    // Reviews might update more frequently
    // Related products change less often
    revalidate: 600 // 10 minutes
  };
}

// Fallback options comparison
export async function getStaticPaths() {
  const popularProducts = await fetchPopularProducts(100);

  return {
    paths: popularProducts.map(p => ({ params: { id: String(p.id) } })),

    // Choose one fallback strategy:

    // false: 404 for non-pre-rendered pages
    // fallback: false

    // true: Show loading state, then render
    // fallback: true

    // 'blocking': Wait for page to generate (recommended)
    fallback: 'blocking'
  };
}

// Component handling fallback: true
export default function ProductPage({ product }) {
  const router = useRouter();

  // Show loading state while page generates
  if (router.isFallback) {
    return <div>Loading product...</div>;
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>Last updated: {product.updatedAt}</p>
    </div>
  );
}

Webhook Integration Example:

// Trigger revalidation from CMS webhook
// Example: Contentful, Sanity, Strapi, etc.

// pages/api/webhook/content-update.js
import crypto from 'crypto';

export default async function handler(req, res) {
  // Verify webhook signature (example for generic webhook)
  const signature = req.headers['x-webhook-signature'];
  const expectedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (signature !== expectedSignature) {
    return res.status(401).json({ message: 'Invalid signature' });
  }

  try {
    const { contentType, slug, action } = req.body;

    // Revalidate specific pages based on content type
    const paths = [];

    if (contentType === 'blog-post') {
      paths.push(`/blog/${slug}`);
      paths.push('/blog'); // Blog listing page
    } else if (contentType === 'product') {
      paths.push(`/products/${slug}`);
      paths.push('/products'); // Product listing page
    }

    // Revalidate all affected paths
    await Promise.all(paths.map(path => res.revalidate(path)));

    return res.json({
      revalidated: true,
      paths,
      action,
      timestamp: new Date().toISOString()
    });
  } catch (err) {
    console.error('Revalidation error:', err);
    return res.status(500).json({ message: 'Error revalidating' });
  }
}

Mermaid Diagram:

flowchart TD
    A[Build Time] --> B[Generate static pages with getStaticProps]
    B --> C[Set revalidate: 60 seconds]
    C --> D[Deploy to CDN]

    D --> E[User Request at t=0]
    E --> F[Serve cached HTML instantly]

    F --> G[User Request at t=65]
    G --> H{Revalidate period expired?}

    H -->|Yes| I[Serve stale cached version]
    I --> J[Trigger background regeneration]
    J --> K[Fetch fresh data]
    K --> L[Generate new HTML]
    L --> M[Update cache]

    H -->|No| N[Serve cached version]

    M --> O[User Request at t=70]
    O --> P[Serve updated cached HTML]

    Q[On-Demand Trigger] --> R[API: res.revalidate path]
    R --> J

    style J fill:#90EE90
    style M fill:#90EE90

ISR Strategy Decision Matrix:

Content Type Revalidate Time Fallback Use Case
Blog Posts 3600 (1 hour) 'blocking' Content rarely changes
Product Pages 300 (5 min) 'blocking' Prices/stock update occasionally
News Articles 60 (1 min) 'blocking' Breaking news updates
Documentation 86400 (24 hours) 'blocking' Infrequent updates
User Profiles N/A false Use SSR instead
Marketing Pages 3600 (1 hour) false Limited, pre-known pages

Benefits of ISR:

  1. Performance: Pages served from CDN like SSG
  2. Freshness: Content updates without full rebuild
  3. Scalability: Generate pages on-demand as needed
  4. Cost Efficiency: No need for always-running servers
  5. Flexibility: Different revalidation times per page

Limitations:

  • Revalidation happens after user request (first user gets stale data)
  • No guarantees on exact revalidation timing
  • Requires hosting platform support (Vercel, Netlify, etc.)
  • Cache purging may vary by platform

References:

↑ Back to top

What is the default rendering behavior in the App Router?

The 30-Second Answer: The App Router defaults to Server Components that render statically at build time when possible, automatically switching to dynamic rendering when it detects dynamic functions like cookies() or headers(). You can control this with export const dynamic config options.

The 2-Minute Answer (If They Want More):

Next.js App Router uses an intelligent automatic optimization system. By default, it attempts to render pages as static HTML at build time (equivalent to SSG in Pages Router), caching the result for fast delivery. This is possible because all components are Server Components by default, and Next.js can execute them during the build process.

However, the App Router automatically switches to dynamic rendering (equivalent to SSR) when it detects certain patterns: calling dynamic functions like cookies(), headers(), or searchParams; reading request-time data; or using cache-busting APIs. This "smart static" approach means you don't need to choose between getStaticProps and getServerSideProps - Next.js makes that decision based on your code.

You can explicitly control this behavior using route segment configuration options. Setting export const dynamic = 'force-static' ensures a route is always pre-rendered, erroring if it uses dynamic features. Conversely, dynamic = 'force-dynamic' opts into SSR even without dynamic functions. The revalidate option enables ISR by specifying regeneration intervals.

Next.js also introduces partial pre-rendering (PPR) in experimental mode, which can render some parts of a page statically while streaming dynamic parts. This provides the best of both worlds: instant static shell with progressive dynamic content loading. The rendering strategy is determined per-route segment, allowing different parts of your application to use different strategies.

Code Example:

// app/page.js - Static by default
// This renders at build time automatically
export default async function HomePage() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'force-cache' // Default behavior
  });

  return <div>Static Content: {data.title}</div>;
}

// app/profile/page.js - Automatically becomes dynamic
// Using cookies() triggers dynamic rendering
import { cookies } from 'next/headers';

export default async function ProfilePage() {
  // This function makes the route dynamic
  const cookieStore = cookies();
  const token = cookieStore.get('auth-token');

  const user = await fetchUser(token.value);

  return <div>Welcome, {user.name}</div>;
}

// app/products/page.js - Explicit static with ISR
export const revalidate = 3600; // Revalidate every hour

export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products');

  return (
    <div>
      {products.map(p => (
        <div key={p.id}>{p.name}</div>
      ))}
    </div>
  );
}

// app/dashboard/page.js - Force dynamic rendering
export const dynamic = 'force-dynamic';
export const revalidate = 0; // Never cache

export default async function DashboardPage() {
  // Always renders on request, never cached
  const realTimeData = await fetchDashboardData();

  return <div>Live Data: {realTimeData.value}</div>;
}

// app/blog/page.js - Force static rendering
export const dynamic = 'force-static';
export const dynamicParams = false; // 404 for unknown routes

export default async function BlogPage() {
  const posts = await fetchPosts();

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </div>
  );
}

// Controlling fetch cache behavior
// Static fetch (default)
const data1 = await fetch('https://api.example.com/data1');

// Static fetch with revalidation
const data2 = await fetch('https://api.example.com/data2', {
  next: { revalidate: 60 } // ISR with 60 second revalidation
});

// Dynamic fetch (never cache)
const data3 = await fetch('https://api.example.com/data3', {
  cache: 'no-store'
});

// Dynamic fetch with tags for on-demand revalidation
const data4 = await fetch('https://api.example.com/data4', {
  next: { tags: ['products'] }
});

// Then revalidate from API route:
// app/api/revalidate/route.js
import { revalidateTag } from 'next/cache';

export async function POST(request) {
  const { tag } = await request.json();
  revalidateTag(tag); // Revalidate all fetches with this tag
  return Response.json({ revalidated: true });
}

// Route Segment Config options
export const dynamic = 'auto'; // Default: auto-detect
// export const dynamic = 'force-static'; // Always static
// export const dynamic = 'force-dynamic'; // Always dynamic
// export const dynamic = 'error'; // Error if dynamic functions used

export const dynamicParams = true; // Default: generate dynamic routes
// export const dynamicParams = false; // 404 for unknown dynamic routes

export const revalidate = false; // Default: cache forever
// export const revalidate = 0; // Never cache
// export const revalidate = 3600; // ISR with 1 hour revalidation

export const fetchCache = 'auto'; // Default: smart caching
// export const fetchCache = 'default-cache'; // Cache by default
// export const fetchCache = 'only-cache'; // Only use cache
// export const fetchCache = 'force-cache'; // Force all cache
// export const fetchCache = 'default-no-store'; // Don't cache by default
// export const fetchCache = 'only-no-store'; // Never cache
// export const fetchCache = 'force-no-store'; // Force no cache

export const runtime = 'nodejs'; // Default: Node.js runtime
// export const runtime = 'edge'; // Edge runtime

export const preferredRegion = 'auto'; // Default: all regions
// export const preferredRegion = 'iad1'; // Specific region
// export const preferredRegion = ['iad1', 'sfo1']; // Multiple regions

Dynamic Functions That Trigger SSR:

// These functions automatically make route dynamic:
import { cookies } from 'next/headers';
import { headers } from 'next/headers';
import { draftMode } from 'next/headers';

// 1. Cookies
const cookieStore = cookies();
const authToken = cookieStore.get('token');

// 2. Headers
const headersList = headers();
const userAgent = headersList.get('user-agent');

// 3. Search params (in page.js)
function SearchPage({ searchParams }) {
  // Using searchParams makes route dynamic
  const query = searchParams.q;
  return <div>Search: {query}</div>;
}

// 4. Draft mode (for CMS preview)
const { isEnabled } = draftMode();

// 5. No-store fetch
fetch('https://api.example.com/data', { cache: 'no-store' });

Mermaid Diagram:

flowchart TD
    A[Next.js App Router Route] --> B{Analyze component}

    B --> C{Uses dynamic functions?}

    C -->|Yes: cookies, headers, etc.| D[Force Dynamic SSR]
    C -->|No| E{Check fetch calls}

    E --> F{Any no-store fetch?}
    F -->|Yes| D
    F -->|No| G{Explicit config?}

    G -->|dynamic = 'force-dynamic'| D
    G -->|dynamic = 'force-static'| H[Static Generation SSG]
    G -->|revalidate = N| I[ISR with N seconds]
    G -->|None| H

    D --> J[Render on every request]
    H --> K[Pre-render at build time]
    I --> L[Pre-render + background regeneration]

    K --> M[Cache forever]
    L --> N[Cache with revalidation]
    J --> O[No caching or CDN cache]

    style D fill:#FFB6C1
    style H fill:#90EE90
    style I fill:#87CEEB

Rendering Strategy Decision Matrix:

Code Pattern Result Why
No dynamic features Static (SSG) Can pre-render safely
cookies() or headers() Dynamic (SSR) Needs request context
searchParams in page Dynamic (SSR) Query params are request-time
fetch() default Static (SSG) Cached fetch
fetch({ cache: 'no-store' }) Dynamic (SSR) Explicitly uncached
fetch({ next: { revalidate: 60 } }) ISR Timed revalidation
export const revalidate = 60 ISR Route-level revalidation
export const dynamic = 'force-dynamic' Dynamic (SSR) Explicitly forced
export const dynamic = 'force-static' Static (SSG) Explicitly forced

Comparison: Pages Router vs App Router:

// PAGES ROUTER - Manual choice
// pages/products.js - Must choose SSG
export async function getStaticProps() {
  const products = await fetchProducts();
  return { props: { products } };
}

// pages/dashboard.js - Must choose SSR
export async function getServerSideProps() {
  const data = await fetchDashboard();
  return { props: { data } };
}

// APP ROUTER - Automatic decision
// app/products/page.js - Automatic SSG
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products');
  return <div>{/* render products */}</div>;
}

// app/dashboard/page.js - Automatic SSR (due to cookies)
import { cookies } from 'next/headers';

export default async function DashboardPage() {
  const token = cookies().get('auth');
  const data = await fetchDashboard(token);
  return <div>{/* render dashboard */}</div>;
}

Caching Behavior:

// Different cache strategies in the same component
export default async function MixedPage() {
  // Static data - cached forever
  const categories = await fetch('https://api.example.com/categories', {
    cache: 'force-cache'
  });

  // ISR data - revalidated every 5 minutes
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 300 }
  });

  // Dynamic data - never cached
  const liveInventory = await fetch('https://api.example.com/inventory', {
    cache: 'no-store'
  });

  // The route becomes dynamic due to no-store fetch
  return (
    <div>
      <Categories data={categories} />
      <Products data={products} />
      <Inventory data={liveInventory} />
    </div>
  );
}

Best Practices:

  1. Default to Static: Let Next.js choose static rendering when possible
  2. Use Dynamic Deliberately: Only call cookies()/headers() when necessary
  3. Granular Caching: Use fetch-level cache controls for mixed strategies
  4. Leverage Streaming: Use Suspense for progressive rendering
  5. Monitor Rendering: Check build output to see which routes are static/dynamic

Build Output Example:

Route (app)                    Size     First Load JS
┌ ○ /                          143 B          87.2 kB  (Static)
├ ○ /about                     157 B          87.3 kB  (Static)
├ λ /dashboard                 245 B          92.1 kB  (Dynamic)
├ ◐ /blog/[slug]              1.23 kB         88.3 kB  (ISR: 3600s)
â”” â—Ź /products                  892 B          88.9 kB  (Static)

â—‹  (Static)   - Pre-rendered at build time
â—Ź  (SSG)      - Pre-rendered with getStaticProps
λ  (SSR)      - Rendered on each request
◐  (ISR)      - Incremental Static Regeneration

References:

↑ Back to top

API Routes

What is the difference between API routes in Pages Router vs Route Handlers in App Router?

The 30-Second Answer: Pages Router API routes use Node.js request/response objects and a single handler function, while App Router Route Handlers use Web standard Request/Response APIs with separate named functions for each HTTP method, supporting both Node.js and Edge runtimes.

The 2-Minute Answer (If They Want More): The fundamental difference lies in the API design and runtime capabilities. Pages Router API routes (pages/api/) use Node.js-specific req and res objects modeled after Express.js, requiring a single default export function that manually checks the HTTP method. This approach is familiar to Node.js developers but locks you into the Node.js runtime.

App Router Route Handlers (app/**/route.js) embrace Web standards by using the native Request and Response objects from the Fetch API. You export separate named functions for each HTTP method (GET, POST, etc.), making the code more declarative and easier to maintain. This also enables support for the Edge Runtime, allowing your API routes to run on edge networks for lower latency.

Request body parsing differs significantly: Pages Router includes automatic body parsing middleware that you can configure or disable, while Route Handlers require you to explicitly call methods like request.json() or request.formData(). Route Handlers also support streaming responses and more modern web APIs.

Location matters too: Pages Router API routes must live in pages/api/ and are completely separate from page routes. Route Handlers can exist anywhere in the app directory structure but cannot coexist with a page at the same route path. Route Handlers also support route segment config options for controlling caching, runtime, and revalidation behavior that aren't available in Pages Router API routes.

Code Example:

// PAGES ROUTER API ROUTE
// pages/api/products/[id].js
export default async function handler(req, res) {
  const { id } = req.query;

  // Manual method checking
  if (req.method === 'GET') {
    // Body is automatically parsed
    const product = await getProduct(id);
    return res.status(200).json(product);
  } else if (req.method === 'PUT') {
    // req.body is already parsed
    const updated = await updateProduct(id, req.body);
    return res.status(200).json(updated);
  } else {
    res.setHeader('Allow', ['GET', 'PUT']);
    return res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

// Disable automatic body parsing if needed
export const config = {
  api: {
    bodyParser: false,
  },
};

// APP ROUTER ROUTE HANDLER
// app/api/products/[id]/route.js
import { NextResponse } from 'next/server';

// Separate named exports for each method
export async function GET(request, { params }) {
  const { id } = params;
  const product = await getProduct(id);
  return NextResponse.json(product);
}

export async function PUT(request, { params }) {
  const { id } = params;
  // Explicitly parse body
  const body = await request.json();
  const updated = await updateProduct(id, body);
  return NextResponse.json(updated);
}

// Route segment config (App Router only)
export const runtime = 'edge'; // Can run on Edge Runtime
export const dynamic = 'force-dynamic'; // Opt out of caching

// Edge Runtime example (App Router only)
// app/api/edge/route.js
export const runtime = 'edge';

export async function GET(request) {
  // This runs on Edge Runtime for low latency
  const geo = request.geo;
  return NextResponse.json({
    country: geo?.country,
    timestamp: Date.now()
  });
}

// Streaming response (App Router only)
// app/api/stream/route.js
export async function GET() {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      controller.enqueue(encoder.encode('data: Hello\n\n'));
      await new Promise(resolve => setTimeout(resolve, 1000));
      controller.enqueue(encoder.encode('data: World\n\n'));
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
    },
  });
}

Mermaid Diagram:

flowchart TD
    A[API Endpoint] --> B{Router Type}

    B -->|Pages Router| C[pages/api/route.js]
    C --> D[Node.js Runtime Only]
    D --> E[Single handler function]
    E --> F[req/res objects]
    F --> G[Auto body parsing]
    G --> H[Manual method check]

    B -->|App Router| I[app/api/route/route.js]
    I --> J[Node.js OR Edge Runtime]
    J --> K[Named method exports]
    K --> L[Request/Response Web APIs]
    L --> M[Explicit body parsing]
    M --> N[Declarative methods]

    H --> O[Response]
    N --> O

    style D fill:#ffcccc
    style J fill:#ccffcc

References:

↑ Back to top

Deployment and Configuration

What is the difference between static export and server deployment?

The 30-Second Answer: Static export generates pure HTML files at build time that can be served from any static host, but loses server-side features like SSR, ISR, API routes, and dynamic routing. Server deployment runs a Node.js server that enables all Next.js features including dynamic rendering, API routes, and on-demand revalidation.

The 2-Minute Answer (If They Want More): Static export (output: 'export') pre-renders all pages to HTML at build time, creating a purely static site similar to traditional static site generators. This is ideal for content that doesn't change frequently and doesn't require server-side logic. The build process generates HTML, CSS, and JavaScript files that can be deployed to any CDN or static hosting service. However, you lose access to Server Components (in App Router), API Routes, Server Actions, Image Optimization, ISR, and any dynamic routing with catch-all routes.

Server deployment maintains a running Node.js server that can execute code at request time. This enables Server-Side Rendering (SSR) where pages are generated per request, Incremental Static Regeneration (ISR) for background updates, API routes for backend logic, middleware for request processing, and Next.js Image Optimization. The server can respond dynamically to user requests, access databases, call external APIs securely (with credentials), and personalize content based on cookies or headers.

The choice depends on your requirements: use static export for maximum performance and simplicity when you don't need dynamic features, or server deployment when you need the full power of Next.js including dynamic rendering, APIs, and real-time data.

Modern hybrid approaches like Vercel's infrastructure allow you to mix static and dynamic pages in the same application, getting the best of both worlds.

Code Example:

// next.config.js - Static Export Configuration
module.exports = {
  output: 'export',
  trailingSlash: true, // Creates /about/index.html instead of /about.html
  images: {
    unoptimized: true, // Image Optimization requires a server
  },
}

// This page WORKS with static export
// app/about/page.js
export default function About() {
  return <h1>About Us</h1>
}

// This page FAILS with static export (uses dynamic rendering)
// app/user/page.js
export default async function User() {
  const res = await fetch('https://api.example.com/user', {
    cache: 'no-store' // Dynamic rendering
  })
  return <div>{res.name}</div>
}

// This FAILS with static export (API route)
// app/api/hello/route.js
export async function GET() {
  return Response.json({ message: 'Hello' })
}
// next.config.js - Server Deployment (default)
module.exports = {
  // No output specified = full server features
}

// This works with server deployment
// app/products/[id]/page.js
export default async function Product({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 3600 } // ISR: revalidate every hour
  })
  return <div>{product.name}</div>
}

// API route works with server deployment
// app/api/submit/route.js
export async function POST(request) {
  const data = await request.json()
  // Server-side logic, database access, etc.
  await saveToDatabase(data)
  return Response.json({ success: true })
}
// Feature comparison
const features = {
  staticExport: {
    hosting: 'CDN, S3, GitHub Pages, Netlify',
    rendering: 'Build-time only',
    apiRoutes: false,
    serverComponents: false,
    imageOptimization: false,
    isr: false,
    dynamicRoutes: 'Limited (must be pre-generated)',
    cost: 'Very low',
    performance: 'Maximum (static files)',
  },
  serverDeployment: {
    hosting: 'Node.js server required',
    rendering: 'Build-time + Request-time',
    apiRoutes: true,
    serverComponents: true,
    imageOptimization: true,
    isr: true,
    dynamicRoutes: 'Full support',
    cost: 'Higher (server costs)',
    performance: 'Fast (with caching)',
  }
}

Mermaid Diagram:

flowchart LR
    subgraph Static Export
        A[Build Time] --> B[Pre-render All Pages]
        B --> C[HTML/CSS/JS Files]
        C --> D[CDN/Static Host]
        D --> E[Client Browser]
    end

    subgraph Server Deployment
        F[Build Time] --> G[Build Optimized Code]
        G --> H[Node.js Server]
        H --> I{Request Type?}
        I -->|Static| J[Cached HTML]
        I -->|Dynamic SSR| K[Generate on Request]
        I -->|ISR| L[Background Regenerate]
        I -->|API| M[Server Logic]
        J --> N[Client Browser]
        K --> N
        L --> N
        M --> N
    end

References:

↑ Back to top

Performance Optimization

What is the Link component and how does it improve performance?

The 30-Second Answer: The Next.js Link component enables client-side navigation between routes without full page reloads, prefetches linked pages in the background when they enter the viewport, and maintains application state across navigation. This creates a near-instant, SPA-like experience while preserving SEO benefits.

The 2-Minute Answer (If They Want More): The Link component is fundamental to Next.js performance strategy. Unlike traditional anchor tags that trigger full page reloads (destroying application state and re-downloading shared resources), Link performs client-side transitions using the router. When you click a Link, Next.js fetches only the new page's JavaScript bundle and data, swaps the content, and updates the URL—all without a browser refresh.

The magic happens with automatic prefetching. When a Link component appears in the viewport, Next.js silently downloads the target page's code and data in the background using low-priority requests. By the time the user clicks, the destination page is already cached, resulting in near-instantaneous navigation. This prefetching behavior is intelligent: it only occurs in production, respects the user's data saver settings, and can be disabled for links you don't want prefetched.

The component integrates seamlessly with Next.js's routing system, handling both shallow routing (updating URL without running data fetching) and scroll management (restoring scroll position when using the back button). It supports advanced patterns like scroll restoration, route transition animations, and programmatic navigation via the router API.

In Next.js 13+ with the App Router, Link has been simplified—you no longer need to manually wrap child elements in anchor tags, and the component automatically handles Server Components. The prefetching works with React Server Components too, streaming and caching the RSC payload for even faster subsequent navigations.

Code Example:

// Basic Link usage (Next.js 13+ App Router)
import Link from 'next/link';

export function Navigation() {
  return (
    <nav>
      {/* Simple link - automatically prefetches when in viewport */}
      <Link href="/about">About</Link>

      {/* Link with custom element */}
      <Link href="/dashboard" className="nav-link">
        Dashboard
      </Link>

      {/* Disable prefetching for specific links */}
      <Link href="/heavy-page" prefetch={false}>
        Heavy Page
      </Link>
    </nav>
  );
}

// Dynamic routes
export function ProductList({ products }) {
  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          <Link href={`/products/${product.slug}`}>
            {product.name}
          </Link>
        </li>
      ))}
    </ul>
  );
}

// Programmatic navigation
'use client';
import { useRouter } from 'next/navigation';

export function LoginForm() {
  const router = useRouter();

  async function handleSubmit(e) {
    e.preventDefault();
    const success = await login(formData);

    if (success) {
      // Programmatic navigation after action
      router.push('/dashboard');
      // Or replace history: router.replace('/dashboard');
      // Or go back: router.back();
    }
  }

  return <form onSubmit={handleSubmit}>...</form>;
}

// Scroll behavior control
export function ArticleLink() {
  return (
    <div>
      {/* Scroll to top on navigation (default) */}
      <Link href="/article" scroll={true}>
        Read Article
      </Link>

      {/* Preserve scroll position */}
      <Link href="/article" scroll={false}>
        Continue Reading
      </Link>
    </div>
  );
}

// Shallow routing (update URL without re-running data fetching)
'use client';
import { useRouter, useSearchParams } from 'next/navigation';

export function FilterableList() {
  const router = useRouter();
  const searchParams = useSearchParams();

  function updateFilter(filter) {
    const params = new URLSearchParams(searchParams);
    params.set('filter', filter);

    // Shallow navigation - updates URL, doesn't re-fetch data
    router.push(`?${params.toString()}`, { shallow: true });
  }

  return (
    <div>
      <button onClick={() => updateFilter('active')}>Active</button>
      <button onClick={() => updateFilter('archived')}>Archived</button>
    </div>
  );
}

// Prefetch on hover instead of viewport (custom optimization)
'use client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';

export function SmartLink({ href, children }) {
  const router = useRouter();

  return (
    <Link
      href={href}
      prefetch={false} // Disable automatic prefetch
      onMouseEnter={() => router.prefetch(href)} // Prefetch on hover
    >
      {children}
    </Link>
  );
}

// External links (automatically handled)
export function MixedLinks() {
  return (
    <div>
      {/* Internal - uses client-side navigation */}
      <Link href="/about">About Us</Link>

      {/* External - renders as regular <a> tag */}
      <Link href="https://example.com">External Site</Link>

      {/* Email/tel - renders as regular <a> tag */}
      <Link href="mailto:hello@example.com">Contact</Link>
    </div>
  );
}

// Advanced: Catching navigation events
'use client';
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';

export function NavigationEvents() {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    // Track navigation in analytics
    const url = `${pathname}?${searchParams}`;
    console.log('Navigated to:', url);
    // gtag('page_view', { page_path: url });
  }, [pathname, searchParams]);

  return null;
}

// Link with active state styling
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';

export function NavLink({ href, children }) {
  const pathname = usePathname();
  const isActive = pathname === href;

  return (
    <Link
      href={href}
      className={isActive ? 'nav-link active' : 'nav-link'}
    >
      {children}
    </Link>
  );
}

Mermaid Diagram:

flowchart TD
    A[Link Component Renders] --> B{In Viewport?}
    B -->|No| C[Wait for Scroll]
    C --> B
    B -->|Yes| D[Prefetch Route Bundle]
    D --> E[Prefetch Route Data]
    E --> F[Store in Cache]

    F --> G[User Clicks Link]
    G --> H{Bundle Cached?}
    H -->|Yes| I[Instant Navigation]
    H -->|No| J[Download Bundle]
    J --> K[Navigate]

    I --> L[Swap Page Component]
    K --> L
    L --> M[Update URL]
    M --> N[Maintain App State]
    N --> O[No Full Page Reload]

    P[Regular <a> Tag] --> Q[User Clicks]
    Q --> R[Full Page Reload]
    R --> S[Destroy App State]
    S --> T[Re-download All Resources]

    style I fill:#90EE90
    style O fill:#90EE90
    style R fill:#FFB6C1
    style T fill:#FFB6C1

References:

↑ Back to top

What is automatic code splitting in Next.js?

The 30-Second Answer: Automatic code splitting in Next.js divides your JavaScript bundle into smaller chunks, loading only the code needed for each page. Each route gets its own bundle, and shared dependencies are intelligently split, dramatically reducing initial load time and improving performance.

The 2-Minute Answer (If They Want More): Next.js implements code splitting at multiple levels without requiring developer configuration. Every file in the pages/ or app/ directory becomes a separate entry point, creating individual bundles for each route. When a user visits a page, they download only the JavaScript needed for that specific page, not the entire application code.

The framework uses webpack's built-in code splitting capabilities (or Turbopack in Next.js 13+) to analyze import statements and create optimized chunks. Common dependencies shared across multiple pages are automatically extracted into shared chunks, preventing code duplication while maintaining efficient caching. If three pages all use React, that library code is downloaded once and reused across all pages.

Route-based splitting happens automatically, but you can also implement component-level splitting using dynamic imports with next/dynamic. This allows you to defer loading heavy components until they're needed, such as loading a chart library only when a user opens a dashboard section. The splitting even extends to CSS modules, ensuring styles are loaded only for the components that need them.

Next.js 13's App Router enhances this further with React Server Components, which aren't included in the client bundle at all. Server Components run exclusively on the server, sending only their rendered HTML to the client, achieving a new level of bundle size reduction. Client Components are still code-split automatically, but now you have granular control over what runs where.

Code Example:

// Automatic route-based splitting (pages/app directory)
// Each of these files becomes a separate bundle automatically

// app/page.js - Home page bundle
export default function HomePage() {
  return <h1>Home</h1>;
}

// app/dashboard/page.js - Dashboard bundle (loaded only when visited)
export default function DashboardPage() {
  return <h1>Dashboard</h1>;
}

// Dynamic imports for component-level splitting
import dynamic from 'next/dynamic';

// Heavy component loaded only when needed
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false, // Disable server-side rendering for this component
});

export function DashboardWithChart() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>
        Show Analytics
      </button>
      {/* Chart bundle downloaded only when showChart becomes true */}
      {showChart && <HeavyChart data={analyticsData} />}
    </div>
  );
}

// Dynamic import with named exports
const ComplexEditor = dynamic(
  () => import('@/components/Editor').then(mod => mod.Editor),
  { ssr: false }
);

// Splitting multiple components
const componentMap = {
  video: dynamic(() => import('@/components/VideoPlayer')),
  audio: dynamic(() => import('@/components/AudioPlayer')),
  document: dynamic(() => import('@/components/DocumentViewer')),
};

export function MediaViewer({ type, src }) {
  const Component = componentMap[type];
  return <Component src={src} />;
}

// Analyzing bundle splits (run this command)
// npm run build
// Next.js shows bundle sizes:
//
// Route (app)                Size     First Load JS
// ┌ ○ /                      142 B          87.4 kB
// ├ ○ /dashboard             1.2 kB         88.5 kB
// â”” â—‹ /analytics             45.3 kB        132.6 kB
//
// â—‹ (Static)  prerendered as static content

// Webpack Bundle Analyzer integration
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // Your Next.js config
});

// Run: ANALYZE=true npm run build
// Opens interactive bundle visualization

// Optimizing shared chunks
// next.config.js
module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.optimization.splitChunks.cacheGroups = {
        ...config.optimization.splitChunks.cacheGroups,
        commons: {
          name: 'commons',
          chunks: 'all',
          minChunks: 2, // Used by at least 2 pages
        },
      };
    }
    return config;
  },
};

Mermaid Diagram:

flowchart TD
    A[Application Code] --> B[Build Process]
    B --> C[Route Analysis]
    C --> D[/page.js → Bundle 1]
    C --> E[/dashboard/page.js → Bundle 2]
    C --> F[/about/page.js → Bundle 3]

    B --> G[Dependency Analysis]
    G --> H{Shared Across Routes?}
    H -->|Yes| I[Shared Chunk]
    H -->|No| J[Route-Specific Code]

    I --> K[Common Libraries]
    I --> L[Shared Components]

    D --> M[User Visits /]
    M --> N[Download: Bundle 1 + Shared Chunks]

    N --> O[Navigate to /dashboard]
    O --> P[Download: Only Bundle 2]
    P --> Q[Reuse: Cached Shared Chunks]

    style I fill:#90EE90
    style K fill:#90EE90
    style L fill:#90EE90

References:

↑ Back to top

Routing

How does file-based routing work in Next.js?

The 30-Second Answer: Next.js uses a file-system based router where folders and files in the app directory (App Router) or pages directory (Pages Router) automatically become routes. Each page.js or page.tsx file in the app directory creates a publicly accessible route based on its folder path.

The 2-Minute Answer (If They Want More): File-based routing eliminates the need for manual route configuration by mapping the file structure directly to URL paths. In the App Router (Next.js 13+), the app directory uses a folder-based structure where each folder represents a route segment, and special files like page.js, layout.js, and loading.js define the UI for that segment.

For example, app/dashboard/settings/page.js automatically creates the route /dashboard/settings. This approach provides automatic code-splitting, where each route is automatically split into separate bundles, improving performance by loading only the necessary code.

The file-based system also supports nested layouts, where parent folders can define layouts that wrap child routes. This creates a natural hierarchy and makes it easy to share UI components across related routes without manually configuring a routing library.

The beauty of this system is its predictability - you can look at the file structure and immediately understand the URL structure of your application, making it easier for teams to navigate and maintain large codebases.

Code Example:

// File structure creates routes automatically:

// app/page.js → /
export default function Home() {
  return <h1>Home Page</h1>;
}

// app/about/page.js → /about
export default function About() {
  return <h1>About Page</h1>;
}

// app/blog/posts/page.js → /blog/posts
export default function BlogPosts() {
  return <h1>Blog Posts</h1>;
}

// app/dashboard/settings/page.js → /dashboard/settings
export default function DashboardSettings() {
  return <h1>Dashboard Settings</h1>;
}

Mermaid Diagram:

flowchart TD
    A[app directory] --> B[page.js → /]
    A --> C[about folder]
    C --> D[page.js → /about]
    A --> E[blog folder]
    E --> F[posts folder]
    F --> G[page.js → /blog/posts]
    A --> H[dashboard folder]
    H --> I[settings folder]
    I --> J[page.js → /dashboard/settings]

References:

↑ Back to top

Authentication and Security

What are the best practices for securing API routes?

The 30-Second Answer: Secure API routes by implementing authentication/authorization checks, validating and sanitizing inputs, using rate limiting, implementing CSRF protection, setting proper CORS policies, using environment variables for secrets, and enabling HTTPS. Always verify requests on the server side and never trust client-side data.

The 2-Minute Answer (If They Want More): API route security in Next.js requires a multi-layered approach. First, always implement authentication to verify user identity and authorization to ensure users can only access permitted resources. Use middleware or utility functions to check authentication tokens or sessions before processing requests.

Input validation is critical - validate all incoming data against expected schemas using libraries like Zod or Yup. Sanitize inputs to prevent injection attacks and validate types, formats, and ranges. Never directly use user input in database queries or system commands without validation.

Rate limiting prevents abuse by restricting the number of requests from a single IP or user within a time window. Implement this using middleware or services like Upstash Rate Limit. For mutations, implement CSRF protection using tokens or SameSite cookie attributes.

Set restrictive CORS policies to only allow requests from trusted origins. Use environment variables for API keys and secrets, never hardcoding them. Implement proper error handling that doesn't leak sensitive information. For App Router, leverage Server Actions for mutations as they provide built-in CSRF protection and don't expose API endpoints.

Always use HTTPS in production, set security headers (Content-Security-Policy, X-Frame-Options, etc.), and consider implementing request signing for sensitive operations. Log security events for monitoring and audit trails.

Code Example:

// lib/auth.ts - Authentication utility
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { NextRequest } from "next/server";

export async function requireAuth() {
  const session = await getServerSession(authOptions);

  if (!session || !session.user) {
    throw new Error("Unauthorized");
  }

  return session;
}

// lib/rate-limit.ts - Rate limiting middleware
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "10 s"),
  analytics: true,
});

export async function checkRateLimit(identifier: string) {
  const { success, limit, reset, remaining } = await ratelimit.limit(
    identifier
  );

  if (!success) {
    throw new Error("Rate limit exceeded");
  }

  return { limit, reset, remaining };
}

// lib/validation.ts - Input validation
import { z } from "zod";

export const createPostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(1).max(10000),
  tags: z.array(z.string()).max(10).optional(),
});

// middleware.ts - Security headers
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Security headers
  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
  response.headers.set(
    "Permissions-Policy",
    "camera=(), microphone=(), geolocation=()"
  );
  response.headers.set(
    "Content-Security-Policy",
    "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
  );

  return response;
}

// app/api/posts/route.ts - Secured API route
import { NextRequest, NextResponse } from "next/server";
import { requireAuth } from "@/lib/auth";
import { checkRateLimit } from "@/lib/rate-limit";
import { createPostSchema } from "@/lib/validation";
import { prisma } from "@/lib/prisma";

export async function GET(request: NextRequest) {
  try {
    // Optional authentication for public endpoints
    const session = await getServerSession(authOptions);

    // Rate limiting
    const ip = request.ip ?? "127.0.0.1";
    await checkRateLimit(`api:posts:${ip}`);

    const posts = await prisma.post.findMany({
      take: 20,
      orderBy: { createdAt: "desc" },
    });

    return NextResponse.json(posts);
  } catch (error) {
    console.error("GET /api/posts error:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}

export async function POST(request: NextRequest) {
  try {
    // Require authentication
    const session = await requireAuth();

    // Rate limiting per user
    await checkRateLimit(`api:posts:create:${session.user.id}`);

    // Parse and validate input
    const body = await request.json();
    const validatedData = createPostSchema.parse(body);

    // Authorization check
    if (!session.user.canCreatePosts) {
      return NextResponse.json(
        { error: "Forbidden" },
        { status: 403 }
      );
    }

    // Create post
    const post = await prisma.post.create({
      data: {
        ...validatedData,
        authorId: session.user.id,
      },
    });

    return NextResponse.json(post, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: "Validation failed", details: error.errors },
        { status: 400 }
      );
    }

    if (error.message === "Unauthorized") {
      return NextResponse.json(
        { error: "Unauthorized" },
        { status: 401 }
      );
    }

    if (error.message === "Rate limit exceeded") {
      return NextResponse.json(
        { error: "Too many requests" },
        { status: 429 }
      );
    }

    console.error("POST /api/posts error:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}

// app/api/posts/[id]/route.ts - Resource-level authorization
export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const session = await requireAuth();

    // Find the post
    const post = await prisma.post.findUnique({
      where: { id: params.id },
    });

    if (!post) {
      return NextResponse.json(
        { error: "Post not found" },
        { status: 404 }
      );
    }

    // Authorization: only author or admin can delete
    if (post.authorId !== session.user.id && !session.user.isAdmin) {
      return NextResponse.json(
        { error: "Forbidden" },
        { status: 403 }
      );
    }

    await prisma.post.delete({
      where: { id: params.id },
    });

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error("DELETE /api/posts/[id] error:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}

// .env.local - Environment variables (example)
// DATABASE_URL="postgresql://..."
// NEXTAUTH_SECRET="your-secret-key"
// NEXTAUTH_URL="http://localhost:3000"
// UPSTASH_REDIS_REST_URL="..."
// UPSTASH_REDIS_REST_TOKEN="..."

Mermaid Diagram:

flowchart TD
    A[API Request] --> B[Security Headers Middleware]
    B --> C[Rate Limit Check]
    C -->|Exceeded| D[Return 429]
    C -->|OK| E[Authentication Check]
    E -->|Failed| F[Return 401]
    E -->|Success| G[Input Validation]
    G -->|Invalid| H[Return 400]
    G -->|Valid| I[Authorization Check]
    I -->|Forbidden| J[Return 403]
    I -->|Allowed| K[Sanitize Input]
    K --> L[Execute Business Logic]
    L --> M{Success?}
    M -->|Error| N[Log Error]
    N --> O[Return Generic Error]
    M -->|Success| P[Return Response]
    P --> Q[Add Security Headers]
    Q --> R[Send Response]

References:

↑ Back to top

Testing and Debugging

How do you test Next.js applications?

The 30-Second Answer: Next.js applications are tested using unit tests with Jest and React Testing Library for components, integration tests with Testing Library for page interactions, and end-to-end tests using Playwright or Cypress for full user flows. Next.js provides built-in support for Jest configuration and works seamlessly with modern testing frameworks.

The 2-Minute Answer (If They Want More): Testing Next.js applications involves multiple layers: unit testing individual components and utilities, integration testing page components and API routes, and end-to-end testing complete user workflows. For unit and integration tests, Jest combined with React Testing Library is the standard approach, allowing you to test component rendering, user interactions, and data fetching behavior.

Next.js 13+ introduced new testing considerations with Server Components and the App Router. Server Components can be tested by mocking their data sources and verifying the rendered output, while Client Components are tested similarly to traditional React components. API routes can be tested by importing the route handlers directly and calling them with mock Request objects.

End-to-end testing with tools like Playwright or Cypress validates the entire application stack, including routing, data fetching, and user interactions. These tests run in real browsers and can test both server-side and client-side rendering behaviors. Next.js's fast refresh and development server make it easy to run tests alongside development.

For performance and accessibility testing, tools like Lighthouse CI can be integrated into your CI/CD pipeline to catch regressions. Visual regression testing with tools like Percy or Chromatic helps ensure UI consistency across changes.

Code Example:

// jest.config.js - Next.js Jest configuration
const nextJest = require('next/jest')

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files
  dir: './',
})

const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  collectCoverageFrom: [
    'app/**/*.{js,jsx,ts,tsx}',
    'components/**/*.{js,jsx,ts,tsx}',
    '!**/*.d.ts',
    '!**/node_modules/**',
  ],
}

module.exports = createJestConfig(customJestConfig)

// jest.setup.js
import '@testing-library/jest-dom'

// __tests__/components/UserProfile.test.tsx - Component unit test
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import UserProfile from '@/components/UserProfile'

describe('UserProfile', () => {
  it('renders user information', () => {
    render(<UserProfile name="John Doe" email="john@example.com" />)

    expect(screen.getByText('John Doe')).toBeInTheDocument()
    expect(screen.getByText('john@example.com')).toBeInTheDocument()
  })

  it('handles edit button click', async () => {
    const user = userEvent.setup()
    const onEdit = jest.fn()

    render(<UserProfile name="John" email="john@example.com" onEdit={onEdit} />)

    await user.click(screen.getByRole('button', { name: /edit/i }))
    expect(onEdit).toHaveBeenCalledTimes(1)
  })
})

// __tests__/app/page.test.tsx - Server Component test
import { render, screen } from '@testing-library/react'
import Home from '@/app/page'

// Mock the data fetching
jest.mock('@/lib/api', () => ({
  getPosts: jest.fn(() => Promise.resolve([
    { id: 1, title: 'Test Post', content: 'Test content' }
  ])),
}))

describe('Home Page', () => {
  it('renders posts from server', async () => {
    render(await Home())

    expect(screen.getByText('Test Post')).toBeInTheDocument()
  })
})

// __tests__/app/api/users/route.test.ts - API route test
import { GET, POST } from '@/app/api/users/route'

describe('/api/users', () => {
  it('GET returns users list', async () => {
    const request = new Request('http://localhost:3000/api/users')
    const response = await GET(request)
    const data = await response.json()

    expect(response.status).toBe(200)
    expect(Array.isArray(data.users)).toBe(true)
  })

  it('POST creates new user', async () => {
    const request = new Request('http://localhost:3000/api/users', {
      method: 'POST',
      body: JSON.stringify({ name: 'Jane', email: 'jane@example.com' }),
      headers: { 'Content-Type': 'application/json' },
    })

    const response = await POST(request)
    const data = await response.json()

    expect(response.status).toBe(201)
    expect(data.user.name).toBe('Jane')
  })
})

// e2e/login.spec.ts - Playwright E2E test
import { test, expect } from '@playwright/test'

test.describe('User Login Flow', () => {
  test('should login successfully with valid credentials', async ({ page }) => {
    await page.goto('http://localhost:3000/login')

    // Fill login form
    await page.fill('input[name="email"]', 'user@example.com')
    await page.fill('input[name="password"]', 'password123')
    await page.click('button[type="submit"]')

    // Wait for navigation and verify dashboard
    await page.waitForURL('**/dashboard')
    await expect(page.locator('h1')).toContainText('Dashboard')
  })

  test('should show error with invalid credentials', async ({ page }) => {
    await page.goto('http://localhost:3000/login')

    await page.fill('input[name="email"]', 'wrong@example.com')
    await page.fill('input[name="password"]', 'wrongpass')
    await page.click('button[type="submit"]')

    await expect(page.locator('.error-message')).toBeVisible()
  })
})

// package.json - Test scripts
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui"
  }
}

Mermaid Diagram:

flowchart TD
    A[Next.js Testing Strategy] --> B[Unit Tests]
    A --> C[Integration Tests]
    A --> D[E2E Tests]

    B --> B1[Jest + RTL]
    B --> B2[Component Logic]
    B --> B3[Utility Functions]
    B --> B4[Custom Hooks]

    C --> C1[Page Components]
    C --> C2[API Routes]
    C --> C3[Server Actions]
    C --> C4[Data Fetching]

    D --> D1[Playwright/Cypress]
    D --> D2[User Flows]
    D --> D3[Navigation]
    D --> D4[Form Submissions]

    B1 --> E[CI/CD Pipeline]
    C1 --> E
    D1 --> E

    E --> F[Deployment]

References:

↑ Back to top

Styling

What styling options are available in Next.js?

The 30-Second Answer: Next.js supports multiple styling approaches including CSS Modules, global CSS, Sass/SCSS, CSS-in-JS libraries (styled-components, Emotion), Tailwind CSS, and the built-in CSS-in-JS solution styled-jsx. You can also use any PostCSS-compatible solution since Next.js has built-in PostCSS support.

The 2-Minute Answer (If They Want More): Next.js provides flexible styling options to match different project needs and developer preferences. CSS Modules offer scoped styling by default, automatically generating unique class names to prevent conflicts. Global CSS can be imported in _app.js for site-wide styles like resets or typography. Sass/SCSS works out of the box after installing the sass package, supporting both .module.scss and global .scss files.

For CSS-in-JS, Next.js has built-in support for styled-jsx, a scoped CSS solution that writes CSS directly in your components. Popular libraries like styled-components and Emotion work with Next.js but require additional configuration for server-side rendering support. Tailwind CSS has become increasingly popular and integrates seamlessly with Next.js through PostCSS.

The App Router (Next.js 13+) also introduced native support for CSS-in-JS with the use client directive, though it recommends CSS Modules or Tailwind for optimal performance. Each approach has trade-offs: CSS Modules offer simplicity and performance, CSS-in-JS provides dynamic theming and props-based styling, while Tailwind enables rapid development with utility classes.

Comparison of Styling Options:

Approach Pros Cons Best For
CSS Modules Automatic scoping, zero runtime, simple No dynamic styles, extra import Component-level styles
Tailwind CSS Fast development, small bundle, consistent HTML verbosity, learning curve Utility-first projects
styled-components Dynamic styling, theming, no naming Runtime cost, SSR config needed Complex theming needs
Sass/SCSS Variables, mixins, nesting Build step, global scope risk Teams familiar with Sass
Global CSS Simple, familiar Naming conflicts, hard to maintain Resets, base styles
styled-jsx Built-in, scoped, no config Less popular, limited features Small projects

Code Example:

// Example using multiple styling approaches in Next.js

// 1. CSS Modules (component-scoped)
// styles/Button.module.css
import styles from './Button.module.css';

export function ButtonWithModules() {
  return <button className={styles.primary}>Click me</button>;
}

// 2. Global CSS (imported in _app.js)
// styles/globals.css
import '../styles/globals.css';

export default function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

// 3. Tailwind CSS (utility classes)
export function ButtonWithTailwind() {
  return (
    <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
      Click me
    </button>
  );
}

// 4. styled-jsx (built-in CSS-in-JS)
export function ButtonWithStyledJsx() {
  return (
    <>
      <button className="primary">Click me</button>
      <style jsx>{`
        .primary {
          padding: 0.5rem 1rem;
          background-color: #3b82f6;
          color: white;
          border-radius: 0.25rem;
        }
        .primary:hover {
          background-color: #2563eb;
        }
      `}</style>
    </>
  );
}

// 5. styled-components (requires additional setup)
import styled from 'styled-components';

const StyledButton = styled.button`
  padding: 0.5rem 1rem;
  background-color: ${props => props.variant === 'primary' ? '#3b82f6' : '#6b7280'};
  color: white;
  border-radius: 0.25rem;

  &:hover {
    background-color: ${props => props.variant === 'primary' ? '#2563eb' : '#4b5563'};
  }
`;

export function ButtonWithStyledComponents() {
  return <StyledButton variant="primary">Click me</StyledButton>;
}

// 6. Sass/SCSS Modules
// styles/Button.module.scss
import styles from './Button.module.scss';

export function ButtonWithSass() {
  return <button className={styles.primary}>Click me</button>;
}

Mermaid Diagram:

flowchart TD
    A[Next.js Styling Options] --> B[Static CSS]
    A --> C[CSS-in-JS]
    A --> D[Utility-First]

    B --> B1[CSS Modules<br/>Component-scoped]
    B --> B2[Global CSS<br/>Site-wide styles]
    B --> B3[Sass/SCSS<br/>Variables & mixins]

    C --> C1[styled-jsx<br/>Built-in, zero config]
    C --> C2[styled-components<br/>Popular, needs setup]
    C --> C3[Emotion<br/>Flexible, performant]

    D --> D1[Tailwind CSS<br/>Utility classes]

    style B1 fill:#a7f3d0
    style D1 fill:#a7f3d0
    style B2 fill:#fde68a
    style C1 fill:#fde68a
    style C2 fill:#fca5a5
    style C3 fill:#fca5a5

    B1 -.Recommended.-> E[Production Apps]
    D1 -.Recommended.-> E

References:

↑ Back to top

Want more questions?

You've seen 15 sample questions. Unlock all 43 En interview questions with detailed explanations, code examples, and expert insights.

43+ questions
Code examples
Expert explanations
Instant access
Unlock Full Access