Skip to content
Chapter 6Lesson 5

Quiz - Modules as a graph

Quiz progress

0 / 0

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.

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.

Two files import each other at the value level:

a.ts
import { fromB } from './b';
export const fromA = fromB + 1;
b.ts
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.

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.

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.

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.

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.

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.

Quiz complete

Score by topic