Top 15 Next.js Interview Coding Challenges with Solutions [2025]
Master Next.js development with these comprehensive coding challenges covering SSR, SSG, API routes, middleware, performance optimization, and modern React patterns. From basic routing to advanced production-ready applications.
Junior Level Challenges
1. Dynamic Routing with getServerSideProps
Difficulty: Beginner | Topics: Dynamic Routes, SSR, Data Fetching
Challenge: Create a dynamic blog post page that fetches data at request time using getServerSideProps.
// pages/blog/[slug].js
export default function BlogPost({ post }) {
return (
{post.title}
);
}
export const getServerSideProps = async ({ params }) => {
const { slug } = params;
try {
const response = await fetch(`${process.env.API_URL}/posts/${slug}`);
if (!response.ok) return { notFound: true };
const post = await response.json();
return { props: { post } };
} catch (error) {
return { notFound: true };
}
};
2. Static Site Generation with getStaticProps
Difficulty: Beginner | Topics: SSG, Build-time Data Fetching
Challenge: Build a products page that pre-renders at build time with cached data.
// pages/products.js
export default function Products({ products }) {
return (
Our Products
{products.map((product) => (
{product.name}
${product.price}
{product.description}
))}
);
}
export const getStaticProps = async () => {
const response = await fetch(`${process.env.API_URL}/products`);
const products = await response.json();
return {
props: { products },
revalidate: 3600 // Revalidate every hour
};
};
3. API Routes with Request Validation
Difficulty: Beginner-Intermediate | Topics: API Routes, Validation, Error Handling
Challenge: Create a user registration API endpoint with input validation and error handling.
// pages/api/auth/register.js
import bcrypt from 'bcryptjs';
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
const { email, password, name } = req.body;
// Validation
if (!email || !password || !name) {
return res.status(400).json({ message: 'All fields required' });
}
if (password.length
4. Custom App with Global State
Difficulty: Intermediate | Topics: Custom App, Context API, Global State
Challenge: Set up a custom _app.js with global authentication state and theme provider.
// pages/_app.js
import { createContext, useContext, useReducer } from 'react';
import '../styles/globals.css';
const AppContext = createContext();
const initialState = {
user: null,
theme: 'light',
isLoading: false
};
function appReducer(state, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
default:
return state;
}
}
function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
{children}
);
}
export const useAppContext = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppContext must be used within AppProvider');
}
return context;
};
export default function MyApp({ Component, pageProps }) {
return (
);
}
5. Image Optimization with Next.js Image
Difficulty: Beginner | Topics: Image Optimization, Performance
Challenge: Create a responsive image gallery with optimized loading and lazy loading.
// components/ImageGallery.js
import Image from 'next/image';
import { useState } from 'react';
export default function ImageGallery({ images }) {
const [selectedImage, setSelectedImage] = useState(null);
return (
{images.map((image) => (
setSelectedImage(image)}
>
))}
{selectedImage && (
setSelectedImage(null)}>
)}
>
);
}
Mid-Level Challenges
6. Incremental Static Regeneration (ISR)
Difficulty: Intermediate | Topics: ISR, Caching, Performance
Challenge: Implement ISR for a news website that updates content automatically without full rebuilds.
// pages/news/[id].js
export default function NewsArticle({ article }) {
return (
{article.title}
By {article.author}
);
}
export const getStaticPaths = async () => {
// Pre-render only popular articles
const response = await fetch(`${process.env.API_URL}/news/popular`);
const popularArticles = await response.json();
const paths = popularArticles.map((article) => ({
params: { id: article.id }
}));
return {
paths,
fallback: 'blocking' // Generate other pages on-demand
};
};
export const getStaticProps = async ({ params }) => {
const response = await fetch(`${process.env.API_URL}/news/${params.id}`);
if (!response.ok) return { notFound: true };
const article = await response.json();
return {
props: { article },
revalidate: 300, // Revalidate every 5 minutes
};
};
7. Custom Document with SEO Optimization
Difficulty: Intermediate | Topics: Custom Document, SEO, Meta Tags
Challenge: Create a custom _document.js with SEO optimization and structured data.
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
render() {
return (
{/* Structured Data */}
);
}
}
export default MyDocument;
8. Middleware for Authentication
Difficulty: Intermediate | Topics: Middleware, Authentication, Route Protection
Challenge: Implement middleware for route-based authentication and authorization.
// middleware.js
import { NextResponse } from 'next/server';
import { verify } from 'jsonwebtoken';
const protectedPaths = ['/dashboard', '/profile', '/admin'];
export function middleware(request) {
const { pathname } = request.nextUrl;
const isProtectedPath = protectedPaths.some(path =>
pathname.startsWith(path)
);
if (!isProtectedPath) return NextResponse.next();
const token = request.cookies.get('auth-token')?.value;
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
try {
const decoded = verify(token, process.env.JWT_SECRET);
const response = NextResponse.next();
response.headers.set('x-user-id', decoded.userId);
return response;
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico).*)'],
};
9. Custom Hook for Data Fetching
Difficulty: Intermediate | Topics: Custom Hooks, SWR, Caching
Challenge: Build a reusable data fetching hook with caching, error handling, and optimistic updates.
// hooks/useApi.js
import { useState, useEffect, useCallback } from 'react';
const cache = new Map();
function useApi(url, options = {}) {
const [data, setData] = useState(options.initialData);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const fetchData = useCallback(async (showLoading = true) => {
if (!url) return;
if (showLoading) setLoading(true);
setError(null);
try {
// Check cache first
if (cache.has(url)) {
const cachedData = cache.get(url);
setData(cachedData);
if (showLoading) setLoading(false);
return;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
cache.set(url, result);
setData(result);
} catch (err) {
setError(err);
} finally {
if (showLoading) setLoading(false);
}
}, [url]);
const mutate = useCallback(async (newData) => {
if (newData !== undefined) {
setData(newData);
cache.set(url, newData);
} else {
await fetchData(false);
}
}, [url, fetchData]);
useEffect(() => {
if (url) fetchData();
}, [url, fetchData]);
return { data, error, loading, mutate, revalidate: () => fetchData(false) };
}
export default useApi;
10. Server-Side Caching with Redis
Difficulty: Expert | Topics: Server-side Caching, Redis, Performance
Challenge: Implement Redis caching for API routes with automatic invalidation.
// lib/redis.js
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
class CacheManager {
async get(key) {
try {
const cached = await redis.get(key);
return cached ? JSON.parse(cached) : null;
} catch (error) {
console.error('Cache get error:', error);
return null;
}
}
async set(key, value, ttl = 3600) {
try {
await redis.setex(key, ttl, JSON.stringify(value));
} catch (error) {
console.error('Cache set error:', error);
}
}
async del(pattern) {
try {
const keys = await redis.keys(pattern);
if (keys.length > 0) await redis.del(...keys);
} catch (error) {
console.error('Cache delete error:', error);
}
}
}
export const cacheManager = new CacheManager();
export function withCache(handler, options = {}) {
return async (req, res) => {
const { ttl = 3600 } = options;
const cacheKey = `api:${req.url}`;
if (req.method === 'GET') {
const cached = await cacheManager.get(cacheKey);
if (cached) {
res.setHeader('X-Cache', 'HIT');
return res.json(cached);
}
}
const originalJson = res.json;
res.json = function(data) {
if (res.statusCode === 200 && req.method === 'GET') {
cacheManager.set(cacheKey, data, ttl);
}
res.setHeader('X-Cache', 'MISS');
return originalJson.call(this, data);
};
return handler(req, res);
};
}
Senior Level Challenges
11. Real-time Updates with WebSockets
Difficulty: Expert | Topics: WebSockets, Real-time, Socket.io
Challenge: Build a real-time chat application with typing indicators and message status.
// pages/api/socketio.js
import { Server as ServerIO } from 'socket.io';
export default function handler(req, res) {
if (!res.socket.server.io) {
const io = new ServerIO(res.socket.server, {
path: '/api/socketio',
});
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('join-room', (roomId) => {
socket.join(roomId);
socket.to(roomId).emit('user-joined', socket.id);
});
socket.on('send-message', async (data) => {
const message = {
id: Date.now(),
content: data.content,
userId: socket.userId,
roomId: data.roomId,
timestamp: new Date()
};
// Save to database
await saveMessage(message);
// Emit to room
io.to(data.roomId).emit('new-message', message);
});
socket.on('typing-start', (roomId) => {
socket.to(roomId).emit('user-typing', {
userId: socket.userId,
isTyping: true
});
});
socket.on('typing-stop', (roomId) => {
socket.to(roomId).emit('user-typing', {
userId: socket.userId,
isTyping: false
});
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
res.socket.server.io = io;
}
res.end();
}
12. Advanced Performance Optimization
Difficulty: Expert | Topics: Performance, Bundle Splitting, Lazy Loading
Challenge: Implement comprehensive performance optimizations including code splitting and resource hints.
// next.config.js
module.exports = {
experimental: {
optimizeCss: true,
optimizeImages: true,
},
images: {
domains: ['example.com'],
formats: ['image/webp', 'image/avif'],
},
webpack: (config, { dev, isServer }) => {
if (!dev && !isServer) {
config.optimization.splitChunks.cacheGroups = {
...config.optimization.splitChunks.cacheGroups,
vendor: {
name: 'vendor',
test: /node_modules/,
chunks: 'all',
priority: 20,
},
};
}
return config;
},
async headers() {
return [
{
source: '/(.*)\\.(js|css|woff2|png|jpg)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
};
// components/LazyComponent.js
import dynamic from 'next/dynamic';
const DynamicChart = dynamic(() => import('./Chart'), {
loading: () => Loading chart...,
ssr: false,
});
function useResourcePreload() {
const preloadResource = (href, as) => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = href;
link.as = as;
document.head.appendChild(link);
};
return { preloadResource };
}
13. Internationalization (i18n)
Difficulty: Advanced | Topics: i18n, Localization, Dynamic Content
Challenge: Build a multi-language application with dynamic content translation.
// next.config.js
module.exports = {
i18n: {
locales: ['en', 'fr', 'es', 'de'],
defaultLocale: 'en',
},
};
// lib/i18n.js
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';
class I18nManager {
cache = new Map();
async loadNamespace(locale, namespace) {
const cacheKey = `${locale}:${namespace}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
try {
const translations = await import(`../locales/${locale}/${namespace}.json`);
this.cache.set(cacheKey, translations.default);
return translations.default;
} catch (error) {
return locale !== 'en' ? this.loadNamespace('en', namespace) : {};
}
}
translate(translations, key, params = {}) {
const keys = key.split('.');
let value = translations;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return key;
}
}
if (typeof value !== 'string') return key;
return Object.entries(params).reduce(
(str, [param, val]) => str.replace(new RegExp(`{{${param}}}`, 'g'), String(val)),
value
);
}
}
export const i18nManager = new I18nManager();
export function useTranslations(namespace = 'common') {
const { locale } = useRouter();
const [translations, setTranslations] = useState({});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadTranslations = async () => {
setIsLoading(true);
const t = await i18nManager.loadNamespace(locale, namespace);
setTranslations(t);
setIsLoading(false);
};
loadTranslations();
}, [locale, namespace]);
const t = (key, params) => i18nManager.translate(translations, key, params);
return { t, isLoading };
}
14. Advanced Error Handling
Difficulty: Advanced | Topics: Error Boundaries, Error Tracking, Fallbacks
Challenge: Implement comprehensive error handling with custom error pages and error tracking.
// components/ErrorBoundary.js
import React, { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({ error });
// Log to monitoring service
console.error('Error caught by boundary:', error);
// Call custom error handler
this.props.onError?.(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
Something went wrong
);
}
return this.props.children;
}
}
// pages/_error.js
function ErrorPage({ statusCode }) {
return (
{statusCode ? `Server Error ${statusCode}` : 'Client Error'}
{statusCode === 404
? 'This page could not be found.'
: 'An error occurred. Please try again later.'}
);
}
ErrorPage.getInitialProps = async ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode };
};
export default ErrorPage;
15. End-to-End Testing with Playwright
Difficulty: Advanced | Topics: E2E Testing, Test Automation, CI/CD
Challenge: Set up comprehensive end-to-end testing with Playwright including visual regression testing.
// playwright.config.js
module.exports = {
testDir: './tests/e2e',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
},
};
// tests/e2e/auth.spec.js
import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => {
test('should login successfully', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'password123');
await Promise.all([
page.waitForNavigation(),
page.click('[data-testid="login-button"]')
]);
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
});
test('should show error with invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'invalid@example.com');
await page.fill('[data-testid="password"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');
await expect(page.locator('[data-testid="error-message"]'))
.toContainText('Invalid credentials');
});
});
// Visual regression test
test('homepage should look correct', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
threshold: 0.3
});
});