Skip to content

Nestjs boilerplate microservice api | Mongodb CRUD - Postgres CRUD | Docker | Husky | Secrets service | HTTP service | Logs service | Authentication | Authorization | Error Handler | Swaggger Documentation | Mongo Generic Repository | Postgres Generic Repository

Notifications You must be signed in to change notification settings

mikemajesty/nestjs-microservice-boilerplate-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸš€ NestJS Microservice Boilerplate API

Enterprise-grade, production-ready NestJS boilerplate with modern architecture patterns

Node.js Version TypeScript NestJS License: MIT

Code Coverage

Statements Branches Functions Lines
Statements Branches Functions Lines

Table of Contents


Architecture Overview

This project implements a pragmatic architecture that combines the best ideas from Clean Architecture, Domain-Driven Design (DDD), and Hexagonal Architecture. Rather than strictly following one pattern, it takes a practical approach: powerful enough for enterprise applications, yet simple enough for any developer to understand and maintain.

The Core Philosophy

The architecture is built around one fundamental principle: protect your business logic. Your domain rules should never depend on frameworks, databases, or external services. If you decide to switch from PostgreSQL to MongoDB, or from Redis to Memcached, your core business logic remains untouched.

Architecture Diagram

How It Compares to Other Architectures

Pattern This Project Key Difference
Clean Architecture βœ… Implements Simplified layers without over-engineering
Domain-Driven Design βœ… Implements Entities and Use Cases without complex aggregates
Hexagonal Architecture βœ… Implements Ports (interfaces) and Adapters (implementations)
Onion Architecture βœ… Implements Core at center, dependencies point inward

Architecture Comparison

Clean Architecture

Clean Architecture organizes code into concentric circles where dependencies point inward. The innermost circle contains business rules, and outer circles contain implementation details.

How we implement it:

  • Entities live in src/core/*/entity β€” pure business objects
  • Use Cases live in src/core/*/use-cases β€” application-specific business rules
  • Interfaces live in src/core/*/repository β€” contracts for external dependencies
  • Frameworks live in src/modules and src/infra β€” NestJS controllers and database implementations

Domain-Driven Design (DDD)

DDD focuses on modeling your business domain. It introduces concepts like Entities, Value Objects, Aggregates, and Repositories.

How we implement it:

  • Entities: Objects with identity that persist over time (UserEntity, RoleEntity)
  • Repository Pattern: Abstract interfaces defining data access contracts
  • Use Cases: Encapsulate business operations (similar to Application Services in DDD)
  • Bounded Contexts: Each module represents a bounded context

What we simplified:

  • Simplified Aggregates β€” entities can be grouped but without strict root enforcement
  • No Domain Events infrastructure β€” use the event system in libs/ when needed
  • No Value Objects as separate classes β€” Zod schemas handle validation

Hexagonal Architecture (Ports and Adapters)

Hexagonal Architecture separates the application from external concerns through Ports (interfaces) and Adapters (implementations).

How we implement it:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        ADAPTERS                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ Controllers β”‚  β”‚ Repositoriesβ”‚  β”‚ External Services   β”‚   β”‚
β”‚  β”‚ (modules/)  β”‚  β”‚ (modules/)  β”‚  β”‚ (infra/)            β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚         β”‚                β”‚                     β”‚              β”‚
β”‚         β–Ό                β–Ό                     β–Ό              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚                      PORTS                               β”‚ β”‚
β”‚  β”‚              (core/*/repository interfaces)              β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                            β”‚                                  β”‚
β”‚                            β–Ό                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚                       CORE                               β”‚ β”‚
β”‚  β”‚           Entities + Use Cases + Interfaces              β”‚ β”‚
β”‚  β”‚                (src/core/)                               β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Ports (Interfaces):

  • ICatRepository β€” defines what operations are available
  • IHttpAdapter β€” defines HTTP client contract
  • ICacheAdapter β€” defines caching contract

Adapters (Implementations):

  • CatRepository in modules/ β€” implements ICatRepository with TypeORM/Mongoose
  • HttpService in infra/ β€” implements IHttpAdapter with Axios
  • RedisService in infra/ β€” implements ICacheAdapter with Redis

Design Decisions

⚠️ Important Notes About This Architecture

This section explains some deliberate choices that differ from traditional implementations. Understanding these decisions will help you work with the codebase effectively.

Why We Call Interfaces "Adapters"

You may notice that some interfaces in this project use the word "Adapter" (e.g., IHttpAdapter, ICacheAdapter). In traditional Hexagonal Architecture:

  • Port: An interface that defines a contract (what operations are available)
  • Adapter: A concrete implementation that fulfills that contract (how it's done)

The academic distinction:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  HEXAGONAL (Traditional)                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                              β”‚
β”‚   Port (Interface)              Adapter (Implementation)     β”‚
β”‚   ─────────────────             ────────────────────────     β”‚
β”‚   IUserRepository        β†’      PostgresUserRepository       β”‚
β”‚   IEmailService          β†’      SendGridEmailService         β”‚
β”‚   ICacheService          β†’      RedisCacheService            β”‚
β”‚                                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Our simplified approach:

We use "Adapter" in interface names because, conceptually, both are abstractions. The fundamental principle is the same: decouple your core business logic from implementation details. Whether you call the interface a "Port" or "Adapter" doesn't change how the pattern works.

// Traditional naming
interface IUserRepository { }      // Port
class PostgresUserRepository { }   // Adapter

// Our naming (simplified)
interface IHttpAdapter { }         // Still an abstraction (contract)
class HttpService { }              // Still an implementation

Why this simplification?

  1. Reduced cognitive load β€” One less concept to explain to new developers
  2. Practical focus β€” The behavior is identical regardless of naming
  3. Consistency β€” All abstractions follow the same I*Adapter pattern

The key takeaway: if it's an interface, it's a contract. If it's a class implementing that interface, it's the implementation. The names are just labels.


Why We Use Abstract Classes Instead of Interfaces

You may notice that repository contracts use abstract class instead of TypeScript interface:

// What we use
export abstract class ICatRepository extends IRepository<CatEntity> {
  abstract findByBreed(breed: string): Promise<CatEntity[]>
}

// Instead of
export interface ICatRepository extends IRepository<CatEntity> {
  findByBreed(breed: string): Promise<CatEntity[]>
}

Why? This is a NestJS/Node.js limitation.

TypeScript interfaces are erased at runtime β€” they don't exist in the compiled JavaScript. NestJS dependency injection relies on runtime tokens to resolve providers. If we used interfaces, we would need to pass a string token:

// ❌ With interface β€” requires string token
@Module({
  providers: [
    {
      provide: 'ICatRepository',  // String token (error-prone, no type safety)
      useClass: CatRepository,
    },
  ],
})

// βœ… With abstract class β€” class itself is the token
@Module({
  providers: [
    {
      provide: ICatRepository,    // Class reference (type-safe, refactorable)
      useClass: CatRepository,
    },
  ],
})

Benefits of abstract classes:

  1. Type safety β€” No magic strings, refactoring tools work correctly
  2. Runtime existence β€” The class exists in compiled JavaScript
  3. Same behavior β€” Acts as a contract just like an interface
  4. Better DX β€” IDE autocomplete and "Go to Definition" work properly

The trade-off: Abstract classes can have implementation details (which interfaces cannot). We simply don't use that feature β€” our abstract classes are pure contracts.


Why the "Middlewares" Folder Contains More Than Middlewares

Yes, we know. The src/middlewares/ folder contains:

  • Middlewares (authentication)
  • Guards (authorization)
  • Interceptors (logging, tracing)
  • Filters (exception handling)

Why didn't we split them?

Honestly? We couldn't find a better name. 🀷

We tried:

  • http-pipeline/ β€” too generic
  • request-handlers/ β€” not quite right
  • cross-cutting/ β€” sounds like a buzzword bingo winner
  • stuff-that-runs-before-and-after-your-code/ β€” accurate but... no

So we stuck with middlewares/ because:

  1. They all operate in the HTTP request/response lifecycle
  2. They're all "things that wrap your controller logic"
  3. Everyone knows where to find them

If you have a better name, PRs are welcome! Until then, just accept that middlewares/ is a "creative interpretation" of the term. πŸ˜„


Why Validations Live Inside Use Cases

This is a fundamental difference from many Clean Architecture implementations.

The traditional approach (Clean Architecture):

Controller β†’ Validates Input β†’ Use Case β†’ Business Logic

In traditional Clean Architecture, input validation happens in the Controller or a dedicated Validation layer before reaching the Use Case. The Use Case assumes it receives valid data.

Our approach:

Controller β†’ Use Case (Validates + Business Logic)

We validate inputs inside the Use Case using Zod schemas.

Why we made this choice:

  1. Testability

    When you test a Use Case, you should test the complete behavior β€” including validation. It's unacceptable to have a Use Case that passes tests but fails in production because validation was bypassed.

    // Our tests validate the complete use case behavior
    it('should throw validation error for invalid email', async () => {
      const input = { email: 'invalid-email', name: 'John' };
      await expect(useCase.execute(input)).rejects.toThrow(ValidationException);
    });
  2. Use Case Integrity

    A Use Case is a complete unit of business logic. If CreateUserUseCase requires a valid email, that validation IS part of the use case β€” not something external to it.

  3. Self-Documenting Code

    Looking at a Use Case, you immediately see what inputs it expects and how they're validated. No need to hunt through multiple layers.

  4. Reduced Duplication

    If multiple controllers call the same Use Case, validations are automatically applied. No risk of one controller forgetting to validate.

Comparison with other approaches:

Approach Validation Location Pros Cons
Traditional Clean Controller/Validator layer Thin use cases Validation can be bypassed, harder to test
DDD Domain entities (Value Objects) Rich domain model Complex, verbose
Our Approach Inside Use Case Complete testability, self-contained See trade-offs below

The trade-off:

If you need to consume a Use Case from multiple entry points with different validation rules, the Use Case validations might be too restrictive.

Solution: For those cases, move specific validations to the Application layer (Controller/Adapter). The Use Case can have minimal validations (or none), and each consumer applies its own rules:

// Controller A - Web API (strict validation)
@Post()
async create(@Body() input: CreateUserInput): Promise<UserCreateOutput> {
  // Validate for web context
  const validated = WebUserSchema.parse(input);
  return this.useCase.execute(validated);
}

// Controller B - Internal service (different validation)
async createFromInternal(input: InternalUserInput): Promise<UserCreateOutput> {
  // Validate for internal context
  const validated = InternalUserSchema.parse(input);
  return this.useCase.execute(validated);
}

Our recommendation: Start with validations inside Use Cases. Only move them out when you have a concrete need for different validation rules per consumer.


What to Avoid in Core

The core/ folder is sacred β€” it contains your business logic and must remain pure and independent. Here are the key rules to follow:

Entities: Avoid Anemic Models

An anemic entity is just a data container with no behavior β€” essentially a DTO. This is an anti-pattern because business logic ends up scattered across use cases and services.

❌ Avoid βœ… Prefer
Entity with only properties Entity with properties and behavior
Business logic in Use Cases Business logic in the Entity when it relates to state
Calculations outside entity Calculations as entity methods

Ask yourself: "Does this logic relate to the entity's state?" If yes, it belongs in the entity.

πŸ“– See detailed examples: Entity Guide β€” includes Rich Entity vs Anemic Entity comparison


Entities: Must Extend BaseEntity

Every Entity must extend the BaseEntity class. This is mandatory in this project.

❌ Avoid βœ… Prefer
class UserEntity { } class UserEntity extends BaseEntity<UserEntity>() { }
export class CatEntity { } export class CatEntity extends BaseEntity<CatEntity>() { }
// ❌ WRONG - Not extending BaseEntity
export class CatEntity {
  id!: string
  name!: string
  breed!: string
  age!: number
  createdAt?: Date
  updatedAt?: Date
  deletedAt?: Date

  constructor(entity: Cat) {
    Object.assign(this, entity)
  }
}

// βœ… CORRECT - Extends BaseEntity
import { BaseEntity } from '@/utils/entity'

export class CatEntity extends BaseEntity<CatEntity>() {
  name!: Cat['name']
  breed!: Cat['breed']
  age!: Cat['age']

  constructor(entity: Cat) {
    super(CatEntitySchema)
    this.validate(entity)
    this.ensureID()
  }
}

What BaseEntity provides:

  1. Common properties β€” id, createdAt, updatedAt, deletedAt are inherited
  2. Validation β€” validate(entity) method validates input against Zod schema
  3. ID generation β€” ensureID() generates UUID if not provided
  4. Status methods β€” isActive(), isDeleted(), activate(), deactivate()
  5. Serialization β€” toObject() returns plain object, clone() creates a copy
  6. Type safety β€” nameOf() provides type-safe property names

Constructor pattern:

Every entity constructor must follow this pattern:

constructor(entity: Cat) {
  super(CatEntitySchema)  // 1. Pass Zod schema to parent
  this.validate(entity)   // 2. Validate and assign properties
  this.ensureID()         // 3. Generate ID if not provided
}

πŸ“– See detailed examples: Entity Guide β€” includes full entity implementation


Use Cases: Never Know Implementations

A Use Case must never, absolutely never know about concrete implementations. It should only work with abstractions (interfaces).

This is the most important rule: the Use Case receives abstractions, never implementations.

  • βœ… Entities (core/*/entity)
  • βœ… Repository interfaces (core/*/repository)
  • βœ… Adapter interfaces (IHttpAdapter, ICacheAdapter, etc.)
  • βœ… Utils and decorators (utils/)
  • βœ… Types and interfaces
❌ Avoid βœ… Prefer
import { Controller } from '@nestjs/common' No framework imports
import { UserRepository } from 'modules/user/repository' import { IUserRepository } from 'core/user/repository'
import { HttpService } from 'infra/http' import { IHttpAdapter } from 'infra/http' (interface only)
Direct database calls (TypeORM, Mongoose) Repository interface methods
new RedisService() Receive ICacheAdapter via constructor

The golden rule:

// ❌ WRONG - Use Case knows the implementation
import { HttpService } from '@/infra/http/service';

class MyUseCase {
  constructor(private http: HttpService) {} // Concrete class!
}

// βœ… CORRECT - Use Case only knows the abstraction
import { IHttpAdapter } from '@/infra/http/adapter';

class MyUseCase implements IUsecase {
  constructor(private http: IHttpAdapter) {} // Interface!
}

Why? The Use Case should work identically whether:

  • Called from a REST controller, GraphQL resolver, CLI, or message queue
  • Using Redis or Memcached for cache
  • Using Axios or Fetch for HTTP
  • Running in tests with mocks

Use Cases: Must Implement IUsecase

Every Use Case must implement the IUsecase interface. This is mandatory in this project.

❌ Avoid βœ… Prefer
class MyUseCase { } class MyUseCase implements IUsecase { }
export class CreateUserUseCase { } export class CreateUserUseCase implements IUsecase { }
// ❌ WRONG - Not implementing IUsecase
export class CatCreateUsecase {
  constructor(private readonly catRepository: ICatRepository) {}

  async execute(input: CatCreateInput): Promise<CatCreateOutput> {
    // ...
  }
}

// βœ… CORRECT - Implements IUsecase
import { IUsecase } from '@/utils/usecase';

export class CatCreateUsecase implements IUsecase {
  constructor(private readonly catRepository: ICatRepository) {}

  @ValidateSchema(CatCreateSchema)
  async execute(input: CatCreateInput): Promise<CatCreateOutput> {
    // ...
  }
}

Why this matters:

  1. Contract enforcement β€” Ensures all Use Cases have the same structure
  2. Dependency injection β€” NestJS can properly inject and resolve Use Cases
  3. Type safety β€” TypeScript validates that execute() method exists
  4. Consistency β€” Every Use Case follows the same pattern across the project

πŸ“– See detailed patterns: Use Case Guide β€” includes architecture diagrams and testing patterns


Repository Interfaces: Avoid Duplicating Generic Methods

The repository interface should only declare methods that don't exist in the generic IRepository<T>. The generic repository already provides 20+ methods:

❌ Avoid βœ… Prefer
Declaring create(), findById(), update() Already inherited from IRepository<T>
Duplicating generic query methods Only add domain-specific queries
// ❌ Wrong - These already exist in IRepository
export abstract class ICatRepository extends IRepository<CatEntity> {
  abstract create(entity: CatEntity): Promise<CatEntity>  // Already exists!
  abstract findById(id: string): Promise<CatEntity>      // Already exists!
}

// βœ… Correct - Only domain-specific methods
export abstract class ICatRepository extends IRepository<CatEntity> {
  abstract paginate(input: CatListInput): Promise<CatListOutput>
  abstract findByBreed(breed: string): Promise<CatEntity[]>
}

πŸ“– See full method list: Repository Guide β€” includes IRepository<T> generic methods and examples


Controllers/Adapters: Avoid Business Logic

Controllers and Adapters must never contain business logic. Their responsibility is limited to:

  1. Orchestration β€” Receive request, call use case, return response
  2. Input standardization β€” Transform and normalize inputs for the use case
❌ Avoid βœ… Prefer
Calculations in controller Move to Use Case or Entity
Conditional business rules Move to Use Case
Data manipulation Move to Use Case
Multiple repository calls Move to Use Case

When input standardization is OK:

We standardize listing inputs (pagination, sorting, search) in the Controller before calling the Use Case:

// βœ… OK - Standardizing pagination inputs (not business logic)
@Get()
@Version('1')
@Permission('cat:list')
async list(@Req() { query }: ApiRequest): Promise<CatListOutput> {
  const input: CatListInput = {
    sort: SortHttpSchema.parse(query.sort),
    search: SearchHttpSchema.parse(query.search),
    limit: Number(query.limit),
    page: Number(query.page)
  }

  return await this.listUsecase.execute(input)
}

// ❌ WRONG - Business logic in controller
@Post()
@Version('1')
@Permission('cat:create')
async create(@Req() { body }: ApiRequest): Promise<CatCreateOutput> {
  // DON'T DO THIS - business logic belongs in Use Case
  if (body.age > 10) {
    body.status = 'senior';
  }
  const discount = body.price * 0.1; // Business calculation!
  return await this.createUsecase.execute({ ...body, discount });
}

πŸ“– See detailed patterns: Controller Guide and Adapter Guide β€” includes examples and best practices


Core: Avoid External Libraries

The core/ folder must remain pure and framework-agnostic. Never import external libraries directly into entities or use cases.

❌ Avoid in Core βœ… Prefer
import axios from 'axios' Use IHttpAdapter interface
import { Repository } from 'typeorm' Use IRepository<T> interface
import moment from 'moment' Use utils/date or native Date
import _ from 'lodash' Use utils/collection or native methods
import Redis from 'ioredis' Use ICacheAdapter interface

Why?

If you import axios directly into a Use Case:

  • You can't easily test it (need to mock axios globally)
  • You can't swap to fetch or another HTTP client
  • Your core business logic is coupled to a specific library
// ❌ WRONG - External library in Use Case
import axios from 'axios';

export class GetExternalDataUseCase {
  async execute(): Promise<ExternalData> {
    const response = await axios.get('https://api.example.com/data');
    return response.data;
  }
}

// βœ… CORRECT - Use abstraction
import { IHttpAdapter } from '@/infra/http/adapter';
import { IUsecase } from '@/utils/usecase';

export class GetExternalDataUseCase implements IUsecase {
  constructor(private readonly http: IHttpAdapter) {}
  
  async execute(): Promise<ExternalData> {
    const response = await this.http.get({ url: 'https://api.example.com/data' });
    return response.data;
  }
}

Allowed in Core:

  • βœ… Zod (validation is part of domain logic)
  • βœ… Native Node.js/JavaScript APIs
  • βœ… Your own utils/ functions

Need an external library? If you need functionality from an external library, create a centralized wrapper in libs/ or utils/:

// ❌ WRONG - Using lodash directly in Use Case
import _ from 'lodash';

export class MyUseCase {
  execute(data: Product[]): Record<string, Product[]> {
    return _.groupBy(data, 'category'); // Direct lodash usage
  }
}

// βœ… CORRECT - Create a centralized wrapper
// utils/collection.ts
import _ from 'lodash';

export const CollectionUtil = {
  groupBy: <T>(array: T[], key: keyof T) => _.groupBy(array, key),
  uniqBy: <T>(array: T[], key: keyof T) => _.uniqBy(array, key),
  // ... expose only what you need
};

// Then in Use Case
import { CollectionUtil } from '@/utils/collection';
import { IUsecase } from '@/utils/usecase';

export class MyUseCase implements IUsecase {
  execute(data: Product[]): Record<string, Product[]> {
    return CollectionUtil.groupBy(data, 'category'); // βœ… Uses wrapper
  }
}

Benefits of centralization:

  • Single point of change if you need to swap libraries
  • Easier to mock in tests
  • Controls which functions are exposed
  • Documents which external libs are used in the project

Types: Use Entity Composition and Proper Naming

When creating Input/Output types, always derive them from the Entity. This is mandatory to avoid property duplication.

Rule 1: Compose from Entity

// ❌ WRONG - Duplicating properties that exist in Entity
type UserCreateInput = {
  name: string;      // Already in UserEntity!
  email: string;     // Already in UserEntity!
  password: string;  // Already in UserEntity!
};

// βœ… CORRECT - Compose from Entity
type UserCreateInput = Pick<UserEntity, 'name' | 'email' | 'password'>;

// βœ… CORRECT - Extend when needed
type UserUpdateInput = Pick<UserEntity, 'id'> & Partial<Pick<UserEntity, 'name' | 'email'>>;

// βœ… CORRECT - Omit sensitive fields for output
type UserOutput = Omit<UserEntity, 'password' | 'deletedAt'>;

Rule 2: Use z.infer for Validated Types

When you need runtime validation, use Zod schema with z.infer. Zod has built-in pick and omit methods for composition:

// βœ… Schema with validation using pick (cleaner)
const UserCreateSchema = UserEntitySchema.pick({
  name: true,
  email: true,
  password: true,
});

// βœ… Schema using omit (exclude fields)
const UserOutputSchema = UserEntitySchema.omit({
  password: true,
  deletedAt: true,
});

// βœ… Infer type from schema
type UserCreateInput = z.infer<typeof UserCreateSchema>;
type UserOutput = z.infer<typeof UserOutputSchema>;

πŸ“– See detailed patterns: Entity Guide β€” includes schema composition examples

Rule 3: Naming Convention β€” Input and Output Only

Never use prefixes or suffixes like DTO, ViewModel, Request, Response. The standard naming convention is:

❌ Avoid βœ… Use
CreateUserDTO UserCreateInput
UserResponseDTO UserCreateOutput
UserViewModel UserOutput
GetUserRequest UserGetInput
UserListResponse UserListOutput

Pattern: {Entity}{Action}{Input|Output}

// Naming examples
type UserCreateInput = Pick<UserEntity, 'name' | 'email' | 'password'>;
type UserCreateOutput = Pick<UserEntity, 'id' | 'name' | 'email' | 'createdAt'>;

type UserUpdateInput = Pick<UserEntity, 'id'> & Partial<Pick<UserEntity, 'name'>>;
type UserUpdateOutput = Pick<UserEntity, 'id' | 'name' | 'updatedAt'>;

type UserListInput = { pagination: PaginationInput; search?: string };
type UserListOutput = { data: UserOutput[]; pagination: PaginationOutput };

Types: Avoid any β€” Always Type When Possible

The any type defeats the purpose of TypeScript. Always provide explicit types when it makes sense and doesn't create unnecessary complexity.

❌ Avoid βœ… Prefer
function process(data: any) function process(data: UserEntity)
const result: any = await fetch() const result: ApiResponse = await fetch()
items.map((item: any) => ...) items.map((item: OrderItem) => ...)

When to type:

  1. Function parameters β€” Always type them
  2. Function return types β€” Type when not obvious from implementation
  3. Variables β€” Type when TypeScript can't infer correctly
  4. Generics β€” Use generics instead of any for flexible types
// ❌ WRONG - Using any
const processItems = (items: any[]): any => {
  return items.map((item: any) => item.value);
}

// βœ… CORRECT - Properly typed
const processItems = <T extends { value: number }>(items: T[]): number[] => {
  return items.map((item) => item.value);
}

// βœ… CORRECT - Using unknown when type is truly unknown
const parseJson = (json: string): unknown => {
  return JSON.parse(json);
}

When any is unavoidable:

Sometimes you genuinely can't type something properly (third-party libraries, complex dynamic types, etc.). In these cases, use eslint-disable to acknowledge the exception:

// βœ… OK - Acknowledged exception with eslint-disable
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleLegacyApi = (response: any): ProcessedData => {
  // Legacy API with unpredictable structure
  return transformLegacyResponse(response);
}

// βœ… OK - Type assertion after validation
const processExternalData = (data: unknown): UserData => {
  if (!isValidUserData(data)) {
    throw new Error('Invalid data');
  }
  return data as UserData;
}

The rule of thumb: If you're reaching for any, ask yourself:

  1. Can I use a specific type? β†’ Use it
  2. Can I use a generic? β†’ Use <T>
  3. Can I use unknown? β†’ Safer than any
  4. None of the above work? β†’ Use any with eslint-disable

Functions: Always Declare Explicit Return Types

This is a project standard: every function must have an explicit return type. TypeScript can infer return types, but explicit declarations improve code readability and catch errors earlier.

❌ Avoid βœ… Prefer
async getUser() async getUser(): Promise<UserEntity>
const sum = (a, b) => const sum = (a: number, b: number): number =>
execute(input) execute(input: CreateInput): Promise<void>
// ❌ WRONG - No explicit return type
async getById(id: string) {
  return await this.repository.findById(id)
}

// ❌ WRONG - Missing Promise<void>
async delete(id: string) {
  await this.repository.delete(id)
}

// βœ… CORRECT - Explicit return types
async getById(id: string): Promise<UserEntity> {
  return await this.repository.findById(id)
}

async delete(id: string): Promise<void> {
  await this.repository.delete(id)
}

// βœ… CORRECT - Even for simple functions
const calculateTotal = (items: OrderItem[]): number => {
  return items.reduce((sum, item) => sum + item.price, 0)
}

Why this matters:

  1. Self-documentation β€” Reading the function signature tells you exactly what to expect
  2. Earlier error detection β€” TypeScript catches mismatches at compile time
  3. Refactoring safety β€” Changing implementation won't accidentally change return type
  4. API contracts β€” Makes interfaces and abstractions crystal clear

Common return types:

Scenario Return Type
Async operation that returns data Promise<EntityType>
Async operation with no return Promise<void>
Sync function returning value string, number, boolean, etc.
Function returning nothing void
Function that may return null Promise<Entity | null>

Aggregates: Multiple Entities in the Same Folder

In DDD, an Aggregate is a cluster of related entities that are treated as a single unit. When entities belong to the same aggregate, they can live together in the same folder.

Example: User Aggregate

If User and Address are always created/updated together and Address has no meaning without a User, they belong to the same aggregate:

core/
└── user/
    β”œβ”€β”€ entity/
    β”‚   β”œβ”€β”€ user.ts           # Aggregate Root
    β”‚   └── address.ts        # Belongs to User aggregate
    β”œβ”€β”€ repository/
    β”‚   β”œβ”€β”€ user.ts           # Main repository
    β”‚   └── address.ts        # Can have its own repository if needed
    └── use-cases/
        β”œβ”€β”€ user-create.ts    # May create User + Address together
        └── address-update.ts # Can update Address independently

When to use aggregates:

Scenario Same Folder (Aggregate) Separate Folders
Entities always created together βœ… β€”
Child has no meaning without parent βœ… β€”
Shared business rules βœ… β€”
Entities are independent β€” βœ…
Different lifecycles β€” βœ…

Key rules:

  1. Aggregate Root β€” One entity is the "root" (e.g., User). External access should go through it
  2. Transactional consistency β€” Operations within an aggregate should be atomic
  3. Own rules β€” Each entity can still have its own validation and behavior
  4. Separate repositories are OK β€” Address can have its own repository for specific queries

Practical example:

// user-create.ts - Creates User with Address in same transaction
import { IUsecase } from '@/utils/usecase';

export class UserCreateUseCase implements IUsecase {
  constructor(
    private readonly userRepository: IUserRepository,
    private readonly addressRepository: IAddressRepository,
  ) {}

  async execute(input: UserCreateInput): Promise<UserCreateOutput> {
    // Create both as part of the same aggregate operation
    const user = new UserEntity(input.user);
    const address = new AddressEntity({ ...input.address, userId: user.id });
    
    await this.userRepository.create(user);
    await this.addressRepository.create(address);
    
    return new UserCreateOutput(user);
  }
}

Don't over-engineer: Not everything needs to be an aggregate. Start simple β€” if you notice entities are always manipulated together, then group them.


Layer Communication Rules

Understanding which layers can communicate with which is crucial for maintaining the architecture.

The Golden Rule

Dependencies always point inward. Inner layers never know about outer layers.

Communication Matrix

Layer Can Access Cannot Access
Core (Entities) Nothing Everything else
Core (Use Cases) Entities, Repository Interfaces Modules, Infra, Libs
Core (Repositories) Entities Everything else (it's just an interface)
Modules Core (all), Infra, Libs β€”
Infra Core Interfaces Core Use Cases, Modules
Libs Nothing from src/ β€”

Visual Representation

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚      MODULES        β”‚
                    β”‚   (Controllers,     β”‚
                    β”‚    Adapters)        β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚ uses
                               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    INFRA     │◄─────│       CORE          │─────►│     LIBS     β”‚
β”‚  (Database,  β”‚      β”‚  (Entities, Use     β”‚      β”‚  (Tokens,    β”‚
β”‚   Cache,     β”‚      β”‚   Cases, Repo       β”‚      β”‚   Events,    β”‚
β”‚   HTTP)      β”‚      β”‚   Interfaces)       β”‚      β”‚   i18n)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚                      β–²
        β”‚                      β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              implements

Practical Example

When a user creates a new cat:

1. Controller (modules/cat/controller.ts)
   └── receives HTTP request

2. Adapter (modules/cat/adapter.ts)
   └── transforms request, calls use case

3. Use Case (core/cat/use-cases/cat-create.ts)
   └── contains business logic
   └── calls repository interface

4. Repository Interface (core/cat/repository/cat.ts)
   └── defines contract (what, not how)

5. Repository Implementation (modules/cat/repository.ts)
   └── implements the interface
   └── uses TypeORM/Mongoose to persist data

The use case never knows if data goes to PostgreSQL, MongoDB, or a mock. It only knows it has a repository that can create(), update(), delete(), and findById().


Project Structure

src/
β”œβ”€β”€ core/                    # 🧠 Business Logic (Framework-agnostic)
β”‚   └── [module]/
β”‚       β”œβ”€β”€ entity/          # Domain entities with Zod validation
β”‚       β”œβ”€β”€ repository/      # Repository interfaces (contracts)
β”‚       └── use-cases/       # Business rules and operations
β”‚           └── __tests__/   # Unit tests for use cases
β”‚
β”œβ”€β”€ modules/                 # πŸ”Œ NestJS Application Layer
β”‚   └── [module]/
β”‚       β”œβ”€β”€ adapter.ts       # Connects controllers to use cases
β”‚       β”œβ”€β”€ controller.ts    # HTTP endpoints
β”‚       β”œβ”€β”€ module.ts        # NestJS module definition
β”‚       β”œβ”€β”€ repository.ts    # Repository implementation
β”‚       └── swagger.ts       # API documentation
β”‚
β”œβ”€β”€ infra/                   # πŸ”§ Infrastructure Layer
β”‚   β”œβ”€β”€ database/            # Database connections and schemas
β”‚   β”œβ”€β”€ cache/               # Redis and in-memory cache
β”‚   β”œβ”€β”€ http/                # HTTP client with circuit breaker
β”‚   β”œβ”€β”€ logger/              # Pino logger configuration
β”‚   β”œβ”€β”€ secrets/             # Environment variables management
β”‚   └── repository/          # Base repository implementations
β”‚
β”œβ”€β”€ libs/                    # πŸ“š Shared Libraries
β”‚   β”œβ”€β”€ event/               # Event emitter system
β”‚   β”œβ”€β”€ i18n/                # Internationalization
β”‚   β”œβ”€β”€ token/               # JWT management
β”‚   └── metrics/             # Prometheus metrics
β”‚
└── utils/                   # πŸ› οΈ Utility Functions
    β”œβ”€β”€ decorators/          # Custom decorators
    β”œβ”€β”€ middlewares/         # HTTP middlewares
    β”œβ”€β”€ interceptors/        # NestJS interceptors
    └── filters/             # Exception filters

Folder Responsibilities

Folder Responsibility Can Import From
core/ Pure business logic, entities, use cases, repository contracts Only itself
modules/ NestJS controllers, dependency injection, route handling core/, infra/, libs/
infra/ External services, databases, cache, HTTP clients core/ (interfaces only)
libs/ Reusable libraries, framework-agnostic utilities Nothing from src/
utils/ Helper functions, decorators, middlewares Anything

Quick Start

Prerequisites

  • Node.js >= 22.0.0
  • Docker >= 20.x
  • Docker Compose >= 2.x

1. Clone and Install

git clone https://github.com/mikemajesty/nestjs-microservice-boilerplate-api.git
cd nestjs-microservice-boilerplate-api

# Use correct Node version
nvm install && nvm use

# Install dependencies
npm install

2. Start Infrastructure

npm run setup

This starts PostgreSQL, MongoDB (replica set), Redis, Zipkin, Prometheus, Grafana, and more.

3. Run the Application

npm run start:dev

The API will be available at http://localhost:5000

4. Test the API

Login with default credentials:

curl -X 'POST' \
  'http://localhost:5000/api/v1/login' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "admin@admin.com",
    "password": "admin"
  }'

5. Explore the API

Open Swagger documentation: http://localhost:5000/api-docs


User Flow

The following diagram illustrates how a request flows through the system:

User Flow Diagram

Flow explanation:

  1. Client sends HTTP request
  2. Controller receives and validates input
  3. Adapter transforms request and calls use case
  4. Use Case executes business logic
  5. Repository (via interface) persists/retrieves data
  6. Response flows back through the same layers

Documentation Guides

Complete documentation for every aspect of this project is available in the guides/ folder. Each guide provides in-depth explanations, examples, and best practices.

πŸ“‚ Core

Business logic layer documentation.

Guide Description
Entity Domain entities with Zod validation
Use Case Business rules and operations
Repository Repository interface patterns
Test Testing use cases

πŸ“‚ Modules

NestJS application layer documentation.

Guide Description
Module NestJS module structure
Controller HTTP endpoints
Adapter Use case adapters
Repository Repository implementations
Test Module testing

External services and integrations.

Guide Description
Database PostgreSQL and MongoDB setup
Cache Redis and in-memory caching
HTTP HTTP client with circuit breaker
Logger Pino logging configuration
Secrets Environment variables
Repository Base repository patterns
Email Email sending with templates

πŸ“‚ Libraries

Shared libraries and utilities.

Guide Description
Token JWT management
Event Event emitter system
i18n Internationalization
Metrics Prometheus metrics

πŸ“‚ Decorators

Custom decorators for common patterns.

Guide Description
Circuit Breaker Resilience pattern
Permission Authorization decorator
Validate Schema Input validation
Log Execution Time Performance logging
Request Timeout Timeout handling
Process Background processing
Thread Worker threads

πŸ“‚ Middlewares

HTTP middleware components.

Guide Description
Authentication JWT authentication
Authorization Role-based access
HTTP Logger Request/response logging
Tracing Distributed tracing
Exception Handler Error handling

πŸ“‚ Tests

Testing utilities and patterns.

Guide Description
Mock Mock data generation
Containers Testcontainers setup
Util Test utilities

πŸ“‚ Setup

Project configuration and setup.

Guide Description
Environment Environment variables
Docker Docker configuration
Husky Git hooks
Package NPM scripts

πŸ“‚ Deploy

Deployment and CI/CD documentation.

Guide Description
Readme Complete deployment guide
Action GitHub Actions workflows

πŸ“‚ Utils

Utility functions and helpers.

Guide Description
Pagination Pagination utilities
Exception Exception handling
Crypto Encryption utilities
Date Date manipulation
Validator Validation helpers
Collection Array utilities
Search Search utilities

Key Features

Authentication & Authorization

  • JWT-based authentication with refresh tokens
  • Role-Based Access Control (RBAC)
  • Permission system with granular control
  • Password reset flow with email

Multi-Database Support

  • PostgreSQL with TypeORM for relational data
  • MongoDB with Mongoose (3-node replica set)
  • Automatic migrations

Observability

  • Distributed Tracing with OpenTelemetry and Zipkin
  • Logging with Pino and Loki
  • Metrics with Prometheus and Grafana
  • Health Checks for all services

Developer Experience

  • CRUD Scaffolding β€” generate complete modules with npm run scaffold
  • 100% Test Coverage β€” comprehensive test suites
  • Type Safety β€” full TypeScript with Zod validation
  • API Documentation β€” Swagger UI with TypeSpec

Resilience

  • Circuit Breaker pattern for external calls
  • Retry Logic with exponential backoff
  • Request Timeout handling

Tech Stack

Category Technologies
Framework NestJS 11.x, TypeScript 5.9.3
Databases PostgreSQL (TypeORM), MongoDB (Mongoose), Redis
Observability OpenTelemetry, Zipkin, Pino, Prometheus, Grafana, Loki
Testing Jest, Supertest, Testcontainers
Code Quality ESLint, Prettier, Husky, Commitlint
DevOps Docker, Docker Compose, PM2, GitHub Actions
Documentation Swagger, TypeSpec

Contributing

Contributions are welcome! Please read our contributing guidelines before submitting a PR.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes using conventional commits (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.


Support


Built with ❀️ by Mike Lima

About

Nestjs boilerplate microservice api | Mongodb CRUD - Postgres CRUD | Docker | Husky | Secrets service | HTTP service | Logs service | Authentication | Authorization | Error Handler | Swaggger Documentation | Mongo Generic Repository | Postgres Generic Repository

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 6