My Next.js boundary rule is simple: protect truth on the server, send safe data across, and keep browser interactivity in the smallest useful client island.

"use client" is not a component preference.

It is a boundary declaration.

Everything imported through that boundary can become part of the browser graph. Props crossing into it are visible to that user. Server-only capabilities must stay on the other side.

I draw the line from responsibility, not file size.

Start with server by default

In the App Router, I begin with a Server Component when the component:

  • reads server data;
  • uses secrets or private SDKs;
  • checks authorization;
  • renders public content;
  • composes other components;
  • does not require browser events or state.

This keeps meaningful content available without a client fetch and reduces browser JavaScript.

Then I add a client boundary only where interaction demands it.

Move the event, not the whole page

Suppose a pricing page needs a monthly/annual toggle.

The page copy, plans, FAQ, and links do not need client state.

Only the toggle and price presentation do.

export default async function PricingPage() {
  const plans = await getPublicPlans();

  return (
    <>
      <PricingIntro />
      <BillingToggle plans={plans} />
      <PricingFaq />
    </>
  );
}

BillingToggle is the client island.

The page is not.

Keep server-only imports server-only

Database clients, filesystem access, private environment variables, admin SDKs, and secret-bearing provider code should live in server-only modules.

I do not rely on “nobody imports this from a client component.”

I make the boundary explicit and keep the dependency direction clean.

The client can call a protected server entry point.

It should not import the provider implementation.

Props are a public DTO

This is the question I ask before passing a prop:

If the user opens the browser tools, is it acceptable for them to see this value?

If not, do not cross the boundary.

Shape a DTO:

type PublicInvoice = {
  number: string;
  status: "draft" | "sent" | "paid";
  total: string;
};

Do not pass a full database record because the component currently uses three fields.

Future UI changes have a habit of exposing the fourth.

Mutations are public server entry points

Route Handlers and Server Actions still receive hostile input.

Inside the mutation:

  • validate input;
  • verify identity;
  • authorize the resource;
  • apply business rules;
  • handle idempotency;
  • return a controlled result;
  • invalidate affected representations.

A hidden button is not authorization.

A typed argument is not validation.

Avoid accidental waterfalls

Server Components can still fetch sequentially.

Bad:

const account = await getAccount();
const projects = await getProjects();

If the calls are independent:

const [account, projects] = await Promise.all([
  getAccount(),
  getProjects(),
]);

If one depends on the other, keep the dependency visible.

Do not parallelize blindly. Parallelize independent work.

Keep provider state out of global client context

Large client providers can pull broad trees into hydration.

I keep context close to the interaction:

  • a modal provider around modals;
  • form state inside the form;
  • table selection inside the table;
  • server data in server composition;
  • URL state in the URL where sharing matters.

Global client state should be earned.

Loading and error ownership follow the boundary

The server route owns:

  • data loading;
  • not-found;
  • protected access;
  • server dependency failures.

The client island owns:

  • pending interaction;
  • optimistic state;
  • browser validation feedback;
  • retry for its own action;
  • focus and announcement behavior.

Users should understand which part is waiting and what they can do next.

The review questions

Before accepting a boundary, I ask:

  1. Does this component need a browser API or event?
  2. Is the client boundary as small as the interaction?
  3. Are props safe for the user to inspect?
  4. Does the mutation re-check trust?
  5. Are independent requests parallel?
  6. Is state living at the narrowest useful owner?
  7. Do loading and error states match that ownership?

That review catches more than counting Client Components.

The result

A good server-client split is not visible to the user.

The page arrives with useful content.

The interaction feels immediate.

Secrets stay server-side.

The code tells the next developer where truth lives.

For the React mental model behind this, read React Server Components: The Boundary That Matters. For API boundaries, read API contracts that survive growth.

If a Next.js app has become one large client tree, send me the highest client boundary and why it exists.

  • #Next.js
  • #React
  • #Server Components
  • #Security
  • #Performance
M H Tawfik (Al Mojakkar Hossain Tawfik)

M H Tawfik (Al Mojakkar Hossain Tawfik)

Al Mojakkar Hossain Tawfik, professionally known as M H Tawfik and Tawfik, is a freelance Full-Stack Web Developer and the founder of SoftWebGrove. His legal-document name is Al Mojakkar Hossain.

Continue reading

Related field notes on engineering, SaaS, freelancing, and building dependable products.