Monorepo Setup for React + React Native + Node.js
Introduction
Managing multiple applications that share code can quickly become a nightmare without proper tooling. Whether you're building a web app with React, a mobile app with React Native, and a backend API with Node.js, keeping code synchronized, dependencies aligned, and deployments coordinated requires a sophisticated approach.
Monorepos (monolithic repositories) solve these challenges by housing multiple related projects in a single repository with shared tooling, dependencies, and workflows. This comprehensive guide will walk you through setting up a production-ready monorepo for React, React Native, and Node.js applications using modern tools like Nx, Lerna, and Yarn Workspaces.
You'll learn how to structure your codebase, share code between platforms, manage dependencies efficiently, and set up automated CI/CD pipelines that scale with your team.
Understanding Monorepos
What is a Monorepo?
A monorepo is a software development strategy where code for many projects is stored in the same repository. Key characteristics include:
- Single Source of Truth: All code lives in one place
- Shared Dependencies: Common packages managed centrally
- Atomic Changes: Update multiple apps in a single commit
- Unified Tooling: Consistent build, test, and deploy processes
- Code Sharing: Reusable components and utilities
Benefits vs. Challenges
Benefits
- Code Sharing: Maximize code reuse across platforms
- Dependency Management: Single lockfile, consistent versions
- Refactoring: Change shared code with confidence
- Developer Experience: Unified tooling and workflows
- Deployment: Coordinate releases across applications
Challenges
- Build Performance: Larger codebase requires optimization
- Tooling Complexity: More sophisticated setup required
- Team Coordination: Requires discipline and conventions
- Repository Size: Can become large over time
Project Structure Overview
Here's our target monorepo structure for a full-stack application:
my-monorepo/
├── apps/
│ ├── web/ # React web application
│ │ ├── src/
│ │ ├── public/
│ │ ├── package.json
│ │ └── webpack.config.js
│ ├── mobile/ # React Native application
│ │ ├── src/
│ │ ├── android/
│ │ ├── ios/
│ │ ├── package.json
│ │ └── metro.config.js
│ └── api/ # Node.js backend
│ ├── src/
│ ├── package.json
│ └── tsconfig.json
├── packages/
│ ├── shared-ui/ # Shared UI components
│ │ ├── src/
│ │ ├── package.json
│ │ └── rollup.config.js
│ ├── shared-types/ # TypeScript types
│ │ ├── src/
│ │ └── package.json
│ ├── shared-utils/ # Common utilities
│ │ ├── src/
│ │ └── package.json
│ └── api-client/ # API client library
│ ├── src/
│ └── package.json
├── tools/
│ ├── build-scripts/
│ ├── eslint-config/
│ └── jest-config/
├── docs/
├── .github/
│ └── workflows/
├── package.json # Root package.json
├── yarn.lock
├── nx.json # Nx configuration
├── lerna.json # Lerna configuration
└── tsconfig.base.json # Base TypeScript config
Setting Up the Monorepo
Method 1: Using Nx (Recommended)
Nx provides excellent tooling for monorepos with built-in support for React, React Native, and Node.js.
Initial Setup
# Install Nx CLI globally
npm install -g @nrwl/cli
# Create new workspace
npx create-nx-workspace@latest my-monorepo --preset=empty --cli=nx --packageManager=yarn
cd my-monorepo
Add Applications
# Add React web application
nx g @nrwl/react:app web --routing --style=styled-components
# Add Node.js API
nx g @nrwl/node:app api --framework=express
# Add React Native application
nx g @nrwl/react-native:app mobile
Add Shared Libraries
# Add shared UI library
nx g @nrwl/react:lib shared-ui --buildable --publishable --importPath=@my-monorepo/shared-ui
# Add shared types library
nx g @nrwl/workspace:lib shared-types --buildable --publishable --importPath=@my-monorepo/shared-types
# Add shared utilities library
nx g @nrwl/workspace:lib shared-utils --buildable --publishable --importPath=@my-monorepo/shared-utils
# Add API client library
nx g @nrwl/workspace:lib api-client --buildable --publishable --importPath=@my-monorepo/api-client
Method 2: Using Lerna + Yarn Workspaces
Initialize Repository
# Initialize Git repository
git init my-monorepo
cd my-monorepo
# Initialize Lerna
npx lerna init
# Set up Yarn Workspaces
yarn init -y
Configure Root Package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "lerna run build",
"test": "lerna run test",
"lint": "lerna run lint",
"clean": "lerna clean",
"bootstrap": "lerna bootstrap",
"dev:web": "lerna run dev --scope=@my-monorepo/web",
"dev:mobile": "lerna run dev --scope=@my-monorepo/mobile",
"dev:api": "lerna run dev --scope=@my-monorepo/api"
},
"devDependencies": {
"lerna": "^7.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.0.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0",
"jest": "^29.0.0"
}
}
Configure Lerna
{
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true,
"packages": ["apps/*", "packages/*"],
"command": {
"publish": {
"conventionalCommits": true,
"message": "chore(release): publish"
},
"bootstrap": {
"ignore": "component-*",
"npmClientArgs": ["--no-package-lock"]
}
}
}
Shared UI Components Library
Package Structure
packages/shared-ui/
├── src/
│ ├── components/
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.native.tsx
│ │ │ ├── Button.stories.tsx
│ │ │ ├── Button.test.tsx
│ │ │ └── index.ts
│ │ ├── Input/
│ │ └── Card/
│ ├── hooks/
│ ├── utils/
│ └── index.ts
├── package.json
├── tsconfig.json
├── rollup.config.js
└── README.md
Platform-Specific Components
// packages/shared-ui/src/components/Button/Button.tsx (Web)
import React from 'react';
import styled from 'styled-components';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
loading?: boolean;
}
const StyledButton = styled.button<{ variant: string; disabled: boolean }>`
padding: 12px 24px;
border-radius: 6px;
border: none;
font-weight: 600;
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
opacity: ${(props) => (props.disabled ? 0.6 : 1)};
background-color: ${(props) =>
props.variant === 'primary' ? '#007bff' : '#6c757d'};
color: white;
&:hover {
background-color: ${(props) =>
props.variant === 'primary' ? '#0056b3' : '#545b62'};
}
`;
export const Button: React.FC<ButtonProps> = ({
title,
onPress,
variant = 'primary',
disabled = false,
loading = false,
}) => {
return (
<StyledButton
variant={variant}
disabled={disabled || loading}
onClick={onPress}
>
{loading ? 'Loading...' : title}
</StyledButton>
);
};
// packages/shared-ui/src/components/Button/Button.native.tsx (React Native)
import React from 'react';
import {
TouchableOpacity,
Text,
StyleSheet,
ActivityIndicator,
ViewStyle,
TextStyle,
} from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
loading?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
}
export const Button: React.FC<ButtonProps> = ({
title,
onPress,
variant = 'primary',
disabled = false,
loading = false,
style,
textStyle,
}) => {
const buttonStyle = [
styles.button,
styles[variant],
disabled && styles.disabled,
style,
];
const textStyles = [styles.text, styles[`${variant}Text`], textStyle];
return (
<TouchableOpacity
style={buttonStyle}
onPress={onPress}
disabled={disabled || loading}
activeOpacity={0.8}
>
{loading ? (
<ActivityIndicator color="white" />
) : (
<Text style={textStyles}>{title}</Text>
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 6,
alignItems: 'center',
justifyContent: 'center',
minHeight: 44,
},
primary: {
backgroundColor: '#007bff',
},
secondary: {
backgroundColor: '#6c757d',
},
disabled: {
opacity: 0.6,
},
text: {
fontWeight: '600',
fontSize: 16,
},
primaryText: {
color: 'white',
},
secondaryText: {
color: 'white',
},
});
Platform Detection
// packages/shared-ui/src/components/Button/index.ts
import { Platform } from 'react-native';
// Use platform-specific implementations
export { Button } from Platform.OS === 'web'
? './Button'
: './Button.native';
Shared Types Package
// packages/shared-types/src/api.ts
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
createdAt: string;
updatedAt: string;
}
export interface Product {
id: string;
name: string;
description: string;
price: number;
images: string[];
categoryId: string;
inStock: boolean;
}
export interface ApiResponse<T> {
data: T;
message: string;
success: boolean;
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
export interface ApiError {
message: string;
code: string;
details?: Record<string, any>;
}
// packages/shared-types/src/navigation.ts
import { StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/native';
export type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
ProductDetails: { productId: string };
Settings: undefined;
};
export type HomeScreenNavigationProp = StackNavigationProp<
RootStackParamList,
'Home'
>;
export type ProfileScreenRouteProp = RouteProp<RootStackParamList, 'Profile'>;
export interface NavigationProps<T extends keyof RootStackParamList> {
navigation: StackNavigationProp<RootStackParamList, T>;
route: RouteProp<RootStackParamList, T>;
}
API Client Package
// packages/api-client/src/client.ts
import {
User,
Product,
ApiResponse,
PaginatedResponse,
} from '@my-monorepo/shared-types';
export interface ApiClientConfig {
baseURL: string;
timeout?: number;
headers?: Record<string, string>;
}
export class ApiClient {
private baseURL: string;
private timeout: number;
private headers: Record<string, string>;
constructor(config: ApiClientConfig) {
this.baseURL = config.baseURL;
this.timeout = config.timeout || 10000;
this.headers = config.headers || {};
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const url = `${this.baseURL}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...this.headers,
...options.headers,
},
signal: AbortSignal.timeout(this.timeout),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
// Auth methods
async login(
email: string,
password: string
): Promise<ApiResponse<{ token: string; user: User }>> {
return this.request('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
}
async register(userData: Partial<User>): Promise<ApiResponse<User>> {
return this.request('/auth/register', {
method: 'POST',
body: JSON.stringify(userData),
});
}
// User methods
async getUser(id: string): Promise<ApiResponse<User>> {
return this.request(`/users/${id}`);
}
async updateUser(
id: string,
userData: Partial<User>
): Promise<ApiResponse<User>> {
return this.request(`/users/${id}`, {
method: 'PUT',
body: JSON.stringify(userData),
});
}
// Product methods
async getProducts(page = 1, limit = 10): Promise<PaginatedResponse<Product>> {
return this.request(`/products?page=${page}&limit=${limit}`);
}
async getProduct(id: string): Promise<ApiResponse<Product>> {
return this.request(`/products/${id}`);
}
async createProduct(
productData: Partial<Product>
): Promise<ApiResponse<Product>> {
return this.request('/products', {
method: 'POST',
body: JSON.stringify(productData),
});
}
setAuthToken(token: string) {
this.headers.Authorization = `Bearer ${token}`;
}
removeAuthToken() {
delete this.headers.Authorization;
}
}
// Factory function for different environments
export const createApiClient = (environment: 'development' | 'production') => {
const config: ApiClientConfig = {
baseURL:
environment === 'production'
? 'https://api.myapp.com'
: 'http://localhost:3001/api',
timeout: 10000,
};
return new ApiClient(config);
};
React Web Application
Package.json
{
"name": "@my-monorepo/web",
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.0",
"styled-components": "^6.0.0",
"@my-monorepo/shared-ui": "*",
"@my-monorepo/shared-types": "*",
"@my-monorepo/api-client": "*",
"@my-monorepo/shared-utils": "*"
},
"scripts": {
"dev": "webpack serve --mode development",
"build": "webpack --mode production",
"test": "jest",
"lint": "eslint src --ext .ts,.tsx"
}
}
Using Shared Components
// apps/web/src/components/LoginForm.tsx
import React, { useState } from 'react';
import { Button, Input } from '@my-monorepo/shared-ui';
import { ApiClient } from '@my-monorepo/api-client';
import { User } from '@my-monorepo/shared-types';
interface LoginFormProps {
onLoginSuccess: (user: User) => void;
}
export const LoginForm: React.FC<LoginFormProps> = ({ onLoginSuccess }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const apiClient = new ApiClient({
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3001/api',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await apiClient.login(email, password);
if (response.success) {
onLoginSuccess(response.data.user);
}
} catch (error) {
console.error('Login failed:', error);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<Input
placeholder="Email"
value={email}
onChange={setEmail}
type="email"
/>
<Input
placeholder="Password"
value={password}
onChange={setPassword}
type="password"
/>
<Button
title="Login"
onPress={() => handleSubmit}
loading={loading}
variant="primary"
/>
</form>
);
};
React Native Application
Metro Configuration
// apps/mobile/metro.config.js
const { getDefaultConfig } = require('@expo/metro-config');
const path = require('path');
const config = getDefaultConfig(__dirname);
// Add monorepo support
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '../..');
config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(workspaceRoot, 'node_modules'),
];
// Map shared packages
config.resolver.alias = {
'@my-monorepo/shared-ui': path.resolve(
workspaceRoot,
'packages/shared-ui/src'
),
'@my-monorepo/shared-types': path.resolve(
workspaceRoot,
'packages/shared-types/src'
),
'@my-monorepo/api-client': path.resolve(
workspaceRoot,
'packages/api-client/src'
),
'@my-monorepo/shared-utils': path.resolve(
workspaceRoot,
'packages/shared-utils/src'
),
};
module.exports = config;
Using Shared Components
// apps/mobile/src/screens/LoginScreen.tsx
import React, { useState } from 'react';
import { View, StyleSheet, Alert } from 'react-native';
import { Button, Input } from '@my-monorepo/shared-ui';
import { createApiClient } from '@my-monorepo/api-client';
import { User } from '@my-monorepo/shared-types';
interface LoginScreenProps {
onLoginSuccess: (user: User) => void;
}
export const LoginScreen: React.FC<LoginScreenProps> = ({ onLoginSuccess }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const apiClient = createApiClient('development');
const handleLogin = async () => {
if (!email || !password) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
setLoading(true);
try {
const response = await apiClient.login(email, password);
if (response.success) {
onLoginSuccess(response.data.user);
}
} catch (error) {
Alert.alert('Error', 'Login failed. Please try again.');
} finally {
setLoading(false);
}
};
return (
<View style={styles.container}>
<Input
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<Input
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button
title="Login"
onPress={handleLogin}
loading={loading}
variant="primary"
style={styles.button}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
},
button: {
marginTop: 20,
},
});
Node.js API Application
Package.json
{
"name": "@my-monorepo/api",
"version": "1.0.0",
"private": true,
"dependencies": {
"express": "^4.18.0",
"cors": "^2.8.5",
"helmet": "^6.0.0",
"mongoose": "^7.0.0",
"jsonwebtoken": "^9.0.0",
"@my-monorepo/shared-types": "*",
"@my-monorepo/shared-utils": "*"
},
"scripts": {
"dev": "nodemon src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "jest"
}
}
Using Shared Types
// apps/api/src/controllers/userController.ts
import { Request, Response } from 'express';
import { User, ApiResponse } from '@my-monorepo/shared-types';
import { validateEmail } from '@my-monorepo/shared-utils';
import { UserModel } from '../models/User';
export class UserController {
async getUser(req: Request, res: Response): Promise<void> {
try {
const { id } = req.params;
const user = await UserModel.findById(id);
if (!user) {
const response: ApiResponse<null> = {
data: null,
message: 'User not found',
success: false,
};
res.status(404).json(response);
return;
}
const response: ApiResponse<User> = {
data: user.toObject(),
message: 'User retrieved successfully',
success: true,
};
res.json(response);
} catch (error) {
const response: ApiResponse<null> = {
data: null,
message: 'Internal server error',
success: false,
};
res.status(500).json(response);
}
}
async createUser(req: Request, res: Response): Promise<void> {
try {
const userData = req.body;
if (!validateEmail(userData.email)) {
const response: ApiResponse<null> = {
data: null,
message: 'Invalid email address',
success: false,
};
res.status(400).json(response);
return;
}
const user = new UserModel(userData);
await user.save();
const response: ApiResponse<User> = {
data: user.toObject(),
message: 'User created successfully',
success: true,
};
res.status(201).json(response);
} catch (error) {
const response: ApiResponse<null> = {
data: null,
message: 'Failed to create user',
success: false,
};
res.status(500).json(response);
}
}
}
Build Configuration
Root TypeScript Configuration
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@my-monorepo/shared-ui": ["packages/shared-ui/src"],
"@my-monorepo/shared-types": ["packages/shared-types/src"],
"@my-monorepo/shared-utils": ["packages/shared-utils/src"],
"@my-monorepo/api-client": ["packages/api-client/src"]
}
},
"exclude": ["node_modules", "dist", "build", "**/*.test.ts", "**/*.test.tsx"]
}
Nx Build Configuration
{
"version": 2,
"projects": {
"web": {
"root": "apps/web",
"sourceRoot": "apps/web/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nrwl/webpack:webpack",
"options": {
"outputPath": "dist/apps/web",
"index": "apps/web/src/index.html",
"main": "apps/web/src/main.tsx",
"polyfills": "apps/web/src/polyfills.ts",
"tsConfig": "apps/web/tsconfig.app.json"
}
},
"serve": {
"executor": "@nrwl/webpack:dev-server",
"options": {
"buildTarget": "web:build",
"port": 3000
}
}
}
},
"mobile": {
"root": "apps/mobile",
"sourceRoot": "apps/mobile/src",
"projectType": "application",
"targets": {
"start": {
"executor": "@nrwl/react-native:start",
"options": {
"port": 8081
}
},
"run-ios": {
"executor": "@nrwl/react-native:run-ios"
},
"run-android": {
"executor": "@nrwl/react-native:run-android"
}
}
},
"api": {
"root": "apps/api",
"sourceRoot": "apps/api/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nrwl/node:build",
"options": {
"outputPath": "dist/apps/api",
"main": "apps/api/src/server.ts",
"tsConfig": "apps/api/tsconfig.app.json"
}
},
"serve": {
"executor": "@nrwl/node:execute",
"options": {
"buildTarget": "api:build"
}
}
}
}
}
}
CI/CD Pipeline
GitHub Actions Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Lint
run: yarn lint
- name: Test
run: yarn test --coverage
- name: Build packages
run: yarn build
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
build-web:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build web app
run: yarn nx build web
- name: Build API
run: yarn nx build api
build-mobile:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Setup EAS CLI
run: yarn global add @expo/eas-cli
- name: Build for iOS
run: |
cd apps/mobile
eas build --platform ios --non-interactive
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
- name: Build for Android
run: |
cd apps/mobile
eas build --platform android --non-interactive
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
Deployment Workflow
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
- name: Install and build
run: |
yarn install --frozen-lockfile
yarn nx build web --prod
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
working-directory: ./dist/apps/web
deploy-api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and push Docker image
run: |
docker build -t my-api:latest ./apps/api
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker push my-api:latest
- name: Deploy to production
run: |
# Deploy to your cloud provider
echo "Deploying API to production..."
Development Workflow
Scripts Setup
{
"scripts": {
"dev": "concurrently \"yarn dev:api\" \"yarn dev:web\" \"yarn dev:mobile\"",
"dev:api": "nx serve api",
"dev:web": "nx serve web",
"dev:mobile": "nx start mobile",
"build": "nx run-many --target=build --all",
"test": "nx run-many --target=test --all",
"lint": "nx run-many --target=lint --all",
"lint:fix": "nx run-many --target=lint --all --fix",
"affected:build": "nx affected:build",
"affected:test": "nx affected:test",
"affected:lint": "nx affected:lint",
"graph": "nx graph",
"dep-graph": "nx dep-graph"
}
}
Development Commands
# Install dependencies
yarn install
# Start all applications in development mode
yarn dev
# Start individual applications
yarn dev:web # React web app on port 3000
yarn dev:api # Node.js API on port 3001
yarn dev:mobile # React Native metro bundler
# Run tests for all projects
yarn test
# Run tests for affected projects only
yarn affected:test
# Build all projects
yarn build
# Build only affected projects
yarn affected:build
# Lint all projects
yarn lint
# View dependency graph
yarn graph
# Add new package to specific app
yarn workspace @my-monorepo/web add lodash
# Add new package to root (dev dependency)
yarn add -W -D jest
# Bootstrap packages (link local dependencies)
lerna bootstrap
Performance Optimization
Build Optimization
Nx Caching
{
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/workspace/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"],
"parallel": 3
}
}
},
"targetDependencies": {
"build": [
{
"target": "build",
"projects": "dependencies"
}
]
}
}
Webpack Bundle Analysis
// apps/web/webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = (config, context) => {
if (process.env.ANALYZE) {
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'server',
openAnalyzer: true,
})
);
}
return config;
};
Code Splitting
// apps/web/src/App.tsx
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Lazy load components
const Home = React.lazy(() => import('./pages/Home'));
const Profile = React.lazy(() => import('./pages/Profile'));
const Products = React.lazy(() => import('./pages/Products'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="/products" element={<Products />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;
Troubleshooting Common Issues
Module Resolution Issues
React Native Metro Issue
// apps/mobile/metro.config.js
const blacklist = require('metro-config/src/defaults/blacklist');
module.exports = {
resolver: {
blacklistRE: blacklist([
// Exclude problematic directories
/packages\/.*\/node_modules\/.*/,
/apps\/web\/node_modules\/.*/,
/apps\/api\/node_modules\/.*/,
]),
},
};
TypeScript Path Mapping
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@my-monorepo/*": ["packages/*/src", "packages/*"]
}
}
}
Build Performance Issues
Selective Building
# Only build affected projects
nx affected:build --base=origin/main
# Build with parallel execution
nx run-many --target=build --all --parallel --maxParallel=3
Dependency Management
# Clean and reinstall dependencies
lerna clean
yarn install
# Check for duplicate dependencies
yarn dedupe
# Analyze bundle size
yarn nx build web --analyze
Best Practices
Code Organization
- Keep shared code platform-agnostic
- Use TypeScript for better type safety
- Implement consistent naming conventions
- Create clear boundaries between packages
Dependency Management
- Pin exact versions for stability
- Use peer dependencies for shared packages
- Regularly update and audit dependencies
- Keep package.json files clean and organized
Testing Strategy
- Write unit tests for shared utilities
- Integration tests for API endpoints
- Component tests for UI libraries
- E2E tests for critical user flows
Performance
- Implement proper caching strategies
- Use code splitting and lazy loading
- Monitor bundle sizes regularly
- Optimize build pipelines
Conclusion
Setting up a monorepo for React, React Native, and Node.js applications provides significant benefits for code sharing, dependency management, and team collaboration. While the initial setup requires more configuration, the long-term advantages include:
Key Benefits Achieved
- Code Reusability: Share components, types, and utilities across platforms
- Consistency: Unified tooling, linting, and testing across all applications
- Developer Experience: Single repository, simplified setup, and coordinated releases
- Maintenance: Easier refactoring and dependency management
- CI/CD: Streamlined build and deployment processes
Success Factors
- Start Simple: Begin with basic structure and evolve as needed
- Tool Selection: Choose tools that fit your team's expertise and requirements
- Documentation: Maintain clear documentation for setup and workflows
- Team Alignment: Establish conventions and ensure team buy-in
- Performance Monitoring: Regularly assess and optimize build performance
Next Steps
- Implement automated dependency updates
- Set up comprehensive monitoring and alerting
- Create detailed contribution guidelines
- Plan for scaling to larger teams
- Consider micro-frontend architecture for larger web applications
A well-implemented monorepo becomes a powerful development platform that scales with your team and applications, providing the foundation for building and maintaining complex multi-platform applications efficiently.