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! ๐๐โจ