By Wallace Ferreira2 min read

Server Components Are the Composition Root

Why this site calls use cases directly from Server Components at build time, passes plain data down as props, and never fetches from a useEffect.

react-server-componentsclean-architecturenextjs

A classic React app has a data-fetching problem: components mount, fire a useEffect, show a spinner, then re-render. The App Router removes the problem instead of managing it better.

The rule

  • Server Components import @repo/application and call use cases directly. This runs at build time (SSG) or on the server — the code never reaches the browser.
  • 'use client' components never import @repo/application. They receive data as props from a Server Component.
  • There is no useEffect for data. If a client component needs server data, a Server Component fetches it and passes it down.
// Server Component — the composition root for this page
export default async function ProjectsPage({ params }: Props) {
  const { locale } = await params;
 
  const result = await new GetPublishedProjects(
    getServerContainer().projectRepository,
    getServerContainer().skillRepository,
  ).execute({ locale });
 
  const projects = result.isRight() ? result.value : [];
 
  return <ProjectList projects={projects} />; // plain data, down as props
}

Why this is Clean Architecture, not a shortcut

The dependency rule is core ← application ← infra ← apps. The Server Component sits at the outermost ring: it is allowed to know about application and to wire the container. It is the composition root — the one place that assembles concrete dependencies — and the App Router gives you one per route, for free, running before any HTML is sent.

The client bundle only ever sees the DTO. It cannot import a use case even by accident, because the import would pull server-only code into the browser build and fail. The architecture boundary is enforced by the bundler, not by discipline.

About the author

Wallace Ferreira