Getting Started with Microservices: Building with Shared TypeScript Packages and GitLab Registry

Introduction: The Day My Node.js Monolith Became Unmaintainable

Two years ago, I was working on a personal project—a comprehensive task management application that started as a simple Express.js monolith. Users, tasks, notifications, file uploads, reporting—everything in one TypeScript codebase. For months, it was perfect. Fast development, easy debugging, straightforward deployment.

Then my user base grew from 50 to 5,000 users, and my simple app became a nightmare.

Every new feature required touching multiple parts of the codebase. A small change to the notification system would break the user authentication. Database queries for reports would slow down the entire application. Deployment became an all-or-nothing gamble—one bug could bring down the entire platform.

I knew I needed to break it apart, but I was terrified of code duplication. How do you share common logic between services? How do you maintain consistent types across multiple TypeScript projects? How do you version and distribute shared code?

That crisis led me to discover the power of microservices with shared npm packages, and it completely transformed how I build scalable applications. Today, I want to share my journey from a single monolith to a distributed system with reusable TypeScript packages published to GitLab's package registry.

Why I Chose Microservices for My Task Management Platform

Before diving into the technical implementation, let me share why microservices made sense for my growing project and when you might consider the same transition:

The Problems I Faced with My Monolith

spinner

My pain points:

  • Scaling bottlenecks: File uploads would consume all server resources, affecting user logins

  • Development conflicts: Multiple features couldn't be developed simultaneously

  • Technology lock-in: Stuck with the same Node.js version and dependencies across all features

  • Deployment anxiety: One bad commit could break everything

  • Team coordination: As I brought on contributors, merge conflicts became frequent

The Microservices Solution I Implemented

spinner

This architecture solved my core problems:

  • Independent scaling: Each service scales based on its specific needs

  • Technology flexibility: Different services can use different Node.js versions

  • Isolated deployments: Updates to one service don't affect others

  • Team autonomy: Different developers can own different services

  • Fault isolation: One service failure doesn't cascade to others

Phase 1: Building My First Microservice with Shared Types

Let me walk you through how I extracted my first service from the monolith—the User Service—and how I handled shared code that multiple services would need.

Creating the Shared Types Package

First, I created a shared types package that would contain common interfaces, types, and utilities that multiple services would use:

Setting Up the Package.json for the Shared Package

TypeScript Configuration for the Shared Package

Phase 2: Building the User Service

Now let me show you how I built my first microservice using the shared types:

User Service Architecture

User Service Implementation

User Service HTTP Controllers

Phase 3: Setting Up GitLab Package Registry for Shared Code

One of the biggest challenges I faced was how to share common code between my microservices. GitLab's package registry became my solution for versioning and distributing shared TypeScript packages.

Setting Up GitLab Package Registry Authentication

First, I needed to configure authentication for publishing packages to GitLab:

GitLab CI/CD Pipeline for Package Publishing

Version Management Strategy

I developed a semantic versioning strategy for my shared packages:

Phase 4: Inter-Service Communication with Shared Events

One of the most challenging aspects of my microservices transition was handling communication between services. Here's how I implemented event-driven communication using shared event types:

Event Publisher Implementation

Service Communication Sequence Diagram

spinner

Task Service Implementation

Phase 5: Package Dependency Management Across Services

Managing dependencies across multiple microservices became crucial as my system grew. Here's how I solved dependency management and kept everything in sync:

Automated Dependency Updates

My Key Learnings and Best Practices

After two years of running my microservices architecture with shared packages, here are the most important lessons I've learned:

1. Start with Clear Boundaries

Don't rush to break everything apart. I spent weeks identifying the right service boundaries:

  • Business capabilities: Each service should own a complete business function

  • Data ownership: Each service should own its data and never share databases

  • Team ownership: Services should align with team structures and responsibilities

2. Shared Package Strategy

My approach to shared packages:

3. Version Management

I learned to be very careful with breaking changes:

  • Semantic versioning: Patch for bugs, minor for features, major for breaking changes

  • Backward compatibility: Keep old versions working for at least one release cycle

  • Gradual rollouts: Update services one at a time, not all at once

4. Monitoring and Observability

With multiple services, monitoring became crucial:

What This Architecture Gave My Project

The transformation from monolith to microservices with shared packages delivered real benefits:

Development Velocity

  • Before: 2-3 days to add a new feature (fear of breaking things)

  • After: Same day feature delivery with confidence

Scalability

  • Before: Entire app would slow down during file uploads

  • After: Each service scales independently based on load

Team Collaboration

  • Before: Merge conflicts and coordination overhead

  • After: Independent development and deployment

Reliability

  • Before: One bug could bring down everything

  • After: Isolated failures with graceful degradation

Conclusion: When Microservices Make Sense

Microservices aren't a silver bullet—they come with complexity overhead. But for my growing task management platform, they solved real problems:

  • Independent scaling based on actual usage patterns

  • Technology flexibility to choose the best tool for each job

  • Team autonomy for faster development cycles

  • Fault isolation for better overall reliability

The key was having a solid shared package strategy with GitLab's package registry. This let me share common code without coupling services together.

My Advice for Your Project

Start with a monolith, then extract services when you hit real pain points:

  1. Identify service boundaries based on business capabilities

  2. Create shared packages for common types and utilities

  3. Set up package registry for versioning and distribution

  4. Implement event-driven communication for service coordination

  5. Invest in monitoring and observability from day one

The transition isn't easy, but with the right approach to shared code and dependency management, microservices can unlock the scalability and velocity your growing application needs.

Remember: the goal isn't perfect architecture—it's building systems that can evolve with your needs while maintaining the development velocity that got you there in the first place.

Last updated