Lesson 40 of 81
An accidental import across the server/client boundary either leaks a secret to the browser or crashes at runtime on a missing browser API. The server-only and client-only packages turn that mistake into a build error instead.
11 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Server Components and Client Components share one module graph — nothing structurally stops a file meant to stay on the server from being imported, directly or transitively, into a Client Component. If that file reads process.env.DATABASE_PASSWORD or calls a database driver, importing it from client code doesn't just fail loudly — in the worst case it bundles that code (and whatever secret it touches) straight into the JavaScript shipped to every visitor's browser.
server-only and client-only are two tiny npm packages that exist for exactly one purpose: importing either one turns an accidental cross-boundary import into a build-time error instead of a silent leak or a runtime crash.
// lib/db.ts
import "server-only";
export async function getUser(id: string) {
// uses a real database connection string — must never reach the client
return db.query("SELECT * FROM users WHERE id = $1", [id]);
}
The import itself does nothing at runtime — there's no function to call, no export to use. Its presence is a marker Next.js's bundler checks for: the moment any Client Component's import chain reaches a file with import "server-only" at the top, the build fails with an error naming the exact file and import path that crossed the line. client-only is the mirror image, for a module that assumes a browser environment (reads window, uses a browser-only API) and would throw if a Server Component tried to render it during SSR.
// lib/stripe-server.ts
import "server-only";
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
export async function createPaymentIntent(amount: number) {
return fetch("https://api.stripe.com/v1/payment_intents", {
method: "POST",
headers: { Authorization: `Bearer ${STRIPE_SECRET_KEY}` },
body: new URLSearchParams({ amount: String(amount) }),
});
}
// app/checkout/checkout-button.tsx
"use client";
// A teammate reaches for this thinking "I'll just call the helper directly" —
// this import fails the build instead of shipping STRIPE_SECRET_KEY to the browser.
import { createPaymentIntent } from "@/lib/stripe-server";
export function CheckoutButton() {
return <button onClick={() => createPaymentIntent(1000)}>Pay</button>;
}
Without import "server-only" in stripe-server.ts, this compiles fine — Next.js has no way to know createPaymentIntent was meant to be server-only, so it bundles the function (and the secret key it closes over) into the client JavaScript for CheckoutButton. Anyone can open devtools and read the key straight out of the bundle. With the marker in place, this exact mistake fails at next build with a clear message instead of shipping.
import "server-only" with no destructured export looks unusual the first time you see it, but that's the entire API surface.import "server-only", the build still fails — the check follows the whole chain, not just the file you touched.server-only doesn't prevent the file's code from running on the server during SSR of a page that also has Client Components — it only blocks the file from being bundled into client-side JavaScript. Server Components can still import and call it freely.server-only "just in case" isn't free. It's a real constraint — a genuinely shared utility (a date formatter, a validation schema) that both server and client code need has to stay unmarked, or you end up duplicating it."How do you make sure a database credential never accidentally ends up in the client bundle, beyond just being careful?" tests whether a candidate knows about a real, specific tool for this class of bug (server-only) versus only having a vague answer like "code review" — the tool exists precisely because review alone reliably misses transitive imports several files deep.