React

Next.js in a monorepo: the structure that holds

A Next.js monorepo pays off when apps share components and config. The minimum structure with pnpm, the packages worth extracting and the real costs.

Key points

  • One lockfile and one React version: two copies break hooks at runtime.
  • `apps/` holds deployments, `packages/` holds shared code with an npm name.
  • The UI package must be transpiled or compiled: Next does not read sources outside its folder.

A Next.js monorepo pays off when two applications genuinely share components, tokens and configuration. The minimum structure is apps/ for deployments, packages/ for shared code and a single lockfile at the root.

The minimum structure

repo/
  apps/
    web/            # app principale, un deploy
      app/
      next.config.ts
      package.json
    docs/           # seconda app, stesso design system
  packages/
    ui/             # componenti condivisi
    config/         # eslint, tsconfig, tailwind
    lib/            # utility pure
  package.json
  pnpm-workspace.yaml
  turbo.json

The workspace is declared in pnpm-workspace.yaml; every folder under apps/ and packages/ carries a package.json with an npm name. Next.js does not know it lives in a monorepo: the module resolver is what links the packages.

What to share and what to keep apart

ResourceWhereWhy
Tokens and UI primitivespackages/uiOne place to update
ESLint and tsconfigpackages/configApps inherit, they do not copy
Shared domain logicpackages/libPure functions, testable alone
Routes and layoutsapps/web/appThey are URLs, not reusable code

A package that ships TSX files must be listed in transpilePackages, otherwise the Next build ignores files outside its own folder. If the package is also used outside Next, compile it and publish the output.

// apps/web/next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  transpilePackages: ["@repo/ui", "@repo/lib"],
};

export default nextConfig;

The command that exposes the problem

# una sola versione di React in tutto il workspace
pnpm why react

# build della sola app: rivela gli import fuori dal pacchetto
pnpm --filter @repo/web build

If pnpm why react prints two versions, hooks break with “Invalid hook call” at runtime and the message never says why. The cause is a package that declares React as a normal dependency instead of a peer.

The record I use as an example

The EneUI catalogue indexes 111,985 components and 1,556 blocks from 1,472 libraries (1 November 2026). A complete block installs as source into packages/ui: dashboard-01, with the dashboard category and the primitives in card.

Where a monorepo costs

  • Remote build needs a cache token and a configuration nobody maintains.
  • The standalone output must ship with the root lockfile, not the app one.
  • A shared package without tests becomes a regression point for every app.
  • The Next upgrade slips, because it must pass on every app before the merge.

Known limit: Next cannot resolve a package that exposes ESM only, with no exports field. The build fails with a syntax error that points at the wrong file, and the fix lives in the package package.json.

For the structure of a single app see App Router structure. For caching that survives the request see the four cache layers; the framework comparison for a marketing site is Next.js vs Vite.

Frequently asked questions

Do I need Turborepo for a Next.js monorepo?

No. A pnpm workspace is enough for two apps and about ten packages. Turborepo adds task caching and a dependency graph once builds pass five minutes.

Where does the design system go?

In packages/ui. It needs an npm name, a package.json and no dependency on the app that consumes it.

How do I avoid two copies of React?

Declare React and React DOM as peerDependencies in shared packages and as dependencies only in the apps. Then check with pnpm why react.

One app or several apps?

One app as long as sharing concerns components only. A second app arrives when it has its own deploy, domain or audience.

Does the Vercel deploy work with no configuration?

With pnpm yes, if the lockfile sits at the root and the build command filters the package. With npm and nested folders you must set the project root directory.