Modern enterprise platforms often contain multiple applications that serve different business capabilities.
At first, maintaining these applications as completely independent projects may seem straightforward. Over time, however, teams often discover that the applications share a significant amount of functionality: authentication, UI components, API clients, validation, utilities, configuration, types, and common business behaviour.
One approach to solving this problem is a frontend monorepo.
In one of the enterprise applications I worked on, multiple React applications supporting different business capabilities were maintained within a single Git repository. These included applications such as:
Agreements
Nominations
Cargo Operations
Deal Check
Administration
Feature Management
Although these applications belonged to the same overall platform, they remained separate applications with their own functional responsibilities.
The monorepo approach allowed us to maintain these applications together while sharing common frontend capabilities.
This article explains the architecture, the reasoning behind the approach, its benefits and challenges, and some of the lessons learned from implementing a multi-application React monorepo.
1. What Is a Monorepo?
A monorepo, or monolithic repository, is a source-control strategy where multiple applications and libraries are maintained in a single repository.
Without a monorepo, we could have:
agreement-ui
nominations-ui
cargo-ops-ui
deal-check-ui
admin-ui
shared-uiEach application could have its own Git repository.
With a monorepo, these projects are brought together:
enterprise-frontend/
│
├── apps/
│ ├── agreements/
│ ├── nominations/
│ ├── cargo-operations/
│ ├── deal-check/
│ ├── administration/
│ └── feature-management/
│
├── packages/
│ ├── ui-components/
│ ├── authentication/
│ ├── api-client/
│ ├── shared-types/
│ ├── utilities/
│ └── configuration/
│
├── package.json
├── configuration/
└── README.mdThe important point is that one repository does not mean one application.
A monorepo can contain multiple independently deployable applications.
That distinction is particularly important in enterprise architecture.
2. Monorepo Does Not Mean Monolith
The terms are sometimes confused.
A monolithic application might look like:
One Application
|
+--------------+------------------+
| | |
Agreements Nominations Cargo Ops
| | |
+--------------+------------------+
|
DatabaseEverything is part of the same application and generally shares the same deployment lifecycle.
A frontend monorepo is different:
Git Monorepo
|
+--------------+------------------+
| | |
▼ ▼ ▼
Agreements Nominations Cargo Ops
App App App
| | |
▼ ▼ ▼
Deployable Deployable Deployable
Application Application ApplicationThe repository is shared, but the applications can remain logically and operationally separate.
This gives us an important architectural principle:
Repository boundaries and application boundaries do not have to be the same thing.
3. Why Use a Monorepo for Multiple React Applications?
Consider an enterprise platform with several React applications.
Each application may require:
Authentication
Authorization
Header
Navigation
Buttons
Dialogs
Data Grid
Notifications
API communication
Validation
Logging
Common utilitiesWith separate repositories, these capabilities can become duplicated.
For example:
Agreement Repository
├── Login
├── Button
├── DataGrid
└── API Client
Nominations Repository
├── Login
├── Button
├── DataGrid
└── API Client
Cargo Repository
├── Login
├── Button
├── DataGrid
└── API ClientThis creates several problems.
A bug fixed in one implementation may need to be fixed in several others.
A UI change may have to be replicated across repositories.
Different teams may use different versions of the same library.
Over time, the applications can start behaving differently.
A monorepo provides another option:
Shared Libraries
|
+-----------------+-----------------+
| | |
▼ ▼ ▼
Agreements Nominations Cargo OpsCommon functionality can be implemented once and consumed by multiple applications.
4. High-Level Architecture
A simplified version of the frontend architecture can be represented as follows:
React Monorepo
|
+--------------------------+-------------------------+
| | |
▼ ▼ ▼
+----------------+ +----------------+ +-----------------+
| Agreements | | Nominations | | Cargo Ops |
| React App | | React App | | React App |
+------+---------+ +---------+-------+ +---------+-------+
| | |
+--------------------------+--------------------------+
|
Shared Frontend Libraries
|
+----------------------+----------------------+
| | |
▼ ▼ ▼
UI Components Authentication API Client
| | |
+----------------------+----------------------+
|
▼
API Gateway
|
+-----------------+-----------------+
| | |
▼ ▼ ▼
Agreement MS Nomination MS Cargo Ops MSThis architecture gives us two important characteristics:
Shared development model
Applications and common libraries live together.
Independent application boundaries
Each application still represents a specific business capability.
5. Applications and Shared Libraries
One of the most important design decisions in a monorepo is separating applications from reusable libraries.
A conceptual structure could be:
apps/
agreements/
nominations/
cargo-operations/
deal-check/
packages/
ui/
authentication/
api-client/
models/
utilities/The apps directory contains executable applications.
The packages directory contains reusable libraries.
For example:
apps/agreements
|
+----> packages/ui
|
+----> packages/authentication
|
+----> packages/api-client
|
+----> packages/utilitiesThe Nominations application can use the same libraries:
apps/nominations
|
+----> packages/ui
|
+----> packages/authentication
|
+----> packages/api-client
|
+----> packages/utilitiesThis is one of the major benefits of the monorepo approach.
6. Shared UI Components
Enterprise applications generally contain many common UI components.
For example:
packages/ui/
Button
Modal
Dialog
DataGrid
DatePicker
Dropdown
Notification
LoadingIndicator
FormControlsInstead of every application implementing its own version of these components, they can consume a common library.
For example:
import { Button } from '@platform/ui';
<Button
variant="primary"
onClick={handleSave}>
Save
</Button>The Agreements application and Cargo Operations application can use the same component.
This provides consistency in:
Appearance
Behaviour
Accessibility
Validation
Error handling
User interaction
A change to a shared component can then be propagated to the applications that consume it.
7. Avoiding the "Shared Everything" Problem
There is an important warning here.
Just because code can be shared does not mean it should be shared.
For example, imagine:
packages/shared/
agreementLogic
nominationLogic
cargoLogic
dealLogic
adminLogic
...Eventually, the shared package can become a dumping ground for unrelated functionality.
Instead, shared code should have a clear purpose.
A useful rule is:
Share technical capabilities aggressively, but share business logic deliberately.
Good candidates for shared libraries:
UI components
Authentication
HTTP client
Logging
Date utilities
Common validation
Configuration
Type definitionsBusiness-specific logic should generally remain within the relevant application unless there is a genuine common domain requirement.
8. Shared Authentication
Authentication is another strong candidate for reuse.
Suppose every application needs to understand:
Login
Token handling
Token refresh
Logout
Session management
Authorization
Route protectionImplementing these separately creates unnecessary duplication.
Instead:
packages/authenticationcan provide common capabilities.
Conceptually:
Authentication Library
|
+------------------+-------------------+
| | |
▼ ▼ ▼
Agreements Nominations Cargo OpsEach application consumes the same authentication behaviour.
This also provides a central place to address changes to authentication mechanisms.
For example, if the identity provider changes, the implementation can potentially be updated in one shared location rather than separately across every application.
9. Authorization and Route Protection
Authentication answers:
Who is the user?
Authorization answers:
What is the user allowed to do?
Different business applications may expose different permissions.
For example:
User
|
+-- Agreements
| +-- View
| +-- Edit
| +-- Approve
|
+-- Nominations
| +-- View
| +-- Create
|
+-- Cargo Operations
+-- View
+-- ManageA shared authorization mechanism can provide common permission handling while individual applications determine which permissions are relevant to their functionality.
A conceptual React route might look like:
<ProtectedRoute
permission="Agreement.Edit">
<AgreementEditPage />
</ProtectedRoute>The actual implementation can vary depending on the authentication and authorization mechanism used.
10. Shared API Client
Another important shared capability is communication with backend APIs.
Without a common API client, every application might independently implement:
HTTP requests
Headers
Authentication tokens
Error handling
Timeouts
Retries
Correlation IDs
LoggingA shared client can centralize these concerns.
Shared API Client
|
+--------------------+--------------------+
| | |
▼ ▼ ▼
Agreements API Nominations API Cargo APIFor example:
const response =
await apiClient.get<AgreementContract>(
`/agreements/${agreementId}`
);The client can handle cross-cutting concerns without forcing every page to implement them repeatedly.
11. Contract-Driven Frontend Integration
This monorepo architecture becomes even more interesting when combined with the contract-driven API approach discussed in the first article.
The complete flow becomes:
React Application
|
v
Shared API Client
|
v
API Gateway
|
v
.NET Microservice
|
v
Contract / DTO
|
v
React ApplicationFor example:
Agreement Application
|
| GET
v
API Gateway
|
v
Agreement Service
|
v
AgreementContract
|
v
React StateThe user modifies the data.
During save:
React State
|
v
AgreementContract
|
v
Shared API Client
|
v
API Gateway
|
v
Agreement Service
|
v
EF Core
|
v
SQL ServerThis creates a clear frontend/backend boundary.
12. Shared TypeScript Contracts
If the backend exposes well-defined contracts, frontend TypeScript models can represent them.
For example:
export interface AgreementContract {
agreementId: number;
agreementNumber: string;
status: string;
effectiveDate: string;
expiryDate?: string;
rowVersion: string;
}The model can be consumed by the Agreements application:
import {
AgreementContract
} from '@platform/models';The benefit is consistency.
Instead of defining the same contract differently in several applications:
Agreement App
AgreementContract
Admin App
AgreementContract
Reporting App
AgreementContracta common representation can be maintained where appropriate.
However, shared contracts should be managed carefully.
If an application has different requirements from another application, forcing both to consume one giant model can create unnecessary coupling.
13. Dependency Graph
One of the most useful concepts in a monorepo is the dependency graph.
Consider:
UI Library
|
+--------------+--------------+
| | |
▼ ▼ ▼
Agreements Nominations Cargo Ops
| | |
+--------------+--------------+
|
API Client
|
Authentication
Join the conversation! Your thoughts help the community grow.