Next.js Interview Questions (Free Preview)
Free sample of 15 from 43 questions available
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 topNext.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:
- React Documentation - Start a New React Project
- Next.js vs Create React App
- Vercel - Next.js vs Create React App
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:
- Next.js Documentation - App Router
- Next.js Documentation - Pages Router
- Upgrading from Pages to App Router
- React Server Components
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 topRendering 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 topWhat 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:
- Performance: Pages served from CDN like SSG
- Freshness: Content updates without full rebuild
- Scalability: Generate pages on-demand as needed
- Cost Efficiency: No need for always-running servers
- 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 topWhat 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:
- Default to Static: Let Next.js choose static rendering when possible
- Use Dynamic Deliberately: Only call cookies()/headers() when necessary
- Granular Caching: Use fetch-level cache controls for mixed strategies
- Leverage Streaming: Use Suspense for progressive rendering
- 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 topAPI 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 topDeployment 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:
- Next.js Static Exports Documentation
- Next.js Output File Tracing
- Static vs Server-Side Rendering Comparison
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 topWhat 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 topRouting
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 topAuthentication 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 topTesting 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 topStyling
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