React Monorepo Architecture for Next.js Teams
React Monorepo Architecture
React monorepo architecture is no longer just a niche setup for large engineering departments. For many SaaS companies, frontend platform teams, and product-led engineering groups, it has become a practical way to manage multiple React and Next.js applications without duplicating the same UI components, configuration files, testing utilities, and deployment logic across every project.
The idea sounds simple: keep related applications and packages in one repository. In practice, though, a good monorepo is not just a big folder with many apps inside it. It is an architecture decision. It affects how teams share code, enforce standards, review changes, run CI, version internal packages, and ship frontend features without stepping on each other.
React itself is built around composition. The official React documentation describes React as a way to build interfaces from individual components and combine them into full screens, pages, and apps. That component model is one reason React teams often feel the pain of duplicated code early: once the same button, form pattern, authentication wrapper, analytics hook, or design token appears in three apps, the need for shared ownership becomes obvious. (React)
For Next.js teams, the pressure is even stronger. A company may have a marketing site, customer dashboard, admin console, documentation portal, partner app, checkout flow, and internal tool. Each one may be a separate Next.js app, but all of them still need shared UI, shared TypeScript settings, shared lint rules, shared API clients, and shared release discipline.
That is where a frontend monorepo can help. Done well, it gives teams a common platform without forcing every product into one giant application. Done poorly, it becomes a slow, tangled repository where nobody knows which package depends on what.
This guide explains how to design React monorepo architecture for real Next.js teams: what to put in the repo, how to structure apps and packages, when to use Turborepo or Nx, how to think about CI, and how to avoid the common traps that turn a good platform idea into daily friction. The requested topic and output structure are based on the provided article brief.
What Is React Monorepo Architecture?
React monorepo architecture is a repository strategy where multiple React-based applications and shared packages live in a single version-controlled codebase.
A typical setup may include:
- One or more Next.js applications
- Shared React component libraries
- Shared hooks and utilities
- Shared TypeScript configuration
- Shared ESLint and formatting configuration
- Shared design tokens
- Shared API clients
- Shared testing utilities
- Build orchestration through tools like Turborepo or Nx
The key point is not only “one repo.” The key point is coordinated development across multiple frontend projects.
A simple version might look like this:
apps/
web/
admin/
docs/
packages/
ui/
config-typescript/
config-eslint/
analytics/
api-client/
auth/
design-tokens/
In this setup, apps/web might be the public product app, apps/admin might be an internal dashboard, and apps/docs might be a documentation site. The packages/ui folder can hold reusable React components. The packages/auth folder can hold shared authentication helpers. The packages/config-typescript folder can hold a base TypeScript configuration used by every app.
That structure helps teams avoid a common pattern: copying code from one React app into another, tweaking it slightly, and then forgetting which version is correct.
A monorepo does not mean every app must deploy together. It also does not mean every team must work in the same folder. A mature frontend monorepo usually separates apps and packages clearly, then uses tooling to run only the tasks that matter for a given change.
Why React and Next.js Teams Move to a Monorepo
Teams rarely adopt a monorepo because it sounds trendy. They usually adopt it because their current workflow has become expensive.
The warning signs are familiar.
A design system exists, but each app has its own version of the same components. A bug gets fixed in the dashboard but remains broken in the admin portal. TypeScript settings drift. ESLint rules differ. The marketing app uses one analytics wrapper while the product app uses another. Developers spend more time asking “Where does this code live?” than actually improving the product.
At first, separate repositories feel clean. Each app has its own lifecycle. Each team can move independently. But as the organization grows, the same independence can turn into fragmentation.
A React monorepo architecture solves this by making shared frontend infrastructure visible and reusable.
Shared UI Without Copy-Paste
React encourages teams to build interfaces out of components. That works beautifully inside a single app. Across multiple apps, it only works well if shared components have a real home.
A frontend monorepo gives your shared components a first-class package:
packages/ui/
src/
button.tsx
card.tsx
dialog.tsx
form-field.tsx
Instead of copying a button into five projects, teams import the same button package:
import { Button } from "@acme/ui";
This does not automatically create a good design system. A messy component library is still messy inside a monorepo. But the architecture makes reuse easier and makes divergence more visible.
Consistent Configuration
Frontend teams often underestimate the value of shared configuration.
A large React estate may need consistent rules for:
- TypeScript strictness
- JSX settings
- Import aliases
- ESLint rules
- Prettier formatting
- Testing setup
- Build targets
- Browser support
- Package export patterns
Without a monorepo, each app often owns its own config. Over time, small differences pile up. One app allows unsafe TypeScript patterns. Another has old lint rules. Another uses a slightly different test setup.
A monorepo can centralize those rules:
packages/
config-typescript/
config-eslint/
config-jest/
That matters because frontend architecture is not only about components. It is also about constraints. Good constraints make large teams faster because developers do not need to re-debate basic rules in every repository.
Faster Platform Changes
Platform work is hard in a multi-repo world. Suppose your team wants to update a shared analytics event format, replace a deprecated package, or enforce a new accessibility rule across every React app.
In separate repositories, that may require many pull requests across many repos. Each repo may have its own CI, owners, release process, and blockers.
In a monorepo, the platform team can change the shared package, update the consuming apps, run affected tests, and review the full impact in one place. That does not remove complexity, but it gives the complexity a single surface area.
Better Developer Experience
A well-built React monorepo can make onboarding easier. A new developer can clone one repository, install dependencies once, run a single command, and inspect related apps and packages together.
That is useful for SaaS teams where features often cross boundaries. A billing change may touch the customer app, admin tools, shared API types, and a UI package. In a monorepo, the relationship between those pieces is easier to inspect.
When a Monorepo Is the Right Choice
A monorepo is useful when your frontend codebase has shared ownership and repeated patterns.
It is usually a strong fit when:
- You maintain multiple React or Next.js apps.
- Several apps use the same design system.
- Teams duplicate hooks, utilities, or config across repositories.
- You need coordinated changes across apps.
- You want one place for shared frontend platform code.
- CI cost is increasing because every project runs too much work.
- You need stronger dependency visibility.
For an enterprise frontend architecture, a monorepo can become the foundation for a developer platform. It gives platform teams a way to define standards and product teams a way to consume those standards without rebuilding the same base layer.
It is also useful for SaaS companies with multiple customer-facing surfaces. A company might have separate apps for onboarding, billing, reporting, account management, support, and admin. Those surfaces may have different release cadences, but they still need a consistent interface and shared business logic.
When a Monorepo Is the Wrong Choice
A monorepo is not always the answer.
It can be a poor fit when:
- Your apps have almost no shared code.
- Teams need strict repository-level isolation.
- You lack ownership rules for shared packages.
- CI is already fragile and nobody owns build tooling.
- Security or compliance requirements demand stronger separation.
- The organization wants a monorepo only because competitors use one.
A monorepo concentrates complexity. That is helpful when the complexity is already shared. It is harmful when unrelated projects are pushed together without a clear platform strategy.
The wrong question is: “Should every React team use a monorepo?”
The better question is: “Do our apps share enough code, standards, and delivery workflows that one coordinated repository would reduce friction?”
If the answer is yes, a monorepo is worth serious consideration. If the answer is no, separate repositories may remain cleaner.
The Core Building Blocks of a React Monorepo
A solid React monorepo has a few core building blocks. Tooling matters, but structure comes first.
Apps
Apps are deployable units. In a Next.js monorepo, each app usually maps to a product surface or website.
Examples:
apps/
marketing/
dashboard/
admin/
docs/
Each app should be able to run, build, test, and deploy independently. That independence is important. A monorepo should not become a single deployable blob unless your product truly works that way.
For most SaaS teams, independent apps inside one repository are the sweet spot.
Packages
Packages are shared units of code. They should have a clear purpose.
Examples:
packages/
ui/
auth/
api-client/
analytics/
feature-flags/
design-tokens/
config-eslint/
config-typescript/
A package should not become a junk drawer. If everything goes into packages/shared, the monorepo will become hard to reason about. Split packages by responsibility, not by vague convenience.
Good packages have clear names, boundaries, and owners.
Workspace Configuration
The workspace layer tells the package manager which folders are part of the monorepo.
Common choices include pnpm workspaces, npm workspaces, Yarn workspaces, or Bun workspaces. The exact package manager choice depends on team preference, ecosystem compatibility, and deployment environment.
For many React and Next.js monorepos, pnpm is popular because it handles workspaces well and encourages stricter dependency behavior. That said, the architecture does not depend on pnpm alone. The real requirement is that your package manager can link local packages reliably and install dependencies consistently.
Task Orchestration
Task orchestration is where tools like Turborepo and Nx enter the picture.
A monorepo needs a way to answer questions like:
- Which packages need to build first?
- Which apps depend on this package?
- Which tests should run for this pull request?
- Which task outputs can be cached?
- Which tasks can run in parallel?
Without orchestration, developers often run everything. That works when the repository is small. It becomes painful when the repo has many apps and packages.
Recommended Folder Structure for a Next.js Monorepo
A practical Next.js monorepo structure should be boring, explicit, and easy to inspect.
Here is a solid baseline:
acme-frontend/
apps/
web/
app/
components/
next.config.ts
package.json
admin/
app/
components/
next.config.ts
package.json
docs/
app/
next.config.ts
package.json
packages/
ui/
src/
package.json
tsconfig.json
auth/
src/
package.json
tsconfig.json
api-client/
src/
package.json
tsconfig.json
analytics/
src/
package.json
tsconfig.json
design-tokens/
src/
package.json
tsconfig.json
config-typescript/
base.json
nextjs.json
package.json
config-eslint/
base.js
next.js
package.json
package.json
turbo.json or nx.json
pnpm-workspace.yaml
tsconfig.json
This structure separates deployable applications from reusable packages. That separation is the backbone of most healthy frontend monorepos.
Keep Apps Thin
Apps should contain app-specific routing, page composition, and product-specific behavior. They should not become storage containers for reusable business logic that other apps need.
For example, apps/dashboard can own dashboard pages and dashboard-only components. But a reusable date picker, billing API client, or role-checking helper probably belongs in packages.
A good rule: if two apps need it, consider moving it into a package. If one app needs it and no other app is likely to need it, keep it local.
Keep Packages Focused
Packages should have a narrow reason to exist.
@acme/ui can own shared UI primitives and composed components.@acme/auth can own authentication helpers.@acme/api-client can own typed API calls.@acme/analytics can own event tracking wrappers.@acme/design-tokens can own colors, spacing, typography scales, and theme variables.
Avoid a package like @acme/common unless you enforce strict boundaries inside it. “Common” packages tend to attract unrelated code. Over time, they become a dependency magnet.
Use Clear Package Names
Internal package names should be predictable:
{
"name": "@acme/ui"
}
Names should communicate ownership and purpose. Avoid clever naming. A new developer should be able to guess what a package does from its name.
Designing Shared React Packages
The biggest benefit of React monorepo architecture is shared frontend code. The biggest risk is shared frontend code.
That sounds contradictory, but it is true. Shared packages can speed up development, or they can create coupling so tight that every change becomes risky.
Start With UI, Config, and Utilities
The safest shared packages are usually:
- UI components
- TypeScript config
- ESLint config
- Design tokens
- Formatting rules
- Small utility functions
- Testing helpers
These packages create consistency without pulling too much business logic into the platform layer.
Business logic can be shared too, but it needs more care. If billing, authorization, or subscription logic differs between apps, do not hide those differences inside a generic helper too early.
Export Stable APIs
Shared packages should expose stable entry points.
For example:
export { Button } from "./button";
export { Dialog } from "./dialog";
export { FormField } from "./form-field";
Avoid encouraging apps to import deeply from internal files:
// Avoid this pattern
import { Button } from "@acme/ui/src/components/button/button";
Deep imports make refactoring harder. They also blur the boundary between public package API and private implementation.
Be Careful With React Server Components
Next.js teams using the App Router need to be careful with shared component packages. Some components are server-safe. Others require client-side behavior and need a "use client" directive.
A shared UI package should make this distinction clear. Do not casually mix server-only helpers, client components, browser APIs, and Node APIs in the same entry point.
For example:
packages/ui/
src/
server/
client/
primitives/
Or use clear file-level conventions:
button.tsx
dialog.client.tsx
theme-provider.client.tsx
The exact convention matters less than consistency. Teams need to know whether importing a component will pull client-side JavaScript into a page.
Use Next.js transpilePackages When Needed
Next.js supports automatically transpiling and bundling dependencies from local packages in monorepos through the transpilePackages option. The official Next.js documentation notes that this option can handle local packages and replaces the older next-transpile-modules approach. (Next.js)
A common setup looks like this:
const nextConfig = {
transpilePackages: ["@acme/ui", "@acme/auth", "@acme/api-client"],
};
export default nextConfig;
This is especially relevant when local packages ship TypeScript or modern JavaScript that the consuming Next.js app needs to process.
The important point is simple: do not assume local workspace packages will behave exactly like published npm packages. Test how they build inside each Next.js app.
Turborepo React Architecture
Turborepo is a popular choice for JavaScript and TypeScript monorepos, especially in the React and Next.js ecosystem. The official Turborepo documentation includes a Next.js monorepo guide and shows using create-turbo to start a repository with multiple Next.js applications. (Turborepo)
Turborepo is usually attractive when teams want a lightweight, task-focused tool that works with existing package scripts.
A Turborepo React setup often includes:
apps/
web/
admin/
packages/
ui/
eslint-config/
typescript-config/
turbo.json
package.json
pnpm-workspace.yaml
Each package defines scripts:
{
"scripts": {
"build": "next build",
"lint": "next lint",
"typecheck": "tsc --noEmit"
}
}
Then turbo.json defines how tasks relate to each other:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"lint": {
"outputs": []
},
"typecheck": {
"outputs": []
}
}
}
The dependsOn rule tells Turborepo that a package should build its dependencies first. The outputs tell it what files represent task results.
Where Turborepo Fits Best
Turborepo is often a good fit when:
- Your monorepo is mostly JavaScript or TypeScript.
- You want minimal framework lock-in.
- Your team already understands package scripts.
- You want caching and task pipelines without heavy structure.
- You use Vercel or a Next.js-heavy deployment model.
- You prefer explicit package ownership over generated workspace conventions.
Turborepo does not force a strict architecture. That can be a strength or a weakness. Experienced teams may like the flexibility. Less mature teams may need stronger guardrails.
Turborepo Strengths
Turborepo’s strengths are clarity and speed of adoption.
You can add it to an existing frontend monorepo without changing every folder. You can keep normal package.json scripts. You can define task dependencies in a central file. You can gradually introduce caching.
For React and Next.js teams, this makes Turborepo a practical first monorepo orchestrator. It handles the “run the right scripts in the right order” problem without requiring the team to adopt a large platform all at once.
Turborepo Trade-Offs
Turborepo gives you orchestration, but it does not automatically solve every architectural problem.
You still need to decide:
- How packages are named
- Which imports are allowed
- How boundaries are enforced
- How shared packages are versioned
- How apps deploy
- How ownership works
- How to prevent dependency sprawl
If your organization needs deep dependency graph enforcement, workspace generators, affected project analysis, and integrated enterprise workflows, Nx may be a better fit.
Nx React Architecture
Nx is another major option for frontend monorepo architecture. It provides task caching, affected commands, project graph features, generators, plugins, and CI-oriented capabilities. Nx’s React-focused page describes features such as caching, distributed tasks, affected targets, and project graph analysis for React monorepos. (Nx)
Nx can be used lightly or deeply. Some teams add Nx to an existing package-based monorepo mainly for caching and affected commands. Others use Nx as the central architecture layer for apps, libraries, generators, enforcement rules, and CI workflows.
A basic Nx-style layout may look like this:
apps/
web/
admin/
libs/
ui/
auth/
api-client/
Some teams prefer packages/ instead of libs/. Nx can support different workspace styles, but the architectural idea is similar: apps are deployable units, libraries or packages are shared code.
Where Nx Fits Best
Nx is often a strong fit when:
- The repository is large or expected to become large.
- Many teams contribute to the same monorepo.
- You need affected-only task execution.
- You want project graph visibility.
- You want generators for consistent project creation.
- You need stronger dependency boundary rules.
- CI cost and runtime are major concerns.
- You want a more integrated developer-platform model.
Nx’s affected command is particularly important for large workspaces. Its documentation explains that Nx can determine the minimum set of projects affected by a change and run tasks only on those projects. It uses Git changes and the project graph to identify impacted projects. (Nx)
That matters because “run every test for every pull request” stops working once the repository grows.
Nx Strengths
Nx is strong when a monorepo needs governance.
It can help answer:
- Which projects depend on this package?
- Which apps are affected by this change?
- Which boundaries should be enforced?
- Which projects need tests in this pull request?
- Which teams own which packages?
- Where are circular dependencies forming?
For enterprise frontend architecture, those answers are valuable. The larger the organization, the more important visibility becomes.
Nx Trade-Offs
Nx can feel heavier than Turborepo, especially for teams that only need basic task orchestration. Its deeper feature set is powerful, but it also requires ownership. Someone needs to understand the workspace model, maintain conventions, and help teams use it correctly.
If your team wants a small, package-script-centered setup, Turborepo may be easier. If your team wants a full monorepo platform with graph-aware workflows, Nx may be worth the extra structure.
Turborepo vs Nx for React and Next.js Teams
The Turborepo vs Nx decision should be based on team needs, not popularity.
Here is a practical comparison.
| Question | Turborepo may fit better | Nx may fit better |
|---|---|---|
| Team size | Small to mid-sized frontend teams | Mid-sized to large engineering orgs |
| Repo complexity | Mostly React/Next.js packages | Many apps, libraries, owners, and dependencies |
| Desired structure | Flexible package-script model | More governed workspace model |
| CI needs | Caching and task pipelines | Affected commands, graph-driven CI, distributed execution |
| Learning curve | Lower | Higher |
| Enforcement | Mostly team-defined | Stronger built-in boundary options |
| Best use case | Fast adoption for JS/TS monorepos | Enterprise monorepo platform |
For many Next.js monorepo teams, Turborepo is the easier starting point. It is especially natural when the repo has a handful of apps and shared packages.
For larger organizations, Nx often becomes more attractive because the problem changes. The challenge is no longer “How do we share a button?” It becomes “How do we coordinate hundreds of projects, enforce boundaries, and keep CI from burning time and money?”
There is no universal winner. There is only a better fit for your operating model.
Enterprise Frontend Architecture Concerns
A frontend monorepo is not just a developer convenience. In enterprise settings, it becomes part of the engineering operating system.
That means architecture decisions must include governance, ownership, security, deployment, and observability.
Ownership
Every shared package needs an owner.
Ownership can be a team, a working group, or a platform function. What matters is that someone is responsible for:
- Reviewing changes
- Maintaining documentation
- Handling breaking changes
- Supporting consuming teams
- Setting package direction
Without ownership, shared packages decay. Developers stop trusting them. Eventually, teams fork patterns locally, and the monorepo loses its value.
Boundaries
A monorepo needs boundaries because physical proximity makes bad imports easy.
For example, the public marketing app should not import internal admin-only code. A low-level UI package should not depend on a product-specific billing package. A design-token package should not import React components.
Healthy dependency direction often looks like this:
apps -> feature packages -> shared packages -> config/tokens
Unhealthy direction looks like this:
packages/ui -> apps/admin
packages/design-tokens -> packages/billing
packages/api-client -> apps/web/components
A good frontend monorepo makes the correct path easier than the wrong path.
Security and Access
A single repository can simplify access, but it can also broaden exposure.
Before moving sensitive frontend code into a monorepo, consider:
- Who can read the repository?
- Who can approve changes to shared packages?
- Which apps contain sensitive business logic?
- Which environment variables are available in CI?
- Which packages are published internally?
- Which deployment tokens are exposed to which workflows?
Frontend code is not a safe place for secrets, but CI pipelines and deployment workflows still need careful access control.
Release Strategy
Monorepos do not require one release.
A good Next.js monorepo can deploy each app independently:
apps/web -> production web app
apps/admin -> internal admin deployment
apps/docs -> documentation deployment
Shared packages are usually versioned implicitly by the repository commit. That is one of the advantages of a monorepo: you can update a package and its consumers in the same pull request.
But if packages are also published externally or consumed by systems outside the monorepo, you need a formal versioning strategy.
CI/CD for a React Monorepo
CI is where monorepo architecture either proves its value or collapses under its own weight.
A naive monorepo CI pipeline runs every lint, test, typecheck, and build task for every pull request. That is simple, but it does not scale.
A better pipeline answers three questions:
- What changed?
- What depends on what changed?
- Which tasks must run now?
Baseline CI Tasks
Most React and Next.js monorepos need these tasks:
lint
typecheck
test
build
e2e
format-check
Not every task must run on every package for every change.
For example, editing documentation may not require rebuilding every Next.js app. Changing a design-token package may require testing the UI package and affected apps. Changing a shared API client may require more typechecking across consumers.
Caching
Caching avoids repeating work when inputs have not changed.
In a monorepo, caching is especially useful because many packages are untouched in a given pull request. If the build output for a package is already valid, the task runner can reuse it instead of rebuilding.
Both Turborepo and Nx support caching-oriented workflows. Nx’s React page specifically highlights task caching and remote caching for avoiding redundant builds. (Nx)
The practical lesson: define task inputs and outputs carefully. Bad cache configuration can produce either missed optimization or, worse, stale results.
Affected-Only Execution
Affected-only execution means CI runs tasks only for projects impacted by the change.
Nx has first-class support for this workflow. Its documentation explains that affected commands use Git and the project graph to determine which projects belong to changed files and which projects depend on them. (Nx)
This is valuable for enterprise frontend architecture because the cost difference can be large. Running 10 relevant builds is better than running 80 unrelated builds.
Turborepo can also reduce unnecessary work through task pipelines and caching, but Nx’s affected-project model is one reason many larger teams evaluate Nx seriously.
Deployment
Each app should have its own deployment pipeline.
A change to apps/admin should not automatically redeploy apps/web unless shared packages changed in a way that affects both. A change to packages/ui may require redeploying several apps, depending on how your deployment system tracks dependencies.
A practical deployment model:
- Build and test affected apps.
- Deploy only apps affected by the change.
- Keep shared packages internal unless external publishing is required.
- Use environment-specific configuration per app.
- Keep deployment ownership clear.
TypeScript Strategy in a React Monorepo
TypeScript can either make a monorepo safer or painfully slow. The difference is configuration.
Use Shared Base Configs
A common pattern is to create shared TypeScript config packages:
packages/config-typescript/
base.json
nextjs.json
react-library.json
Then each app extends the relevant config:
{
"extends": "@acme/config-typescript/nextjs.json",
"compilerOptions": {
"baseUrl": "."
}
}
This keeps strictness and module settings consistent.
Avoid One Giant TypeScript Project
Do not treat the whole monorepo as one massive TypeScript project unless you have a very specific reason. That can make typechecking slow and hard to isolate.
Instead, each app and package should have its own tsconfig.json. Shared config should provide defaults, but each project should remain independently understandable.
Use Project References Carefully
TypeScript project references can help with build order and incremental compilation, but they also add complexity. For some teams, package-level typechecking through the task runner is enough. For others, project references are worth it.
The decision should be based on repository size, build time, and team familiarity.
Managing Dependencies
Dependency management is one of the hardest parts of frontend monorepo architecture.
A monorepo makes it easier to see dependency drift, but it does not automatically prevent it.
Define Dependency Rules
Teams should define rules such as:
- Apps can depend on packages.
- Shared packages cannot depend on apps.
- Low-level packages cannot depend on high-level product packages.
- UI packages should avoid product-specific business logic.
- Config packages should not depend on runtime packages.
- Packages should declare their own dependencies honestly.
These rules reduce surprise.
Avoid Hidden Dependencies
A common monorepo mistake is relying on dependencies that happen to be installed at the root but are not declared in the package that uses them.
Each package should declare what it needs. This makes packages more portable and reduces “works on my machine” issues.
Be Careful With Version Drift
In a monorepo, you may want one version of React, Next.js, TypeScript, and testing libraries across the workspace. That simplifies maintenance.
But not every dependency must be identical everywhere. Some apps may need different versions during migration. The key is to make version differences intentional, documented, and temporary where possible.
Designing a Shared UI Package
A shared UI package is often the first major package in a React monorepo. It is also where many teams learn hard lessons.
Start With Primitives
Begin with low-level components:
- Button
- Input
- Textarea
- Checkbox
- Radio
- Select
- Dialog
- Card
- Badge
- Tooltip
- Tabs
These primitives should be reusable across apps.
Avoid pushing highly specific product widgets into the core UI package too early. A “SubscriptionPlanUpgradeEnterpriseBanner” component probably does not belong in the same layer as Button.
Separate Design Tokens
Design tokens often deserve their own package:
packages/design-tokens/
src/
colors.ts
spacing.ts
typography.ts
This lets non-React surfaces or CSS layers use the same visual language.
Document Components
A UI package without documentation becomes tribal knowledge.
Documentation should include:
- Component purpose
- Props
- Usage examples
- Accessibility notes
- Do and don’t patterns
- Migration notes for breaking changes
Storybook can help, but the tool matters less than the discipline. Developers need to know how to use the component correctly.
Accessibility Should Be Built In
Shared UI is the right place to improve accessibility once and benefit many apps.
For example, dialog focus management, button semantics, form labels, error messages, and keyboard behavior should be handled carefully in the shared layer.
Do not treat accessibility as a later polish step. In a monorepo, mistakes in shared UI spread quickly.
Business Logic in a Frontend Monorepo
Not all shared code belongs in a shared package.
Business logic needs careful placement because it often changes faster than UI primitives.
Good Candidates for Shared Business Packages
These can work well as packages:
- API clients
- Authentication helpers
- Permission-checking utilities
- Feature-flag clients
- Analytics event wrappers
- Validation schemas
- Date and currency formatting utilities
These are useful because they centralize behavior that should be consistent.
Risky Candidates
These need caution:
- Product-specific workflows
- Pricing logic
- Checkout rules
- Complex onboarding flows
- Role-specific UI decisions
- App-specific state management
The more context a piece of logic needs, the less likely it belongs in a generic shared package.
A good test: can the package explain its purpose without naming one app? If not, it may belong closer to the app.
App-Specific Code vs Shared Code
A common monorepo mistake is sharing too early.
Sharing code creates a contract. Once multiple apps depend on a package, changing that package requires more care. That is good when the code is truly shared. It is wasteful when two apps only look similar for now.
Use this simple decision path:
| Situation | Recommendation |
|---|---|
| One app uses the code | Keep it inside the app |
| Two apps use nearly identical code | Consider a shared package |
| Multiple apps need consistent behavior | Move to a package |
| Code is experimental | Keep it local until stable |
| Code has app-specific assumptions | Keep it local or create a feature package |
| Code defines platform standards | Put it in a shared package |
The goal is not maximum sharing. The goal is useful sharing.
Next.js Monorepo Patterns
Next.js adds specific concerns to React monorepo architecture.
App Router and Shared Packages
With the App Router, teams need to understand server and client boundaries. A shared package that imports browser-only APIs can cause problems when used in server components. A package that mixes server and client behavior can increase bundle size or create confusing runtime errors.
Keep server-safe utilities separate from client-only components. Use clear entry points.
Example:
packages/auth/
src/
server.ts
client.ts
Then imports become explicit:
import { getServerSession } from "@acme/auth/server";
import { useUser } from "@acme/auth/client";
This prevents accidental mixing.
Environment Variables
Each Next.js app should own its environment configuration.
Shared packages should avoid reading app-specific environment variables directly unless that is a deliberate platform decision. Instead, pass configuration into shared clients.
Better:
createApiClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL });
Riskier:
// Hidden inside shared package
const baseUrl = process.env.NEXT_PUBLIC_API_URL;
Hidden environment assumptions make shared packages harder to reuse.
Deployment Output
Next.js deployment behavior can vary based on hosting platform and configuration. In monorepos, pay attention to app root paths, package tracing, local dependencies, and build outputs.
Do not assume that because an app builds locally, it will deploy correctly in every environment. Test each app’s production build and deployment path.
Governance for Frontend Platform Teams
Frontend platform teams often own the monorepo foundation. Their job is not to control every feature. Their job is to make the paved road reliable.
A platform team should define:
- Workspace structure
- Shared package rules
- Code ownership
- Review policy
- CI standards
- Release conventions
- Documentation standards
- Migration process
- Deprecation policy
The platform team should also avoid becoming a bottleneck. Product teams need the ability to move quickly. The platform should provide guardrails, not endless approval gates.
Create a Paved Road
A paved road is the recommended path for common work.
For example:
- Create a new Next.js app using a standard template.
- Create a new package using a standard package structure.
- Add a UI component using documented conventions.
- Run local checks with one command.
- Deploy an app through a known pipeline.
If the paved road is easier than improvising, teams will use it.
Document Decisions
Monorepos accumulate decisions. Without documentation, every new developer has to rediscover them.
Useful docs include:
- “How this repo is organized”
- “How to create a new app”
- “How to create a new package”
- “How to add a UI component”
- “How CI works”
- “How dependency boundaries work”
- “How releases work”
- “How to migrate from old patterns”
Keep docs close to the code. A docs/ folder or internal documentation app can work.
Common Mistakes in React Monorepo Architecture
A monorepo can fail in predictable ways.
Mistake 1: Creating a Shared Package for Everything
Not every repeated line deserves a package.
Over-sharing creates tight coupling. Teams then hesitate to change shared code because too many apps may break.
Share code when consistency matters. Keep code local when independence matters.
Mistake 2: No Ownership
A shared package without an owner becomes risky. Nobody knows who approves changes. Nobody handles bugs. Nobody plans migrations.
Every important package needs an owner.
Mistake 3: Weak Boundaries
If every package can import every other package, the dependency graph becomes a web.
Set dependency rules early. It is easier to prevent bad imports than to untangle them later.
Mistake 4: Slow CI
A monorepo with slow CI frustrates everyone. Developers start skipping checks, splitting changes awkwardly, or avoiding shared packages.
Invest in affected tasks, caching, and parallel execution before the repo becomes painful.
Mistake 5: Unclear App Deployment
Each app should have a clear deployment path. Developers should know what deploys when a package changes.
If a shared package changes, the system should make affected apps visible.
Mistake 6: Treating Tooling as Architecture
Turborepo and Nx are tools. They are not a substitute for decisions about ownership, boundaries, package design, and release strategy.
Choose tooling after you understand the architecture you need.
Migration Strategy: From Multi-Repo to Monorepo
Moving from multiple repositories to a monorepo should be staged. Big-bang migrations are risky.
Step 1: Inventory the Current Frontend Estate
List every app, shared library, duplicated component, config file, deployment pipeline, and dependency version.
You need to know what exists before merging it.
Step 2: Define the Target Structure
Decide how apps and packages will be organized.
Example:
apps/
web/
admin/
docs/
packages/
ui/
auth/
api-client/
config-eslint/
config-typescript/
Do not start by moving code randomly. Define the destination first.
Step 3: Move One App First
Pick one non-critical or moderately complex app. Move it into the monorepo and get install, dev, build, test, and deploy working.
This proves the foundation.
Step 4: Add Shared Config Packages
Move TypeScript, ESLint, and formatting rules into shared packages. This creates early value without forcing major product changes.
Step 5: Add UI and Utility Packages
Move stable shared components and utilities next. Start with code that is already duplicated and low risk.
Step 6: Migrate Remaining Apps Gradually
Bring apps into the monorepo one at a time. Keep deployment working throughout.
Step 7: Add Caching and Affected CI
Once multiple apps and packages exist, improve CI. Add task caching, affected execution, and clearer dependency visibility.
Commercial Context: Build vs Buy vs Platform Tools
React monorepo architecture often has commercial implications. Engineering managers need to think beyond code layout.
Internal Platform Cost
A monorepo requires maintenance:
- Tooling upgrades
- CI optimization
- Package governance
- Developer support
- Documentation
- Migration work
- Review discipline
This is not free. The return comes from reduced duplication, faster coordinated changes, better consistency, and lower long-term maintenance cost.
Tooling Cost
Some features may involve paid services, especially remote caching, distributed execution, enterprise support, analytics, or managed CI optimization.
The right decision depends on team size and CI cost. A small team may not need paid features. A large team may save money by reducing wasted compute and developer waiting time.
Avoid buying tooling before fixing architecture. Paid caching will not rescue a poorly structured repo with unclear dependencies.
Consulting and Platform Engineering
For larger organizations, monorepo migration may justify dedicated platform engineering work. This can be internal or external.
The valuable work is not just setup. It includes designing boundaries, migration paths, governance, and CI strategy.
Practical Architecture Blueprint
Here is a practical blueprint for a SaaS team with multiple Next.js apps.
Repository Structure
apps/
marketing/
app/
admin/
docs/
packages/
ui/
design-tokens/
auth/
api-client/
analytics/
feature-flags/
config-eslint/
config-typescript/
test-utils/
Dependency Direction
apps/* can import packages/*
feature packages can import shared packages
shared packages should avoid importing feature packages
config packages should stay low-level
apps should not import from other apps
CI Pipeline
For pull requests:
install
format-check
lint affected projects
typecheck affected projects
test affected projects
build affected apps
run selected e2e tests when needed
For main branch:
install
run required checks
build affected apps
deploy affected apps
publish internal artifacts if required
Package Ownership
@acme/ui -> design system/platform team
@acme/auth -> identity/platform team
@acme/api-client -> frontend platform/API team
@acme/analytics -> data/platform team
@acme/config-eslint -> frontend platform team
@acme/design-tokens -> design system team
Ownership avoids confusion when changes affect multiple consumers.
How to Choose the First Shared Packages
Start with packages that create high value and low risk.
Good First Packages
- TypeScript config
- ESLint config
- Prettier config
- Design tokens
- Basic UI primitives
- Test utilities
- Analytics wrapper
These packages usually improve consistency quickly.
Packages to Delay
- Complex business workflows
- App-specific state management
- Experimental features
- Half-finished design system components
- Product-specific UI sections
Delay these until the monorepo has clear patterns.
Measuring Success
A React monorepo should improve engineering flow. Measure that.
Useful indicators include:
- Fewer duplicated components
- Faster CI for typical pull requests
- Easier cross-app changes
- Reduced config drift
- More consistent UI
- Faster onboarding
- Clearer package ownership
- Fewer dependency conflicts
- Better visibility into affected apps
Avoid vanity metrics. A large number of packages does not prove success. A huge monorepo does not prove maturity. The real test is whether teams can ship safer changes with less friction.
Conclusion: React Monorepo Architecture Is a Platform Decision
React monorepo architecture works best when it is treated as a platform decision, not a folder preference.
For Next.js teams, a monorepo can bring order to shared UI, shared configuration, app-specific deployments, and frontend platform governance. It can make cross-app changes easier, reduce duplication, and give engineering managers better visibility into how frontend systems connect.
But the benefits are not automatic. A good monorepo needs clear app and package boundaries, strong ownership, reliable CI, careful dependency management, and tooling that matches the organization’s scale.
Turborepo is often a strong fit for React and Next.js teams that want lightweight task orchestration and fast adoption. Nx is often a strong fit for larger enterprise frontend architecture where affected commands, project graph visibility, and stronger governance matter.
The best architecture is the one your teams can maintain. Start with a clear structure. Share code deliberately. Keep apps independently deployable. Invest in CI before it hurts. And most importantly, treat the monorepo as a product for developers, not just a place where code happens to live.
FAQs
What is React monorepo architecture?
React monorepo architecture is a way to keep multiple React apps and shared packages in one repository. It usually includes apps, UI libraries, TypeScript config, lint rules, utilities, and build orchestration.
Is a monorepo good for Next.js projects?
Yes, a monorepo can work well for Next.js projects when several apps share UI, configuration, API clients, or platform code. Next.js also supports transpiling local monorepo packages through transpilePackages, which helps apps consume shared workspace packages. (Next.js)
Should I use Turborepo or Nx for a React monorepo?
Use Turborepo if you want a lighter task runner with caching and simple package-script workflows. Use Nx if you need deeper project graph visibility, affected-only execution, generators, and stronger monorepo governance.
What should go inside a frontend monorepo?
A frontend monorepo can include Next.js apps, shared React components, design tokens, TypeScript configs, ESLint configs, API clients, analytics utilities, authentication helpers, and test utilities. Keep app-specific code inside apps unless multiple apps genuinely need it.
What should not go into a shared React package?
Avoid putting app-specific workflows, unstable experiments, product-specific state logic, or highly contextual business rules into shared packages too early. Shared packages create contracts, so they should contain code that is stable and genuinely reusable.
Can each app deploy separately in a monorepo?
Yes. In most React and Next.js monorepos, each app should remain independently deployable. A monorepo controls source organization and shared code; it does not require every app to release at the same time.
How do monorepos improve CI for frontend teams?
Monorepos can improve CI by using task caching, dependency graphs, and affected-only execution. Nx, for example, can identify projects affected by a change and run tasks only on those projects, which can reduce unnecessary work in larger repositories. (Nx)
Is a monorepo only for large enterprise teams?
No. Small teams can use a monorepo too, especially if they maintain multiple React or Next.js apps. However, the architecture becomes more valuable as shared code, shared standards, and cross-app changes increase.
Does a monorepo replace a design system?
No. A monorepo can provide a good home for a design system, but it does not create one automatically. You still need component standards, accessibility rules, documentation, ownership, and review discipline.
What is the biggest risk of React monorepo architecture?
The biggest risk is unmanaged coupling. If packages import each other without clear rules, the repository can become slow and fragile. Strong boundaries, ownership, and CI discipline are essential.