Skip to content

This guide keeps server-only code out of the browser in a Next.js app, with the nextjs preset.

  • Read Getting started.
  • You have a Next.js app that uses the App Router and 'use client' files.

A file that starts with 'use client' runs in the browser. So does each file that it reaches through runtime imports. The preset refuses a chain of imports from a client file to the package server-only or to a Node built-in module, such as node:fs.

A chain stops at a file that starts with 'use server'. Next.js changes an import of a server action into a network call, so the code of the action does not go to the browser.

The preset has one rule, nextjs/client-stays-client. It counts only runtime imports, because the compiler removes an import type.

The example app has a cart page, a client button, and a module that reads the cart from disk:

  • archangel.config.ts
  • tsconfig.json
  • Directoryapp/
    • Directorycart/
      • actions.ts
      • page.tsx
  • Directorycomponents/
    • cart-button.tsx
  • Directorylib/
    • cart.ts
    • format.ts
  1. Put the preset in extends:

    archangel.config.ts
    import { defineConfig, nextjs } from '@enhance-eng/archangel';
    export default defineConfig({
    extends: [nextjs()],
    });

    nextjs() checks the whole repository. To check one app of a monorepo, give its folder: nextjs({ root: 'apps/web' }).

  2. Check the app:

    Terminal
    $ npx archangel check
    error[may-not-import]: components/cart-button.tsx reaches server-only through components/cart-button.tsx → lib/format.ts → lib/cart.ts → server-only
    --> components/cart-button.tsx:3:29
    |
    3 | import { formatTotal } from '@/lib/format';
    | ^^^^^^^^^^^^^^ remove an import of the chain, or move what the file needs into code that does not reach it
    |
    = note: rule: nextjs/client-stays-client
    = note: reason: Code that runs in the browser cannot use server-only code or Node built-in modules.
    = note: see: https://nextjs.org/docs/app/getting-started/server-and-client-components
    checked 6 files and 9 imports: 1 problem
    For what a code means, run `archangel docs may-not-import`.

    The button is a client component. It imports lib/format.ts to format the total. format.ts imports lib/cart.ts for one constant, and cart.ts imports server-only. So the button pulls server code into the browser. The error shows the full chain.

These are the files of the chain:

components/cart-button.tsx
'use client';
import { checkout } from '@/app/cart/actions';
import { formatTotal } from '@/lib/format';
export function CartButton({ total }: { total: number }) {
return <button onClick={() => checkout()}>Pay {formatTotal(total)}</button>;
}
lib/format.ts
import { currency } from './cart';
export function formatTotal(cents: number): string {
return `${currency} ${(cents / 100).toFixed(2)}`;
}
lib/cart.ts
import 'server-only';
import { readFile } from 'node:fs/promises';
export const currency = 'USD';
export async function cartTotal(): Promise<number> {
return Number(await readFile('cart.txt', 'utf8'));
}
export async function saveCart(): Promise<void> {}

The button also imports checkout from app/cart/actions.ts, which also imports lib/cart.ts. The rule does not refuse that chain, because actions.ts starts with 'use server'.

  1. Move the constant into format.ts, so that format.ts imports nothing from the server:

    lib/format.ts
    export const currency = 'USD';
    export function formatTotal(cents: number): string {
    return `${currency} ${(cents / 100).toFixed(2)}`;
    }
  2. Check again:

    Terminal
    $ npx archangel check
    checked 6 files and 8 imports: no problems

archangel why shows the shortest chain of imports from one file to a file or a package:

Terminal
$ npx archangel why components/cart-button.tsx server-only
components/cart-button.tsx:2 imports '@/app/cart/actions'
app/cart/actions.ts:2 imports '@/lib/cart'
lib/cart.ts:1 imports 'server-only'
server-only
Terminal
$ npx archangel test
pass nextjs/client-stays-client accepted #1 app/example-actions.ts, app/example-page.tsx
pass nextjs/client-stays-client rejected #1 app/example-helper.ts, app/example-page.tsx
2 examples, none failed; 0 rules without a rejected example

The preset brings its own examples: one client page that reaches node:fs through a helper, and one that reaches it only through a 'use server' file.

  • Presets lists the options of nextjs.
  • Conditions tells you how to write your own rule about chains of imports, with transitively and stopAt.