afaqrashid.com  ·  portfolio · 2026 ed.

server-only in Next.js: What It Does, the Errors, and the Leak It Prevents (Tested)

Putting import "server-only" at the top of a file makes the build fail if that file ever ends up in a Client Component. Use it on any module that reads secrets, talks to the database or calls a private API. I tested what it does, and what happens without it, on Next.js 16.4.4.

// lib/secret.ts
import "server-only";

export function getSecret() {
  return process.env.MY_SECRET ?? "unset";
}

The error when a Client Component imports it

I imported getSecret into a component marked "use client". next build stopped with two errors for the same line:

Error: Turbopack build failed with 2 errors:
./lib/secret.ts:1:1
Error: You're importing a module that depends on "server-only". This API is only
available in Server Components in the App Router, but you are using it in the Pages Router.

./lib/secret.ts:1:1
Error: 'server-only' cannot be imported from a Client Component module
It should only be used from a Server Component.
Import traces:
  Client Component Browser:
    ./lib/secret.ts [Client Component Browser]
    ./app/so/show.tsx [Client Component Browser]
    ./app/so/show.tsx [Server Component]
    ./app/so/page.tsx [Server Component]

Ignore the "Pages Router" wording in the first one: this project had no pages folder at all. The second error is the useful one, and the import trace under it shows the chain. Here it says show.tsx is the Client Component that pulled lib/secret.ts in, so that's the file to change.

The fix

  • Call the function in a Server Component (or a Server Action or Route Handler) and pass the result down as a prop. When I removed "use client" from show.tsx, the build passed.
  • If the Client Component needs to trigger the work, call a Server Action from it instead of importing the module.
  • Pass only the values the client needs, not the whole object a database call returns.

What happens without server-only: a tested leak

It's easy to assume a secret can't leak because a non-NEXT_PUBLIC_ variable is undefined in the browser. I removed the import "server-only" line, kept the "use client" component, and built with MY_SECRET=sk_test_leak_456:

  • The build passed with no warning.
  • The secret was not in the JavaScript sent to the browser. Searching .next/static found nothing.
  • The secret was in the prerendered HTML (.next/server/app/so.html), because Client Components are also rendered on the server, where the variable is set.
  • In the browser, the page showed "unset" once React took over, because the client value is undefined. The HTML source, fetched from the same page, still contained sk_test_leak_456.

So looking at the page tells you nothing has leaked, while anyone who views the source can read the secret. With server-only, the same code doesn't build.

Do you need to install the server-only package?

Not for Next.js itself. My test project didn't have server-only in package.json or node_modules, and Next still recognised the import and enforced it. TypeScript didn't complain either, even with noUncheckedSideEffectImports turned on. Outside Next, for example in a test runner, the import is resolved like any other package, so install it (npm install server-only) if your tests import those modules.

client-only: the opposite guard

import "client-only" marks code that must only run in the browser, for example code that uses window or localStorage. When a Server Component imported such a module, the build failed with:

Error: You're importing a component that imports client-only. It only works in a Client
Component but none of its parents are marked with "use client", so they're Server
Components by default.

The fix is the reverse: import that module only from files marked "use client".

Where to put import "server-only"

  • Database clients and query functions (lib/db.ts).
  • Anything that reads a secret: API keys, tokens, signing keys.
  • Wrappers for private APIs, such as a Shopify Admin API client.
  • A data access layer: the functions that check the user and return only safe fields.

Related

Sources