Quiz - Modules as a graph
You need a local helper in lib/format.ts that exports a single formatPrice function used by a handful of components. A teammate suggests export default formatPrice so callers can import it as any name they like. What’s the senior cut?
Use a named export — export const formatPrice = .... Default exports earn their weight only where the framework demands them (page.tsx, layout.tsx, etc.); everywhere else, named exports win because renames propagate and the call-site spelling is fixed.
Use a default export — it’s the cleaner shape for a single-function module and lets the importer pick a shorter local name.
Either is fine — the named-vs-default question is purely stylistic in 2026.
The named-vs-default debate is closed in 2026. Defaults are framework-mandated (App Router special files, route-handler exports), and a small set of third-party packages ship a default. Everywhere else, named exports win — renames propagate through every caller, tree-shaking is reliable, and the import line names exactly what it pulls in. “Importer picks the name” is a downside, not a feature, in app code.
A client component imports a pure formatDate helper from lib/utils.ts. The same file also exports getCurrentUser, which queries the database. The build succeeds, but the client bundle balloons by 800KB. Why, and what’s the structural fix?
The bundler walks every reachable static-import edge from a 'use client' entry, including getCurrentUser’s database dependencies. Split the file into lib/format.ts (pure) and lib/auth.ts with import 'server-only' as its first line — the build will then refuse to ship if any client file ever reaches the server seam.
formatDate accidentally captured a server-only closure; rename the export to break the reference.
Tree-shaking is disabled inside 'use client' files; add "sideEffects": false to package.json to fix it.
The graph is the answer. Static imports draw eager edges, and the bundler crawls every reachable module from a client entry — so a single helper file mixing pure and server-only code drags the server world along. The structural fix is file-per-responsibility plus import 'server-only' at the server seam, which converts the bug class from “ships silently” to “build error pointing at the offending chain.”
Two files import each other at the value level:
import { fromB } from './b';export const fromA = fromB + 1;import { fromA } from './a';export const fromB = fromA + 1;An entry module imports a.ts. What happens, and what’s the experienced reflex?
Produces NaN at runtime — b.ts reads a partial a.ts whose fromA hasn’t been assigned yet, so fromB = undefined + 1. The fix is to extract the shared symbol into a third module both sides import from (a Y-shape, no cycle).
Build fails — modern bundlers refuse to emit cyclic value-level edges.
Both exports settle to 1 — the runtime re-evaluates each module until the cycle stabilizes.
Value-level cycles read at the top level hit a partial-module window: whichever side starts second sees the other mid-evaluation and reads undefined. The arithmetic produces NaN; the build is happy. The reflex is structural — extract shared symbols into a third module so the graph becomes a Y-shape with deterministic evaluation order. (Type-only cycles, by contrast, are erased and harmless.)
Which of these setup tasks belong in a lazy cached getter (getX()) rather than top-level work at module load?
A Postgres connection pool used by some routes but not others.
A Stripe SDK that only initializes when STRIPE_SECRET_KEY is set.
Env-var validation with Zod that every server file depends on.
A signing key loaded from a secret manager and used to verify every incoming request.
Top-level work earns its weight when it’s cheap, mandatory for every consumer, and deterministic — env validation and the universally-used signing key both qualify. Lazy init is for expensive (a connection pool) or conditional (Stripe only when configured) work, so importers that never need the resource don’t pay for it. Hiding env validation behind a getter defers the failure to first request, which is the opposite of fail-closed.
A developer adds a top-level await loadFlagsFromService() in a leaf module feature-flags.ts that fetches feature flags from a remote service on every cold start. The page that ultimately imports it now takes two seconds longer to render. Why?
Top-level await propagates upward along static-import edges — every module above feature-flags.ts becomes implicitly async and cannot finish its own top-level code until the leaf resolves. In a Server Component, the page render sits at the top of that chain, so the slow leaf becomes a render-blocker.
await at module scope is illegal in Next.js 16; the slowdown is a fallback path that synchronously polls until the promise resolves.
Top-level await disables HTTP keep-alive on the importing page, so each request re-opens a TCP connection.
Top-level await is a graph-level change, not a local one. The wait cascades to every static importer, all the way to the entry. That makes it the right reach for fast, deterministic, mandatory work (env validation), and the wrong reach for slow per-request fetches — those belong inside a Server Component with Suspense, where streaming can render around the wait.
Better Auth’s published Session.user.id is string, but your project has a branded UserId. Every Server Action that reads the session is casting session.user.id as UserId. What’s the senior reach?
Skip declare module here — Better Auth ships its own extension contract (additionalFields + typeof auth.$Infer.Session) for adding fields, and for the bare-string-to-brand case, re-brand at the query boundary with the brand factory. Augmenting declare module 'better-auth' would lie about what the runtime actually hydrates.
Augment declare module 'better-auth' { interface Session { user: { id: UserId } } } in types/better-auth.d.ts — the augmentation is the standard reach for any third-party type mismatch.
Weaken every receiving function to accept string — the brand has outlived its usefulness once a third-party library is involved.
Question zero is “does the library ship its own extension contract?” Better Auth does — $Infer derives the type from the same config that hydrates the runtime, so the type and the value stay in sync. A declare module augmentation here would tell the type checker the field is branded while the runtime still hands you a bare string, re-introducing the bug class the brand existed to prevent. The right re-brand happens at the application boundary with the brand factory.
A teammate writes the following in types/next-intl.d.ts and the augmentation never fires:
declare module 'next-intl' { interface AppConfig { Messages: { home: { title: string } }; }}What’s the most likely cause?
The file has no top-level import or export, so TypeScript treats it as a global ambient declaration. declare module 'next-intl' then declares a non-existent global module instead of merging into the package. Adding import type { ... } from '...' at the top flips the file to module mode and the augmentation fires.
AppConfig must be declared as a type alias, not an interface, for the augmentation to merge.
Augmentation files have to live inside src/; placing them in a top-level types/ directory hides them from tsc.
A .d.ts file with no top-level imports or exports is an ambient script file — declare module 'x' inside it declares a global module by that name rather than augmenting the real package. The fix is a top-level import/export so TypeScript treats the file as a module. The interface-vs-type rule runs the other way: interfaces merge, type aliases refuse — so AppConfig must stay an interface.
What does '@/db' resolve to, and where is the resolution rule defined?
A TypeScript path alias resolved against the paths field of tsconfig.json — not a node_modules lookup at all. Both tsc and the bundler read the same tsconfig.json and agree on the resolution.
A package-name lookup — Node walks up the directory tree looking for node_modules/@/db/package.json.
A subpath import resolved through the exports field of the @ package in node_modules.
The @/ prefix is a project-defined alias in tsconfig.json’s paths, not a scoped npm package name. Both the type checker and the bundler resolve it against the same config. Bare specifiers split three ways: package names ('react') resolved via node_modules and the package’s exports field, subpaths ('next/headers') gated by exports keys, and @/* aliases resolved through tsconfig.json.
Quiz complete
Score by topic