Monorepo Setup for React + React Native + Node.js

ReactJS, React Native, Node.js, Architecture|SEPTEMBER 12, 2025|0 VIEWS
Build and manage multi-platform applications with shared code using modern monorepo tools

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

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

  1. Code Reusability: Share components, types, and utilities across platforms
  2. Consistency: Unified tooling, linting, and testing across all applications
  3. Developer Experience: Single repository, simplified setup, and coordinated releases
  4. Maintenance: Easier refactoring and dependency management
  5. 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.