Scenario-based multiple-choice questions covering Next.js App Router, data fetching, performance, security, and architectural patterns.
Focus: App Router routing structure, Page vs Layout, use client vs use server, dynamic routing syntax.
- App Router Fundamentals: Route segments, layouts,
use client, dynamic params, catch-all routes.
Focus: Data fetching strategies, Server Actions, caching behaviors, SEO via Metadata API, hydration errors.
- Data Fetching & Server Actions: ISR with
revalidate, Server Actions in forms,cache: 'no-store',generateMetadata, hydration errors.
Focus: Partial Prerendering (PPR), middleware routing, Dynamic IO, security in Server Actions, error.tsx boundaries.
- Advanced Server Rendering: PPR, middleware
matcher,unstable_noStore, Server Action authorization,error.tsxscope.
Focus: Multi-zone architectures, unstable_cache with tag-based invalidation, Edge vs Node.js runtimes, micro-frontend integrations.
- Architecture & Runtime: Multi-zone with
basePath,unstable_cachetags,revalidateTag, Module Federation, Edge vs Node.js runtime.
Q. Given the following app/ directory structure, which file is responsible for rendering the content at the URL /dashboard/settings?
app/
├── layout.tsx
├── page.tsx
├── dashboard/
│ ├── layout.tsx
│ └── settings/
│ ├── layout.tsx
│ └── page.tsx
- A)
app/dashboard/layout.tsx - B)
app/dashboard/settings/layout.tsx - C)
app/dashboard/settings/page.tsx - D)
app/layout.tsx
Answer & Explanation
Answer: C) app/dashboard/settings/page.tsx
Explanation: In the Next.js App Router, the page.tsx file is the UI unique to a route segment and the only file that makes a route publicly accessible. app/dashboard/settings/page.tsx is the file rendered at /dashboard/settings.
Why the distractors are wrong:
- A —
app/dashboard/layout.tsxwraps all routes under/dashboardbut does not render content at any specific URL on its own. - B —
app/dashboard/settings/layout.tsxwraps children under/dashboard/settingsbut is not itself the rendered content. - D —
app/layout.tsxis the root layout that wraps every page in the app but renders nothing specifically at/dashboard/settings.
Q. A developer adds a <nav> component inside app/dashboard/layout.tsx. Which statement is true about this layout?
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<nav>Dashboard Nav</nav>
{children}
</div>
);
}- A) The
<nav>is destroyed and re-mounted every time the user navigates between dashboard sub-routes. - B) The
<nav>persists across navigations between dashboard sub-routes without re-mounting. - C) The
<nav>is only rendered on the initial full-page load and not during client-side navigation. - D) The layout component re-renders but the
<nav>DOM node is never updated after the first mount.
Answer & Explanation
Answer: B) The <nav> persists across navigations between dashboard sub-routes without re-mounting.
Explanation: Layouts in the App Router preserve their state and DOM nodes across navigations between sibling and child routes. This is a core advantage of the App Router over the Pages Router, where page-level wrappers (_app.tsx) were re-mounted on every navigation.
Why the distractors are wrong:
- A — Describes Pages Router behavior or full-page reload behavior, not App Router segment layouts.
- C — Next.js App Router layouts persist during client-side navigation, not only on the initial load.
- D — The key distinction is that the component is not destroyed and re-mounted; it can re-render if its props change, but React will reconcile the DOM rather than replace it.
Q. A developer writes the following component. What is the minimum required change to make it work correctly in the Next.js App Router?
// app/components/Counter.tsx
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
);
}- A) Add
export const runtime = 'edge'at the top of the file. - B) Add
'use client';as the very first line of the file. - C) Move the component into the
pages/directory instead ofapp/. - D) Wrap the component in a
<Suspense>boundary in the parent layout.
Answer & Explanation
Answer: B) Add 'use client'; as the very first line of the file.
Explanation: By default, all components in app/ are React Server Components and cannot use useState, browser APIs, or event handlers like onClick. Adding 'use client' at the top of the file marks it as a Client Component boundary, enabling the use of React hooks and browser interactivity.
Why the distractors are wrong:
- A —
export const runtime = 'edge'configures the server execution environment for a route or middleware; it has no effect on component type (Server vs. Client). - C — Moving to
pages/would work but is not the minimum required change; it also adopts an entirely different router architecture. - D —
<Suspense>handles async loading states and streaming; it does not grant access to React hooks in Server Components.
Q. A Next.js App Router project has the file app/products/[id]/page.tsx. What is the correct way to access the dynamic id parameter inside the page component?
// app/products/[id]/page.tsx
export default function ProductPage(/* ??? */) {
// access id here
}- A)
function ProductPage({ params }: { params: { id: string } }) - B)
function ProductPage({ query }: { query: { id: string } }) - C)
function ProductPage() { const { id } = useRouter().query; } - D)
function ProductPage({ searchParams }: { searchParams: { id: string } })
Answer & Explanation
Answer: A) function ProductPage({ params }: { params: { id: string } })
Explanation: In the App Router, dynamic route segment values are passed to page.tsx and layout.tsx components via the params prop. The object keys mirror the folder bracket names: for [id], the key is id.
Why the distractors are wrong:
- B —
queryis not a recognized prop in the App Router. It was arouter.querypattern in the Pages Router. - C —
useRouter()fromnext/navigation(App Router) exposes navigation methods but does not exposequery. Dynamic params are accessed via theuseParams()hook in Client Components or via theparamsprop in Server Components. - D —
searchParamsis a separate App Router prop used for URL query strings (e.g.,?tab=settings), not for dynamic path segments defined with bracket syntax.
Q. A developer needs a route that matches both /docs (with no additional segments) and /docs/guide/intro. Which folder naming convention achieves this in the App Router?
- A)
app/docs/[...slug]/page.tsx - B)
app/docs/[[...slug]]/page.tsx - C)
app/docs/[slug]/page.tsx - D)
app/docs/(slug)/page.tsx
Answer & Explanation
Answer: B) app/docs/[[...slug]]/page.tsx
Explanation: [[...slug]] is an optional catch-all route. When slug is absent (i.e., the URL is /docs), the segment receives undefined. When segments are present (e.g., /docs/guide/intro), it receives ['guide', 'intro']. This makes it the only option that matches both the zero-segment and multi-segment cases.
Why the distractors are wrong:
- A —
[...slug]is a required catch-all. It does not match the base path/docsbecause at least one segment is mandatory; accessing/docswould return a 404. - C —
[slug]matches exactly one dynamic segment (e.g.,/docs/intro) and does not match zero or multiple segments. - D — Parentheses
(slug)in the App Router denote Route Groups, which affect layout organization without creating URL segments. They do not create catch-all patterns.
Q. A developer writes the following data-fetching code in a Server Component. What is the correct description of its caching behavior?
// app/news/page.tsx
async function getNews() {
const res = await fetch('https://api.example.com/news', {
next: { revalidate: 3600 },
});
return res.json();
}
export default async function NewsPage() {
const news = await getNews();
return <ul>{news.map((n) => <li key={n.id}>{n.title}</li>)}</ul>;
}- A) The page is statically generated at build time and never updated unless manually redeployed.
- B) The page is always server-rendered fresh on every request with no caching.
- C) The page is cached and regenerated in the background at most once per hour after a request triggers it.
- D) The fetch result is cached in the browser's service worker for one hour.
Answer & Explanation
Answer: C) The page is cached and regenerated in the background at most once per hour after a request triggers it.
Explanation: next: { revalidate: 3600 } configures Incremental Static Regeneration (ISR) via Next.js's server-side Data Cache. The response is stored in the cache and served to subsequent requests. After 3,600 seconds (1 hour), the next incoming request triggers a background regeneration while continuing to serve the stale cached response — the classic stale-while-revalidate pattern.
Why the distractors are wrong:
- A — Describes a fully static page with
cache: 'force-cache'and no revalidation period; that page would never update post-deployment. - B — Describes
cache: 'no-store'orexport const dynamic = 'force-dynamic', which bypass the Data Cache entirely. - D —
next: { revalidate }controls Next.js's server-side Data Cache, not the browser cache or service workers.
Q. Which of the following is the correct implementation of a Server Action used directly in an HTML <form>?
A)
// app/contact/page.tsx
'use client';
async function submitForm(formData: FormData) {
'use server';
const name = formData.get('name');
await saveToDb(name);
}
export default function ContactPage() {
return (
<form action={submitForm}>
<input name="name" />
<button>Submit</button>
</form>
);
}B)
// app/contact/page.tsx
async function submitForm(formData: FormData) {
'use server';
const name = formData.get('name');
await saveToDb(name);
}
export default function ContactPage() {
return (
<form action={submitForm}>
<input name="name" />
<button>Submit</button>
</form>
);
}C)
// app/contact/page.tsx
'use server';
async function submitForm(formData: FormData) {
const name = formData.get('name');
await saveToDb(name);
}
export default function ContactPage() {
return (
<form action={submitForm}>
<input name="name" />
<button>Submit</button>
</form>
);
}D)
// app/actions.ts
'use server';
export async function submitForm(formData: FormData) {
const name = formData.get('name');
await saveToDb(name);
}
// app/contact/page.tsx
import { submitForm } from './actions';
export default function ContactPage() {
return (
<form action={submitForm}>
<input name="name" />
<button>Submit</button>
</form>
);
}Answer & Explanation
Answer: D) Separate actions.ts file with 'use server' at the module level, imported into the page.
Explanation: Option D is the canonical pattern. A dedicated file marked 'use server' exports one or more server functions. A Server Component page imports and passes the action as the <form action={...}> prop. This pattern cleanly separates concerns and works for both Server and Client Component consumers.
Why the distractors are wrong:
- A — A file marked
'use client'cannot contain inline'use server'function directives. The two module-level directives are mutually exclusive; Next.js will throw a build error. - B — Technically valid (an inline
'use server'inside a Server Component function body is permitted), but Option D represents the recommended architectural pattern. - C — Placing
'use server'at the module level of a file that also exports a React component as its default export would mark all exports (including the component) as server-only, causing a build error when Next.js attempts to render the component.
Q. A developer debates whether cache: 'no-store' and next: { revalidate: 0 } are equivalent in Next.js. Which statement is most accurate?
// Option A
fetch(url, { cache: 'no-store' });
// Option B
fetch(url, { next: { revalidate: 0 } });- A) They are completely identical; both skip the Data Cache on every request.
- B)
cache: 'no-store'opts out of caching entirely, whilerevalidate: 0still writes to the cache but immediately marks it as stale, potentially allowing a single stale read. - C)
revalidate: 0is not a valid Next.js option; only integer values ≥ 1 are accepted. - D)
cache: 'no-store'only affects the browser cache, whilerevalidate: 0affects the server-side Data Cache.
Answer & Explanation
Answer: B) cache: 'no-store' opts out of caching entirely, while revalidate: 0 still writes to the cache but immediately marks it as stale, potentially allowing a single stale read.
Explanation: These two options have subtle but important semantic differences. cache: 'no-store' (a Web Fetch API standard option) instructs Next.js never to read from or write to the Data Cache. next: { revalidate: 0 } still participates in the Data Cache infrastructure but with a TTL of zero, which means cached entries are immediately considered stale — the background revalidation cycle still applies. Their interactions with generateStaticParams and route-segment dynamic config can differ in edge cases.
Why the distractors are wrong:
- A — They are not identical. Their behavior diverges in how they interact with the Data Cache and the route rendering mode determination.
- C —
revalidate: 0is an explicitly documented and valid Next.js option used to force dynamic rendering via the Data Cache path. - D — Both options affect Next.js's server-side Data Cache. Neither option directly controls the browser HTTP cache.
Q. A developer wants to set a dynamic <title> and og:image for a product detail page. Which implementation is correct?
A)
export const metadata = {
title: 'Product Detail',
openGraph: { images: ['/og-default.png'] },
};B)
export async function generateMetadata({ params }) {
const product = await fetchProduct(params.id);
return {
title: product.name,
openGraph: { images: [product.imageUrl] },
};
}C)
'use client';
import { Helmet } from 'react-helmet';
export default function ProductPage({ params }) {
return <Helmet><title>Product</title></Helmet>;
}D)
export async function getServerSideProps({ params }) {
const product = await fetchProduct(params.id);
return { props: { title: product.name } };
}Answer & Explanation
Answer: B) generateMetadata async function accessing params.
Explanation: generateMetadata is the App Router's native mechanism for generating dynamic metadata per request. It receives { params, searchParams }, can perform async data fetching, and returns a metadata object. Next.js automatically renders the result as <title>, <meta>, and <link> tags in the <head>.
Why the distractors are wrong:
- A — Static
export const metadatais evaluated once at build time and cannot reference runtime values likeparams.id. It produces the same metadata for every product. - C —
react-helmetis a client-side library. It injects tags into the DOM after JavaScript loads, which means search engine crawlers may not see the correct metadata. The App Router's Metadata API renders tags server-side. - D —
getServerSidePropsis a Pages Router API that does not exist in the App Router. Even in the Pages Router, it could not directly populate<head>tags — that required a separate<Head>component fromnext/head.
Q. A developer sees a React hydration error in production. Which code pattern in a Client Component is the most likely cause?
A)
'use client';
export default function Timestamp() {
return <p>Built at: {process.env.NEXT_PUBLIC_BUILD_TIME}</p>;
}B)
'use client';
import { useState } from 'react';
export default function Toggle() {
const [on, setOn] = useState(false);
return <button onClick={() => setOn(!on)}>{on ? 'ON' : 'OFF'}</button>;
}C)
'use client';
export default function RandomId() {
return <div id={`item-${Math.random()}`}>Content</div>;
}D)
'use client';
export default function StaticText() {
return <p>Hello World</p>;
}Answer & Explanation
Answer: C) Math.random() generating a different value on server and client.
Explanation: Hydration errors occur when the HTML rendered on the server does not match what React generates during client-side hydration. Math.random() produces a unique value on each invocation, so the id generated during SSR will differ from the one generated during the client hydration pass, producing a mismatch that React cannot reconcile.
Why the distractors are wrong:
- A —
NEXT_PUBLIC_BUILD_TIMEis a build-time environment variable inlined into both the server bundle and the client bundle during the build. Its value is identical in both environments. - B —
useState(false)has a deterministic initial value (false) that is the same in both the server render and the client hydration pass. No mismatch occurs. - D — A static string literal (
Hello World) is byte-for-byte identical in server-rendered HTML and client hydration output. No mismatch.
Q. A developer enables Partial Prerendering and writes the following page. What does Next.js serve to the first visitor of /feed?
// next.config.ts
const nextConfig = { experimental: { ppr: true } };
// app/feed/page.tsx
import { Suspense } from 'react';
import StaticHeader from '@/components/StaticHeader';
import DynamicFeed from '@/components/DynamicFeed'; // internally calls cookies()
export default function FeedPage() {
return (
<>
<StaticHeader />
<Suspense fallback={<p>Loading feed…</p>}>
<DynamicFeed />
</Suspense>
</>
);
}- A) The entire page is blocked until
DynamicFeedresolves; nothing is sent to the browser early. - B) A static HTML shell containing
<StaticHeader>and the<p>Loading feed…</p>fallback is served from the CDN instantly;DynamicFeedstreams in dynamically per request. - C) Both
StaticHeaderandDynamicFeedare rendered at build time; PPR has no effect whencookies()is used. - D) The page falls back to full CSR because
DynamicFeedusescookies(), which PPR does not support.
Answer & Explanation
Answer: B) A static HTML shell containing <StaticHeader> and the <p>Loading feed…</p> fallback is served from the CDN instantly; DynamicFeed streams in dynamically per request.
Explanation: Partial Prerendering (PPR) is designed precisely for this pattern. At build time, Next.js renders the static parts of the page (<StaticHeader>) and the <Suspense> fallback (<p>Loading feed…</p>) into a static HTML shell that is cached on the CDN. When a user visits /feed, the shell is served instantly. The server then streams the rendered output of <DynamicFeed> (which reads cookies() and is therefore dynamic) into the open Suspense hole.
Why the distractors are wrong:
- A — PPR specifically avoids this full-blocking behavior. The static shell is the whole point.
- C — PPR never renders dynamic components (those using
cookies(),headers(), etc.) at build time. The dynamic boundary is intentional. - D — PPR fully supports dynamic APIs like
cookies()andheaders()within<Suspense>boundaries. This is the core use case of the feature.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};- A) All routes in the application, including static files and API routes.
- B) Only the exact paths
/dashboardand/admin, not their sub-routes. - C) Any path starting with
/dashboardor/admin, including their sub-routes (:path*matches zero or more segments). - D) Only routes that contain the literal string
:pathin the URL.
Answer & Explanation
Answer: C) Any path starting with /dashboard or /admin, including their sub-routes (:path* matches zero or more segments).
Explanation: In Next.js middleware matcher config, :path* is a named parameter with a wildcard that matches zero or more path segments. This means the pattern /dashboard/:path* matches /dashboard (zero additional segments), /dashboard/analytics, /dashboard/users/edit, and so on.
Why the distractors are wrong:
- A — Without a
matcherconfig (or withmatcher: ['/']), middleware runs on all routes. The presence of amatcherexplicitly limits its scope. - B —
:path*matches sub-routes because*allows zero or more segments. To match only the exact paths, the config would bematcher: ['/dashboard', '/admin']with no wildcard. - D —
:pathis Next.js path-matching syntax for a named parameter, not a literal URL string. The colon prefix denotes a pattern parameter, not a URL literal character.
Q. A developer has a Server Component that queries a database but notices Next.js is caching the result between requests. Which code addition correctly opts the component out of static rendering?
import { unstable_noStore as noStore } from 'next/cache';
export default async function LiveOrderCount() {
// ??? add noStore call here
const count = await db.orders.count();
return <p>Live orders: {count}</p>;
}- A) Call
noStore()inside auseEffecthook at the start of the function body. - B) Call
noStore()at the top of the async function body, before anyawaitcalls. - C) Export
export const dynamic = 'force-dynamic'from the route segment file and removenoStore. - D) Both B and C are valid and produce equivalent results.
Answer & Explanation
Answer: D) Both B and C are valid and produce equivalent results.
Explanation: Both approaches are documented Next.js mechanisms and are interchangeable for this use case. Calling noStore() at the top of the Server Component's async function body signals to Next.js at render time that this component must not be statically cached. Exporting export const dynamic = 'force-dynamic' is the route-segment-level config equivalent that forces the entire route segment into dynamic rendering mode.
Why the distractors are wrong:
- A —
noStore()is designed for Server Components.useEffectis a Client Component hook and cannot be used in a Server Component. Attempting this would cause a build error.
// app/actions.ts
'use server';
import { db } from '@/lib/db';
export async function deletePost(postId: string) {
await db.posts.delete({ where: { id: postId } });
}// app/dashboard/page.tsx (Client Component)
<button onClick={() => deletePost(post.id)}>Delete</button>- A) The Server Action is missing the
asynckeyword on the function declaration. - B) Server Actions cannot be called from Client Component event handlers; they must be used in
<form action={...}>only. - C) The action does not verify that the currently authenticated user owns
postId, allowing any authenticated user to delete any post (broken object-level authorization). - D)
db.posts.deletemust be wrapped in atry/catch; the missing error handling is the critical flaw.
Answer & Explanation
Answer: C) The action does not verify that the currently authenticated user owns postId, allowing any authenticated user to delete any post (broken object-level authorization).
Explanation: This is a Broken Object Level Authorization (BOLA) vulnerability, also known as Insecure Direct Object Reference (IDOR). Any authenticated user who knows (or can guess) a postId can call deletePost to delete it — there is no verification that the requestor owns the post. An attacker can enumerate IDs and delete other users' content. The fix is to retrieve the current session, verify the authenticated user's identity, and confirm ownership before executing the deletion.
Why the distractors are wrong:
- A — The function is correctly declared with
async. This is not a bug. - B — Server Actions can be called from Client Component event handlers as of Next.js 14+. This is an explicitly supported and documented pattern.
- D — Absent error handling is a code quality and reliability concern, not a security vulnerability. The authorization gap is the critical security flaw.
Q. A developer adds an error.tsx inside app/dashboard/. Which statement about its behavior is correct?
// app/dashboard/error.tsx
'use client';
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}- A) This file catches errors thrown in
app/dashboard/layout.tsxas well asapp/dashboard/page.tsx. - B) This file catches errors thrown in
app/dashboard/page.tsxand its children, but not errors thrown inapp/dashboard/layout.tsx. - C) The
resetfunction performs a full browser page reload to re-attempt rendering. - D) The
'use client'directive is optional here;error.tsxcan be a Server Component.
Answer & Explanation
Answer: B) This file catches errors thrown in app/dashboard/page.tsx and its children, but not errors thrown in app/dashboard/layout.tsx.
Explanation: In the App Router, error.tsx wraps the page.tsx (and its entire child subtree) of the same route segment in a React Error Boundary. However, the error boundary is rendered inside the layout.tsx of the same segment. Because a component cannot catch its own render errors via an error boundary, layout.tsx errors propagate upward to the parent segment's error.tsx (or to global-error.tsx for the root).
Why the distractors are wrong:
- A —
error.tsxdoes not wraplayout.tsxat the same level. It sits inside it. - C —
reset()calls the React Error Boundary's reset mechanism, which re-renders the boundary's children (re-attempts the failing segment). It is a React-level reset, not a full browser reload. - D —
error.tsxmust be a Client Component. React Error Boundaries rely oncomponentDidCatch/getDerivedStateFromErrorlifecycle semantics, which are inherently client-side features unavailable in Server Components.
Q. A company runs two independent Next.js applications that should appear under one domain: a marketing site at https://example.com and a dashboard app at https://example.com/dashboard. Which configuration approach is correct for the dashboard app?
A)
// next.config.js (dashboard app)
module.exports = { basePath: '/dashboard' };
// A reverse proxy routes /dashboard/* to the dashboard Next.js serverB)
// next.config.js (dashboard app)
module.exports = { assetPrefix: '/dashboard' };
// No reverse proxy needed; Next.js handles routing internallyC)
// next.config.js (marketing app only)
module.exports = {
async rewrites() {
return [
{
source: '/dashboard/:path*',
destination: 'https://dashboard-app.internal/:path*',
},
];
},
};
// No basePath is set on the dashboard appD) Both A and C are valid multi-zone approaches; A uses basePath for same-server deployments while C uses rewrites for cross-origin proxying.
Answer & Explanation
Answer: D) Both A and C are valid multi-zone approaches; A uses basePath for same-server deployments while C uses rewrites for cross-origin proxying.
Explanation: Next.js Multi-Zones documentation describes two complementary strategies. Option A uses basePath: '/dashboard' so all internal <Link> hrefs and static asset paths are prefixed automatically, with a reverse proxy routing /dashboard/* traffic to this app's server. Option C uses rewrites in the host app to proxy /dashboard/* to the dashboard app's origin URL — the dashboard app does not need basePath in this configuration.
Why the distractors are wrong:
- A alone — Correct pattern, but incomplete answer because C is also valid.
- B —
assetPrefixaffects only the CDN path for static assets (JS, CSS bundles) but does not redirect page routing. It cannot create a multi-zone setup on its own, and a reverse proxy is always required. - C alone — Correct pattern, but incomplete answer because A is also valid.
Q. A developer uses unstable_cache to cache a database query with tag-based invalidation. Which implementation is correct?
A)
import { unstable_cache } from 'next/cache';
export const getCachedUser = unstable_cache(
async (userId: string) => db.users.findUnique({ where: { id: userId } }),
['user-detail'],
{ tags: ['user'], revalidate: 3600 }
);B)
import { unstable_cache } from 'next/cache';
export const getCachedUser = unstable_cache(
async (userId: string) => db.users.findUnique({ where: { id: userId } }),
['user-detail'],
{ tag: 'user', revalidate: 3600 } // singular 'tag'
);C)
export const getCachedUser = async (userId: string) => {
return fetch(`/api/users/${userId}`, {
next: { tags: ['user'], revalidate: 3600 },
}).then((r) => r.json());
};D)
import { cache } from 'react';
export const getCachedUser = cache(
async (userId: string) => db.users.findUnique({ where: { id: userId } })
);Answer & Explanation
Answer: A) unstable_cache with tags: string[] array and revalidate in options.
Explanation: The unstable_cache API signature is unstable_cache(fn, keyParts, options) where options accepts { tags: string[], revalidate: number }. The tags field must be an array of strings. This enables revalidateTag('user') to purge all cache entries sharing that tag.
Why the distractors are wrong:
- B — The option key is
tags(plural, array), nottag(singular string). Usingtaginstead oftagsmeans no cache tag is registered, sorevalidateTagwill have no effect on this entry. - C — Making an internal
fetchcall to/api/users/:idadds an unnecessary HTTP round-trip to the same server.unstable_cachewith a direct database call is the correct, efficient approach for caching non-fetchdata sources. - D —
react.cacheprovides per-request deduplication (memoizing calls within a single render pass). It does not persist data across requests, does not support cache tags, and does not accept a revalidation TTL. It solves a different problem (avoiding duplicate DB queries in the same render tree).
Q. An e-commerce platform needs to immediately invalidate all product listing caches whenever a product is updated via an admin Server Action. Which implementation is correct?
// app/admin/actions.ts
'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';A)
export async function updateProduct(id: string, data: ProductData) {
await db.products.update({ where: { id }, data });
revalidateTag('products');
}
// All fetch() calls and unstable_cache results tagged 'products' are invalidatedB)
export async function updateProduct(id: string, data: ProductData) {
await db.products.update({ where: { id }, data });
await revalidatePath('/products'); // revalidateTag is incorrect for this use case
}C)
export async function updateProduct(id: string, data: ProductData) {
await db.products.update({ where: { id }, data });
revalidateTag('products');
}
// Only fetch() calls with the 'products' tag are invalidated; unstable_cache is unaffectedD)
export async function updateProduct(id: string, data: ProductData) {
await db.products.update({ where: { id }, data });
revalidateTag('products'); // This triggers a full rebuild of all static pages
}Answer & Explanation
Answer: A) revalidateTag('products') invalidates all cache entries — both fetch() calls and unstable_cache results — tagged 'products'.
Explanation: revalidateTag is the precise API for on-demand, tag-based cache invalidation. It marks all cache entries — both fetch() calls and unstable_cache results — that were registered with the specified tag as stale. On the next incoming request for those resources, they will be regenerated from the source.
Why the distractors are wrong:
- B —
revalidatePathinvalidates caches associated with a specific URL path, not all resources that share a semantic label. It is a coarser tool and does not invalidate all product-related data across different paths. - C — The claim that
unstable_cacheis unaffected byrevalidateTagis incorrect.revalidateTaginvalidates bothfetch()cache entries andunstable_cacheresults that share the tag. - D —
revalidateTagdoes not trigger a full static rebuild. It marks tagged entries as stale and Next.js regenerates them lazily on the next request, identical to how on-demand ISR works.
Q. A team wants to embed a React component from a remote Webpack Module Federation app into a Next.js host application. Which statement is most accurate?
- A) Next.js natively supports Module Federation without additional configuration; remote components are loaded as standard dynamic imports.
- B) The
@module-federation/nextjs-mfpackage adapts Module Federation for the Next.js runtime. The remote component should be consumed vianext/dynamicwithssr: falseunless both host and remote explicitly configure SSR support, to avoid hydration mismatches. - C) Module Federation is only supported in the
pages/router; the App Router cannot consume federated modules because Server Components do not support async remote loading. - D) Federated modules must be re-exported through a Next.js API route (
/api/mf-proxy) before Client Components can consume them.
Answer & Explanation
Answer: B) The @module-federation/nextjs-mf package adapts Module Federation for the Next.js runtime. The remote component should be consumed via next/dynamic with ssr: false unless both host and remote explicitly configure SSR support.
Explanation: The @module-federation/nextjs-mf package bridges Webpack's Module Federation runtime with Next.js's custom bundler pipeline. Remote federated modules are loaded asynchronously from an external origin at runtime, so they must be consumed with next/dynamic to prevent issues during the SSR pass. Setting ssr: false is the safe default unless both the host and remote are explicitly configured to support SSR, which requires coordinating chunk loading between two separate Webpack builds.
Why the distractors are wrong:
- A — Next.js does not natively support Module Federation. It requires the
@module-federation/nextjs-mfplugin to handle the integration. Standard dynamic imports cannot load from a remote Webpack container'sremoteEntry.js. - C — While App Router integration has historically been more complex, the
@module-federation/nextjs-mfpackage supports consuming federated modules as Client Components in the App Router. It is not restricted to the Pages Router. - D — There is no need for an API route proxy. The federated module's
remoteEntry.jsis fetched directly by the Webpack runtime in the browser. Routing through an API proxy would add latency and defeat the purpose of Module Federation.
Q. An architect must choose the runtime for a Route Handler that: (1) queries a PostgreSQL database via a TCP connection, (2) handles file uploads > 10 MB, and (3) uses the Node.js crypto module for encryption. Which decision is correct?
// app/api/process/route.ts
export const runtime = '???';- A)
'edge'— Edge Runtime is always faster and should be preferred for all Route Handlers. - B)
'edge'— Edge Runtime supports TCP connections and the Web Crypto API, satisfying all three requirements. - C)
'nodejs'(default) — Edge Runtime does not support TCP-based database connections, has a 4 MB request body size limit, and does not include the Node.jscryptomodule; only the Node.js runtime satisfies all three requirements. - D)
'nodejs'— but only because Edge Runtime is unavailable for Route Handlers and is restricted to Middleware only.
Answer & Explanation
Answer: C) 'nodejs' (default) — Edge Runtime does not support TCP-based database connections, has a 4 MB request body size limit, and does not include the Node.js crypto module.
Explanation: The Edge Runtime executes in a V8 isolate with a restricted API surface inherited from Cloudflare Workers / Vercel Edge: (1) No TCP connections — PostgreSQL drivers (pg, Prisma, Drizzle ORM) require TCP, making them incompatible; only HTTP-based edge-compatible drivers work. (2) 4 MB request body limit — File uploads exceeding 4 MB are rejected. (3) No Node.js built-in modules — node:crypto is not available; only the Web Crypto API (globalThis.crypto) is accessible. The Node.js runtime (the default) supports all three requirements.
Why the distractors are wrong:
- A — Edge Runtime is not universally faster. For I/O-bound workloads with database connections, it is incompatible. Its speed advantage comes from geographic distribution and cold-start performance, not raw throughput.
- B — Edge Runtime does not support TCP connections, has a body size limit, and does not include
node:crypto. All three stated facts in option B are incorrect. - D — Edge Runtime is available for Route Handlers via
export const runtime = 'edge'. It is not restricted to Middleware only.