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:
- Code Sharing 🔄: Reuse common utilities across packages
- Atomic Changes ⚡: Change multiple packages in one commit
- Consistent Tooling 🛠️: Same build tools and configs everywhere
- 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
- 🎯 Keep Packages Focused: Each package should have a single responsibility
- 📝 Document Dependencies: Make inter-package relationships clear
- 🚀 Use Task Orchestration: Let Nx/Lerna handle build order
- 🛡️ Enforce Consistency: Shared configs for linting, testing
- ✨ 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:
- 💻 Practice with the exercises above
- 🏗️ Convert an existing multi-repo project to a monorepo
- 📚 Move on to our next tutorial: TypeScript with Docker
- 🌟 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! 🎉🚀✨