Our offices

  • Exceev Consulting
    61 Rue de Lyon
    75012, Paris, France
  • Exceev Technology
    332 Bd Brahim Roudani
    20330, Casablanca, Morocco

Follow us

4 min read - Building Scalable and Maintainable Systems with NestJS

Backend Architecture & Monorepo Strategy

The monorepo vs multi-repo debate has shifted. For teams building complex backend systems with NestJS, monorepos have moved from experimental to the default recommendation — not because they are trendy, but because the tooling has matured enough to make them practical at every scale.

This guide covers the patterns, tooling, and trade-offs for running NestJS applications in a monorepo, from initial setup through production deployment.

What you'll learn

  • When a monorepo makes sense (and when it does not) for NestJS projects
  • How to set up an Nx-powered NestJS monorepo from scratch
  • Shared library patterns for authentication, logging, and data models
  • Microservices communication within a monorepo
  • CI/CD strategies that keep build times manageable

TL;DR

A NestJS monorepo shines when multiple services share code (auth, DTOs, utilities), when teams need atomic cross-service changes, and when you want a single CI/CD pipeline. Use Nx for computation caching, affected-command analysis, and dependency graph visualization. Avoid monorepos if services are truly independent with no shared code.

What Is a Monorepo (and What It Is Not)

A monorepo is a single repository containing multiple projects that can be built, tested, and deployed independently. It is not a monolith — each project within the repo can have its own deployment pipeline and runtime.

Tech giants (Google, Meta, Microsoft) have used monorepos for years. What changed is that tools like Nx and Turborepo made this approach viable for teams of 5, not just teams of 5,000.

Why NestJS Fits the Monorepo Model

NestJS's modular architecture — where every feature is a module with explicit imports and exports — maps naturally onto shared libraries within a monorepo:

Simplified dependency management. All services share a single node_modules. No version drift between services using different versions of the same library.

Atomic commits. Updating a shared DTO and every service that uses it happens in a single commit. No coordinated multi-repo PRs.

Code sharing without publishing. Shared libraries (auth guards, logging interceptors, TypeORM entities) are imported directly — no private npm registry needed.

Unified tooling. One ESLint config, one Prettier config, one test runner, one CI pipeline.

Setting Up an Nx-Powered NestJS Monorepo

Nx is the most mature monorepo tool for the Node.js ecosystem. Here is a practical setup:

npx create-nx-workspace@latest my-platform --preset=nest

This generates a workspace with a single NestJS app. Add more apps and libraries as needed:

# Add a second NestJS service
nx g @nx/nest:application api-gateway

# Create a shared library
nx g @nx/nest:library shared-auth
nx g @nx/nest:library shared-dto

Workspace Structure

my-platform/
├── apps/
│   ├── api-gateway/        # HTTP gateway service
│   │   └── src/
│   ├── billing-service/    # Billing microservice
│   │   └── src/
│   └── notification-service/
│       └── src/
├── libs/
│   ├── shared-auth/        # Auth guards, strategies, decorators
│   ├── shared-dto/         # Request/response DTOs, validation
│   ├── shared-database/    # TypeORM entities, migrations
│   └── shared-logging/     # Logger interceptors, correlation IDs
├── nx.json
├── tsconfig.base.json
└── package.json

TypeScript Path Aliases

Nx configures path aliases automatically so shared libraries are importable cleanly:

// In any app or library
import { AuthGuard, CurrentUser } from '@my-platform/shared-auth'
import { CreateUserDto, UserResponseDto } from '@my-platform/shared-dto'
import { LoggingInterceptor } from '@my-platform/shared-logging'

Shared Library Patterns

Authentication Library

A shared auth library provides guards, decorators, and strategies used across all services:

// libs/shared-auth/src/lib/guards/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    return super.canActivate(context)
  }
}

// libs/shared-auth/src/lib/decorators/current-user.decorator.ts
export const CurrentUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest()
  return request.user
})

DTO Library with Validation

Shared DTOs ensure consistent request/response shapes across services:

// libs/shared-dto/src/lib/user.dto.ts
export class CreateUserDto {
  @IsEmail()
  email: string

  @IsString()
  @MinLength(8)
  password: string
}

export class UserResponseDto {
  id: string
  email: string
  createdAt: Date
}

Microservices Communication

Within a monorepo, NestJS microservices can communicate through multiple transport layers:

TCP for synchronous service-to-service calls (low latency, simple setup).

RabbitMQ or Redis for event-driven communication (decoupled, resilient).

gRPC for high-performance, schema-driven communication (strong typing, efficient serialization).

// api-gateway calling billing-service via TCP
@Injectable()
export class BillingClient {
  private client: ClientProxy

  constructor() {
    this.client = ClientProxyFactory.create({
      transport: Transport.TCP,
      options: { host: 'localhost', port: 3001 },
    })
  }

  calculateBill(userId: string): Observable<BillingAmount> {
    return this.client.send('calculate_bill', { userId })
  }
}

CI/CD Strategies for Monorepos

The biggest concern with monorepos is build time. Nx solves this with two features:

Affected commands. Only build, test, and lint the projects affected by a given change:

nx affected --target=build
nx affected --target=test

Computation caching. Nx caches task outputs locally and (with Nx Cloud) remotely. If a library has not changed, its build output is reused.

A practical CI pipeline:

# .github/workflows/ci.yml
steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 0
  - run: npm ci
  - run: npx nx affected --target=lint --base=origin/main
  - run: npx nx affected --target=test --base=origin/main
  - run: npx nx affected --target=build --base=origin/main

When a Monorepo Is Not the Right Choice

A monorepo adds overhead. Skip it when:

  • Services are truly independent with no shared code
  • Teams are in different organizations with different release cadences
  • You have fewer than 2 deployable services
  • Your CI infrastructure cannot handle a larger repository

Start small, add services as complexity demands

NestJS and Nx form a productive combination for teams building multi-service backends. The monorepo removes coordination overhead (cross-repo PRs, version drift, duplicated configs) while Nx keeps build times in check. Start with a single app and one shared library, validate the workflow, then add services as complexity demands it. Need help architecting your NestJS monorepo? Let's talk.

We should talk.

Exceev works with startups and SMEs on consulting, open-source tooling, and production-ready software.

More articles

Running a Consultancy on Open-Source Business Tools: Our Operations Playbook

How Exceev runs its business operations on Twenty CRM, ZeroMail, n8n automation, Ghost publishing, Cal.com scheduling, and Postiz social publishing. An operations playbook for consultancies that want control over their business stack.

Read more

Self-Hosting Our Infrastructure: The Observability, Security, and Deployment Stack

How Exceev self-hosts its infrastructure with Grafana, Prometheus, Loki, k6, Coolify, Infisical, Docker, Tailscale, Cloudflared, Beszel, and Duplicati. An operational deep dive into observability, deployment, security, and resilience.

Read more

Tell us about your project

Our offices

  • Exceev Consulting
    61 Rue de Lyon
    75012, Paris, France
  • Exceev Technology
    332 Bd Brahim Roudani
    20330, Casablanca, Morocco