@sisques-labs/nestjs-kit
v0.7.1
Published
NestJS Kit with DDD/CQRS base classes, value objects, repository abstractions, and GraphQL utilities
Readme
@sisques-labs/nestjs-kit
NestJS Kit — a shared NestJS library providing Domain-Driven Design (DDD) and CQRS building blocks, validated value objects, repository abstractions, optional MongoDB and TypeORM helpers, GraphQL DTOs and plugins, and optional Winston logger configuration (for use with nest-winston in consuming apps) for microservices and modular monoliths.
Table of Contents
- Publishing
- Installation
- Peer Dependencies
- Local development
- Module Setup
- Domain Layer
- Application Layer
- Infrastructure Layer
- Transport Layer (GraphQL)
- Enums
Publishing
The package is published to the public npm registry as @sisques-labs/nestjs-kit (see publishConfig in package.json). Releases are automated with GitHub Actions.
| Workflow | File | Trigger |
|---|---|---|
| CI | .github/workflows/ci.yml | Push and pull requests targeting main |
| Release | .github/workflows/release.yml | Manual (workflow_dispatch) |
CI
Runs pnpm install --frozen-lockfile, pnpm lint (ESLint with --fix), pnpm build, and pnpm test.
Release workflow
Open GitHub → Actions → Release → Run workflow and choose:
| Input | Purpose |
|---|---|
| version | patch, minor, or major (SemVer) |
| release_type | stable (default dist-tag) or prerelease alpha / beta (separate dist-tags) |
The job then: runs lint and tests, bumps the version with npm version, builds, publishes with pnpm publish using the NPM_TOKEN repository secret, commits package.json / lockfile, creates a git tag, pushes to main, and creates a GitHub Release with generated notes.
Repository setup: add an npm automation token with publish rights as the NPM_TOKEN secret (GitHub → Settings → Secrets and variables → Actions).
Publish from your machine
npm login
pnpm build
pnpm publishThe prepublishOnly script runs npm run build before publish so dist/ is current.
Installation
The package is public on npm; a normal install is enough:
pnpm add @sisques-labs/nestjs-kit
# or: npm install / yarn add @sisques-labs/nestjs-kitUse your organization’s registry or mirror policy if applicable.
If you previously used @sisques-labs/shared-nestjs, uninstall it and depend on @sisques-labs/nestjs-kit instead; replace every import path from '@sisques-labs/shared-nestjs' to '@sisques-labs/nestjs-kit' (API unchanged).
Peer Dependencies
Install only what your app uses. Peers marked optional in peerDependenciesMeta can be omitted if you do not import that part of the library.
# Core NestJS (required for any integration)
pnpm add @nestjs/common @nestjs/core reflect-metadata rxjs
# CQRS (command handlers, EventBus)
pnpm add @nestjs/cqrs
# MongoDB module + repositories in this package
pnpm add mongodb @nestjs/config
# TypeORM module + repositories in this package
pnpm add typeorm @nestjs/typeorm @nestjs/config
# GraphQL DTOs, Apollo, complexity plugin
pnpm add graphql @nestjs/graphql @nestjs/apollo @apollo/server graphql-query-complexity
# class-validator / class-transformer (typical for GraphQL inputs)
pnpm add class-validator class-transformer
# Winston logging (shared config + nest-winston in your app)
pnpm add nest-winston winston winston-daily-rotate-fileLocal development
For contributors working on this repository:
| Script | Description |
|---|---|
| pnpm install | Installs dependencies; prepare runs Husky and pnpm build. |
| pnpm build | Compiles TypeScript to dist/ (nest build). |
| pnpm lint | ESLint with --fix on src, apps, libs, test. |
| pnpm lint:check | ESLint without autofix (used by Husky pre-commit). |
| pnpm test | Jest unit tests (*.spec.ts under src/). |
| pnpm test:cov | Tests with coverage. |
| pnpm format | Prettier on src and test TypeScript. |
Git hooks: Husky runs pnpm lint:check and pnpm test on pre-commit (see .husky/pre-commit). To skip hooks for a one-off commit: HUSKY=0 git commit ....
Module Setup
The library is opt-in by feature. Import MongoModule, TypeOrmModule, GraphQL pieces (DTOs, registerSharedGraphqlEnums, mappers, plugins), and domain exports only when you need them.
SharedModule is an optional empty Module kept for backward compatibility; it registers no providers and is not @Global().
Optional database modules:
MongoModule— providesMongoService(MONGODB_URI,MONGODB_DATABASEviaConfigService).TypeOrmModule— registersTypeOrmModule.forRootAsyncusingDATABASE_*config; requiresConfigModulein the app (for exampleConfigModule.forRoot({ isGlobal: true })).
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MongoModule, TypeOrmModule } from '@sisques-labs/nestjs-kit';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
MongoModule, // omit if you do not use MongoDB
TypeOrmModule, // omit if you do not use TypeORM
],
})
export class AppModule {}Logging is separate: this library does not register WinstonModule. Import WinstonModule from nest-winston in your app and pass createSharedWinstonLoggerOptions() or defaultSharedWinstonLoggerOptions—see Logging (Winston).
Domain Layer
Base Aggregate
BaseAggregate extends @nestjs/cqrs AggregateRoot and wires createdAt and updatedAt as DateValueObject properties. Add identity and domain fields in your subclass (for example a UuidValueObject or app-specific id type).
import {
BaseAggregate,
DateValueObject,
EmailValueObject,
UuidValueObject,
} from '@sisques-labs/nestjs-kit';
export class UserAggregate extends BaseAggregate {
constructor(
private readonly _id: UuidValueObject,
private _email: EmailValueObject,
createdAt: DateValueObject,
updatedAt: DateValueObject,
) {
super(createdAt, updatedAt);
}
get id(): UuidValueObject {
return this._id;
}
}Use apply(), commit(), and related AggregateRoot APIs for domain events as usual.
Value Objects
All value objects are immutable and validate their input on construction, throwing a typed domain exception on invalid data.
Basic Scalars
| Class | Description |
|---|---|
| StringValueObject | String with optional min/max length, pattern, and trim |
| NumberValueObject | Numeric value with validation |
| BooleanValueObject | Boolean wrapper |
| DateValueObject | Date wrapper |
| JsonValueObject | Valid JSON value |
| EnumValueObject<T> | Typed enum wrapper |
Format-Specific
| Class | Description |
|---|---|
| EmailValueObject | RFC 5322 compliant. Methods: getLocalPart(), getDomain() |
| UuidValueObject | RFC 4122. Methods: getVersion(), isNil(), static generate() |
| PhoneValueObject | E.164 format. Methods: getCountryCode(), toE164() |
| UrlValueObject | Valid URL |
| IpValueObject | IPv4/IPv6 validation |
| HexValueObject | Hexadecimal string |
| ColorValueObject | Hex, RGB, HSL, and named colors with conversion methods |
| SlugValueObject | URL-friendly slug. Methods: toHumanReadable(), addPrefix(), addSuffix(), static generateSlug() |
| PasswordValueObject | Strength scoring, common password detection. Methods: getStrengthScore(), meetsRequirements() |
| LocaleValueObject | BCP 47 locale (50+ supported). Methods: getLanguageCode(), getCountryCode(), getDisplayName() |
| TimezoneValueObject | IANA timezone |
| PhoneCodeValueObject | Phone dial code |
| LengthUnitValueObject | Unit of length measurement |
| DimensionsValueObject | Length, width, height with unit and optional bounds |
| NumericRangeValueObject | Min/max numeric range |
Usage example:
import {
EmailValueObject,
UuidValueObject,
PasswordValueObject,
} from '@sisques-labs/nestjs-kit';
const email = new EmailValueObject('[email protected]');
console.log(email.getDomain()); // 'example.com'
const id = UuidValueObject.generate();
console.log(id.getVersion()); // 4
const password = new PasswordValueObject('MyS3cur3P@ss!');
console.log(password.getStrengthScore()); // number 0-5
console.log(password.meetsRequirements()); // booleanMany value objects also have a folder-level README.md under src/shared/domain/value-objects/<name>/ with API tables and examples.
Aggregate and entity IDs
The library exports UuidValueObject for validated RFC 4122 UUID strings (construction, generate(), getVersion(), isNil(), etc.).
For aggregate- or entity-specific identifiers (nominal typing per bounded context), define thin subclasses or wrappers in your application, for example:
import { UuidValueObject } from '@sisques-labs/nestjs-kit';
export class UserId extends UuidValueObject {
// optional: narrow type or factory methods for your domain
}Domain Exceptions
BaseException is the root exception class. Each value object has a corresponding typed exception thrown on validation failure.
import { BaseException } from '@sisques-labs/nestjs-kit';
// BaseException provides:
// - timestamp: Date
// - getDetailedMessage(): string → "[ClassName]: message"
// - toJSON(): objectAvailable typed exceptions (all extend BaseException):
InvalidBooleanException, InvalidColorException, InvalidDimensionsException, InvalidEmailException, InvalidEnumValueException, InvalidHexException, InvalidIpException, InvalidJsonException, InvalidLocaleException, InvalidNumberException, InvalidNumericRangeException, InvalidPasswordException, InvalidPhoneException, InvalidStringException, InvalidTimezoneException, InvalidUrlException, InvalidUuidException
Criteria & Pagination
Use Criteria to build type-safe query parameters with filters, sorts, and pagination.
import {
Criteria,
FilterOperator,
SortDirection,
} from '@sisques-labs/nestjs-kit';
const criteria = new Criteria(
[{ field: 'email', operator: FilterOperator.EQUALS, value: '[email protected]' }],
[{ field: 'createdAt', direction: SortDirection.DESC }],
{ page: 1, perPage: 20 },
);PaginatedResult<T> wraps paginated query results:
import { PaginatedResult } from '@sisques-labs/nestjs-kit';
// { data: T[], total: number, page: number, perPage: number }
const result: PaginatedResult<User> = await repository.findByCriteria(criteria);Repository Interfaces
Implement these interfaces in your infrastructure layer to keep the domain free of database concerns.
import {
IBaseReadRepository,
IBaseWriteRepository,
} from '@sisques-labs/nestjs-kit';
// Read side: findById, findByCriteria, save, delete
interface IUserReadRepository extends IBaseReadRepository<UserAggregate> {}
// Write side: findById, save, delete
interface IUserWriteRepository extends IBaseWriteRepository<UserAggregate> {}Factory Interfaces
Factories handle deserialization of aggregates from different sources.
import { IReadFactory, IWriteFactory } from '@sisques-labs/nestjs-kit';
// Read factory: creates view models from aggregates, DTOs, or primitives
class UserReadFactory implements IReadFactory<UserViewModel, UserAggregate, UserDto> {
create(data: UserDto): UserViewModel { ... }
fromAggregate(aggregate: UserAggregate): UserViewModel { ... }
fromPrimitives(primitives: object): UserViewModel { ... }
}
// Write factory: creates aggregates from commands or primitives
class UserWriteFactory implements IWriteFactory<UserAggregate, CreateUserCommand, UserPrimitives> {
create(command: CreateUserCommand): UserAggregate { ... }
fromPrimitives(primitives: UserPrimitives): UserAggregate { ... }
}View Models
BaseViewModel provides a base for read-side projections with typed accessors for id, createdAt, and updatedAt.
import { BaseViewModel } from '@sisques-labs/nestjs-kit';
export class UserViewModel extends BaseViewModel {
// Inherited: getId(), getCreatedAt(), getUpdatedAt()
}Domain Events
IBaseEventData and IEventMetadata provide a structured shape for domain events with aggregate and entity metadata.
import { IBaseEventData, IEventMetadata } from '@sisques-labs/nestjs-kit';
// IEventMetadata shape:
// {
// aggregateRootId: string;
// aggregateRootType: string;
// entityId: string;
// entityType: string;
// eventType: string;
// }Application Layer
Command Handlers
BaseCommandHandler integrates the EventBus to publish domain events automatically after command execution.
import { BaseCommandHandler } from '@sisques-labs/nestjs-kit';
import { CommandHandler, EventBus } from '@nestjs/cqrs';
@CommandHandler(CreateUserCommand)
export class CreateUserCommandHandler extends BaseCommandHandler<CreateUserCommand> {
constructor(
private readonly repository: IUserWriteRepository,
eventBus: EventBus,
) {
super(eventBus);
}
async execute(command: CreateUserCommand): Promise<void> {
const user = UserWriteFactory.create(command);
await this.repository.save(user);
this.publishEvents(user); // publishes domain events from the aggregate
}
}BaseUpdateCommandHandler adds utilities to extract changed fields from update commands, useful for partial updates.
Service Interface
IBaseService is a marker interface for application services.
import { IBaseService } from '@sisques-labs/nestjs-kit';
@Injectable()
export class UserService implements IBaseService {}Infrastructure Layer
Logging (Winston)
Optional Winston LoggerOptions (JSON file rotation + console formats) for use with WinstonModule.forRoot from nest-winston in the consuming app. Install nest-winston, winston, and winston-daily-rotate-file there; this package lists winston and winston-daily-rotate-file as optional peers.
Main exports: createSharedWinstonLoggerOptions, defaultSharedWinstonLoggerOptions, mergeSharedWinstonLoggerOptions, createSharedJsonLogFormat, createSharedConsoleLogFormat, and SharedWinstonLoggerFactoryOptions.
Full guide: src/shared/infrastructure/logging/README.md
MongoDB
Import MongoModule from this package when you use MongoDB repositories. It is not part of SharedModule.
Environment Variables
The shared MongoService reads:
MONGODB_URI=mongodb://localhost:27017
MONGODB_DATABASE=my_databaseBase Repository
Extend BaseMongoDatabaseRepository (do not only implements IBaseReadRepository) so this includes MongoService, getCollection, buildMongoQuery, buildSortQuery, executeQueryWithPagination, and calculatePagination from BaseDatabaseRepository.
Published dist/**/*.d.ts use relative imports (rewritten at build with tsc-alias), so consumers do not need the kit’s @/ path aliases to resolve inherited types.
import {
BaseMongoDatabaseRepository,
MongoService,
Criteria,
PaginatedResult,
} from '@sisques-labs/nestjs-kit';
@Injectable()
export class UserMongoReadRepository extends BaseMongoDatabaseRepository {
private static readonly COLLECTION = 'users';
constructor(mongoService: MongoService) {
super(mongoService);
}
async findByCriteria(
criteria: Criteria,
): Promise<PaginatedResult<UserViewModel>> {
const collection = this.getCollection(UserMongoReadRepository.COLLECTION);
const mongoQuery = this.buildMongoQuery(criteria);
const sortQuery = this.buildSortQuery(criteria);
const { skip, limit } = await this.calculatePagination(criteria);
const [rows, total] = await this.executeQueryWithPagination(
collection,
mongoQuery,
sortQuery,
skip,
limit,
);
return new PaginatedResult<UserViewModel>(
rows.map((doc) => /* yourMongoMapper.toViewModel(doc) */ doc as UserViewModel),
total,
criteria.pagination.page,
criteria.pagination.perPage,
);
}
}FilterOperator → MongoDB operator mapping:
| Enum value | MongoDB operator |
|---|---|
| EQUALS | $eq |
| NOT_EQUALS | $ne |
| LIKE | $regex |
| IN | $in |
| GT | $gt |
| LT | $lt |
| GTE | $gte |
| LTE | $lte |
Base DTO
import { BaseMongoDto } from '@sisques-labs/nestjs-kit';
// Type: { id: string; createdAt: Date; updatedAt: Date }
type UserMongoDto = BaseMongoDto & {
email: string;
name: string;
};TypeORM
Import TypeOrmModule when you use TypeORM. It is not part of SharedModule. Use ConfigModule.forRoot (global or imported) so ConfigService is available; options are built at runtime via buildTypeOrmModuleOptions inside forRootAsync—no database env is read when you merely import the package.
Environment Variables
DATABASE_DRIVER=postgres
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=secret
DATABASE_DATABASE=my_database
DATABASE_SYNCHRONIZE=false
DATABASE_MIGRATIONS_TABLE_NAME=migrationsOptional: NODE_ENV (affects query logging). For TypeORM CLI migrations that use data-source.ts, the same variables must be set in the environment when the CLI runs.
Base Entity
import { BaseTypeormEntity } from '@sisques-labs/nestjs-kit';
import { Entity, Column } from 'typeorm';
@Entity('users')
export class UserTypeormEntity extends BaseTypeormEntity {
// Inherited: id (UUID, primary key), createdAt, updatedAt, deletedAt (soft delete)
@Column()
email: string;
}Base Repository
import {
BaseTypeormMasterRepository,
TypeormMasterService,
} from '@sisques-labs/nestjs-kit';
@Injectable()
export class UserTypeormRepository extends BaseTypeormMasterRepository {
constructor(typeormService: TypeormMasterService) {
super(typeormService);
}
async findById(id: string): Promise<UserAggregate | null> {
const repo = this.getRepository(UserTypeormEntity);
const entity = await repo.findOneBy({ id });
return entity ? UserWriteFactory.fromPrimitives(entity) : null;
}
}Base DTO
import { BaseTypeormDto } from '@sisques-labs/nestjs-kit';
// Type: { id: string; createdAt: Date; updatedAt: Date }
type UserTypeormDto = BaseTypeormDto & {
email: string;
};Transport Layer (GraphQL)
If you use @nestjs/graphql, call registerSharedGraphqlEnums() once before schema generation (for example at the top of main.ts before NestFactory.create, or from a small module imported by AppModule). Add MutationResponseGraphQLMapper and ComplexityPlugin to your own GraphQL module’s providers when you use them—this package does not register them via SharedModule.
import { registerSharedGraphqlEnums } from '@sisques-labs/nestjs-kit';
registerSharedGraphqlEnums();Input DTOs
BaseFindByCriteriaInput
Composite input for list queries combining filters, sorts, and pagination.
query {
users(
criteria: {
filters: [{ field: "email", operator: EQUALS, value: "[email protected]" }]
sorts: [{ field: "createdAt", direction: DESC }]
pagination: { page: 1, perPage: 20 }
}
) {
total
page
perPage
totalPages
data { id email }
}
}import { BaseFindByCriteriaInput } from '@sisques-labs/nestjs-kit';
@Resolver()
export class UserResolver {
@Query(() => UsersPaginatedResult)
users(@Args('criteria') criteria: BaseFindByCriteriaInput) {
return this.userService.findByCriteria(criteria);
}
}Individual input types: BaseFilterInput, BaseSortInput, BasePaginationInput, NumericRangeInput.
Response DTOs
BasePaginatedResultDto
import { BasePaginatedResultDto } from '@sisques-labs/nestjs-kit';
import { ObjectType, Field } from '@nestjs/graphql';
@ObjectType()
export class UsersPaginatedResult extends BasePaginatedResultDto {
@Field(() => [UserDto])
data: UserDto[];
// Inherited: total, page, perPage, totalPages (computed automatically)
}MutationResponseDto
import { MutationResponseDto } from '@sisques-labs/nestjs-kit';
// Shape: { success: boolean; message?: string; id?: string }
@Mutation(() => MutationResponseDto)
createUser(@Args('input') input: CreateUserInput): Promise<MutationResponseDto> { ... }MutationResponseArrayDto
import { MutationResponseArrayDto } from '@sisques-labs/nestjs-kit';
// Shape: { success: boolean; message?: string; ids: string[] }
@Mutation(() => MutationResponseArrayDto)
deleteUsers(@Args('ids', { type: () => [String] }) ids: string[]): Promise<MutationResponseArrayDto> { ... }Mappers
MutationResponseGraphQLMapper is a NestJS injectable that maps domain results to MutationResponseDto. Register it in the module that declares your resolvers (or a dedicated GraphQL module).
import { MutationResponseGraphQLMapper } from '@sisques-labs/nestjs-kit';
@Module({
providers: [MutationResponseGraphQLMapper, UserResolver],
})
export class UserGraphqlModule {}
@Resolver()
export class UserResolver {
constructor(private readonly mutationMapper: MutationResponseGraphQLMapper) {}
@Mutation(() => MutationResponseDto)
async createUser(@Args('input') input: CreateUserInput) {
const result = await this.commandBus.execute(new CreateUserCommand(input));
return this.mutationMapper.map(result);
}
}Complexity Plugin
ComplexityPlugin is an Apollo Server plugin (@Plugin() from @nestjs/apollo) that rejects operations whose estimated complexity exceeds 1000 (see graphql-query-complexity). It is exported from this package—add it to your GraphQL module’s providers (or equivalent) so Nest discovers the plugin.
To assign complexity weights to fields use the @Complexity decorator from @nestjs/graphql:
import { Field, ObjectType, Complexity } from '@nestjs/graphql';
@ObjectType()
export class UserDto {
@Field()
@Complexity(1)
id: string;
}Register the plugin (for example next to your GraphQL module):
import { Module } from '@nestjs/common';
import { ComplexityPlugin } from '@sisques-labs/nestjs-kit';
@Module({
providers: [ComplexityPlugin],
})
export class GraphqlPluginsModule {}Enums
TypeScript enums are exported for domain and GraphQL use. For GraphQL schema generation, call registerSharedGraphqlEnums() once (see Transport Layer (GraphQL)).
import {
FilterOperator,
SortDirection,
LengthUnitEnum,
UserRoleEnum,
UserStatusEnum,
} from '@sisques-labs/nestjs-kit';
FilterOperator.EQUALS // 'eq'
FilterOperator.NOT_EQUALS // 'ne'
FilterOperator.LIKE // 'like'
FilterOperator.IN // 'in'
FilterOperator.GT // 'gt'
FilterOperator.LT // 'lt'
FilterOperator.GTE // 'gte'
FilterOperator.LTE // 'lte'
SortDirection.ASC
SortDirection.DESCLicense
MIT — Sisques Labs
