Next.js interview questions and answers for 2025
Next.js Interview Questions for Freshers and Intermediate Levels
When should you choose Next.js over a traditional React application?
You should choose Next.js over a traditional React application when you need:
- Better SEO – Next.js supports server-side rendering (SSR) and static site generation (SSG), ensuring search engines can index content properly.
- Faster Performance – Features like automatic code-splitting, image optimization, and incremental static regeneration (ISR) improve load times.
- Built-in Routing – Next.js provides a file-based routing system, eliminating the need for external routing libraries like React Router.
- API Routes – Next.js allows you to create serverless API endpoints directly within the project, reducing backend complexity.
- Scalability – With hybrid rendering (SSG + SSR + ISR), Next.js enables efficient content updates without rebuilding the entire app.
- Easy Deployment – Vercel (Next.js’s native platform) offers seamless deployment with automatic optimizations.
- Edge Functions & Middleware – Next.js supports middleware for request handling and Edge Functions for improved performance at the network level.
Use Next.js if your project requires speed, SEO, scalability, and a streamlined development experience. However, if you only need a basic, CSR-heavy app, plain React may suffice.
How does Next.js handle server-side rendering (SSR) compared to client-side rendering (CSR)?
Next.js supports Server-Side Rendering (SSR) and Client-Side Rendering (CSR), each serving different use cases.
- SSR (Server-Side Rendering)
- The page is generated on the server for each request using
getServerSideProps()
. - Useful for dynamic data that changes frequently (e.g., personalized dashboards, real-time updates).
- Improves SEO and first-page load time since the content is fully rendered before reaching the browser.
- The page is generated on the server for each request using
- CSR (Client-Side Rendering)
- The page loads with minimal initial content, and data fetching happens in the browser using
useEffect()
or API calls. - Suitable for single-page applications (SPAs) and interactive UIs where SEO is not a priority.
- Reduces server load but may cause slower first loads due to additional JavaScript execution.
- The page loads with minimal initial content, and data fetching happens in the browser using
Key Difference:
- SSR loads fully-rendered content upfront, improving SEO and performance for first-time visits.
- CSR loads faster initially but fetches content dynamically, leading to potential delays in rendering.
Next.js allows developers to choose between SSR and CSR based on performance and SEO needs.
What is static site generation (SSG) in Next.js, and when should it be used?
Static Site Generation (SSG) in Next.js pre-builds pages at compile time, generating static HTML files that can be served instantly. It is implemented using getStaticProps()
.
When to Use SSG:
- SEO-Optimized Content – Pre-rendered pages improve search engine indexing.
- Blogs & Marketing Pages – Content that doesn’t change often benefits from fast-loading static pages.
- E-Commerce Product Listings – If product details don’t update frequently, SSG reduces server load.
- Documentation & Knowledge Bases – Static content allows instant, scalable delivery.
Key Benefits:
- Fast Performance – Pre-built pages load instantly.
- Lower Server Load – No need to generate pages dynamically for each request.
- Supports Incremental Static Regeneration (ISR) – Pages can be updated without a full site rebuild.
Supports Incremental Static Regeneration (ISR) – Allows individual static pages to be updated after deployment without rebuilding the entire site, making it ideal when you want the benefits of static generation but need some pages to update periodically based on content changes.
Use SSG when content can be pre-generated and doesn’t require real-time updates per request.
How does Next.js improve SEO compared to a standard React application?
Next.js improves SEO by offering server-side rendering (SSR), static site generation (SSG), and optimized metadata handling, which help search engines crawl and index pages effectively.
Key SEO Advantages Over Standard React:
- Pre-Rendered Pages (SSR & SSG) – Unlike React’s client-side rendering (CSR), which loads content dynamically, Next.js serves fully-rendered HTML to search engines, improving indexability and page rankings.
- Faster Page Load Times – Next.js features like automatic code splitting, lazy loading, and image optimization enhance performance, improving Core Web Vitals, a key SEO ranking factor.
- Efficient Metadata Handling – The
next/head
component allows easy management of title tags, meta descriptions, and Open Graph tags, which are crucial for SEO and social sharing. - Optimized Routing – Next.js supports clean, SEO-friendly URLs using its file-based routing system.
- Incremental Static Regeneration (ISR) – Allows pages to update without full site rebuilds, ensuring fresh content while keeping the benefits of static performance.
Conclusion:
Next.js significantly enhances SEO, performance, and user experience, making it a better choice than a standard React SPA for content-driven and search-engine-focused applications.
What are API routes in Next.js, and how do they work?
API routes in Next.js allow you to create serverless backend endpoints within your application.
In the App Router (Next.js 13+), API routes are created inside the /app/api/
directory, using a special route.js
or route.ts
file for each endpoint.
In the older Pages Router, API routes are placed inside the /pages/api/
directory.
How They Work (App Router):
- Each folder inside
/app/api/
defines an API route (e.g.,/app/api/user/route.js
becomes/api/user
). - You export HTTP method handlers like
GET
,POST
,PUT
, orDELETE
directly. - Instead of using
req
andres
, you work with Next.js Request and Response objects.
Example API Route (app/api/hello/route.js
):
import { NextResponse } from "next/server";
export async function GET(request) {
return NextResponse.json({ message: "Hello from Next.js API route!" });
}
Note:
In older Pages Router projects (/pages/api/
), you define API routes using a default export function that receives req
and res
, similar to Express.js.
Key Benefits of Next.js API Routes:
- Eliminates the Need for a Separate Backend – Backend logic stays within the Next.js project.
- Optimized for Serverless Deployments – Works seamlessly with Vercel, AWS Lambda, and other serverless platforms.
- Built-in Middleware Support – Can handle authentication, validation, and database interactions.
API routes are ideal for handling form submissions, authentication, database interactions, and third-party API integrations in a Next.js application.
Explain the difference between getStaticProps(), getServerSideProps(), and getInitialProps().
These three functions control how data is fetched and when pages are generated in Next.js Pages Router (the traditional routing system).
In the modern App Router (Next.js 13+), server components and fetch
are used instead for data fetching.
Pages Router Functions:
1. getStaticProps()
(SSG – Static Site Generation)
- Runs at build time and pre-generates static HTML.
- Ideal for content that doesn’t change frequently (e.g., blogs, product pages).
- Improves performance since pages are cached and served instantly.
Example:
export async function getStaticProps() {
const data = await fetch("https://api.example.com/posts");
return { props: { posts: data } };
}
2. getServerSideProps()
(SSR – Server-Side Rendering)
- Runs on every request, generating pages dynamically on the server.
- Ideal for frequently changing data (e.g., user dashboards, real-time updates).
- Ensures fresh content but may increase server load.
Example:
export async function getServerSideProps() {
const data = await fetch("https://api.example.com/latest-news");
return { props: { news: data } };
}
3. getInitialProps()
(Deprecated)
- Runs on both server and client, making it less efficient.
- Previously used in class components and
_app.js
, but now considered legacy. - Replaced by
getStaticProps
andgetServerSideProps
for better performance.
Key Differences (Pages Router):
Function | When It Runs | Use Case | Performance |
getStaticProps |
Build time (SSG) | Static content (blogs, docs) | Fast & cached |
getServerSideProps |
On each request (SSR) | Dynamic content (auth data) | Slower, real-time |
getInitialProps |
Server & client | Legacy, avoid using | Less efficient |
Note:
In the App Router (Next.js 13+), data fetching is handled differently using async Server Components and fetch()
inside the component itself, removing the need for getStaticProps
or getServerSideProps
.
For modern projects, it’s recommended to use the App Router data fetching methods instead.
How does file-based routing work in Next.js?
Next.js uses a file-based routing system, meaning the structure of your files inside the /pages
directory determines the app’s routes automatically—no need for React Router.
Key Concepts:
- Basic Routes:
- A file inside
/pages
corresponds to a route. - Example:
/pages/about.js
→/about
- A file inside
- Dynamic Routes:
- Use square brackets (
[param]
) to create dynamic segments. - Example:
/pages/post/[id].js
→/post/123
- Use square brackets (
- Nested Routes:
- Folder structure defines nested paths.
Example:
/pages
/blog
index.js → /blog
[slug].js → /blog/my-post
- API Routes:
- Inside
/pages/api/
, files act as API endpoints instead of pages. - Example:
/pages/api/user.js
→/api/user
- Inside
- Catch-All Routes:
[...]
captures multiple segments dynamically.- Example:
/pages/docs/[...slug].js
→/docs/a/b/c
Why Use File-Based Routing?
- No need for extra routing libraries (like React Router).
- Automatically optimized for performance.
- Easier to manage as the app scales.
This system simplifies navigation and route management while ensuring efficiency and scalability.
What is the difference between dynamic and static routes in Next.js?
Next.js supports static and dynamic routes based on the file-based routing system.
1. Static Routes
- Defined using regular file names inside the
/pages
directory. - The URL structure is fixed and does not change.
Example:
/pages/about.js → /about
/pages/contact.js → /contact
- Best for: Pages with fixed content (e.g., Home, About, Contact).
2. Dynamic Routes
- Defined using square brackets (
[param]
) to create variable paths. - URL structure is flexible, allowing dynamic values.
Example:
/pages/product/[id].js → /product/123
/pages/user/[username].js → /user/john-doe
- Requires fetching data (e.g.,
getStaticProps
,getServerSideProps
). - Best for: Pages with dynamic content (e.g., user profiles, blog posts).
Key Differences:
Feature | Static Routes | Dynamic Routes |
URL Structure | Fixed (/about ) |
Flexible (/product/:id ) |
File Naming | Regular .js file |
Uses [param].js |
Data Fetching | Not needed | Required for dynamic content |
Use Cases | Fixed pages (About, Contact) | Dynamic pages (Products, Users) |
Conclusion:
Static routes are for fixed content, while dynamic routes allow flexible, data-driven pages. Next.js optimizes both for better performance and SEO.
What is the purpose of the _app.js and _document.js files in a Next.js application?
Next.js provides two special files, _app.js
and _document.js
, to control the structure and behavior of your application when using the Pages Router.
In the newer App Router (Next.js 13+), these roles are handled differently.
Pages Router (pages/
Directory):
1. _app.js
(Custom App Component)
- Wraps all pages and persists layout across page changes.
- Used for global styles, context providers, and state management.
Example:
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
export default MyApp;
- Best for:
- Global CSS imports (
import "../styles/globals.css"
) - Shared layouts (headers, footers)
- Theme providers (e.g., Context API, Redux)
- Global CSS imports (
2. _document.js
(Custom Document Component)
- Controls the base HTML and
<head>
structure (executed only on the server). - Useful for injecting meta tags, fonts, and third-party scripts.
Example:
import { Html, Head, Main, NextScript } from "next/document";
export default function MyDocument() {
return (
<Html lang="en">
<Head>
<link rel="stylesheet" href="https://example.com/fonts.css" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
- Best for:
- Adding custom fonts and external stylesheets
- Setting language and accessibility attributes (
<Html lang="en">
) - Preloading critical assets
App Router (app/
Directory in Next.js 13+):
- In the App Router,
_app.js
and_document.js
are replaced by the rootlayout.js
orlayout.tsx
files. - Global styles, metadata, and layouts are now handled inside the
app/layout.js
file.
Example structure:
app/
layout.js --> Handles global structure (like _app.js + _document.js)
page.js --> Your page content
- Styles defined in
app/layout.js
do not apply to legacypages/*
routes.
Migration Tip:
If you are migrating from the Pages Router:
- Copy global styles and layouts from
_app.js
and_document.js
into yourapp/layout.js
. - Keep
_app.js
and_document.js
while migrating if you still have pages in thepages/
directory. - Once fully migrated to the
app/
directory, you can safely delete_app.js
and_document.js
.
Key Differences:
Feature | _app.js / _document.js (Pages Router) |
layout.js (App Router) |
Purpose | Global styles, layouts, HTML structure | Unified layouts and global config |
Location | pages/ folder |
app/ folder |
Runs On | _app.js : Server & Client, _document.js : Server only |
Server and Client (depending on component) |
Migration | Needed during partial migration | Replaces them fully when migration is complete |
Conclusion:
- In the Pages Router, use
_app.js
for layouts/state and_document.js
for HTML structure. - In the App Router, use
layout.js
for both purposes. - When migrating, keep
_app.js
and_document.js
temporarily to avoid breaking legacy routes, and delete them after full migration.
How can you add dynamic API routes in Next.js?
In Next.js App Router (Next.js 13+), dynamic API routes are created inside the /app/api/
directory.
These routes handle dynamic parameters and return JSON responses, similar to dynamic page routing.
Steps to Create a Dynamic API Route (App Router)
- Define a Dynamic API Route:
- Use square brackets (
[param]
) in the folder name under/app/api/
. - Inside the folder, create a
route.js
orroute.ts
file. - Example:
/app/api/user/[id]/route.js
→ Accessible at/api/user/:id
.
- Use square brackets (
- Handle Dynamic Parameters in the Route File:
- Extract parameters from the
params
argument provided by Next.js.
- Extract parameters from the
export async function GET(request, { params }) {
const { id } = params; // Extract dynamic ID from the URL
return Response.json({ message: `Fetching user with ID: ${id}` });
}
Advanced: Catch-All API Routes (App Router)
- Use
[...slug]
in the folder name to capture multiple segments.
Example: /app/api/post/[...slug]/route.js
→ Handles /api/post/a/b/c
.
export async function GET(request, { params }) {
const { slug } = params;
return Response.json({ message: `Fetching post: ${slug.join("/")}` });
}
Key Benefits of Dynamic API Routes in App Router:
- Flexible: Dynamically handle URL parameters in serverless APIs.
- Modern Structure: Organized using folders and route files (
route.js
), not just filenames. - Serverless-Ready: Works seamlessly with platforms like Vercel, AWS Lambda, and others.
Note:
In older Pages Router projects (/pages/api/[param].js
), dynamic API routes were based on dynamic filenames.
In the App Router, dynamic routes are folder-based and defined with a route.js
file.
Conclusion:
Dynamic API routes in the Next.js App Router allow you to build clean, scalable serverless endpoints that adapt based on URL parameters, using the new /app/api/
folder structure.
How does Next.js handle CSS and global styles?
Next.js supports multiple ways to handle CSS and global styles, allowing developers to style applications efficiently.
In the App Router (Next.js 13+), global styles are handled differently compared to the old Pages Router.
1. Global CSS
- Imported inside the
app/layout.js
(orlayout.tsx
) file, not in_app.js
. - Applies styles globally across the entire application.
Example:
import '../styles/globals.css';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
- You can import multiple global styles here without restrictions (unlike the Pages Router which allowed only one global import in
_app.js
).
2. CSS Modules
- Scoped to a specific component, preventing class name conflicts.
- Uses the
.module.css
extension.
Example
import styles from './Button.module.css';
export default function Button() {
return <button className={styles.primary}>Click Me</button>;
}
✅ CSS Modules work the same in both the App Router and Pages Router.
3. Styled Components & CSS-in-JS
- You can use libraries like styled-components or Emotion for component-scoped styling.
Example with styled-components
:
import styled from 'styled-components';
const Button = styled.button`
background: blue;
color: white;
padding: 10px;
`;
export default function MyComponent() {
return <Button>Click Me</Button>;
}
✅ No changes needed for styled-components in the App Router.
4. Tailwind CSS & Other Frameworks
- Next.js fully supports Tailwind CSS in the App Router.
- Global Tailwind styles should be imported inside
app/layout.js
. - Tailwind is configured using
tailwind.config.js
as usual.
Note:
In older Pages Router projects, global CSS was imported in _app.js
.
In App Router projects, you should import global styles inside layout.js
.
Conclusion
Next.js (App Router) allows styling using global CSS (via layout.js
), CSS Modules, CSS-in-JS solutions, and frameworks like Tailwind CSS.
The best approach depends on your project structure, team preferences, and scalability needs.
What are environment variables in Next.js, and how do you configure them?
Environment variables in Next.js store sensitive or configurable values, such as API keys, database URLs, and feature flags, without exposing them in the source code.
How to Configure Environment Variables
- Create a
.env.local
file in the project root:
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_SECRET=mysecretkey
- Access Environment Variables in Next.js
- Server-side (only available on the server):
const secret = process.env.DATABASE_SECRET;
- Client-side (must start with NEXT_PUBLIC_):
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
Types of .env
Files
.env.local
– Used for local development (ignored by Git)..env.production
– Used for production environments..env.development
– Used for development mode.
Key Rules
- Client-side variables must start with
NEXT_PUBLIC_
. - Restart the server after changing environment variables.
Conclusion
Next.js environment variables help manage configurations securely by keeping sensitive data out of the frontend and version control.
How can you implement image optimization in Next.js?
Next.js provides built-in image optimization using the next/image
component, which improves performance by automatically resizing, compressing, and lazy-loading images.
Steps to Use Image Optimization
- Import and Use the
next/image
Component
import Image from 'next/image';
function MyComponent() {
return (
);
}
export default MyComponent;
- The
width
andheight
props ensure proper layout. - Images are optimized on demand and served in modern formats like WebP.
- Use Remote Images (External URLs)
- Add the domain to
next.config.js
:
- Add the domain to
module.exports = {
images: {
domains: ['example.com'],
},
};
Then use:
<Image src="https://example.com/image.jpg" width={600} height={400} alt="Remote Image" />
3. Enable Blur Placeholder for Lazy Loading
Benefits of Next.js Image Optimization
- Automatic resizing and compression based on the device.
- Lazy loading to improve performance.
- Modern formats (WebP, AVIF) for better efficiency.
- CDN caching for faster delivery.
Conclusion
Next.js**next/image
simplifies image handling by providingautomatic optimization, lazy loading, and responsive support**, improving both performance and SEO. However, when usingexternal images, you must whitelist allowed domains in the next.config.js
file; otherwise, Next.js will block loading those images for security reasons.
What is ISR (Incremental Static Regeneration), and how does it work in Next.js?
Incremental Static Regeneration (ISR) allows Next.js to update static pages after deployment without rebuilding the entire site.
It combines the benefits of Static Site Generation (SSG) with the ability to serve fresh content dynamically.
In the App Router (Next.js 13+), ISR is handled differently compared to the old Pages Router — it uses the fetch
function options directly inside server components.
How ISR Works in the App Router
- Pages or components fetch data using
fetch()
with arevalidate
option. - The
revalidate
value (in seconds) defines how often the data should be refreshed in the background. - When a request comes in after the revalidation time, Next.js fetches fresh data, regenerates the page, and updates the cache.
Example of ISR in App Router (Server Component)
async function PostsPage() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 10 }, // Regenerates every 10 seconds
});
const posts = await res.json();
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
export default PostsPage;
✅ Key changes in the App Router:
- No need for
getStaticProps()
. - ISR is controlled inside
fetch()
using thenext: { revalidate }
option. - Works inside server components naturally.
When to Use ISR
- For blogs, product listings, dashboards, and news sites where data updates periodically but doesn’t require real-time changes.
- When avoiding full builds while still delivering SEO-friendly and fast pages.
Conclusion
ISR in the App Router allows server components to automatically revalidate cached data after a specified time.
It ensures content stays fresh without slowing performance or requiring full redeployments, making modern apps more scalable and dynamic.
How does Next.js handle middleware, and what are its use cases?
Next.js middleware allows developers to execute custom logic before a request is processed. Middleware runs on the Edge Runtime, making it efficient for handling requests at the server level.
How Middleware Works
- Middleware is placed in
middleware.js
at the root of the project. - It runs before rendering a page and can modify the request or response.
- Uses the
NextRequest
andNextResponse
objects to handle requests.
Example: Redirecting Users Based on Authentication
import { NextResponse } from 'next/server';
export function middleware(req) {
const token = req.cookies.get('authToken');
if (!token) {
return NextResponse.redirect(new URL('/login', req.url));
}
return NextResponse.next();
}
- If the user is not authenticated, they are redirected to
/login
. - If authenticated, the request proceeds as usual.
Use Cases of Middleware in Next.js
- Authentication & Authorization – Protect routes by checking authentication tokens.
- Redirects & Rewrites – Modify requests dynamically based on conditions.
- Geo-based Content Personalization – Serve region-specific content.
- Rate Limiting & Security – Prevent excessive API requests or filter unwanted traffic.
Conclusion
Middleware in Next.js provides a lightweight, efficient way to intercept requests, making it useful for auth checks, redirects, and content personalization at the server level.
How do you optimize performance in a Next.js application?
Optimizing performance in Next.js involves leveraging its built-in features for faster page loads, efficient data fetching, and reduced JavaScript bundle size.
Key Optimization Techniques
- Use Static Site Generation (SSG) & Incremental Static Regeneration (ISR)
- Pre-render pages at build time using
getStaticProps()
. - Use ISR (
revalidate
) to refresh data without a full rebuild.
- Pre-render pages at build time using
- Enable Server-Side Rendering (SSR) Only When Needed
- Use
getServerSideProps()
for frequently updated data to avoid unnecessary server calls. - Prefer SSG or ISR whenever possible to reduce server load.
- Use
- Optimize Images with
next/image
- Automatically resizes, compresses, and lazy-loads images.
Example:
<Image src="/example.jpg" width={500} height={300} alt="Optimized Image" />
- Reduce JavaScript Bundle Size
- Use dynamic imports with lazy loading:
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('../components/HeavyComponent'), { ssr: false });
- Minimize dependencies and unused code.
- Implement Caching and CDN
- Next.js caches static assets by default.
- Deploy on Vercel or configure a CDN for faster global delivery.
- Optimize API Calls
- Use API Routes efficiently to handle backend logic.
- Cache API responses where possible to reduce redundant requests.
- Use Middleware for Efficient Request Handling
- Implement middleware to handle authentication and redirects at the edge.
Conclusion
Next.js provides built-in performance optimizations like SSG, ISR, next/image
, and caching. Properly using these features ensures faster load times, better SEO, and improved user experience.
How do you implement authentication and authorization in a Next.js app?
Authentication and authorization in Next.js can be handled using API routes, middleware, and third-party authentication providers like NextAuth.js or Firebase.
1. Using NextAuth.js (OAuth, Credentials, JWT-based Authentication)
NextAuth.js simplifies authentication with built-in OAuth providers like Google, GitHub, and credentials-based login.
Steps to Implement NextAuth.js:
- Install NextAuth:
npm install next-auth
- Create an API route in the App Router (app/api/auth/[…nextauth]/route.js):
import NextAuth from "next-auth";
import GitHubProvider from "next-auth/providers/github";
const handler = NextAuth({
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
],
secret: process.env.NEXTAUTH_SECRET,
});
export { handler as GET, handler as POST };
✅ In the App Router, you export both GET
and POST
handlers.
- Protect pages using
useSession()
:
import { useSession } from "next-auth/react";
export default function Dashboard() {
const { data: session } = useSession();
if (!session) return <p>Access Denied</p>;
return <p>Welcome, {session.user.name}</p>;
}
2. Custom Authentication with API Routes (App Router)
For manual authentication (e.g., database-backed login):
- Create a login API route (
app/api/login/route.js
):
import { NextResponse } from "next/server";
export async function POST(request) {
const { username, password } = await request.json();
if (username === "admin" && password === "password") {
return NextResponse.json({ token: "secure-token" });
} else {
return NextResponse.json(
{ message: "Invalid credentials" },
{ status: 401 }
);
}
}
✅ Notice how in the App Router, you define method-specific handlers like POST(request)
instead of using req
and res
.
- Store the returned token (e.g., in cookies or local storage) and validate it for protected routes.
3. Using Middleware for Authorization
- Restrict access based on authentication tokens using middleware.js:
import { NextResponse } from "next/server";
export function middleware(request) {
const token = request.cookies.get("authToken");
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
✅ Middleware runs before the request is processed, allowing redirection if authentication fails.
Conclusion
Authentication in Next.js (App Router) can be handled via NextAuth.js (for OAuth and JWT), custom API routes (for manual authentication), and middleware (for access control), providing a secure and flexible system based on your project’s needs.
Explain how you can fetch data on the client side in Next.js.
In Next.js, client-side data fetching happens in the browser after the page has loaded. This is useful for dynamic, user-specific, or frequently changing data that doesn’t need SEO optimization.
Methods for Client-Side Data Fetching
- Using
useEffect()
withfetch()
- Best for fetching data after the component mounts.
import { useState, useEffect } from 'react';
function Posts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/api/posts')
.then((res) => res.json())
.then((data) => setPosts(data));
}, []);
return <div>{posts.map((post) => <p key={post.id}>{post.title}</p>)}</div>;
}
export default Posts;
- Using SWR (React Hook for Data Fetching)
- Provides automatic caching, revalidation, and real-time updates.
- Requires installation:
npm install swr
Example:
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
function Posts() {
const { data, error } = useSWR('/api/posts', fetcher);
if (error) return <p>Error loading posts</p>;
if (!data) return <p>Loading...</p>;
return <div>{data.map((post) => <p key={post.id}>{post.title}</p>)}</div>;
}
export default Posts;
When to Use Client-Side Fetching?
- When SEO is not a priority (e.g., dashboards, user profiles).
- When data is user-specific or frequently updated.
- When you need real-time data with automatic updates (e.g., notifications, stock prices).
Conclusion
Client-side data fetching in Next.js can be done using fetch() with useEffect() for simple use cases or SWR for a more robust solution. SWR is preferred when you need automatic caching, stale-while-revalidate behavior, real-time data updates, error handling, and better performance without writing additional state management logic manually.
What is the role of getLayout() in Next.js applications?
In Next.js, getLayout()
was a popular pattern in the Pages Router to define custom layouts per page.
In the App Router (Next.js 13+), layouts are handled differently using nested layout.js
files.
How Layouts Work in the App Router
- Every folder inside the
app/
directory can have its ownlayout.js
file. - Layouts are automatically shared between pages within the same folder.
- You don’t manually define or apply
getLayout()
; Next.js handles layouts natively.
Example: Defining a Layout in the App Router
app/
dashboard/
layout.js
page.js
settings/
layout.js
page.js
app/dashboard/layout.js
export default function DashboardLayout({ children }) {
return (
<div>
<Sidebar />
<main>{children}</main>
</div>
);
}
- The
DashboardLayout
will automatically wrap all pages inside thedashboard/
folder.
When getLayout()
Was Used (Pages Router)
- Pages manually defined
getLayout()
to wrap a page with a custom layout. - It was applied inside
_app.js
.
Now in the App Router, this manual approach is replaced by nested layouts for better structure and built-in optimization.
When to Use Layouts in App Router
- To maintain persistent UI elements like headers, sidebars, or footers across specific sections.
- To easily nest different layouts (e.g., dashboard layout vs. public layout).
- To avoid reloading layouts during page navigation, improving performance.
Conclusion
In the Pages Router, getLayout()
was used to customize layouts per page manually.
In the App Router, layouts are handled automatically through nested layout.js
files, providing a more scalable and consistent way to manage page structure across your application.
How can you configure internationalization (i18n) in Next.js?
Next.js has built-in internationalization (i18n) routing, allowing applications to support multiple languages without additional libraries.
Steps to Configure i18n in Next.js
- Enable i18n in
next.config.js
module.exports = {
i18n: {
locales: ['en', 'fr', 'de'], // Supported languages
defaultLocale: 'en', // Default language
},
};
- Users are automatically redirected based on their browser’s language.
- Use
useRouter()
to Detect Locale in Components
import { useRouter } from 'next/router';
function HomePage() {
const { locale } = useRouter();
return <p>Current language: {locale}</p>;
}
export default HomePage;
- Creating Locale-Specific Pages
- URLs are automatically prefixed with locale codes (
/fr/about
,/de/about
). - You can use localized JSON files for translations.
- URLs are automatically prefixed with locale codes (
- Manually Switching Locales
import { useRouter } from 'next/router';
function LanguageSwitcher() {
const router = useRouter();
const changeLanguage = (lang) => {
router.push(router.pathname, router.asPath, { locale: lang });
};
return (
<button onClick={() => changeLanguage('fr')}>Switch to French</button>
);
}
export default LanguageSwitcher;
Benefits of Next.js i18n Support
- Automatic locale detection and routing.
- No need for third-party i18n libraries.
- SEO-friendly URLs for different languages.
Conclusion
Next.js makes multi-language support seamless by handling locale-based routing, translations, and redirects with minimal configuration.
Explain the next/head component and when to use it.
The next/head
component in Next.js allows you to modify the HTML <head>
section of a page, enabling dynamic control over metadata like title, description, viewport settings, and Open Graph tags.
How to Use next/head
import Head from 'next/head';
function AboutPage() {
return (
<>
<Head>
<title>About Us - My Website</title>
<meta name="description" content="Learn more about our company and mission." />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<h1>About Us</h1>
</>
);
}
export default AboutPage;
- The
<title>
tag updates dynamically for each page. - Meta tags improve SEO and social media previews.
When to Use next/head
- SEO Optimization – Set page-specific titles, descriptions, and keywords.
- Social Media Sharing – Define Open Graph (
og:title
,og:image
) and Twitter meta tags. - Performance Enhancements – Add preloads for fonts, scripts, or stylesheets.
- Favicon & Theme Colors – Customize the browser tab appearance.
Conclusion
next/head
ensures each page has optimized metadata, improving SEO, user experience, and social media previews dynamically.
How can you enable absolute imports and module path aliases in Next.js?
Next.js allows absolute imports and module path aliases to simplify file imports, avoiding long relative paths like ../../../components/Button
.
Steps to Enable Absolute Imports & Aliases
- Modify
jsconfig.json
(for JavaScript) ortsconfig.json
(for TypeScript) in the root directory:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["components/*"],
"@utils/*": ["utils/*"]
}
}
}
@components
points tocomponents/
@utils
points toutils/
- Use Aliases in Imports
import Button from '@components/Button'; // Instead of '../../components/Button'
import formatDate from '@utils/formatDate'; // Instead of '../../utils/formatDate'
- Restart the Development Server
Run:
npm run dev
Benefits of Absolute Imports & Aliases
- Improves code readability and maintainability.
- Avoids deeply nested relative paths.
- Easier refactoring when moving files.
Conclusion
By configuring jsconfig.json
or tsconfig.json
, Next.js supports cleaner, more readable imports, making development more efficient.
How does Next.js handle redirects and rewrites?
Next.js provides built-in support for redirects and rewrites through the next.config.js
file, allowing developers to modify request paths without extra middleware or server logic.
Redirects in Next.js
- Redirects change the URL in the browser and send the user to a new location.
- Defined in
next.config.js
:
module.exports = {
async redirects() {
return [
{
source: "/old-page",
destination: "/new-page",
permanent: true, // 301 Redirect
},
];
},
};
- Use
permanent: true
for SEO-friendly 301 redirects (cached by browsers). - Use
permanent: false
for temporary 302 redirects.
Rewrites in Next.js
- Rewrites keep the original URL visible but serve content from a different path.
- Useful for proxying requests without changing the user’s URL.
Example:
module.exports = {
async rewrites() {
return [
{
source: "/blog/:slug",
destination: "https://external-api.com/posts/:slug",
},
];
},
};
- The user sees
/blog/post-title
, but the content comes fromexternal-api.com
.
Key Differences
Feature | Redirect | Rewrite |
URL Change | Yes (browser sees new URL) | No (URL stays the same) |
Use Case | Moving pages, SEO fixes | Proxying, API integrations |
Conclusion
Next.js redirects modify the URL, while rewrites serve content from a different location without changing the URL. Both are configured in next.config.js
for flexible request handling.
What are the differences between middleware in Next.js 12 and Next.js 13+?
Next.js 12 introduced middleware, but Next.js 13+ improved it with better performance, flexibility, and Edge Runtime enhancements.
Key Differences
Feature | Next.js 12 | Next.js 13+ |
Execution | Runs on Node.js | Optimized for Edge Runtime |
Location | Inside /middleware.js |
Same, but with better performance |
Response Handling | next() for forwarding requests |
Uses NextResponse.next() |
Streaming Support | No | Yes (better response modification) |
Performance | Slower on cold starts | Faster with Edge functions |
Example Middleware in Next.js 13+
import { NextResponse } from 'next/server';
export function middleware(req) {
const token = req.cookies.get('authToken');
if (!token) {
return NextResponse.redirect(new URL('/login', req.url));
}
return NextResponse.next();
}
Improvements in Next.js 13+
- Optimized for Edge Runtime (lower latency).
- Better streaming support for faster responses.
- More efficient redirects and rewrites.
Conclusion
Next.js 13+ enhances middleware performance with faster execution, improved Edge handling, and better streaming support, making it more efficient for real-world applications. Additionally, Next.js 13+ introduced the App Router, a major structural change that works seamlessly with middleware to handle advanced routing and layout patterns in modern applications.
How does Next.js handle caching and revalidation of static pages?
Next.js optimizes performance by caching static pages and using Incremental Static Regeneration (ISR) to revalidate content without a full rebuild.
1. Caching in Next.js
- Pre-rendered static pages are stored in a CDN cache for faster delivery.
- Pages generated with
getStaticProps()
are cached and served instantly. - API routes and SSR pages (
getServerSideProps()
) are not cached since they run on every request.
2. Revalidation Using ISR
- ISR allows automatic updates to static pages after deployment.
- Set
revalidate
ingetStaticProps()
to refresh content at defined intervals.
Example:
export async function getStaticProps() {
const data = await fetch("https://api.example.com/posts").then(res => res.json());
return {
props: { posts: data },
revalidate: 60, // Revalidate every 60 seconds
};
}
- Users get cached content until the revalidation period expires.
- The page is updated in the background while serving the cached version.
3. Clearing the Cache Manually
- You can trigger revalidation on demand using the
res.revalidate()
API inside an API route.
Example
export default async function handler(req, res) {
await res.revalidate('/blog');
res.json({ revalidated: true });
}
Conclusion
Next.js automatically caches static pages and allows incremental updates via ISR, ensuring fast performance while keeping content fresh without requiring full redeployments.
How do you handle state management in Next.js applications?
Next.js, like React, supports multiple state management approaches depending on the complexity and scope of the application.
1. Local State (useState & useReducer)
- Used for component-specific state (e.g., form inputs, modals).
Example:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
export default Counter;
2. Context API (Global State)
- Best for lightweight global state management (e.g., user authentication).
Example:
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
3. Third-Party State Management (Redux, Zustand, Recoil)
- Redux Toolkit – Best for large-scale apps with complex state.
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
},
});
export const { increment } = counterSlice.actions;
export const store = configureStore({ reducer: { counter: counterSlice.reducer } });
- Zustand – A simpler alternative for global state management.
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}));
Choosing the Right Approach
Approach | Use Case |
useState / useReducer |
Local state within components |
Context API | Lightweight global state (e.g., theme, auth) |
Redux Toolkit | Complex, large-scale applications |
Zustand / Recoil | Simple, scalable global state |
Conclusion
Next.js supports React’s built-in state management, Context API for global state, and external libraries like Redux and Zustand for handling complex state efficiently.
Next.js Interview Questions for Experienced Levels
How does Next.js optimize application performance beyond automatic code splitting?
Next.js provides several built-in optimizations to enhance performance beyond automatic code splitting, ensuring faster load times and better scalability.
1. Static Generation & Incremental Static Regeneration (ISR)
- In the Pages Router,
getStaticProps()
pre-builds pages at compile time for fast loading. - In the App Router, data fetching with
fetch()
andnext: { revalidate }
inside server components achieves the same optimization. - ISR allows updating static pages without a full rebuild, keeping content fresh.
2. Optimized Image Loading (next/image
)
- Automatic image resizing, lazy loading, and WebP support reduce bandwidth usage.
Example:
<Image src="/example.jpg" width={500} height={300} alt="Optimized Image" />
3. Server-Side Rendering (SSR) Only When Needed
- In the Pages Router,
getServerSideProps()
is used for dynamic data fetching. - In the App Router, you can fetch data inside server components, and responses are streamed automatically or cached based on configuration.
- Edge Functions allow faster server-side execution closer to users.
4. Middleware for Efficient Request Handling
- Intercepts requests before they hit the backend, reducing load and improving response times.
- Example use case: authentication checks, A/B testing.
5. Caching and CDN Optimization
- Next.js caches static assets and API responses to reduce redundant network requests.
- Deploying on Vercel or a CDN ensures global performance improvements.
6. Prefetching and Lazy Loading
- Prefetching automatically loads pages before user interaction for faster navigation.
- Dynamic imports (
next/dynamic
) reduce initial load by loading components on demand.
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), { ssr: false });
Note: getStaticProps()
and getServerSideProps()
are used in the Pages Router. In the App Router, data fetching happens directly inside server components using fetch() with caching and revalidation strategies.
Conclusion
Next.js enhances performance with SSG, ISR, optimized images, edge functions, caching, and prefetching, ensuring fast and scalable applications.
What are the key differences between React Server Components (RSC) and traditional SSR in Next.js?
Next.js supports both React Server Components (RSC) and Traditional Server-Side Rendering (SSR), but they serve different purposes in rendering and data fetching.
1. Execution Location
- RSC: Runs entirely on the server without sending JavaScript to the client.
- SSR: Runs on the server but sends the rendered HTML and hydration script to the client.
2. Performance and Client Load
- RSC: Lighter and faster because it does not send unnecessary JavaScript.
- SSR: Can lead to higher client-side JavaScript execution due to hydration.
3. Data Fetching
- RSC (App Router): Fetches data directly on the server inside server components, improving performance and security without needing special data-fetching functions.
- SSR (Pages Router): Uses
getServerSideProps()
, meaning data is fetched on every request and passed into the page component, which increases server load.
4. When to Use Each
Feature | React Server Components (RSC) | Traditional SSR |
Best For | Large, interactive UIs with minimal client JS | Pages needing frequent real-time updates |
Client JS | None (only HTML & styles sent) | Sends full React components with hydration |
Data Fetching | Runs directly on the server | Uses getServerSideProps() per request |
Note:
- React Server Components (RSC) are used in the App Router (
/app/
directory) starting from Next.js 13+. - Traditional SSR (
getServerSideProps()
) is a Pages Router (/pages/
directory) feature in earlier versions and can still be used if needed.
Conclusion
React Server Components improve performance by reducing JavaScript bundle size and shifting more work to the server, while SSR remains useful for dynamic content that changes per request. Next.js leverages both to optimize applications based on use case.
How does Next.js handle hydration, and what are the potential pitfalls?
Hydration in Next.js is the process of attaching client-side React interactivity to pre-rendered HTML from Server-Side Rendering (SSR) or Static Site Generation (SSG). It ensures that React components become interactive once the page loads.
How Next.js Handles Hydration
- Pre-renders HTML using SSR or SSG.
- Sends the JavaScript bundle to the client.
- Reconstructs the component tree in the browser and attaches event listeners.
Potential Pitfalls of Hydration
- Hydration Mismatch (Client-Server Inconsistency)
- Happens when the server-rendered HTML differs from the client-rendered output.
- Example issue: Using
useEffect()
to fetch data that modifies the UI post-render. - Solution (App Router): Fetch data directly in server components to ensure consistent server-side rendering.
- Solution (Pages Router): Use
getStaticProps()
orgetServerSideProps()
for consistent data during pre-rendering.
- Heavy JavaScript Execution
- If the JavaScript bundle is too large, hydration can be slow.
- Solution: Use code splitting (
next/dynamic
) to load components lazily.
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), { ssr: false });
- Blocking the Main Thread
- Large amounts of JavaScript execution during hydration can block the UI.
- Solution: Use React Server Components (RSC) to reduce client-side processing.
Note:
- In the Pages Router,
getStaticProps()
andgetServerSideProps()
were used to fetch consistent data during pre-rendering. - In the App Router, server components fetch data directly using
fetch()
at the server level before hydration.
Conclusion
Next.js optimizes hydration by pre-rendering pages, but pitfalls like mismatches, slow JavaScript execution, and UI blocking must be managed using server-side data fetching, lazy loading, and React Server Components.
What is the role of Suspense in Next.js, and how does it improve data fetching?
Suspense in Next.js is a React feature that allows components to wait for data to load before rendering, improving user experience by managing loading states efficiently.
How Suspense Works in Next.js
- It pauses rendering until the data is available.
- Displays a fallback UI while waiting for content.
- Optimized for React Server Components (RSC) and asynchronous data fetching.
Example: Using Suspense for Data Fetching
import { Suspense } from "react";
import ProductList from "./ProductList";
export default function HomePage() {
return (
<Suspense fallback={<p>Loading products...</p>}>
<ProductList />
</Suspense>
);
}
- The
fallback
prop shows “Loading products…” whileProductList
fetches data.
How Suspense Improves Data Fetching
- Optimized Server-Side Rendering (SSR)
- Suspense works with React Server Components (RSC) to load data before sending HTML to the client, reducing flickering.
- Reduces Client-Side JavaScript Execution
- Fetching data server-side instead of inside
useEffect()
minimizes JavaScript processing on the client.
- Fetching data server-side instead of inside
- Improves Performance with Streaming
- Suspense allows progressive loading, where parts of a page load as data becomes available, reducing perceived wait times.
Note:
- Suspense for data fetching is fully supported in the App Router (Next.js 13+).
- In the Pages Router, Suspense was only supported for client-side components and not for data fetching.
Conclusion
Suspense in Next.js enhances data fetching by delaying rendering until data is ready, reducing client-side processing, and enabling faster, more interactive pages with progressive loading.
How does Next.js handle caching strategies for both static and dynamic content?
Next.js optimizes performance using built-in caching strategies for static and dynamic content, ensuring faster page loads and reduced server load.
1. Static Content Caching (SSG & ISR)
- Static Site Generation (SSG): Pages are built ahead of time and served from the cache.
- Incremental Static Regeneration (ISR): Allows automatic cache updates without rebuilding the entire site.
Example (App Router server component):
async function PostsPage() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 }, // Revalidate every 60 seconds
});
const data = await res.json();
return (
<div>{data.map(post => <p key={post.id}>{post.title}</p>)}</div>
);
}
export default PostsPage;
2. Dynamic Content Caching (SSR & API Routes)
- Server-Side Rendering (SSR) pages are not cached and are regenerated on each request.
- API Routes can use HTTP cache headers to control caching.
- Example of API response caching:
export default function handler(req, res) {
res.setHeader("Cache-Control", "s-maxage=300, stale-while-revalidate=60");
res.json({ message: "This response is cached for 5 minutes" });
}
s-maxage=300
: Cache for 5 minutes.stale-while-revalidate=60
: Serve stale content while fetching a fresh version.
3. Edge and CDN Caching
- Deploying on Vercel or Cloudflare automatically caches static content.
- Dynamic responses can be cached at the edge for faster global delivery.
Note:
- In the Pages Router, static caching and ISR were configured using
getStaticProps()
with arevalidate
field. - In the App Router, caching is handled inside server components using
fetch()
with thenext: { revalidate }
option.
Conclusion
Next.js optimizes caching by serving static pages instantly (SSG, ISR), controlling API response caching, and leveraging CDN caching for performance. Proper caching reduces server load and improves scalability.
How can you optimize API routes in Next.js for high-performance applications?
Optimizing Next.js API routes ensures faster response times, reduced server load, and better scalability.
1. Use Cache-Control
Headers
- Reduce unnecessary API calls by caching responses.
Example (App Router API route):
import { NextResponse } from "next/server";
export async function GET() {
const response = NextResponse.json(
{ message: "This response is cached for 5 minutes" },
{ status: 200 }
);
response.headers.set("Cache-Control", "s-maxage=300, stale-while-revalidate=60");
return response;
}
2. Optimize Database Queries
- Use indexes in databases like PostgreSQL or MongoDB.
- Fetch only required fields to reduce payload size.
Example (MongoDB query optimization):
const users = await db.collection("users").find({}, { projection: { password: 0 } }).toArray();
3. Implement Rate Limiting
- Prevent API abuse using middleware like
express-rate-limit
or Next.js Middleware.
Example using App Router Middleware:
import { NextResponse } from 'next/server';
let requestCount = 0;
export function middleware(request) {
requestCount++;
if (requestCount > 100) {
return NextResponse.json(
{ error: "Rate limit exceeded" },
{ status: 429 }
);
}
return NextResponse.next();
}
4. Use Streaming for Large Responses
- Send data in chunks instead of waiting for the full response.
Example (App Router API route):
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode("First chunk..."));
setTimeout(() => {
controller.enqueue(encoder.encode("Final chunk"));
controller.close();
}, 1000);
}
});
return new Response(stream);
}
5. Move Heavy Computation to Background Jobs
- Offload processing to serverless functions or background workers instead of blocking API responses.
Conclusion
Optimizing API routes in Next.js involves caching, query optimization, rate limiting, streaming, and offloading heavy tasks, ensuring high performance and scalability.
Note:
- In the Pages Router, API routes used
req
andres
objects insidepages/api/
. - In the App Router, API routes are organized in
app/api/
folders with method-specific exports likeGET()
,POST()
insideroute.js
files.
Explain how to handle request validation and error handling in Next.js API routes.
Next.js API routes need proper validation to ensure data integrity and error handling to improve reliability.
1. Request Validation
- Use Zod or Yup to validate incoming request data.
Example using Zod (App Router style):
import { z } from "zod";
import { NextResponse } from "next/server";
const schema = z.object({
name: z.string().min(3),
email: z.string().email(),
});
export async function POST(request) {
const body = await request.json();
const validation = schema.safeParse(body);
if (!validation.success) {
return NextResponse.json(
{ error: validation.error.errors },
{ status: 400 }
);
}
return NextResponse.json({ message: "Valid request" }, { status: 200 });
}
2. Error Handling
- Use
try-catch
to handle errors gracefully. - Return appropriate HTTP status codes (
400
,401
,500
).
Example (App Router style):
import { NextResponse } from "next/server";
export async function GET() {
try {
const res = await fetch("https://api.example.com/data");
const data = await res.json();
return NextResponse.json(data, { status: 200 });
} catch (error) {
return NextResponse.json(
{ error: error.message || "Internal Server Error" },
{ status: 500 }
);
}
}
3. Centralized Error Handling (Middleware Approach)
- Use Next.js Middleware to handle errors globally.
Example (App Router Middleware):
import { NextResponse } from "next/server";
export function middleware(request) {
const authHeader = request.headers.get("Authorization");
if (!authHeader) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
return NextResponse.next();
}
Conclusion
Handling request validation and errors in Next.js API routes ensures data consistency, security, and better user experience.
Using Zod for validation, try-catch for error handling, and middleware for global handling makes APIs more robust in both Pages Router and App Router.
Note:
- In the Pages Router, API routes used a
handler(req, res)
pattern insidepages/api/
. - In the App Router, API routes are defined with method-specific exports like
GET(request)
orPOST(request)
insideroute.js
files.
What are the best practices for integrating GraphQL with Next.js?
Integrating GraphQL with Next.js allows efficient data fetching and API management while maintaining high performance and scalability.
1. Choose a GraphQL Client
- Apollo Client – Feature-rich, caching, and state management.
- urql – Lightweight and optimized for performance.
- Relay – Best for large-scale apps with complex data needs.
2. Setting Up Apollo Client in Next.js (App Router)
- Install dependencies:
npm install @apollo/client graphql
- Create an Apollo Client instance:
// lib/apolloClient.js
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
const client = new ApolloClient({
uri: "https://api.example.com/graphql",
cache: new InMemoryCache(),
});
export default client;
- Create an Apollo Provider wrapper:
// app/providers.jsx
import { ApolloProvider } from "@apollo/client";
import client from "../lib/apolloClient";
export function Providers({ children }) {
return {children};
}
- Use it inside your app/layout.js:
import { Providers } from "./providers";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
3. Fetching Data in Next.js
- Using
useQuery
(Client-Side Fetching)
import { gql, useQuery } from "@apollo/client";
const GET_POSTS = gql`
query {
posts {
id
title
}
}
`;
export default function Posts() {
const { data, loading, error } = useQuery(GET_POSTS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <ul>{data.posts.map((post) => <li key={post.id}>{post.title}</li>)}</ul>;
}
- Fetching GraphQL Data Inside a Server Component
// app/posts/page.jsx
import client from "@/lib/apolloClient";
import { gql } from "@apollo/client";
const GET_POSTS = gql`
query {
posts {
id
title
}
}
`;
export default async function PostsPage() {
const { data } = await client.query({ query: GET_POSTS });
return (
<ul>
{data.posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
✅ In the App Router, you can directly fetch data inside a server component without getServerSideProps()
.
4. Optimize Performance
- Use Caching (
InMemoryCache
) to reduce redundant requests. - Avoid Overfetching by selecting only required fields in queries.
- Use Server Components for SEO optimization and faster performance.
Conclusion
Integrating GraphQL with Next.js requires choosing the right client, optimizing queries with caching, and leveraging server components for better performance.
Using Apollo Client with server-side and client-side components ensures scalable, high-performance data fetching.
Note:
- In the Pages Router, GraphQL data was often fetched using
getServerSideProps()
orgetStaticProps()
inside page components. - In the App Router, data fetching happens directly inside server components for better performance and scalability.
How do you implement rate limiting in Next.js API routes?
Rate limiting in Next.js API routes prevents excessive requests, protecting the server from abuse and improving performance.
1. Using a Simple In-Memory Rate Limiter
- Tracks request counts using a JavaScript object (works best for single-instance deployments)
import { NextResponse } from "next/server";
const rateLimit = {};
const WINDOW_MS = 60 * 1000; // 1 minute
const MAX_REQUESTS = 5;
export async function GET(request) {
const ip = request.headers.get("x-forwarded-for") || "unknown";
if (!rateLimit[ip]) {
rateLimit[ip] = { count: 0, startTime: Date.now() };
}
const currentTime = Date.now();
if (currentTime - rateLimit[ip].startTime > WINDOW_MS) {
rateLimit[ip] = { count: 1, startTime: currentTime };
} else {
rateLimit[ip].count++;
}
if (rateLimit[ip].count > MAX_REQUESTS) {
return NextResponse.json(
{ error: "Too many requests. Please try again later." },
{ status: 429 }
);
}
return NextResponse.json({ message: "Request successful" });
}
Limitations:
- Works only in single-server setups (not distributed).
- Memory usage increases with unique IPs.
2. Using Redis for Scalable Rate Limiting
- Redis is ideal for distributed applications.
- Install
ioredis
:
npm install ioredis
- Implement rate limiting using Redis:
import { NextResponse } from "next/server";
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
const WINDOW_SEC = 60;
const MAX_REQUESTS = 5;
export async function GET(request) {
const ip = request.headers.get("x-forwarded-for") || "unknown";
const requests = await redis.incr(ip);
if (requests === 1) {
await redis.expire(ip, WINDOW_SEC);
}
if (requests > MAX_REQUESTS) {
return NextResponse.json(
{ error: "Too many requests. Try again later." },
{ status: 429 }
);
}
return NextResponse.json(
{ message: "Request successful" },
{ status: 200 }
);
}
Advantages of Redis:
- Works in serverless and multi-instance environments.
- Supports automatic expiry to free up memory.
3. Using Next.js Middleware for Global Rate Limiting
- Middleware can handle rate limiting before the request reaches the API route.
import { NextResponse } from "next/server";
const requests = new Map();
const WINDOW_MS = 60000;
const MAX_REQUESTS = 5;
export function middleware(request) {
const ip = request.ip || request.headers.get("x-forwarded-for") || "unknown";
const now = Date.now();
const userData = requests.get(ip);
if (!userData) {
requests.set(ip, { count: 1, startTime: now });
} else {
if (now - userData.startTime < WINDOW_MS) {
userData.count++;
if (userData.count > MAX_REQUESTS) {
return NextResponse.json(
{ error: "Too many requests" },
{ status: 429 }
);
}
} else {
requests.set(ip, { count: 1, startTime: now });
}
}
return NextResponse.next();
}
Conclusion
Rate limiting in Next.js can be implemented in-memory for simple cases, with Redis for scalability, or using middleware for pre-filtering requests.
For production, Redis-based solutions are preferred to handle high-traffic applications effectively.
Note:
- In the Pages Router, API routes used a
handler(req, res)
pattern insidepages/api/
. - In the App Router, API routes are defined with method-specific exports like
GET(request)
orPOST(request)
insideroute.js
files.
How does streaming work with getServerSideProps() and API routes in Next.js?
Streaming in Next.js allows progressive data rendering, improving performance by sending chunks of data before the full response is ready. This reduces perceived wait times and speeds up page loads.
1. Streaming with React Server Components and Suspense
- In the App Router, pages can stream data automatically by using React Server Components and Suspense.
- You don’t need
getServerSideProps()
anymore. - Example using Suspense:
import React, { Suspense } from 'react';
// This component might fetch data from your API endpoint
async function fetchStreamedData() {
const response = await fetch('/api/stream');
const reader = response.body.getReader();
let text = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
text += new TextDecoder().decode(value);
}
return text;
}
function StreamedComponent({ data }) {
return <pre>{data}</pre>;
}
function App() {
const dataPromise = fetchStreamedData();
return (
<Suspense fallback={<div>Loading streamed data...</div>}>
<StreamedComponent data={dataPromise} />
</Suspense>
);
}
export default App;
- This ensures the page renders immediately with placeholders and loads real data progressively.
2. Streaming in API Routes
- In the App Router, API responses can be sent in chunks using ReadableStream.
Example:
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode("First chunk...\n"));
setTimeout(() => {
controller.enqueue(encoder.encode("Second chunk...\n"));
controller.enqueue(encoder.encode("Final chunk."));
controller.close();
}, 2000);
},
});
return new Response(stream, {
headers: { "Content-Type": "text/plain" },
});
}
- The client receives data progressively rather than waiting for the full response.
Benefits of Streaming in Next.js
- Faster perceived load times by showing content early.
- Reduces time to first byte (TTFB) for better user experience.
- Efficient data fetching by avoiding blocking the UI.
Conclusion
Next.js streams data progressively using React Server Components with Suspense for pages and ReadableStream for API routes, improving performance and reducing wait times.
Note:
- In the Pages Router,
getServerSideProps()
was used to fetch all data before rendering. - In the App Router, data is streamed automatically inside React Server Components, without needing special data-fetching functions.
How do you implement user authentication efficiently using Next.js Middleware?
Next.js Middleware allows authentication checks before a request reaches a page or API route, improving security and performance by handling auth logic at the Edge or server-side.
1. Protecting Routes with Middleware
- Redirects unauthenticated users to the login page before rendering the protected page.
Example:
import { NextResponse } from "next/server";
export function middleware(req) {
const token = req.cookies.get("authToken"); // Get token from cookies
if (!token) {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"], // Protect all /dashboard routes
};
- How It Works:
- If no token is found, the user is redirected to
/login
. - If authenticated, the request proceeds normally.
- If no token is found, the user is redirected to
2. Verifying Tokens (JWT Authentication)
- Middleware can decode and verify JWT tokens before allowing access.
Example using jsonwebtoken
:
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
export function middleware(req) {
const token = req.cookies.get("authToken");
try {
jwt.verify(token, process.env.JWT_SECRET); // Verify JWT
return NextResponse.next();
} catch (error) {
return NextResponse.redirect(new URL("/login", req.url));
}
}
export const config = {
matcher: ["/protected/:path*"], // Protect specific routes
};
3. Benefits of Using Middleware for Authentication
- Runs before page loads → Prevents unnecessary rendering.
- Works at the Edge → Faster request processing.
- Reduces API calls → No need for client-side auth checks on every page load.
Conclusion
Using Next.js Middleware for authentication ensures efficient, server-side access control by validating tokens before rendering protected pages, leading to better security and performance. As a side note, developers can also use NextAuth.js to handle authentication automatically through configuration, offering built-in support for sessions, providers, and middleware.
What are the trade-offs between running middleware at the Edge versus running it server-side?
Next.js allows middleware to run at the Edge (Edge Functions) or on the server (Node.js runtime). Each approach has advantages and trade-offs depending on performance, security, and scalability needs.
1. Edge Middleware
Pros:
- Lower Latency – Runs closer to the user, reducing response times.
- Scalability – Handles global traffic efficiently with Edge networks (e.g., Vercel, Cloudflare).
- Prevents Unnecessary Requests – Can block or modify requests before they reach the origin server.
Cons:
- Limited APIs – Cannot use full Node.js features (e.g., file system, some database connections).
- Cold Starts in Some Providers – May experience slight delays in serverless environments.
- Data Fetching Limitations – Cannot make direct API calls before forwarding requests.
2. Server-Side Middleware (Node.js Runtime)
Pros:
- Full Node.js Support – Can access databases, file system, and third-party APIs.
- Better for Heavy Processing – Handles complex logic before rendering pages.
- More Flexible Request Handling – Can modify request headers, cookies, and body content deeply.
Cons:
- Higher Latency – Requests travel to a centralized server instead of being handled at the Edge.
- Increased Load on Backend – Every request reaches the origin server, increasing costs.
- Scaling Challenges – Requires additional infrastructure to handle large traffic.
Choosing the Right Approach
Feature | Edge Middleware | Server-Side Middleware |
Performance | Faster (low latency) | Slower (higher latency) |
Scalability | High (Edge network) | Lower (centralized server) |
Access to Node.js APIs | Limited | Full |
Data Fetching | Restricted | Full API access |
Best For | Authentication, geo-based personalization, redirects | Complex logic, database interactions |
Conclusion
- Use Edge Middleware for fast authentication, geo-based content, and redirects.
- Use Server-Side Middleware when full Node.js capabilities (e.g., database queries) are required.
- In many cases, a hybrid approach combining both is the best solution.
How can you use Next.js Middleware for geo-based content delivery?
Next.js Middleware allows geo-based content delivery by detecting a user’s location from the request headers and serving region-specific content before the page renders.
1. Detecting User Location in Middleware
- Next.js automatically provides
req.geo
(on platforms like Vercel) to access country, region, and city.
Example:
import { NextResponse } from "next/server";
export function middleware(req) {
const country = req.geo?.country || "US"; // Default to US if no data
const url = req.nextUrl.clone();
if (country === "FR") {
url.pathname = "/fr"; // Redirect French users
} else if (country === "DE") {
url.pathname = "/de"; // Redirect German users
}
return NextResponse.rewrite(url); // Rewrite without changing the URL
}
export const config = {
matcher: ["/"], // Apply middleware to the homepage
};
- What This Does:
- Detects the user’s country.
- Rewrites the request to the appropriate localized version without changing the URL.
2. Serving Dynamic Content Based on Location
- Instead of redirects, you can modify the response content dynamically.
Example:
export function middleware(req) {
const country = req.geo?.country || "US";
const response = NextResponse.next();
response.headers.set("X-Country", country); // Pass location to frontend
return response;
}
- The frontend can then use this header to display localized content.
3. Benefits of Geo-Based Middleware
- Faster than client-side detection (avoids additional API calls).
- No extra page reloads (rewrites instead of redirects).
- Efficient Edge Processing – Works at the Edge network, reducing latency.
Conclusion
Next.js Middleware enables fast, location-based content delivery by detecting a user’s geo-location at the Edge and dynamically serving localized content or redirects before rendering.
What are the limitations of Edge Functions in Next.js compared to traditional SSR?
Edge Functions in Next.js run closer to users for lower latency but have some limitations compared to traditional Server-Side Rendering (SSR) running on a Node.js server.
1. Limited Access to Node.js APIs
- Edge Functions run in a lightweight runtime, meaning:
- No access to the file system (fs module).
- No built-in support for server-side WebSockets.
- Limited compatibility with some npm packages.
- SSR on Node.js supports full Node.js features, including file operations and long-running tasks.
2. Restrictions on Database Connections
- Edge Functions cannot maintain persistent database connections (e.g., PostgreSQL, MySQL) due to their stateless nature.
- They work best with serverless databases (e.g., DynamoDB, FaunaDB, Planetscale) or HTTP-based database queries (e.g., Prisma Data Proxy).
- SSR on Node.js can maintain direct database connections efficiently.
3. Execution Time Limits
- Edge Functions are designed for fast, lightweight execution and have strict time limits (often <5 seconds).
- SSR on Node.js can handle longer-running processes, making it better for heavy computations.
4. No Built-in Streaming Support
- Edge Functions do not yet fully support React Streaming (
getServerSideProps()
with partial rendering). - SSR on Node.js allows streaming responses, progressively loading content for better performance.
5. Deployment Considerations
- Edge Functions require Edge-compatible platforms (e.g., Vercel, Cloudflare Workers).
- SSR on Node.js can be deployed on any server (AWS, DigitalOcean, traditional VPS).
Choosing Between Edge Functions and SSR
Feature | Edge Functions | Traditional SSR (Node.js) |
Latency | Faster (runs closer to users) | Slower (centralized server) |
Node.js APIs | Limited | Full access |
Database Support | Works best with serverless DBs | Supports persistent DB connections |
Execution Time | Short (<5s) | Longer allowed |
Streaming | Limited support | Fully supported |
Conclusion
Edge Functions are best for authentication, A/B testing, geo-based content, and caching, while SSR on Node.js is better for database-heavy apps, complex computations, and streaming responses. The right choice depends on application needs and infrastructure.
How can you optimize security using Next.js Middleware?
Next.js Middleware helps improve security by filtering requests before they reach the application, preventing unauthorized access, injection attacks, and other threats.
1. Implement Authentication and Authorization
- Middleware can verify JWT tokens or session cookies before allowing access.
Example:
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
export function middleware(req) {
const token = req.cookies.get("authToken");
try {
jwt.verify(token, process.env.JWT_SECRET);
return NextResponse.next();
} catch (error) {
return NextResponse.redirect(new URL("/login", req.url));
}
}
export const config = { matcher: ["/dashboard/:path*"] };
- Ensures only authenticated users access protected routes.
2. Prevent API Abuse with Rate Limiting
- Middleware can throttle requests to prevent DDoS attacks.
Example:
const requests = new Map();
export function middleware(req) {
const ip = req.ip;
const now = Date.now();
if (!requests.has(ip)) requests.set(ip, { count: 1, startTime: now });
else {
const userData = requests.get(ip);
if (now - userData.startTime < 60000) {
userData.count++;
if (userData.count > 5) return new Response("Too many requests", { status: 429 });
} else {
requests.set(ip, { count: 1, startTime: now });
}
}
return NextResponse.next();
}
- Limits users to 5 requests per minute.
3. Secure Headers to Prevent Attacks
- Middleware can enforce CSP, XSS protection, and clickjacking prevention.
Example:
export function middleware(req) {
const response = NextResponse.next();
response.headers.set("Content-Security-Policy", "default-src 'self'");
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-XSS-Protection", "1; mode=block");
return response;
}
- Protects against cross-site scripting (XSS) and clickjacking.
4. Block Unwanted Traffic (Geo-Blocking & IP Filtering)
- Restrict access based on IP address or location.
Example:
export function middleware(req) {
const country = req.geo?.country;
if (country === "CN") {
return new Response("Access Denied", { status: 403 });
}
return NextResponse.next();
}
- Blocks access from certain regions.
Conclusion
Next.js Middleware enhances security by authenticating users, limiting API abuse, enforcing security headers, and blocking malicious traffic before requests reach the application.
How does Next.js handle nested dynamic routes efficiently?
Next.js automatically manages nested dynamic routes using its file-based routing system, allowing scalable and efficient URL structures without extra configurations.
1. Defining Nested Dynamic Routes (App Router)
- Use square brackets (
[param]
) to create dynamic segments inside the/app
directory.
Example structure:
/app
/blog
/[category]
/[post]
page.js → /blog/:category/:post
- This structure allows URLs like:
/blog/technology/nextjs-performance
/blog/design/ui-ux-trends
2. Accessing Dynamic Parameters
- In the App Router, dynamic route parameters are passed automatically via the
params
prop in yourpage.js
component.
Example:
export default function BlogPostPage({ params }) {
const { category, post } = params;
return <h1>Category: {category} - Post: {post}</h1>;
}
- No need for
useRouter()
orgetStaticProps()
. - Optimizations:
- Static pages are automatically cached at build time unless dynamic rendering is explicitly configured.
3. Catch-All Routes for Scalability
- Use
[...slug]
to create catch-all dynamic routes for variable depth URLs (e.g.,/blog/category/post/sub-post
). - Folder structure:
export default function BlogPostPage({ params }) {
const { category, post } = params;
return <h1>Category: {category} - Post: {post}</h1>;
}
Example page component:
export default function SlugPage({ params }) {
return <p>Path: {params.slug.join(" / ")}</p>;
}
- This supports URLs like
/blog/tech/nextjs/performance
without needing to predefine every segment.
Conclusion
Next.js efficiently handles nested dynamic routes in the App Router using folder-based routing, automatic params
injection, and catch-all segments, ensuring scalability and high performance without extra configuration.
Note:
- In the Pages Router, nested dynamic routes used
pages/
,getStaticPaths()
, andgetStaticProps()
. - In the App Router, dynamic parameters are accessed automatically through the
params
prop inside server components.
How do you prefetch routes in Next.js to improve navigation speed?
Next.js automatically prefetches routes linked with the next/link
component, improving navigation speed by loading pages in the background before the user clicks a link.
1. Default Route Prefetching with next/link
(App Router)
- In the App Router, using
next/link
automatically prefetches pages when links appear in the viewport.
Example:
import Link from "next/link";
export default function HomePage() {
return (
Go to About
);
}
- No need for an
<a>
tag insideLink
. - When
/about
is visible, Next.js preloads its JavaScript and assets automatically.
2. Manually Enabling or Disabling Prefetching
- You can control preloading behavior with the
prefetch
prop.
Example (disable prefetching):
<Link href="/contact" prefetch={false}>
Contact Us
</Link>
- Disabling prefetching can save bandwidth for rarely visited pages.
3. Prefetching Programmatically Using useRouter
- In the App Router, use
useRouter
fromnext/navigation
to programmatically prefetch routes.
Example (prefetch on hover):
import { useRouter } from "next/navigation";
export default function PrefetchButton() {
const router = useRouter();
return (
<button onMouseEnter={() => router.prefetch("/dashboard")}>
Dashboard (Hover to Prefetch)
</button>
);
}
- This approach helps prefetch only when needed, like on user interaction.
4. Benefits of Route Prefetching in Next.js
- Reduces perceived load time – Pages load almost instantly after clicking.
- Optimized bandwidth usage – Prefetching happens only for visible links.
- Works seamlessly with Static Generation (SSG) and ISR – Prefetched pages are served instantly.
Conclusion
Next.js automatically prefetches routes linked with next/link
, and manual prefetching using useRouter
improves performance by reducing navigation delays while optimizing resource usage.
Note:
- In the Pages Router,
Link
required a child<a>
tag andnext/router
was used for prefetching. - In the App Router,
Link
directly renders links, anduseRouter
is imported fromnext/navigation
for programmatic prefetching.
How can you implement custom route handlers in Next.js?
Custom route handlers in Next.js allow you to define server-side logic for requests directly inside your app.
In the App Router (Next.js 13+), this is done inside the app/api/
directory.
1. Creating API Route Handlers with the App Router (app/api/
)
- API routes are now created under the
app/api
directory with file-based routing.
Example (app/api/user/route.js
):
// app/api/user/route.js
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "User data" }, { status: 200 });
}
export async function POST(request) {
const data = await request.json();
return NextResponse.json({ received: data }, { status: 201 });
}
- Each HTTP method (GET, POST, PUT, DELETE) is handled by exported functions.
- Handlers run on the Edge Runtime by default for faster performance.
Use Cases:
- Building RESTful APIs inside your Next.js application.
- Handling form submissions, user authentication, and external API calls.
2. Legacy Support: pages/api/
(Pages Router)
- In the Pages Router, custom APIs were created inside
pages/api/
.
Example (pages/api/hello.js
):
export default function handler(req, res) {
if (req.method === "GET") {
res.status(200).json({ message: "Hello from Next.js API route!" });
} else {
res.status(405).json({ error: "Method Not Allowed" });
}
}
- Note:This method still works but is considered legacy. New apps should prefer
app/api/
.
3. Middleware-Based Custom Route Handling
- Middleware allows you to intercept requests globally before they reach a route.
Example (middleware.js
):
import { NextResponse } from "next/server";
export function middleware(request) {
const authToken = request.cookies.get("authToken");
if (!authToken) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/api/:path*"], // Apply middleware to all API routes
};
- Middleware is useful for authentication, rate limiting, geo-blocking, and logging.
Conclusion
In Next.js with the App Router, custom route handlers are created inside the app/api/
directory using exported HTTP functions.
Middleware can also be used to preprocess or block requests before they hit the route handlers.
The Pages Router (pages/api/
) method is still available for legacy support but should be avoided in new projects.
Note:
- In the Pages Router, API routes were created inside
pages/api/
. - In the App Router, API routes live in
app/api/
and use server or edge handlers directly.
What are the potential challenges of deep linking in a Next.js application?
Deep linking allows users to navigate directly to a specific page within a Next.js app, but it comes with challenges related to dynamic routing, authentication, and SEO.
1. Handling Dynamic Routes (App Router)
- Challenge: If a deep link points to a dynamic route (e.g.,
/post/:id
), the page may fail if the required data isn’t available.
For static pages (e.g., using generateStaticParams()), only pre-rendered paths are available. If a user navigates directly to a deep link that wasn’t included in the pre-generated list, they’ll hit a 404.
For server-rendered or incrementally generated pages, the app can fetch data on the fly using fetch in page.js with caching strategies or by setting the route segment to dynamic = “force-dynamic”. This ensures the route can resolve dynamically at request time.
- Solution: Use
generateStaticParams()
to pre-generate paths or enable dynamic rendering with dynamic = “force-dynamic” in route segments.
Example (dynamic route handler):
// app/blog/[slug]/page.js
export async function generateStaticParams() {
const posts = await fetchPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({ params }) {
const post = await fetchPost(params.slug);
return <div>{post.title}</div>;
}
2. Authentication and Protected Routes
- Challenge: Users accessing deep links may not be authenticated, leading to unauthorized access or redirects.
- Solution: Protect routes at the layout or with Middleware in the App Router to check authentication before rendering.
3. SEO and Page Previews
- Challenge: Search engines and social media previews may not properly render deep links if client-side fetching is required.
- Solution: Use Server Components and dynamic metadata (
generateMetadata()
) to ensure proper SEO and page previews.
Example:
export async function generateMetadata({ params }) {
const post = await fetchPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
}
4. Client-Side Navigation vs. Direct Access
- Challenge: Some pages rely on client-side state, which is lost on deep linking (e.g., form progress).
- Solution: Persist important state using URL query parameters, cookies, or local storage.
5. CDN and Caching Issues
- Challenge: Deep links may load outdated content if the CDN caches old pages.
- Solution: Use Incremental Static Regeneration (ISR) with
revalidate
options in the App Router to refresh content automatically.
Example:
export const revalidate = 60; // Revalidate every 60 seconds
Conclusion
Deep linking in Next.js (App Router) requires careful handling of dynamic routes, authentication at the layout or middleware level, server-rendered metadata for SEO, and smart caching to ensure seamless navigation and fresh content.
Note:
- In the Pages Router, deep linking challenges were handled with
getStaticPaths()
andgetServerSideProps()
. - In the App Router, use
generateStaticParams()
, dynamic rendering options, and server components to address the same concerns.
What are the best practices for handling global state in a Next.js application?
Best Practices for Handling Global State in a Next.js Application
These solutions are not specific to Next.js—they are general React.js patterns that can be applied within a Next.js app.
Managing global state in a Next.js app efficiently ensures smooth performance, scalability, and maintainability. The best approach depends on the application’s complexity and data persistence needs.
1. Context API for Lightweight State Management
- Ideal for small-scale global state (e.g., theme, authentication).
Example:
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
{children}
);
}
export function useTheme() {
return useContext(ThemeContext);
}
- Best For: Simple global state that doesn’t require frequent updates.
2. Redux Toolkit for Large-Scale Applications
- Best when dealing with complex state across multiple components.
Example setup:
import { configureStore, createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: { increment: (state) => { state.value += 1; } },
});
export const { increment } = counterSlice.actions;
export const store = configureStore({ reducer: { counter: counterSlice.reducer } });
- Best For: Large applications needing centralized state management.
3. Zustand for Minimalist State Management
- A lighter alternative to Redux, offering better performance.
- Example:
import create from "zustand";
const useStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}));
- Best For: Applications needing efficient, simple state updates.
4. Persisting State with Local Storage or Cookies
- Use local storage for non-sensitive UI preferences.
- Use cookies for authentication or user sessions (server-side accessible).
Conclusion
For small applications, Context API or Zustand works well. For larger apps, Redux Toolkit ensures structured state management. Choosing the right method depends on state complexity and performance needs.
How do you optimize server-side state hydration in Next.js?
Server-side state hydration in Next.js ensures that server-fetched data is correctly transferred to the client while maintaining performance and avoiding hydration mismatches.
1. Use Efficient Server-Side Data Fetching
- In the App Router, fetch data directly inside server components using
async
functions andfetch()
. - In the Pages Router, use
getServerSideProps()
to fetch data server-side.
Example (Pages Router using getServerSideProps()
):
export async function getServerSideProps() {
const data = await fetch("https://api.example.com/posts").then((res) => res.json());
return { props: { posts: data.slice(0, 10) } }; // Send only first 10 posts
}
- Benefit: Reduces initial page load time by sending only required data.
2. Avoid Client-Server Mismatches
- Ensure server-rendered and client-rendered data match to prevent hydration errors.
Example of a common issue (Pages Router):
export async function getServerSideProps() {
return { props: { timestamp: Date.now() } }; // Causes mismatch
}
- Fix: Generate timestamps only on the client using
useEffect()
.
3. Use Lazy Loading for Large Data
- Load heavy components or additional data after hydration to avoid blocking the main thread.
Example:
import { useEffect, useState } from "react";
function Page({ initialData }) {
const [extraData, setExtraData] = useState(null);
useEffect(() => {
fetch("/api/extra")
.then((res) => res.json())
.then(setExtraData);
}, []);
return <div>{extraData ? extraData.value : initialData}</div>;
}
4. Minimize JSON Stringification Overhead
- Problem: Deeply nested objects increase the cost of
JSON.stringify()
during server-to-client transfer. - Solution: Normalize or flatten data structures to make serialization faster and lighter.
5. Use React Server Components for Partial Hydration
- In the App Router, React Server Components allow parts of the UI to load without sending unnecessary JavaScript to the client, improving overall performance.
Conclusion
Optimizing server-side state hydration in Next.js involves fetching minimal necessary data, preventing client-server mismatches, lazy loading additional data, minimizing serialization overhead, and using React Server Components where possible for better performance.
How can you persist state across page navigations in a Next.js app?
In Next.js, navigating between pages typically resets local state, but you can persist state using various strategies based on the use case.
1. Using URL Query Parameters
- Store state in the URL so it persists across navigations.
Example:
import { useRouter } from "next/router";
function Filters({ filters }) {
const router = useRouter();
const updateFilter = (newFilter) => {
router.push({ pathname: "/products", query: { category: newFilter } }, undefined, { shallow: true });
};
return <button onClick={() => updateFilter("electronics")}>Set Filter</button>;
}
export default Filters;
- Best For: Search filters, pagination, and user preferences.
2. Using React Context API
- Store global state using Context, which persists across page navigations.
Example:
import { createContext, useContext, useState } from "react";
const CartContext = createContext();
export function CartProvider({ children }) {
const [cart, setCart] = useState([]);
return {children};
}
export function useCart() {
return useContext(CartContext);
}
- Wrap
_app.js
with<CartProvider>
to persist cart data. - Best For: Shopping carts, authentication state.
3. Using Local Storage or Session Storage
- Save state in the browser to restore it after page reloads.
Example:
import { useState, useEffect } from "react";
function usePersistentState(key, initialValue) {
const [state, setState] = useState(() => {
try {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(state));
} catch {
// Handle storage write errors silently
}
}, [key, state]);
return [state, setState];
}
export default usePersistentState;
- Best For: Remembering theme preferences, form drafts.
4. Using Redux or Zustand for Global State
- State management libraries like Redux Toolkit or Zustand persist data across pages.
- Best For: Large-scale applications with complex state.
Conclusion
Persisting state across navigations can be done using URL parameters, Context API, local storage, or state management libraries, depending on the data’s importance and longevity.
What are the performance trade-offs between client-side and server-side state management in Next.js?
Next.js allows both client-side and server-side state management, each with trade-offs depending on performance, SEO, and scalability needs.
1. Client-Side State Management
- Uses React’s
useState
, Context API, Redux, or Zustand. - Pros:
- Faster interactions since state is managed in memory.
- Reduced server load as data is handled on the client.
- Works well for UI state, form inputs, and local user settings.
- Cons:
- Poor SEO as content is rendered on the client.
- State resets on refresh unless persisted in local storage.
- Can cause slower first render if heavy computations are needed.
2. Server-Side State Management
- Uses
getServerSideProps()
– in pages router, API routes, or external databases. - Pros:
- SEO-friendly as pages are pre-rendered with data.
- Consistent data fetched per request, ensuring accuracy.
- Offloads heavy state processing from the client.
- Cons:
- Higher latency due to server requests on every page load.
- Increased server load with high-traffic applications.
- More complex caching strategies are needed for performance.
Choosing the Right Approach
Feature | Client-Side State | Server-Side State |
Performance | Faster interactions | Slower initial load |
SEO | Not SEO-friendly | Fully SEO-friendly |
Data Freshness | Stale until refreshed | Always up-to-date |
Best For | UI state, themes, local data | Dynamic data, authentication |
Conclusion
Use client-side state for fast UI interactions, and server-side state when data must be fresh, SEO-friendly, and shared across requests. A hybrid approach is best for performance optimization.
How do you handle load balancing when deploying a Next.js application on AWS or DigitalOcean?
Load balancing ensures a Next.js application scales efficiently by distributing traffic across multiple servers, improving performance and reliability.
1. Using AWS Load Balancer with EC2 Instances
- Deploy multiple EC2 instances running Next.js behind an Application Load Balancer (ALB).
- ALB handles automatic traffic distribution and SSL termination.
- Steps:
- Deploy Next.js on multiple EC2 instances.
- Attach instances to an Auto Scaling Group (ASG).
- Configure ALB to route requests across instances.
2. Load Balancing in AWS with ECS (Containerized Deployment)
- Use AWS Elastic Container Service (ECS) with Fargate for serverless container management.
- Steps:
- Build and push Next.js app as a Docker image.
- Deploy to ECS using a Task Definition.
- Use AWS ALB to distribute traffic to running containers.
3. Load Balancing on DigitalOcean with Droplets
- Deploy multiple Droplets and place them behind a DigitalOcean Load Balancer.
- Steps:
- Deploy Next.js on multiple Droplets.
- Add all instances to a DigitalOcean Load Balancer.
- Enable health checks to remove unhealthy instances.
4. Using Serverless and Edge Deployments (No Load Balancer Needed)
- Deploy on AWS Lambda (via Vercel or SST) or Cloudflare Workers to handle scaling without a load balancer.
- Ideal for serverless and Edge-based applications.
5. Optimizing Database Connections
- Use Amazon RDS or DigitalOcean Managed Databases with read replicas for scaling database queries.
Conclusion
For AWS, use ALB with EC2 or ECS (Fargate). For DigitalOcean, use Droplets with a Load Balancer. If possible, go serverless (Vercel, AWS Lambda, Cloudflare Workers) to avoid load balancing complexity altogether.
What are the key differences between self-hosting Next.js and deploying it on Vercel?
Key Differences Between Self-Hosting Next.js and Deploying on Vercel
Next.js can be self-hosted on your own server or deployed to Vercel, the platform built specifically for Next.js applications. Each approach has trade-offs based on performance, scalability, and control.
1. Deployment and Hosting
- Vercel: Fully managed deployment with automatic scaling, serverless functions, and Edge optimizations.
- Self-Hosting: Requires setting up a server (AWS, DigitalOcean, or VPS) with manual configuration.
2. Performance and Scalability
- Vercel: Optimized for serverless functions and Edge computing, ensuring low-latency responses.
- Self-Hosting: Requires load balancing and auto-scaling (e.g., Kubernetes, Nginx, or AWS ALB).
3. Server-Side Features
- Vercel: No persistent backend; API routes run as serverless functions with execution limits.
- Self-Hosting: Full access to long-lived processes, WebSockets, and database connections.
4. Cost Considerations
- Vercel: Free for small projects, but paid plans apply for high traffic and serverless function limits.
- Self-Hosting: More predictable pricing, but requires managing infrastructure and scaling.
5. Flexibility and Customization
- Vercel: Best for serverless and static sites, but limited control over backend infrastructure.
- Self-Hosting: Allows custom caching, database optimizations, and fine-tuned performance tweaks.
Choosing the Right Option
Feature | Vercel | Self-Hosting |
Ease of Deployment | Simple (CI/CD built-in) | Requires manual setup |
Scalability | Auto-scales via serverless | Needs load balancing |
Backend Flexibility | Limited (serverless) | Full control (long-lived processes) |
Best For | Startups, fast deployments, serverless apps | Enterprise, custom backends, large-scale apps |
Conclusion
Vercel is best for hassle-free, serverless deployments with automatic scaling, while self-hosting provides full control over infrastructure but requires more setup and maintenance.
How can you use WebSockets in a Next.js API route for real-time applications?
Next.js API routes run as serverless functions by default, which do not support persistent WebSocket connections. To use WebSockets, you need to self-host Next.js or use a WebSocket server (e.g., Socket.io, WebSocket API).
1. Using WebSockets with a Custom Express Server
- Next.js must run in custom server mode (not serverless).
- In this setup, the Express server handles both Next.js page routing and WebSocket connections.
Example with Socket.io:
const { createServer } = require("http");
const { Server } = require("socket.io");
const next = require("next");
const app = next({ dev: process.env.NODE_ENV !== "production" });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = createServer((req, res) => handle(req, res)); // Integrates Next.js page handling
const io = new Server(server);
io.on("connection", (socket) => {
console.log("User connected:", socket.id);
socket.on("message", (data) => {
io.emit("message", data);
});
socket.on("disconnect", () => {
console.log("User disconnected");
});
});
server.listen(3000, () => console.log("Server running on port 3000"));
});
- In this setup, the Express server replaces the default Next.js API routes, so all routing and real-time logic is handled manually.
- Best For: Self-hosted deployments (e.g., AWS, DigitalOcean) where fine-grained control is needed.
2. Using an External WebSocket Service (e.g., Pusher, Firebase, Supabase)
- Works with serverless Next.js deployments (Vercel, Cloudflare).
Example with Pusher:
import Pusher from "pusher";
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_KEY,
secret: process.env.PUSHER_SECRET,
cluster: "us2",
useTLS: true,
});
export default async function handler(req, res) {
if (req.method === "POST") {
await pusher.trigger("chat", "message", { text: req.body.text });
res.status(200).json({ success: true });
} else {
res.status(405).json({ error: "Method Not Allowed" });
}
}
- Best For: Serverless environments (Vercel, Firebase, AWS Lambda).
Conclusion
For self-hosted Next.js, use Socket.io with an Express server. For serverless deployments, use an external WebSocket provider like Pusher or Firebase.
How do you monitor and debug performance bottlenecks in a Next.js production environment?
To ensure a fast and scalable Next.js application, monitoring and debugging performance bottlenecks is crucial. This involves tracking server response times, analyzing rendering performance, and optimizing API calls.
1. Use Vercel Analytics or Next.js Built-in Metrics
- Vercel Analytics provides real-time performance monitoring.
- Enable Next.js performance insights by logging render times:
export function reportWebVitals(metric) {
console.log(metric);
}
- Best for: Tracking load times, hydration delays, and route changes.
2. Monitor API Response Times
- Use APM tools like Datadog, New Relic, or OpenTelemetry to track slow API endpoints.
- Log response times in API routes:
export default async function handler(req, res) {
const start = Date.now();
const data = await fetch("https://api.example.com/data").then((r) => r.json());
console.log(`API response time: ${Date.now() - start}ms`);
res.status(200).json(data);
}
- Best for: Identifying slow API responses.
3. Enable Logging and Error Tracking
- Use Sentry or LogRocket for error monitoring and performance tracking.
- Install Sentry:
npm install @sentry/nextjs
- Configure _app.js:
import * as Sentry from "@sentry/nextjs";
Sentry.init({ dsn: process.env.SENTRY_DSN });
4. Optimize Database Queries and Caching
- Use query optimization, indexing, and caching to improve performance.
- Add
Cache-Control
headers for API responses:
res.setHeader("Cache-Control", "s-maxage=600, stale-while-revalidate=30");
5. Analyze JavaScript Bundle Size
- Use
next build
to analyze the bundle:
next build && next analyze
- Optimize by removing unused dependencies and lazy-loading heavy components.
Conclusion
Monitoring Next.js performance requires Vercel Analytics, API logging, error tracking tools (Sentry), database optimization, and bundle size analysis to identify and fix bottlenecks efficiently.
Next.js Coding Interview Questions
Implement a Next.js app with three pages: Home, About, and Contact using next/link(App Router).
1. Create a New Next.js App
npx create-next-app@latest nextjs-app-router-app
cd nextjs-app-router-app
npm install
When prompted, select “Use App Router”.
2. Create the Pages (Home, About, Contact)
Inside the app/
directory, create these folders and files:
/app
/about
page.js
/contact
page.js
page.js
app/page.js (Home Page)
import Link from "next/link";
export default function Home() {
return (
<div>
<h1>Home Page</h1>
<nav>
<Link href="/about">About</Link> | <Link href="/contact">Contact</Link>
</nav>
</div>
);
}
app/about/page.js (About Page)
import Link from "next/link";
export default function About() {
return (
<div>
<h1>About Page</h1>
<nav>
<Link href="/">Home</Link> | <Link href="/contact">Contact</Link>
</nav>
</div>
);
}
app/contact/page.js (Contact Page)
import Link from "next/link";
export default function Contact() {
return (
<div>
<h1>Contact Page</h1>
<nav>
<Link href="/">Home</Link> | <Link href="/about">About</Link>
</nav>
</div>
);
}
3. Start the Next.js App
npm run dev
Visit:
http://localhost:3000/
→ Home Pagehttp://localhost:3000/about
→ About Pagehttp://localhost:3000/contact
→ Contact Page
Conclusion
This creates a basic Next.js App Router project with three pages and fast client-side navigation using next/link
.
Implement a dynamic route that displays blog posts based on id (/blog/[id]) (App Router).
1. Create the Project Structure
In the app/
directory:
/app
/blog
/[id]
page.js
page.js
layout.js
page.js
2. Implement the Dynamic Blog Post Page (app/blog/[id]/page.js
)
import { notFound } from "next/navigation";
// Mocked data
const posts = [
{ id: "1", title: "Post 1", content: "This is the content of post 1." },
{ id: "2", title: "Post 2", content: "This is the content of post 2." },
{ id: "3", title: "Post 3", content: "This is the content of post 3." },
];
export async function generateStaticParams() {
return posts.map((post) => ({
id: post.id,
}));
}
export default async function BlogPost({ params }) {
const post = posts.find((p) => p.id === params.id);
if (!post) {
notFound(); // App Router-specific 404 handling
}
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
3. Create a Blog Listing Page (app/blog/page.js
)
import Link from "next/link";
export default function Blog() {
const posts = [{ id: "1", title: "Post 1" }, { id: "2", title: "Post 2" }, { id: "3", title: "Post 3" }];
return (
<div>
<h1>Blog</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
);
}
4. Run the Application
npm run dev
- Visit
http://localhost:3000/blog
→ See blog posts list. - Click on “Post 1” → Navigates to
/blog/1
, showing the post details.
Conclusion
This creates a dynamic blog route in Next.js App Router using generateStaticParams()
for static generation at build time. Replace the mock data with a real API call if needed.
Implement nested routing to display user profiles under /users/[id]/profile(App Router).
1. Create the Nested Route Structure
In the app/
directory:
/app
/users
/[id]
/profile
page.js
page.js
layout.js
page.js
2. Implement the User Profile Page (app/users/[id]/profile/page.js
)
export default async function UserProfile({ params }) {
// Simulate fetching user data
const user = {
id: params.id,
name: `User ${params.id}`,
email: `user${params.id}@example.com`,
bio: "This is a sample user profile.",
};
return (
<div>
<h1>{user.name}'s Profile</h1>
<p>Email: {user.email}</p>
<p>Bio: {user.bio}</p>
</div>
);
}
3. Create the Users Listing Page (app/users/page.js
)
import Link from "next/link";
export default function Users() {
const users = [
{ id: "1", name: "Alice" },
{ id: "2", name: "Bob" },
{ id: "3", name: "Charlie" },
];
return (
<div>
<h1>Users List</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
<Link href={`/users/${user.id}/profile`}>{user.name}'s Profile</Link>
</li>
))}
</ul>
</div>
);
}
4. Run the Application
npm run dev
- Visit
http://localhost:3000/users
→ See a list of users. - Click on “Alice’s Profile” → Redirects to
/users/1/profile
, showing Alice’s details.
Conclusion
This setup implements nested dynamic routing in Next.js App Router using folders and Server Components, creating clean URLs like /users/[id]/profile
for user profiles.
Fetch and display a list of products from an API using (App Router).
1. Implement the Products Page (app/products/page.js
)
export default async function ProductsPage() {
const response = await fetch("https://fakestoreapi.com/products", {
next: { revalidate: 60 }, // Enable ISR (Incremental Static Regeneration)
});
const products = await response.json();
return (
<div>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
<h3>{product.title}</h3>
<p>Price: ${product.price}</p>
<img src={product.image} alt={product.title} width="100" />
</li>
))}
</ul>
</div>
);
}
- Note:
next: { revalidate: 60 }
in the fetch request achieves ISR (revalidates every 60 seconds) in the App Router.
2. Run the Application
npm run dev
- Visit
http://localhost:3000/products
to see the server-rendered product list.
3. How It Works in App Router
- Data fetching happens inside Server Components (no
getStaticProps()
needed). - Automatic static optimization + ISR using
revalidate
in fetch options.
Conclusion
This implementation fetches product data inside a Server Component in the App Router and uses Incremental Static Regeneration for fast performance and SEO benefits.
Fetch and display user details from an API using (App Router).
1. Create the User Page (app/user/page.js
)
export default async function UserPage() {
const response = await fetch("https://jsonplaceholder.typicode.com/users/1", {
cache: "no-store", // Disable caching to fetch fresh data on every request
});
const user = await response.json();
return (
<div>
<h1>User Details</h1>
<p><strong>Name:</strong> {user.name}</p>
<p><strong>Email:</strong> {user.email}</p>
<p><strong>Phone:</strong> {user.phone}</p>
<p><strong>Company:</strong> {user.company.name}</p>
<p><strong>Website:</strong> {user.website}</p>
</div>
);
}
- Important:
cache: "no-store"
ensures fresh data is fetched on every request (equivalent to SSR behavior).
2. Run the Application
npm run dev
- Visit
http://localhost:3000/user
to see live user details fetched dynamically.
3. How It Works in App Router
- No need for
getServerSideProps()
. - Server Components fetch and render data at request time.
- Fresh data without rebuilding or revalidating manually.
Conclusion
This setup uses Server Components and fetch with no-store
caching to simulate Server-Side Rendering in the Next.js App Router, providing dynamic, always-fresh content.
Implement Incremental Static Regeneration (ISR) in the App Router to refresh news articles every 60 seconds
1. Create the News Page (app/news/page.js
)
export default async function NewsPage() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5", {
next: { revalidate: 60 }, // Enable ISR: revalidate every 60 seconds
});
const newsArticles = await response.json();
return (
<div>
<h1>Latest News</h1>
<ul>
{newsArticles.map((article) => (
<li key={article.id}>
<h3>{article.title}</h3>
<p>{article.body}</p>
</li>
))}
</ul>
<p><em>Data refreshes every 60 seconds.</em></p>
</div>
);
}
2. Run the Application
npm run dev
- Visit
http://localhost:3000/news
. - The page will regenerate with fresh data every 60 seconds in the background.
3. How ISR Works in the App Router
- No
getStaticProps()
anymore. fetch(..., { next: { revalidate: 60 } })
enables Incremental Static Regeneration.- Next.js serves cached HTML, and revalidates it transparently after 60 seconds when a new request comes in.
Conclusion
In the App Router, ISR is handled by adding revalidate
inside the fetch
call, making it simpler and more flexible than the old Pages Router approach.
Fetch and display client-side data using useEffect() and fetch()(App Router Version).
1. Implement the Client-Side Data Fetching Page (app/client-data/page.jsx
)
'use client'; // Mark as Client Component
import { useEffect, useState } from "react";
export default function ClientData() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5");
const data = await response.json();
setPosts(data);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
return (
<div>
<h1>Client-Side Data Fetching</h1>
{loading ? <p>Loading...</p> : (
<ul>
{posts.map((post) => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
)}
</div>
);
}
2. Run the Application
npm run dev
- Visit
http://localhost:3000/client-data
→ See the posts fetched dynamically on the client side.
3. Key Points for App Router
- Add
'use client';
at the very top → Marks it as a Client Component. - Otherwise, the behavior (useEffect + fetch) stays the same.
Conclusion
In the App Router, client-side data fetching is still valid but needs 'use client'
at the top of the file or component.
This ensures dynamic, real-time content without server-side prefetching, but it’s less SEO-friendly compared to server fetching.
Create a Next.js API route (/api/hello) that returns a JSON message (App Router Version).
1. Create the API Route (app/api/hello/route.js
)
Inside the app/api/hello/
directory, create a file named route.js
:
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "Hello from Next.js API!" });
}
2. Run the Application
Start the Next.js development server:
npm run dev
- Visit
http://localhost:3000/api/hello
→ You should see:
{
"message": "Hello from Next.js API!"
}
3. How This Works in App Router
- You define HTTP methods (
GET
,POST
, etc.) as exported functions. - Use
Response
objects (Web API standard), notres.status().json()
like before. - This aligns with modern serverless API standards.
4. Expanding the API Route (Optional)
You can also handle POST requests in the App Router:
import { NextResponse } from "next/server";
export async function POST(request) {
const body = await request.json();
return NextResponse.json(
{ message: `Received POST with data: ${body.name}` },
{ status: 201 }
);
}
- Now your API supports both GET and POST!
Conclusion
In the App Router, API routes are created in app/api/
using exported HTTP methods like GET
and POST
.
They follow standard Web APIs and are easier to optimize for serverless environments and Edge functions.
Implement an API route that handles user authentication using JWT (App Router Version)
1. Install Dependency
npm install jsonwebtoken
2. Create the Login API Route (app/api/auth/login/route.js
)
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
const SECRET_KEY = process.env.JWT_SECRET || "your-secret-key";
export async function POST(request) {
const { username, password } = await request.json();
// Mock user data
const user = { id: 1, username: "admin", password: "password123" };
if (username === user.username && password === user.password) {
const token = jwt.sign({ userId: user.id, username: user.username }, SECRET_KEY, { expiresIn: "1h" });
return NextResponse.json({ token }, { status: 200 });
} else {
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
}
}
3. Create the Auth Verification Route (app/api/auth/me/route.js
)
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
const SECRET_KEY = process.env.JWT_SECRET || "your-secret-key";
export async function GET(request) {
const authHeader = request.headers.get("authorization");
const token = authHeader?.split(" ")[1];
if (!token) {
return NextResponse.json({ error: "Token missing" }, { status: 401 });
}
try {
const decoded = jwt.verify(token, SECRET_KEY);
return NextResponse.json({ user: decoded }, { status: 200 });
} catch (error) {
return NextResponse.json({ error: "Invalid token" }, { status: 401 });
}
}
4. Testing the API
- Login:
POST /api/auth/login
with JSON{ "username": "admin", "password": "password123" }
- Verify Auth:
GET /api/auth/me
with headerAuthorization: Bearer <token>
Conclusion
In the App Router, API routes live under app/api/
and you must use exported HTTP functions (POST
, GET
) instead of Express-style handlers.
Build an API route that fetches data from a third-party API and caches it using Cache-Control(App Router Version).
1. Create the API Route (app/api/products/route.js
)
import { NextResponse } from "next/server";
export async function GET() {
try {
const response = await fetch("https://fakestoreapi.com/products");
const products = await response.json();
return NextResponse.json(products, {
status: 200,
headers: {
"Cache-Control": "s-maxage=300, stale-while-revalidate=60",
},
});
} catch (error) {
return NextResponse.json(
{ error: "Failed to fetch data" },
{ status: 500 }
);
}
}
2. How Caching Works Here
s-maxage=300
→ Caches the response at the CDN (e.g., Vercel Edge Network) for 5 minutes.stale-while-revalidate=60
→ Allows serving stale data for 1 minute while refreshing the cache in the background.
3. Testing
Visit:
http://localhost:3000/api/products
You should get product data and see cache headers in the response.
Check with:
curl -I http://localhost:3000/api/products
Result:
Cache-Control: s-maxage=300, stale-while-revalidate=60
Conclusion
Using the App Router approach with GET
functions makes your API routes Edge-compatible, easier to cache, and more future-proof.
This example fetches third-party data and uses caching to optimize performance and reduce API server load.
Create an API route that processes form submissions and stores them in a database (App Router Version).
1. Create the API Route (app/api/form/route.js
)
import { NextResponse } from "next/server";
import connectDB from "@/lib/mongodb";
import Form from "@/lib/models/Form";
export async function POST(req) {
await connectDB();
try {
const { name, email, message } = await req.json();
if (!name || !email || !message) {
return NextResponse.json({ error: "All fields are required" }, { status: 400 });
}
const formEntry = new Form({ name, email, message });
await formEntry.save();
return NextResponse.json({ message: "Form submitted successfully" }, { status: 201 });
} catch (error) {
return NextResponse.json({ error: "Database error" }, { status: 500 });
}
}
2. Adjust the FormComponent
No changes are needed to the form itself. It will still send a POST
request to /api/form
.
Form submission code stays the same:
// app/api/form/route.js
import { NextResponse } from "next/server";
export async function POST(request) {
const form = await request.json();
// Your form processing logic here
return NextResponse.json({ message: "Form submitted successfully!" }, { status: 201 });
}
3. How It Works
- The App Router API endpoint (
app/api/form/route.js
) listens for POST requests. - The request body is read using
await req.json()
. - Form data is saved to MongoDB using Mongoose.
- The server responds with either a success message or an error message.
Conclusion
By migrating the API route to the App Router format, you ensure the code is future-proof, edge-compatible, and aligned with Next.js best practices.
This example still handles form submission and database storage, but using the newer structure.
Implement a global theme switcher using React Context API.
This task involves creating a global theme switcher using React Context API, allowing users to toggle between light and dark themes across all pages.
1. Create a Theme Context (context/ThemeContext.js
)
Create a context/
folder and add a ThemeContext.js file.
Project Structure
/context
ThemeContext.js
/pages
_app.js
index.js
/components
ThemeToggle.js
/styles
globals.css
context/ThemeContext.js
import { createContext, useContext, useState, useEffect } from "react";
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
// Load theme from localStorage (if exists)
useEffect(() => {
const savedTheme = localStorage.getItem("theme") || "light";
setTheme(savedTheme);
}, []);
// Toggle theme and save to localStorage
const toggleTheme = () => {
const newTheme = theme === "light" ? "dark" : "light";
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div className={theme}>{children}</div>
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
2. Wrap _app.js
with the Theme Provider
Modify _app.js
to wrap the entire app with ThemeProvider
.
pages/_app.js
import { ThemeProvider } from "../context/ThemeContext";
import "../styles/globals.css";
export default function MyApp({ Component, pageProps }) {
return (
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
);
}
3. Create a Theme Toggle Button (components/ThemeToggle.js
)
This button will switch between light and dark themes.
components/ThemeToggle.js
import { useTheme } from "../context/ThemeContext";
export default function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme} style={{ margin: "10px", padding: "5px 10px" }}>
Switch to {theme === "light" ? "Dark" : "Light"} Mode
</button>
);
}
4. Update the Home Page (pages/index.js
)
Add the theme toggle button to the homepage.
pages/index.js
import ThemeToggle from "../components/ThemeToggle";
export default function Home() {
return (
<div>
<h1>Next.js Theme Switcher</h1>
<ThemeToggle />
<p>Click the button above to switch themes.</p>
</div>
);
}
5. Define Theme Styles in Global CSS (styles/globals.css
)
Add styles for light and dark themes.
styles/globals.css
body {
transition: background 0.3s, color 0.3s;
}
.light {
background-color: white;
color: black;
}
.dark {
background-color: #1a1a1a;
color: white;
}
6. Run the Application
Start the Next.js server:
npm run dev
- Visit
http://localhost:3000/
→ Click the theme switch button to toggle between light and dark modes.
Conclusion
This implementation uses React Context API to manage the global theme state, persists user preference in localStorage
, and applies light/dark mode styles dynamically.
Use Redux Toolkit to manage a shopping cart state across multiple pages.
1. Move Pages to app/
Project structure:
/app
/products
page.js
/cart
page.js
layout.js
/components
Navbar.js
/store.js
2. Create the Redux Store (store.js
)
// store.js
import { configureStore, createSlice } from "@reduxjs/toolkit";
// Cart slice
const cartSlice = createSlice({
name: "cart",
initialState: {
items: [],
},
reducers: {
addItem: (state, action) => {
const existing = state.items.find((item) => item.id === action.payload.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({ ...action.payload, quantity: 1 });
}
},
removeItem: (state, action) => {
state.items = state.items.filter((item) => item.id !== action.payload);
},
clearCart: (state) => {
state.items = [];
},
},
});
export const { addItem, removeItem, clearCart } = cartSlice.actions;
export const store = configureStore({
reducer: {
cart: cartSlice.reducer,
},
});
3. Wrap Redux Store in app/layout.js
// app/layout.js
import { Provider } from "react-redux";
import { store } from "../store";
export const metadata = {
title: "Shopping App",
description: "Simple shopping cart with Redux Toolkit",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Provider store={store}>{children}</Provider>
</body>
</html>
);
}
4. Update Pages to app/products/page.js
and app/cart/page.js
app/products/page.js
"use client";
import Navbar from "@/components/Navbar";
import { useDispatch } from "react-redux";
import { addItem } from "@/store";
const products = [
{ id: 1, name: "Laptop", price: 1200 },
{ id: 2, name: "Phone", price: 800 },
{ id: 3, name: "Headphones", price: 150 },
];
export default function ProductsPage() {
const dispatch = useDispatch();
return (
<div>
<Navbar />
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
<h3>{product.name} - ${product.price}</h3>
<button onClick={() => dispatch(addItem(product))}>Add to Cart</button>
</li>
))}
</ul>
</div>
);
}
app/cart/page.js
"use client";
import Navbar from "@/components/Navbar";
import { useSelector, useDispatch } from "react-redux";
import { removeItem, clearCart } from "@/store";
export default function CartPage() {
const cart = useSelector((state) => state.cart.items);
const dispatch = useDispatch();
return (
<div>
<Navbar />
<h1>Shopping Cart</h1>
{cart.length === 0 ? (
<p>Your cart is empty.</p>
) : (
<ul>
{cart.map((item) => (
<li key={item.id}>
<h3>{item.name} - ${item.price} x {item.quantity}</h3>
<button onClick={() => dispatch(removeItem(item.id))}>Remove</button>
</li>
))}
</ul>
)}
{cart.length > 0 && <button onClick={() => dispatch(clearCart())}>Clear Cart</button>}
</div>
);
}
5. Use the Same Navbar.js
No changes needed, just use App Router paths for imports (e.g., @/components/Navbar
).
6. Run the App
npm run dev
Visit:
http://localhost:3000/products
http://localhost:3000/cart
Implement Zustand for managing UI state (e.g., modal visibility).
This task involves using Zustand to manage global UI state, specifically toggling a modal dialog across multiple pages.
1. Move Pages to app/
Structure:
/app
/about
page.js
page.js
layout.js
/components
Modal.js
ModalToggle.js
/store
modalStore.js
2. Wrap Zustand in app/layout.js
You don’t need extra wrappers for Zustand (it works outside Context), but layout ensures consistency:
import "./globals.css"; // Optional styling
import Modal from "@/components/Modal";
export const metadata = {
title: "Zustand Modal Example",
description: "Next.js App Router + Zustand Modal Example",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Modal />
</body>
</html>
);
}
- Modal is placed outside the page but inside the layout, so it is available globally across pages.
3. Pages Use Only the Toggle
app/page.js
import ModalToggle from "@/components/ModalToggle";
export default function HomePage() {
return (
<div>
<h1>Home Page</h1>
<ModalToggle />
</div>
);
}
app/about/page.js
import ModalToggle from "@/components/ModalToggle";
export default function AboutPage() {
return (
<div>
<h1>About Page</h1>
<ModalToggle />
</div>
);
}
4. No change needed for store/modalStore.js
and components/Modal.js
, components/ModalToggle.js
.
Create Zustand Store and Components
✅ These files are used to control modal visibility globally across pages. Let’s include their implementations:
store/modalStore.js
// store/modalStore.js
import { create } from "zustand";
const useModalStore = create((set) => ({
isOpen: false,
openModal: () => set({ isOpen: true }),
closeModal: () => set({ isOpen: false }),
}));
export default useModalStore;
components/Modal.js
// components/Modal.js
"use client";
import useModalStore from "../store/modalStore";
export default function Modal() {
const { isOpen, closeModal } = useModalStore();
if (!isOpen) return null;
return (
<div style={styles.overlay}>
<div style={styles.modal}>
<h2>Modal Title</h2>
<p>This is a modal dialog.</p>
<button onClick={closeModal}>Close</button>
</div>
</div>
);
}
const styles = {
overlay: {
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.5)",
display: "flex",
justifyContent: "center",
alignItems: "center",
},
modal: {
background: "white",
padding: "20px",
borderRadius: "8px",
textAlign: "center",
},
};
components/ModalToggle.js
// components/ModalToggle.js
"use client";
import useModalStore from "../store/modalStore";
export default function ModalToggle() {
const { openModal } = useModalStore();
return <button onClick={openModal}>Open Modal</button>;
}
✅ Zustand is App Router–safe natively.
5. Run the App
npm run dev
- Visit
/
- Visit
/about
- Open the modal anywhere → Zustand keeps the state global.
Secure a Next.js page by implementing middleware-based authentication.
1. Project Structure
/app
/login
page.js
/dashboard
page.js
/middleware.js
2. middleware.js
✅ Middleware code stays almost the same — just change matcher
if you want:
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
const SECRET_KEY = process.env.JWT_SECRET || "your-secret-key";
export function middleware(req) {
const token = req.cookies.get("authToken");
if (!token) {
return NextResponse.redirect(new URL("/login", req.url));
}
try {
jwt.verify(token, SECRET_KEY);
return NextResponse.next();
} catch (error) {
return NextResponse.redirect(new URL("/login", req.url));
}
}
export const config = {
matcher: ["/dashboard"], // Protect the /dashboard route
};
3. app/login/page.js
// app/api/auth/route.js
import { NextResponse } from "next/server";
export async function POST(request) {
const { username, password } = await request.json();
if (username === "admin" && password === "password") {
return NextResponse.json({ token: "secure-token" }, { status: 200 });
}
return NextResponse.json({ message: "Invalid credentials" }, { status: 401 });
}
// the component
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const [form, setForm] = useState({ username: "", password: "" });
const [error, setError] = useState(null);
const router = useRouter();
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setError(null);
const response = await fetch("/api/auth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (response.ok) {
router.push("/dashboard");
} else {
setError("Invalid credentials");
}
};
return (
<div>
<h1>Login</h1>
{error && <p style={{ color: "red" }}>{error}</p>}
<form onSubmit={handleSubmit}>
<input type="text" name="username" placeholder="Username" onChange={handleChange} required />
<input type="password" name="password" placeholder="Password" onChange={handleChange} required />
<button type="submit">Login</button>
</form>
</div>
);
}
4. app/dashboard/page.js
export default function DashboardPage() {
return (
<div>
<h1>Welcome to Dashboard</h1>
<p>Only authenticated users can access this page.</p>
</div>
);
}
5. pages/api/auth.js
✅ API routes still live under pages/api/
even when using the App Router. No change needed.
Summary for this task:
- Middleware works the same in both Pages Router and App Router.
- Pages like
/login
and/dashboard
should move underapp/
to match Next.js 13+ best practices.
Implement Google OAuth authentication using NextAuth.js.
This task involves setting up Google OAuth authentication in Next.js using NextAuth.js, allowing users to log in with their Google accounts.
1. Install NextAuth.js
Run the following command to install NextAuth:
npm install next-auth
2. Implement a NextAuth API Route (pages/api/auth/[...nextauth].js
)
NextAuth handles authentication through an API route at /api/auth/
.
pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async session({ session, token }) {
session.user.id = token.sub;
return session;
},
},
});
How It Works:
- Configures Google as an authentication provider.
- Uses environment variables for Google Client ID and Secret.
- Stores user session data.
3. Get Google OAuth Credentials
- Go to Google Cloud Console.
- Create a new project.
- Navigate to APIs & Services → Credentials.
- Click Create Credentials → OAuth Client ID.
- Under Application type, choose Web application.
- Add the following Authorized Redirect URIs:
http://localhost:3000/api/auth/callback/google
- Copy the Client ID and Client Secret.
4. Set Up Environment Variables (.env.local
)
Create a .env.local
file in the project root and add:
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
NEXTAUTH_SECRET=your-random-secret-key
NEXTAUTH_URL=http://localhost:3000
Restart the server after adding environment variables.
5. Implement a Login Page (pages/login.js
)
This page allows users to log in and log out.
pages/login.js
import { signIn, signOut, useSession } from "next-auth/react";
export default function Login() {
const { data: session } = useSession();
return (
<div>
{session ? (
<>
<h1>Welcome, {session.user.name}!</h1>
<img src={session.user.image} alt="User Avatar" width="50" />
<p>Email: {session.user.email}</p>
<button onClick={() => signOut()}>Sign Out</button>
</>
) : (
<>
<h1>Please Log In</h1>
<button onClick={() => signIn("google")}>Sign in with Google</button>
</>
)}
</div>
);
}
How It Works:
- If the user is logged in, it displays their name, email, and profile picture.
- If the user is not logged in, it shows a Sign in with Google button.
6. Protect a Page Using Authentication (pages/protected.js
)
This page is only accessible if the user is authenticated.
pages/protected.js
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
export default function ProtectedPage() {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === "unauthenticated") {
router.push("/login");
}
}, [status, router]);
if (status === "loading") {
return <p>Loading...</p>;
}
return (
<div>
<h1>Protected Page</h1>
<p>Welcome, {session?.user?.name}! You have access to this page.</p>
</div>
);
}
How It Works:
- Redirects unauthenticated users to the login page.
- Displays content only if the user is logged in.
7. Wrap the App with NextAuth Provider (pages/_app.js
)
Modify _app.js
to wrap the entire app with the NextAuth provider.
pages/_app.js
import { SessionProvider } from "next-auth/react";
export default function MyApp({ Component, pageProps }) {
return (
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
);
}
Why?
- Ensures that authentication state persists across pages.
8. Run the Application
Start the Next.js server:
npm run dev
- Visit
http://localhost:3000/login
→ Click Sign in with Google. - Log in with your Google account.
- Visit
http://localhost:3000/protected
→ You should be authenticated.
Conclusion
This implementation integrates Google OAuth authentication in Next.js using NextAuth.js, enabling secure user login and protected routes with minimal setup.
Optimize images using next/image and enable lazy loading.
This task involves optimizing images in Next.js using the next/image
component, which provides automatic lazy loading, resizing, and format conversion (WebP, AVIF) for improved performance.
1. Use next/image
in a Component (components/OptimizedImage.js
)
Create a reusable optimized image component.
components/OptimizedImage.js
import Image from "next/image";
export default function OptimizedImage() {
return (
<div>
<h2>Optimized Image Example</h2>
<Image
src="https://via.placeholder.com/800x500"
alt="Optimized Example"
width={800}
height={500}
priority={false}
/>
</div>
);
}
Why Use next/image
Instead of <img>
?
- Lazy loading by default (loads images only when visible).
- Automatic format conversion (e.g., WebP, AVIF).
- Optimized sizes for different screen resolutions.
2. Use the Optimized Image Component in a Page (pages/index.js
)
Modify the home page to include the optimized image.
pages/index.js
import OptimizedImage from "../components/OptimizedImage";
export default function Home() {
return (
<div>
<h1>Next.js Image Optimization</h1>
<OptimizedImage />
</div>
);
}
3. Enable Remote Image Domains (If Using External Images)
If your images are hosted on a remote domain, Next.js blocks them by default.
To allow external images, update next.config.js
:
next.config.js
module.exports = {
images: {
domains: ["via.placeholder.com"], // Add allowed image domains
},
};
Restart the server for changes to take effect:
npm run dev
4. Enable Image Lazy Loading (Default in next/image
)
Lazy loading is enabled by default in Next.js. However, you can control it using the priority
prop:
<Image
src="https://via.placeholder.com/800x500"
alt="Example"
width={800}
height={500}
priority={false} // Enable lazy loading
/>
priority={true}
loads the image immediately (e.g., for above-the-fold images).
5. Use Different Layout Options for Better Performance
Next.js provides different layout modes for responsive images:
<Image
src="https://via.placeholder.com/800x500"
alt="Responsive Example"
layout="intrinsic" // Keeps aspect ratio
width={800}
height={500}
/>
Layout Mode | Behavior |
intrinsic |
Keeps original aspect ratio |
responsive |
Adapts width based on screen size |
fill |
Fills parent container (useful for backgrounds) |
6. Run the Application
Start the Next.js development server:
npm run dev
- Visit
http://localhost:3000/
→ The image loads efficiently with lazy loading enabled.
Conclusion
This implementation optimizes images using next/image
, enabling lazy loading, responsive sizes, and automatic format conversion, ensuring better performance and faster page loads.
Implement route prefetching for faster navigation between pages.
This task involves using Next.js route prefetching to improve navigation speed by preloading pages in the background before the user clicks a link.
1. Use next/link
for Automatic Prefetching
Next.js automatically prefetches pages linked with next/link
when they appear in the viewport.
components/Navbar.js
import Link from "next/link";
export default function Navbar() {
return (
<nav>
<ul>
<li><Link href="/" prefetch={true}>Home</Link></li>
<li><Link href="/about" prefetch={true}>About</Link></li>
<li><Link href="/contact" prefetch={true}>Contact</Link></li>
</ul>
</nav>
);
}
How It Works:
- When a
Link
is in the viewport, Next.js preloads the target page. - Prefetching happens only in production mode.
2. Disable Prefetching for Less Important Pages
If prefetching isn’t needed for certain links, disable it using prefetch={false}
.
<Link href="/privacy-policy" prefetch={false}>Privacy Policy</Link>
Why?
- Prevents unnecessary bandwidth usage for rarely visited pages.
3. Manually Prefetch Routes Using next/router
You can manually prefetch routes when a user hovers over a button.
components/PrefetchButton.js
import { useRouter } from "next/router";
export default function PrefetchButton() {
const router = useRouter();
return (
<button
onMouseEnter={() => router.prefetch("/services")}
onClick={() => router.push("/services")}
>
Go to Services (Prefetched)
</button>
);
}
How It Works:
- Prefetches
/services
when the user hovers over the button. - Navigating is instant since the page is already loaded.
4. Test Route Prefetching
Start the Next.js development server:
npm run dev
- Open DevTools → Network Tab → Disable Cache.
- Hover over a prefetched link.
- Observe that Next.js preloads the page before clicking.
5. Optimized Prefetching Strategy
Scenario | Prefetch Strategy |
Common pages (Home, About) | ✅ Keep prefetch={true} (default) |
Rarely visited pages (Privacy Policy) | ❌ Disable prefetching |
Pages accessed via user action (e.g., dropdown) | 🎯 Manually prefetch on hover |
Conclusion
This implementation optimizes Next.js route prefetching, ensuring instant navigation for common pages while preventing unnecessary data fetching.
Reduce JavaScript bundle size by dynamically importing a large component using next/dynamic().
This task involves using Next.js dynamic imports (next/dynamic
) to lazy load large components, reducing initial JavaScript bundle size and improving performance.
1. Install next/dynamic
(Included in Next.js by Default)
No installation required—next/dynamic
is built into Next.js.
2. Create a Large Component (components/HeavyComponent.js
)
This represents a large component that should only load when needed.
components/HeavyComponent.js
export default function HeavyComponent() {
return (
<div>
<h2>Heavy Component Loaded</h2>
<p>This component is dynamically imported to reduce the initial JavaScript bundle size.</p>
</div>
);
}
Why Lazy Load?
- Avoids loading unnecessary JavaScript upfront.
- Improves page load times and First Contentful Paint (FCP).
3. Use Dynamic Import for Lazy Loading in a Page (pages/index.js
)
Modify the home page to dynamically import HeavyComponent
.
pages/index.js
import dynamic from "next/dynamic";
import { useState } from "react";
// Dynamically import the component (not loaded initially)
const HeavyComponent = dynamic(() => import("../components/HeavyComponent"), {
loading: () => <p>Loading...</p>, // Optional: Show a fallback while loading
ssr: false, // Ensure this component is only loaded on the client
});
export default function Home() {
const [showComponent, setShowComponent] = useState(false);
return (
<div>
<h1>Next.js Dynamic Import Example</h1>
<button onClick={() => setShowComponent(true)}>Load Heavy Component</button>
{showComponent && <HeavyComponent />}
</div>
);
}
How This Works:
dynamic(() => import("..."))
→ Loads the component only when needed.ssr: false
→ Ensures the component is not preloaded during SSR.loading: () => <p>Loading...</p>
→ Displays a placeholder while loading.
4. Run the Application and Analyze Bundle Size
Start the Next.js server:
npm run dev
- Visit
http://localhost:3000/
. - Open DevTools → Network Tab → JavaScript.
- Click “Load Heavy Component” and observe that the component loads dynamically only when clicked.
5. Verify Bundle Size Reduction
Run the Next.js build analysis to check bundle optimization:
npm run build && next analyze
- Observe that
HeavyComponent.js
is not included in the initial bundle.
Conclusion
This implementation reduces the JavaScript bundle size by lazy loading large components using next/dynamic()
, ensuring better page load performance while keeping essential scripts lightweight.
Implement geo-based content delivery using Next.js Middleware (e.g., detect user location and serve region-specific content).
This task involves using Next.js Middleware to detect the user’s location based on request headers and serve region-specific content.
1. Create the Middleware File (middleware.js
)
Next.js Middleware runs before the request reaches the page, allowing us to detect the user’s location and modify the response.
Project Structure
/middleware.js
/pages
index.js
us.js
fr.js
de.js
middleware.js
import { NextResponse } from "next/server";
export function middleware(req) {
const country = req.geo?.country || "US"; // Default to US if no data
const url = req.nextUrl.clone();
if (country === "FR") {
url.pathname = "/fr"; // Redirect French users
} else if (country === "DE") {
url.pathname = "/de"; // Redirect German users
} else {
url.pathname = "/us"; // Default to US page
}
return NextResponse.rewrite(url); // Rewrite without changing the visible URL
}
// Apply middleware to all requests
export const config = {
matcher: ["/"],
};
How It Works:
- Detects user location from
req.geo.country
(available on Vercel or Cloudflare). - Redirects users based on their country.
- Uses NextResponse.rewrite(), so the URL stays the same, but the page content changes.
2. Create Region-Specific Pages
Each page serves localized content based on the detected region.
pages/us.js
(For US Users)
export default function USPage() {
return (
<div>
<h1>Welcome, US User!</h1>
<p>This content is tailored for users in the United States.</p>
</div>
);
}
pages/fr.js
(For French Users)
export default function FRPage() {
return (
<div>
<h1>Bienvenue, utilisateur français !</h1>
<p>Ce contenu est destiné aux utilisateurs en France.</p>
</div>
);
}
pages/de.js
(For German Users)
export default function DEPage() {
return (
<div>
<h1>Willkommen, deutscher Benutzer!</h1>
<p>Dieser Inhalt ist für Benutzer in Deutschland bestimmt.</p>
</div>
);
}
Why This Works:
- Users visiting
/
are automatically rewritten to their region-specific page. - The URL remains
/
, but different content is displayed.
3. Deploy to Vercel (Required for Geo-Detection)
Since req.geo
only works on Edge Networks like Vercel, deploy the app:
vercel deploy
Vercel Auto-Detects User Location
- In Vercel logs, you’ll see
req.geo.country
returning real user locations.
4. Run the Application Locally (Without Geo-Detection)
Since req.geo
is not available locally, manually test it by visiting:
http://localhost:3000/us
→ US contenthttp://localhost:3000/fr
→ French contenthttp://localhost:3000/de
→ German content
Conclusion
This implementation detects user location using Next.js Middleware and serves region-specific content dynamically based on their country.
Perfect for localization, region-based promotions, or GDPR compliance.
Use next/head to set dynamic meta tags for a blog post page.
This task involves using next/head
to dynamically set SEO-friendly meta tags for a blog post page, improving search engine visibility and social media sharing.
1. Use next/head
in a Dynamic Blog Page (pages/blog/[id].js
)
This page dynamically fetches blog posts and sets unique meta tags based on the post content.
Project Structure
/pages
/blog
[id].js
index.js
/components
SEO.js
pages/blog/[id].js
import Head from "next/head";
import { useRouter } from "next/router";
// Simulated blog data (replace with a real API call)
const blogPosts = [
{ id: "1", title: "Next.js SEO Guide", description: "Learn how to optimize Next.js for SEO.", author: "John Doe" },
{ id: "2", title: "Server-Side Rendering vs. Static Generation", description: "Understand SSR and SSG differences.", author: "Jane Smith" },
];
export async function getStaticPaths() {
const paths = blogPosts.map((post) => ({ params: { id: post.id } }));
return { paths, fallback: false };
}
export async function getStaticProps({ params }) {
const post = blogPosts.find((p) => p.id === params.id);
return { props: { post } };
}
export default function BlogPost({ post }) {
const router = useRouter();
const url = `https://myblog.com${router.asPath}`;
return (
<div>
<Head>
<title>{post.title} | My Blog</title>
<meta name="description" content={post.description} />
<meta name="author" content={post.author} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.description} />
<meta property="og:url" content={url} />
<meta property="og:type" content="article" />
<meta name="twitter:card" content="summary_large_image" />
</Head>
<h1>{post.title}</h1>
<p>{post.description}</p>
<p><strong>Author:</strong> {post.author}</p>
</div>
);
}
2. Create a Blog Listing Page (pages/blog/index.js
)
import Link from "next/link";
const blogPosts = [
{ id: "1", title: "Next.js SEO Guide" },
{ id: "2", title: "Server-Side Rendering vs. Static Generation" },
];
export default function Blog() {
return (
<div>
<h1>Blog Posts</h1>
<ul>
{blogPosts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
);
}
Users can click a blog post to navigate to /blog/[id]
.
3. Run the Application
Start the Next.js development server:
npm run dev
- Visit
http://localhost:3000/blog
→ See the list of blog posts. - Click a post → Dynamic meta tags update for each post.
4. Verify Meta Tags (SEO & Social Media Preview)
- Open the Page Source (
Ctrl + U
) → Check<title>
and<meta>
tags. - Use Facebook Sharing Debugger or Twitter Card Validator to test social previews.
Conclusion
This implementation uses next/head
to dynamically set SEO-friendly meta tags for blog posts, improving search rankings and social media previews.
Important Note:
This example uses the Pages Router (pages/
directory) and next/head
.
If you are using the App Router (app/
directory), you should use the generateMetadata()
function inside app/blog/[id]/page.js
instead of next/head
to set dynamic meta tags.
Example with App Router:
// app/blog/[id]/page.js
export async function generateMetadata({ params }) {
const post = await fetchPost(params.id);
return {
title: `${post.title} | My Blog`,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
url: `https://myblog.com/blog/${params.id}`,
type: "article",
},
twitter: {
card: "summary_large_image",
},
};
}
export default function BlogPost({ params }) {
// Render the blog post content here
}
Implement Open Graph meta tags to enhance social media previews.
This task involves using Open Graph (OG) meta tags to improve social media previews when sharing a Next.js page on platforms like Facebook, Twitter, and LinkedIn.
1. Create an SEO Component (components/SEO.js
)
Create a reusable component to dynamically set Open Graph meta tags.
Project Structure
/components
SEO.js
/pages
index.js
blog.js
components/SEO.js
import Head from "next/head";
import { useRouter } from "next/router";
export default function SEO({ title, description, image }) {
const router = useRouter();
const siteUrl = "https://mywebsite.com"; // Replace with your domain
const pageUrl = `${siteUrl}${router.asPath}`;
const ogImage = image || `${siteUrl}/default-image.jpg`; // Fallback image if none provided
return (
<Head>
{/* Standard Meta Tags */}
<title>{title} | My Website</title>
<meta name="description" content={description} />
{/* Open Graph Meta Tags for Facebook, LinkedIn */}
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:url" content={pageUrl} />
<meta property="og:type" content="website" />
{/* Twitter Card Meta Tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImage} />
</Head>
);
}
2. Use the SEO Component on Pages (pages/index.js
)
Add the SEO component to a page to dynamically inject meta tags.
pages/index.js
import SEO from "../components/SEO";
export default function Home() {
return (
<div>
<SEO
title="Next.js Open Graph Example"
description="Learn how to implement Open Graph meta tags in Next.js for better social media previews."
image="https://via.placeholder.com/1200x630" // Replace with your real OG image
/>
<h1>Welcome to My Website</h1>
<p>This page is optimized for social media sharing.</p>
</div>
);
}
✅ What Happens?
- The Open Graph tags (
og:title
,og:description
,og:image
) and Twitter tags are set dynamically. - When users share your page, social media platforms generate the correct rich preview.
3. Test Your Open Graph Tags
- View Page Source (
Ctrl + U
) → Look for<meta>
tags. - Facebook Debugger: Facebook Sharing Debugger
- Twitter Card Validator: Twitter Card Validator
You may need to “Scrape Again” in Facebook Debugger after updates.
Conclusion
This implementation uses Open Graph meta tags to enhance social media previews in Next.js, helping to boost engagement, shares, and visibility when your links are shared.
Important Note:
This example uses the Pages Router (pages/
directory) with next/head
.
If you are using the App Router (app/
directory), you should use the generateMetadata()
function instead to set Open Graph and Twitter meta tags dynamically.
Example with App Router:
// app/page.js or app/blog/page.js
export const metadata = {
title: "Next.js Open Graph Example | My Website",
description: "Learn how to implement Open Graph meta tags in Next.js for better social media previews.",
openGraph: {
title: "Next.js Open Graph Example",
description: "Learn how to implement Open Graph meta tags in Next.js for better social media previews.",
images: ["https://via.placeholder.com/1200x630"],
url: "https://mywebsite.com",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Next.js Open Graph Example",
description: "Learn how to implement Open Graph meta tags in Next.js for better social media previews.",
images: ["https://via.placeholder.com/1200x630"],
},
};
Connect a Next.js app to MongoDB using Mongoose and display user data.
This task involves:
- Connecting Next.js to MongoDB using Mongoose.
- Fetching user data from MongoDB via an API route.
- Displaying users on a Next.js page.
1. Install Mongoose
npm install mongoose dotenv
2. Set Up MongoDB Connection (lib/mongodb.js
)
Create a lib/
folder and add the MongoDB connection utility.
Project Structure
/lib
mongodb.js
models
User.js
/pages/api
users.js
/pages
users.js
lib/mongodb.js
import mongoose from "mongoose";
const MONGODB_URI = process.env.MONGODB_URI;
if (!MONGODB_URI) {
throw new Error("Please define the MONGODB_URI environment variable");
}
let cached = global.mongoose;
if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
async function connectDB() {
if (cached.conn) return cached.conn;
if (!cached.promise) {
cached.promise = mongoose.connect(MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
}
cached.conn = await cached.promise;
return cached.conn;
}
export default connectDB;
✅ Why?
- Avoids multiple database connections during hot reload in development.
- Caches the database connection.
3. Create a Mongoose User Model (lib/models/User.js
)
lib/models/User.js
import mongoose from "mongoose";
const UserSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
});
export default mongoose.models.User || mongoose.model("User", UserSchema);
✅ What This Does:
- Defines a User with
name
andemail
. - Ensures email uniqueness.
4. Create an API Route to Fetch Users (pages/api/users.js
)
pages/api/users.js
import connectDB from "../../lib/mongodb";
import User from "../../lib/models/User";
export default async function handler(req, res) {
await connectDB();
if (req.method === "GET") {
try {
const users = await User.find({});
return res.status(200).json(users);
} catch (error) {
return res.status(500).json({ error: "Failed to fetch users" });
}
} else {
return res.status(405).json({ error: "Method Not Allowed" });
}
}
✅ What Happens:
- Connects to MongoDB.
- Fetches all users and returns them as JSON.
5. Create a Next.js Page to Display Users (pages/users.js
)
pages/users.js
import { useState, useEffect } from "react";
export default function UsersPage() {
const [users, setUsers] = useState([]);
useEffect(() => {
async function fetchUsers() {
const response = await fetch("/api/users");
const data = await response.json();
setUsers(data);
}
fetchUsers();
}, []);
return (
<div>
<h1>Users List</h1>
{users.length === 0 ? (
<p>No users found</p>
) : (
<ul>
{users.map((user) => (
<li key={user._id}>
<strong>{user.name}</strong> - {user.email}
</li>
))}
</ul>
)}
</div>
);
}
✅ What Happens:
- Client-side fetch from
/api/users
. - Displays the list of users.
6. Set Up Environment Variables (.env.local
)
Create a .env.local
file:
MONGODB_URI=mongodb+srv://your-username:your-password@cluster.mongodb.net/your-database
Then restart your development server:
npm run dev
7. (Optional) Add Test Users to MongoDB
You can add test users with a script.
scripts/addUsers.js
import connectDB from "../lib/mongodb";
import User from "../lib/models/User";
async function addUsers() {
await connectDB();
await User.create([
{ name: "John Doe", email: "john@example.com" },
{ name: "Jane Smith", email: "jane@example.com" },
]);
console.log("Users added!");
process.exit();
}
addUsers();
Run the script:
node scripts/addUsers.js
8. Run the Application
npm run dev
Visit:
http://localhost:3000/users
→ See the user list loaded from MongoDB.
Conclusion
This implementation connects a Next.js app to MongoDB with Mongoose, fetches user data via an API route, and displays it client-side, completing a full-stack integration.
Use Prisma ORM to fetch and display a list of products from a PostgreSQL database.
This task involves:
- Setting up Prisma ORM for database interaction.
- Connecting Next.js to PostgreSQL using Prisma.
- Fetching and displaying products from the database.
1. Install Prisma and PostgreSQL Client
npm install @prisma/client
npx prisma init
✅ This creates a prisma/
folder and a .env
file.
2. Configure PostgreSQL Connection (.env
)
Update your .env
file:
DATABASE_URL=postgresql://username:password@localhost:5432/mydatabase
Replace username
, password
, and mydatabase
with your actual PostgreSQL credentials.
3. Define the Product Model (prisma/schema.prisma
)
Edit prisma/schema.prisma
:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Product {
id Int @id @default(autoincrement())
name String
price Float
description String
createdAt DateTime @default(now())
}
✅ Defines a Product model with id
, name
, price
, description
, and createdAt
.
4. Migrate the Database
Run:
npx prisma migrate dev --name init
✅ This creates the products table in your PostgreSQL database.
5. Seed the Database with Test Products (prisma/seed.js
)
prisma/seed.js
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
await prisma.product.createMany({
data: [
{ name: "Laptop", price: 1200.99, description: "High-performance laptop" },
{ name: "Smartphone", price: 699.49, description: "Latest smartphone model" },
{ name: "Headphones", price: 149.99, description: "Noise-canceling headphones" },
],
});
console.log("Database seeded!");
}
main()
.catch((e) => console.error(e))
.finally(() => prisma.$disconnect());
Then run:
node prisma/seed.js
✅ Adds sample products to your database.
6. Create an API Route to Fetch Products (pages/api/products.js
)
pages/api/products.js
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default async function handler(req, res) {
if (req.method !== "GET") {
return res.status(405).json({ error: "Method Not Allowed" });
}
try {
const products = await prisma.product.findMany();
return res.status(200).json(products);
} catch (error) {
return res.status(500).json({ error: "Failed to fetch products" });
}
}
✅ What Happens:
- Fetches all products from PostgreSQL.
- Returns them as JSON.
7. Display Products on a Next.js Page (pages/products.js
)
pages/products.js
import { useEffect, useState } from "react";
export default function ProductsPage() {
const [products, setProducts] = useState([]);
useEffect(() => {
async function fetchProducts() {
const response = await fetch("/api/products");
const data = await response.json();
setProducts(data);
}
fetchProducts();
}, []);
return (
<div>
<h1>Products</h1>
{products.length === 0 ? (
<p>No products found</p>
) : (
<ul>
{products.map((product) => (
<li key={product.id}>
<h3>{product.name} - ${product.price.toFixed(2)}</h3>
<p>{product.description}</p>
</li>
))}
</ul>
)}
</div>
);
}
✅ What Happens:
- Client-side fetch from
/api/products
. - Displays a list of product names, prices, and descriptions.
8. Run the Application
npm run dev
Visit:
http://localhost:3000/products
→ Displays your products from PostgreSQL.
Conclusion
This implementation connects a Next.js app to PostgreSQL using Prisma ORM, fetches products via an API route, and displays them on a page, completing a full database-to-frontend flow.
Implement a real-time chat app using WebSockets (Socket.io) in a Next.js API route.
1. Install Dependencies
npm install socket.io socket.io-client
2. Create the WebSocket Server (app/api/socket/route.js
)
The App Router supports file-based routing under app/api
, so we define the Socket.io server here.
// app/api/socket/route.js
import { Server } from "socket.io";
let io;
export function GET(req) {
if (!io) {
io = new Server({
path: "/api/socket",
cors: {
origin: "*",
methods: ["GET", "POST"],
},
});
io.on("connection", (socket) => {
console.log("User connected:", socket.id);
socket.on("chatMessage", (msg) => {
io.emit("chatMessage", msg);
});
socket.on("disconnect", () => {
console.log("User disconnected:", socket.id);
});
});
global.io = io;
}
return new Response("Socket server is running");
}
Note: This is suitable only for custom server or development; App Router doesn’t support full WebSocket lifecycle in production on Vercel. For production use, deploy on custom Node.js server or use external providers.
3. Create ChatBox Component
// components/ChatBox.js
"use client";
import { useState, useEffect } from "react";
import io from "socket.io-client";
let socket;
export default function ChatBox() {
const [message, setMessage] = useState("");
const [messages, setMessages] = useState([]);
useEffect(() => {
socket = io({
path: "/api/socket",
});
socket.on("chatMessage", (msg) => {
setMessages((prev) => [...prev, msg]);
});
return () => {
if (socket) socket.disconnect();
};
}, []);
const sendMessage = () => {
if (message.trim()) {
socket.emit("chatMessage", message);
setMessage("");
}
};
return (
<div>
<h2>Chat Room</h2>
<div style={{ height: 200, overflowY: "auto", border: "1px solid #ccc", padding: 10 }}>
{messages.map((msg, i) => (
<p key={i}>{msg}</p>
))}
</div>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type a message..."
/>
<button onClick={sendMessage}>Send</button>
</div>
);
}
4. Create the Chat Page (app/chat/page.js
)
// app/chat/page.js
import dynamic from "next/dynamic";
const ChatBox = dynamic(() => import("../../components/ChatBox"), { ssr: false });
export default function ChatPage() {
return (
<div>
<h1>Real-Time Chat</h1>
<ChatBox />
</div>
);
}
5. Run the App
npm run dev
Open http://localhost:3000/chat
in two tabs and chat live.
Conclusion
This setup uses the App Router and Socket.io to build a real-time chat app. In production, for persistent WebSocket support, consider running your app on a custom server (e.g., Node.js + Express) or using services like Pusher or Ably.
Next.js Developer hiring resources
Our clients
Popular Next.js Development questions
What is the difference between Next.js and Vue.js?
Next.js is a React framework that couples server-side rendering with static generation in a unified framework. On the other hand, Vue.js is a separate JavaScript framework dedicated to building a flexible and reactive front-end user interface. Next.js finds its best utility in complex web applications, to be highly performant, whereas Vue.js works smoothly in creating dynamic front-end interfaces with relative simplicity.
What language does Next.js use?
Next.js primarily uses JavaScript but also supports TypeScript, a statically typed superset of JavaScript.
Can Next.js be used as a Back-end?
Yes, Next.js can be used as a Back-end. It natively supports API routes allowing one to create server-side endpoints within it.
Does Next.js replace Node.js?
Next.js does not replace Node.js but complements it. Next.js is a React framework for building web applications and often calculates on Node.js for the ease of server-side functionality. Whereas Node.js is for doing server-side work, some of the features of Next.js carry server-side rendering and static page generation.
What problem does Next.js solve?
Next.js makes it easier to build React applications by adding server-side rendering, static site generation, and API routes out of the box, hence making web apps faster and more SEO-friendly. It handles hard work concerning pre-rendering, routing, and performance optimization to ease the development process so developers can easily create dynamic, scalable, production-ready applications.
Interview Questions by role
Interview Questions by skill
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions
Interview Questions