+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 240 of 354

๐Ÿ— ๏ธ Monorepo with TypeScript: Lerna and Nx

Master monorepo with typescript: lerna and nx in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿ’ŽAdvanced
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand monorepo fundamentals ๐ŸŽฏ
  • Apply monorepo concepts in real projects ๐Ÿ—๏ธ
  • Debug monorepo issues ๐Ÿ›
  • Write type-safe monorepo code โœจ

๐ŸŽฏ Introduction

Welcome to the exciting world of monorepos with TypeScript! ๐ŸŽ‰ In this guide, weโ€™ll explore how to manage multiple packages in a single repository using Lerna and Nx.

Youโ€™ll discover how monorepos can transform your TypeScript development experience. Whether youโ€™re building component libraries ๐Ÿ“š, microservices ๐ŸŒ, or full-stack applications ๐Ÿ’ป, understanding monorepos is essential for scaling your projects efficiently.

By the end of this tutorial, youโ€™ll feel confident setting up and managing TypeScript monorepos! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Monorepos

๐Ÿค” What is a Monorepo?

A monorepo is like a huge apartment building ๐Ÿข where each apartment (package) is separate but shares common infrastructure like utilities and maintenance. Think of it as having all your related projects living in one neighborhood, making it easy to share resources and coordinate changes.

In TypeScript terms, a monorepo allows you to:

  • โœจ Share code between multiple packages
  • ๐Ÿš€ Manage dependencies consistently
  • ๐Ÿ›ก๏ธ Enforce coding standards across projects
  • ๐Ÿ“ฆ Publish multiple packages from one repository

๐Ÿ’ก Why Use Monorepos?

Hereโ€™s why developers love monorepos:

  1. Code Sharing ๐Ÿ”„: Reuse common utilities across packages
  2. Atomic Changes โšก: Change multiple packages in one commit
  3. Consistent Tooling ๐Ÿ› ๏ธ: Same build tools and configs everywhere
  4. Dependency Management ๐Ÿ“ฆ: Avoid version conflicts and duplication

Real-world example: Imagine building a design system ๐ŸŽจ. With a monorepo, you can have packages for React components, Vue components, and documentation, all sharing the same design tokens!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Lerna Setup

Letโ€™s start with Lerna, the classic monorepo tool:

# ๐Ÿ‘‹ Initialize a new monorepo
npx lerna init --typescript

# ๐ŸŽจ Install dependencies
npm install --save-dev lerna typescript @types/node

# ๐Ÿš€ Create your first package
lerna create @mycompany/core --yes
lerna create @mycompany/utils --yes

๐Ÿ’ก Explanation: Lerna creates a packages/ directory where each subdirectory is a separate npm package. The --typescript flag sets up TypeScript configurations.

๐ŸŽฏ Basic Lerna Configuration

Hereโ€™s your lerna.json configuration:

{
  "version": "0.0.0",
  "npmClient": "npm",
  "command": {
    "publish": {
      "conventionalCommits": true,
      "message": "๐Ÿš€ Release new versions"
    },
    "bootstrap": {
      "ignore": "component-*",
      "npmClientArgs": ["--no-package-lock"]
    }
  },
  "packages": [
    "packages/*"
  ]
}

๐ŸŒŸ Nx Setup

Now letโ€™s look at Nx, the modern monorepo solution:

# ๐ŸŽฎ Create new Nx workspace
npx create-nx-workspace@latest myworkspace --preset=ts

# ๐Ÿ“ฆ Add TypeScript project
nx g @nrwl/node:application api
nx g @nrwl/node:library shared-utils

# ๐Ÿš€ Run specific project
nx serve api
nx test shared-utils

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: E-commerce Monorepo with Lerna

Letโ€™s build a real e-commerce system:

// ๐Ÿ“ packages/types/src/index.ts
// ๐ŸŽฏ Shared types for all packages
export interface Product {
  id: string;
  name: string;
  price: number;
  emoji: string; // Every product needs personality! 
  category: 'electronics' | 'clothing' | 'books';
}

export interface User {
  id: string;
  name: string;
  email: string;
  preferences: {
    favoriteEmoji: string; // ๐Ÿ˜Š User's favorite emoji
    theme: 'light' | 'dark';
  };
}

// ๐Ÿ›’ Shopping cart operations
export interface CartItem {
  product: Product;
  quantity: number;
  addedAt: Date;
}
// ๐Ÿ“ packages/cart/src/cart-service.ts
// ๐Ÿ›๏ธ Cart management package
import { Product, CartItem, User } from '@mycompany/types';

export class CartService {
  private items: Map<string, CartItem> = new Map();
  
  // โž• Add item to cart
  addItem(product: Product, quantity: number = 1): void {
    const existingItem = this.items.get(product.id);
    
    if (existingItem) {
      // ๐Ÿ“ˆ Update quantity
      existingItem.quantity += quantity;
      console.log(`๐Ÿ”„ Updated ${product.emoji} ${product.name} (${existingItem.quantity})`);
    } else {
      // ๐Ÿ†• Add new item
      this.items.set(product.id, {
        product,
        quantity,
        addedAt: new Date()
      });
      console.log(`โœ… Added ${product.emoji} ${product.name} to cart!`);
    }
  }
  
  // ๐Ÿ’ฐ Calculate total
  getTotal(): number {
    let total = 0;
    this.items.forEach(item => {
      total += item.product.price * item.quantity;
    });
    return total;
  }
  
  // ๐Ÿ“‹ Get cart summary
  getSummary(): string {
    const itemCount = Array.from(this.items.values())
      .reduce((sum, item) => sum + item.quantity, 0);
    
    return `๐Ÿ›’ Cart: ${itemCount} items โ€ข $${this.getTotal().toFixed(2)}`;
  }
  
  // ๐ŸŽŠ Apply user preferences
  getPersonalizedMessage(user: User): string {
    const total = this.getTotal();
    const emoji = user.preferences.favoriteEmoji;
    
    return `${emoji} Hey ${user.name}! Your cart total is $${total.toFixed(2)}`;
  }
}
// ๐Ÿ“ packages/notifications/src/notification-service.ts
// ๐Ÿ“ง Notification system
import { User, CartItem } from '@mycompany/types';

export class NotificationService {
  // ๐Ÿ”” Send cart abandonment reminder
  async sendAbandonmentReminder(user: User, items: CartItem[]): Promise<void> {
    const itemNames = items.map(item => 
      `${item.product.emoji} ${item.product.name}`
    ).join(', ');
    
    const message = `
      ๐Ÿ‘‹ Hey ${user.name}!
      
      You left some awesome items in your cart:
      ${itemNames}
      
      Don't miss out! Complete your order now ๐Ÿš€
      
      ${user.preferences.favoriteEmoji} Happy shopping!
    `;
    
    console.log('๐Ÿ“ง Sending email:', message);
    // ๐Ÿš€ In real app, integrate with email service
  }
  
  // ๐ŸŽ‰ Send order confirmation
  async sendOrderConfirmation(user: User, total: number): Promise<void> {
    console.log(`๐ŸŽŠ Order confirmed for ${user.name}: $${total.toFixed(2)}`);
  }
}

๐ŸŽฏ Try it yourself: Add a discount service package that calculates coupon discounts!

๐ŸŽฎ Example 2: Game Development Monorepo with Nx

Letโ€™s make it fun with a game system:

// ๐Ÿ“ libs/game-engine/src/lib/game-engine.ts
// ๐ŸŽฎ Core game engine
export interface GameObject {
  id: string;
  x: number;
  y: number;
  emoji: string;
  type: 'player' | 'enemy' | 'powerup';
}

export class GameEngine {
  private objects: Map<string, GameObject> = new Map();
  private score = 0;
  
  // ๐ŸŽฏ Add game object
  addObject(obj: GameObject): void {
    this.objects.set(obj.id, obj);
    console.log(`โœจ Spawned ${obj.emoji} at (${obj.x}, ${obj.y})`);
  }
  
  // ๐ŸŽฎ Move object
  moveObject(id: string, deltaX: number, deltaY: number): void {
    const obj = this.objects.get(id);
    if (obj) {
      obj.x += deltaX;
      obj.y += deltaY;
      console.log(`๐Ÿƒ ${obj.emoji} moved to (${obj.x}, ${obj.y})`);
    }
  }
  
  // ๐Ÿ’ฅ Check collisions
  checkCollisions(): void {
    const objects = Array.from(this.objects.values());
    
    for (let i = 0; i < objects.length; i++) {
      for (let j = i + 1; j < objects.length; j++) {
        if (this.isColliding(objects[i], objects[j])) {
          this.handleCollision(objects[i], objects[j]);
        }
      }
    }
  }
  
  private isColliding(a: GameObject, b: GameObject): boolean {
    return Math.abs(a.x - b.x) < 1 && Math.abs(a.y - b.y) < 1;
  }
  
  private handleCollision(a: GameObject, b: GameObject): void {
    console.log(`๐Ÿ’ฅ Collision: ${a.emoji} hit ${b.emoji}!`);
    
    if (a.type === 'player' && b.type === 'powerup') {
      this.score += 100;
      this.objects.delete(b.id);
      console.log(`๐ŸŒŸ Power-up collected! Score: ${this.score}`);
    }
  }
}
// ๐Ÿ“ apps/space-game/src/main.ts
// ๐Ÿš€ Space shooter game
import { GameEngine, GameObject } from '@myworkspace/game-engine';

class SpaceGame {
  private engine = new GameEngine();
  
  // ๐ŸŽฎ Initialize game
  start(): void {
    console.log('๐Ÿš€ Space Game Starting!');
    
    // ๐Ÿ›ธ Create player
    const player: GameObject = {
      id: 'player',
      x: 0,
      y: 0,
      emoji: '๐Ÿ›ธ',
      type: 'player'
    };
    
    // ๐Ÿ‘พ Create enemies
    const enemy: GameObject = {
      id: 'enemy1',
      x: 5,
      y: 3,
      emoji: '๐Ÿ‘พ',
      type: 'enemy'
    };
    
    // โญ Create power-up
    const powerup: GameObject = {
      id: 'powerup1',
      x: 2,
      y: 1,
      emoji: 'โญ',
      type: 'powerup'
    };
    
    this.engine.addObject(player);
    this.engine.addObject(enemy);
    this.engine.addObject(powerup);
    
    // ๐ŸŽฏ Move player towards power-up
    this.engine.moveObject('player', 2, 1);
    this.engine.checkCollisions();
  }
}

// ๐ŸŽ‰ Start the game!
const game = new SpaceGame();
game.start();

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Topic 1: Nx Task Dependencies

When youโ€™re ready to level up, configure complex build pipelines:

// ๐Ÿ“ nx.json
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "@nrwl/workspace/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "lint"],
        "parallel": 3
      }
    }
  },
  "targetDependencies": {
    "build": [
      {
        "target": "build",
        "projects": "dependencies"
      }
    ]
  },
  "implicitDependencies": {
    "package.json": "*",
    "tsconfig.base.json": "*"
  }
}

๐Ÿ—๏ธ Advanced Topic 2: Lerna Version Management

For the brave developers managing complex releases:

// ๐Ÿ“ scripts/custom-version.ts
// ๐Ÿš€ Custom versioning strategy
import { execSync } from 'child_process';

interface VersionOptions {
  conventionalCommits: boolean;
  createRelease: boolean;
  gitTagVersion: boolean;
  emoji: string; // ๐ŸŽฏ Because releases need personality!
}

function customVersion(options: VersionOptions): void {
  const { emoji, conventionalCommits } = options;
  
  console.log(`${emoji} Starting release process...`);
  
  // ๐Ÿ“Š Analyze changes
  const changes = execSync('git log --oneline HEAD~10..HEAD').toString();
  const hasBreaking = changes.includes('BREAKING CHANGE');
  const hasFeatures = changes.includes('feat:');
  
  // ๐ŸŽฏ Determine version bump
  let versionArg = 'patch';
  if (hasBreaking) {
    versionArg = 'major';
    console.log('๐Ÿ’ฅ Breaking changes detected - major version bump!');
  } else if (hasFeatures) {
    versionArg = 'minor';
    console.log('โœจ New features detected - minor version bump!');
  }
  
  // ๐Ÿš€ Execute lerna version
  execSync(`lerna version ${versionArg} --conventional-commits --create-release github`);
  console.log(`๐ŸŽ‰ Release completed with ${versionArg} version bump!`);
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Circular Dependencies

// โŒ Wrong way - creates circular dependency!
// packages/a/src/index.ts
import { utilFromB } from '@mycompany/b';

// packages/b/src/index.ts  
import { utilFromA } from '@mycompany/a';  // ๐Ÿ’ฅ Circular!
// โœ… Correct way - extract shared code!
// packages/shared/src/index.ts
export const sharedUtil = () => {
  console.log('๐Ÿ”„ Shared functionality!');
};

// packages/a/src/index.ts
import { sharedUtil } from '@mycompany/shared';

// packages/b/src/index.ts
import { sharedUtil } from '@mycompany/shared';

๐Ÿคฏ Pitfall 2: Version Mismatch Hell

// โŒ Dangerous - different versions everywhere!
// packages/a/package.json
{
  "dependencies": {
    "lodash": "^4.17.0"  // ๐Ÿ˜ฐ Old version
  }
}

// packages/b/package.json
{
  "dependencies": {
    "lodash": "^4.21.0"  // ๐Ÿ˜ฑ Different version!
  }
}
// โœ… Safe - use workspace constraints!
// package.json (root)
{
  "workspaces": [
    "packages/*"
  ],
  "devDependencies": {
    "lodash": "^4.21.0"  // โœ… Single source of truth
  }
}

// ๐Ÿ”ง Or use Lerna's hoist feature
// lerna.json
{
  "command": {
    "bootstrap": {
      "hoist": true  // ๐Ÿš€ Hoist common dependencies
    }
  }
}

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Keep Packages Focused: Each package should have a single responsibility
  2. ๐Ÿ“ Document Dependencies: Make inter-package relationships clear
  3. ๐Ÿš€ Use Task Orchestration: Let Nx/Lerna handle build order
  4. ๐Ÿ›ก๏ธ Enforce Consistency: Shared configs for linting, testing
  5. โœจ Automate Everything: CI/CD pipelines that understand your monorepo

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Social Media Monorepo

Create a type-safe social media platform monorepo:

๐Ÿ“‹ Requirements:

  • ๐Ÿ“ฑ Frontend app (React/Vue/Angular)
  • ๐ŸŒ API service (Node.js/Express)
  • ๐Ÿ“š Shared types library
  • ๐Ÿ”ง Common utilities package
  • ๐Ÿงช Shared testing utilities
  • ๐Ÿ“Š Analytics service

๐Ÿš€ Bonus Points:

  • Add automated publishing workflow
  • Implement cross-package testing
  • Create shared component library
  • Set up development environment scripts

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
# ๐ŸŽฎ Initialize Nx workspace
npx create-nx-workspace@latest social-platform --preset=ts

# ๐Ÿ“ฆ Add packages
nx g @nrwl/react:app frontend
nx g @nrwl/node:app api
nx g @nrwl/node:lib shared-types
nx g @nrwl/node:lib shared-utils
nx g @nrwl/node:lib analytics-service
// ๐Ÿ“ libs/shared-types/src/lib/types.ts
// ๐ŸŽฏ All our social media types
export interface User {
  id: string;
  username: string;
  displayName: string;
  avatar: string;
  emoji: string; // ๐Ÿ˜Š User's signature emoji
  bio?: string;
  followers: number;
  following: number;
}

export interface Post {
  id: string;
  authorId: string;
  content: string;
  emoji: string; // ๐Ÿ“ Post mood emoji
  timestamp: Date;
  likes: number;
  reposts: number;
  replies: number;
  media?: {
    type: 'image' | 'video' | 'gif';
    url: string;
    alt?: string;
  }[];
}

export interface CreatePostRequest {
  content: string;
  emoji: string;
  media?: File[];
}
// ๐Ÿ“ libs/shared-utils/src/lib/formatting.ts
// ๐ŸŽจ Shared formatting utilities
export const formatPostDate = (date: Date): string => {
  const now = new Date();
  const diffMs = now.getTime() - date.getTime();
  const diffMins = Math.floor(diffMs / (1000 * 60));
  
  if (diffMins < 1) return 'โšก Just now';
  if (diffMins < 60) return `๐Ÿ• ${diffMins}m ago`;
  if (diffMins < 1440) return `๐Ÿ• ${Math.floor(diffMins / 60)}h ago`;
  
  return `๐Ÿ“… ${date.toLocaleDateString()}`;
};

export const formatUserStats = (followers: number, following: number): string => {
  const formatCount = (count: number): string => {
    if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
    if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
    return count.toString();
  };
  
  return `๐Ÿ‘ฅ ${formatCount(followers)} followers โ€ข ${formatCount(following)} following`;
};
// ๐Ÿ“ apps/api/src/services/post-service.ts
// ๐ŸŒ API service using shared types
import { Post, CreatePostRequest, User } from '@social-platform/shared-types';
import { formatPostDate } from '@social-platform/shared-utils';

export class PostService {
  private posts: Map<string, Post> = new Map();
  
  // ๐Ÿ“ Create new post
  async createPost(authorId: string, request: CreatePostRequest): Promise<Post> {
    const post: Post = {
      id: Date.now().toString(),
      authorId,
      content: request.content,
      emoji: request.emoji,
      timestamp: new Date(),
      likes: 0,
      reposts: 0,
      replies: 0
    };
    
    this.posts.set(post.id, post);
    console.log(`โœ… Created post: ${post.emoji} ${post.content.substring(0, 50)}...`);
    
    return post;
  }
  
  // ๐Ÿ“Š Get user's timeline
  async getTimeline(userId: string): Promise<Post[]> {
    const posts = Array.from(this.posts.values())
      .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
      .slice(0, 20);
    
    console.log(`๐Ÿ“ฑ Retrieved ${posts.length} posts for timeline`);
    return posts;
  }
  
  // โค๏ธ Like a post
  async likePost(postId: string, userId: string): Promise<void> {
    const post = this.posts.get(postId);
    if (post) {
      post.likes++;
      console.log(`โค๏ธ Post liked: ${post.emoji} now has ${post.likes} likes`);
    }
  }
}
// ๐Ÿ“ libs/analytics-service/src/lib/analytics.ts
// ๐Ÿ“Š Analytics tracking
import { Post, User } from '@social-platform/shared-types';

export interface AnalyticsEvent {
  type: 'post_created' | 'post_liked' | 'user_followed';
  userId: string;
  postId?: string;
  targetUserId?: string;
  emoji: string; // ๐Ÿ“Š Event category emoji
  timestamp: Date;
}

export class AnalyticsService {
  private events: AnalyticsEvent[] = [];
  
  // ๐Ÿ“ˆ Track event
  trackEvent(event: Omit<AnalyticsEvent, 'timestamp'>): void {
    const fullEvent: AnalyticsEvent = {
      ...event,
      timestamp: new Date()
    };
    
    this.events.push(fullEvent);
    console.log(`๐Ÿ“Š Tracked: ${event.emoji} ${event.type} by ${event.userId}`);
  }
  
  // ๐Ÿ“‹ Get user engagement stats
  getUserEngagement(userId: string, days: number = 7): {
    postsCreated: number;
    likesGiven: number;
    emoji: string;
  } {
    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() - days);
    
    const userEvents = this.events.filter(e => 
      e.userId === userId && e.timestamp >= cutoff
    );
    
    const stats = {
      postsCreated: userEvents.filter(e => e.type === 'post_created').length,
      likesGiven: userEvents.filter(e => e.type === 'post_liked').length,
      emoji: this.getEngagementEmoji(userEvents.length)
    };
    
    console.log(`๐Ÿ“ˆ User ${userId} engagement: ${stats.emoji} ${JSON.stringify(stats)}`);
    return stats;
  }
  
  private getEngagementEmoji(eventCount: number): string {
    if (eventCount >= 50) return '๐Ÿ”ฅ'; // Super active
    if (eventCount >= 20) return 'โญ'; // Very active  
    if (eventCount >= 10) return '๐Ÿ‘'; // Active
    if (eventCount >= 5) return '๐Ÿ˜Š';  // Moderate
    return '๐Ÿ˜ด'; // Quiet
  }
}

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Setup monorepos with Lerna and Nx confidently ๐Ÿ’ช
  • โœ… Avoid common mistakes that trip up beginners ๐Ÿ›ก๏ธ
  • โœ… Apply best practices in real projects ๐ŸŽฏ
  • โœ… Debug monorepo issues like a pro ๐Ÿ›
  • โœ… Build scalable applications with TypeScript! ๐Ÿš€

Remember: Monorepos are powerful tools for organizing complex projects. Start simple and grow your setup as needed! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered TypeScript monorepos!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the exercises above
  2. ๐Ÿ—๏ธ Convert an existing multi-repo project to a monorepo
  3. ๐Ÿ“š Move on to our next tutorial: TypeScript with Docker
  4. ๐ŸŒŸ Share your monorepo setup with others!

Remember: Every monorepo expert was once a beginner. Keep coding, keep learning, and most importantly, have fun! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ