skip to content

How do meta-frameworks let you declare which modules are server-only, and what invariant do those schemes share?

level: middleimportance: must knowfreq 58%

answer

  1. four ways, one rule
  2. name it, fence it, mark it, island it
  3. the default side differs by framework
  4. nothing client-reachable imports server-only code

basics

~20 s

Meta-frameworks use file or directory naming, a fenced section of the route module, an explicit opt-in marker for client modules, or islands that declare only interactive parts. All enforce one invariant: nothing a client entry imports may reach server-only code.

solid answer

~40 s

There are four recurring schemes. **Naming** — a file suffix or a directory the client build refuses to include. **Fencing** — one route module holds both halves, and the copy compiled for the browser drops the server-only exports. **An explicit marker** — a declaration at the top of a module that makes it a client entry point, so it and everything it imports are bundled. **Declaring only the interactive parts** — the page is server-rendered and individual units are annotated as needing browser JavaScript. Frameworks differ in which side is the default, and it is fair to say so. Underneath, all four state the same invariant: no module reachable from a client entry may import server-only code. The schemes differ in ergonomics and in how they fail, not in the property being enforced.

go deeper

for a junior

Know that the split is declared, not guessed: some file, folder or marker tells the build which side a module belongs to. Be able to name at least two of the schemes.

for a middle

Explain all four schemes and the one invariant behind them, and be precise that a client marker designates an entry point — the boundary follows its imports outward rather than stopping at the file.

for a senior

Talk about failure modes: which scheme fails silently, where a shared import defeats a fenced section, and why declaration without a build-time guard is documentation rather than enforcement.

for a principal

Weigh defaults as a policy choice. A server-by-default scheme makes mistakes loud and cheap; a client-by-default one makes them silent, which changes how much enforcement you must buy to reach the same risk level.

## Why anything has to be declared at all The build has to decide, for every module, whether it belongs in the server-side compilation, the browser bundle, or both. It can infer a great deal from the import graph, but it cannot infer **intent**: whether a module that happens to be importable from client code was ever meant to be. So every meta-framework offers some way for the author to state the side a module belongs to, and then enforces the statement mechanically. ## The four common schemes | Scheme | What you write | Default side | Typical failure mode | |---|---|---|---| | Naming convention | a file suffix, or a directory whose contents the client build refuses | whatever the framework's default is | the convention is honoured only where the tooling is wired for it | | Fenced section of a route module | server-only exports in one file alongside the client-facing ones; the client copy is compiled without them | server for the fenced part | an import used by *both* halves survives the strip and crosses | | Explicit client marker | a marker at the top of a module declaring it a client entry point | server by default, client on opt-in | forgetting the marker breaks interactivity loudly, which is the safe failure | | Declaring only the interactive parts | the page is server-rendered; individual units are annotated as needing browser JavaScript | server for everything unannotated | annotating a large wrapper drags its whole subtree across | Two of these describe a **default** and two describe a **carve-out**, and frameworks genuinely differ in which default they pick. Some treat every module as server-side until something opts into the client; others treat the application as a client app with server-only escape hatches. Both can be made safe; they fail differently, which is the part worth being able to say out loud. ## The invariant all four enforce Strip away the syntax and every scheme is a way of stating and checking the same property: > **No module reachable by import from a client entry point may import server-only code.** That is why the schemes are interchangeable in principle and not in ergonomics. The naming convention states it by file location. The fenced section states it by which exports survive a second compilation of the same file. The marker states it by naming the boundary's entry point and letting reachability do the rest. Islands state it by making interactivity the exception rather than the rule. ## The boundary extends outward, not just inward The single most misunderstood consequence: a marker applies to the module it sits in **and to everything that module imports**. It designates an entry point, not a single file. Mark a module near the root of a page and its whole import subtree is compiled for the browser — which is how a small interactive control ends up pulling a formatting library and, in bad cases, a shared helper that touches privileged code. The corollary is a placement rule: push the declaration **down** the tree, as close as possible to the unit that genuinely needs the browser, and keep shared modules on the neutral side of the line — importable from both, containing nothing that must not be published. ## Where each scheme leaks - A **naming convention** with no build-time check is documentation. It leaks the first time someone adds an import that the convention does not describe, because nothing evaluates it. - A **fenced section** leaks through shared imports: a helper used by both the server-only part and the client-facing part cannot be stripped, so whatever it reaches comes along. - A **marker-based** scheme leaks upward — a marker placed too high pulls a subtree across — and through third-party packages that ship no declaration of their own, forcing a wrapper. - **Island-style declaration** leaks when the annotated unit is a container rather than a leaf. ## Declaration is not enforcement A declaration says what you meant. It does not, on its own, stop a chain of imports from contradicting it three modules later. That is why the schemes above are normally paired with a build-time guard: an import placed in the sensitive module that resolves to an error in the client compilation, so a violating graph **fails the build** and names the chain rather than emitting a bundle. The practical shape of a mature setup is therefore layered: a default that makes the safe side automatic, an explicit declaration where the exception lives, a guard that turns the invariant into something the build checks on every commit, and a look at the built output for the cases none of those covered.

  • Which default is safer: server-by-default with an opt-in client marker, or client-by-default with server-only carve-outs?
    Server-by-default fails loudly: forgetting the marker breaks an interaction you notice immediately. Client-by-default fails silently, because a forgotten carve-out simply ships. Either can be made safe, but the second needs stronger build-time guards, since its mistakes produce a working page that happens to contain something it should not.
  • When one route module holds both server and client code, how does the client copy avoid the server half?
    The build compiles the module twice. For the browser copy it keeps only the exports client code needs and drops the fenced server exports along with imports used solely by them. That is why an import shared by both halves is the usual leak: it survives the strip and brings whatever it reaches with it.
  • A third-party package needs browser APIs but ships no declaration of its own. What do you do?
    Wrap it. Create a thin module of your own that carries the client declaration and re-exports what you need, then import the wrapper. That keeps the boundary explicit and local, instead of pushing the declaration up to whatever page happened to use the package first.

saying these in an interview costs you the question

  • Thinks every meta-framework declares the split the same way.
  • Believes a client marker affects only the file it appears in, not its imports.
  • Assumes a server directory is enforced by the language rather than the build.
  • Says a naming convention is enough without any build-time check.
  • Thinks a fenced server section is stripped from the server build too.