@verkut/common
v1.0.0
Published
Shared building blocks for NestJS applications: request-scoped context, domain primitives, error handling, API errors, HTTP filters, and utility functions.
Readme
@verkut/common
Shared building blocks for NestJS applications: request-scoped context, domain primitives, error handling, API errors, HTTP filters, and utility functions.
Installation
npm install @verkut/common
# or
pnpm add @verkut/commonPeer dependencies
npm install @nestjs/common rxjsContext
AsyncLocalStorage-based context propagation with HTTP middleware, gRPC interceptor, and a decorator for background jobs.
Quick start
import { Module } from '@nestjs/common';
import { ContextModule } from '@verkut/common';
@Module({
imports: [
ContextModule.forRoot(),
],
})
export class AppModule {}Module configuration
ContextModule.forRoot({
// HTTP header to read correlation ID from (default: 'x-correlation-id')
headerName: 'x-trace-id',
// Custom ID generator (default: crypto.randomUUID())
idGenerator: () => nanoid(),
// Key name in context store (default: 'correlationId')
contextKey: 'traceId',
});Async configuration is also supported via forRootAsync. The module is global — import once in the root module.
HTTP context
HttpContextMiddleware is applied automatically to all routes by ContextModule. On each request it reads correlation ID from the configured header (or generates one), then wraps the request in AsyncLocalStorage.run().
gRPC context
GrpcContextInterceptor extracts correlation ID from gRPC metadata. Register it manually:
import { APP_INTERCEPTOR } from '@nestjs/core';
import { GrpcContextInterceptor } from '@verkut/common';
@Module({
providers: [{ provide: APP_INTERCEPTOR, useClass: GrpcContextInterceptor }],
})
export class AppModule {}@WithContext decorator
For background jobs, cron tasks, and event handlers that run outside HTTP/gRPC scope. Automatically adds className and methodName to the context:
import { WithContext } from '@verkut/common';
@Injectable()
export class PaymentProcessor {
@WithContext()
async processQueue() {
// context: { className: 'PaymentProcessor', methodName: 'processQueue' }
}
@WithContext({ source: 'cron' })
async runDailyReport() {
// context: { className: 'PaymentProcessor', methodName: 'runDailyReport', source: 'cron' }
}
// Dynamic context — receives method arguments
@WithContext((data) => ({ messageId: data.id }))
async handleMessage(data: { id: string; payload: unknown }) {
// context: { className: 'PaymentProcessor', methodName: 'handleMessage', messageId: data.id }
}
}If called within an existing context, parent context fields are merged.
ContextStore API
import { ContextStore } from '@verkut/common';
ContextStore.getContext(); // shallow copy of current context
ContextStore.updateContext({ userId: '123' }); // add fields to current context
ContextStore.runWithCtx(() => { /* ... */ }, ctx); // run in a new scope (merges with parent)
ContextStore.getRawStore(); // raw store or undefined
ContextStore.generateId(); // crypto.randomUUID()Domain
Infrastructure-independent primitives for DDD-style architecture are available from a dedicated entry point:
import {
AggregateRoot,
ChangeTracker,
DataTransfer,
Entity,
Event,
TrackableEntity,
UuidId,
} from '@verkut/common/domain';| Export | Description |
|--------|------------|
| DataTransfer | Shallow-immutable command/query/result base with protected semantic readers |
| Event | Separate shallow-immutable domain-event data base |
| Entity | Minimal entity with required immutable ID and identity equality |
| AggregateRoot | Entity that owns pending domain events |
| ChangeTracker | Standalone shallow state tracker with from/to changes |
| TrackableEntity | Optional Entity wrapper around ChangeTracker |
| Identifier | Identity value-object contract and built-in ID implementations |
| IUseCase | Use-case interface |
Commands, results, and events do not expose generic set() or fill() methods.
Concrete types provide semantic getters and factories:
type CreateUserProps = {
email: string;
};
class CreateUserCommand extends DataTransfer<CreateUserProps> {
public constructor(props: CreateUserProps) {
super(props);
}
public email(): string {
return this.getOrThrow('email');
}
}The bases take a top-level snapshot; they deliberately do not deep-clone or
deep-freeze arbitrary values. Nested state must therefore consist of immutable
value objects and readonly collections. A concrete factory must copy (and may
freeze) input arrays and metadata before passing them to super.
Mutable Date instances are not safe event values. Prefer an immutable time
value object, an ISO-8601 string, or an epoch number. Contract tests should
verify that mutating source arrays or metadata cannot change the created
command or event.
There is no generic toObject() method. A concrete type may expose a deliberate
toPrimitives() containing only its public contract, while persistence and
outbox representations belong in explicit mappers.
Entity contains only identity. Concrete entities own their state and expose
named domain behavior. Tracking is opt-in and persistence mapping belongs to
infrastructure adapters.
acceptChanges() and acceptEvents() must be called only after a successful
transaction commit.
ChangeTracker accepts only a plain object (including an object with a null
prototype) as state. Arrays, Date, and class instances are rejected. Tracking
is shallow: nested values and the from/to values in a change set must obey
the same immutability contract.
Error — Domain exceptions
CoreException is the base for all domain errors. Each concrete exception has an errorCode string constant.
| Exception | Error code | Purpose |
|-----------|-----------|---------|
| CommonException | (abstract) | Base for project-specific exceptions |
| InternalException | INTERNAL_ERROR | Unexpected internal errors |
| ValidationException | VALIDATION_ERROR | Validation failures (carries validationErrors map) |
| NotFoundException | NOT_FOUND | Entity not found |
| ExternalApiException | EXTERNAL_API_ERROR | External API failures |
| UnauthorizedException | UNAUTHORIZED | Authentication failures |
BASE_ERROR_CODES — constant with all base error code strings.
Prisma error handling
import { handlePrismaError, PRISMA_ERROR_CODES } from '@verkut/common';
handlePrismaError(error, {
[PRISMA_ERROR_CODES.UNIQUE_CONSTRAINT]: () => new MyConflictException(),
[PRISMA_ERROR_CODES.NOT_FOUND]: () => new MyNotFoundException(),
});Error — API errors
API-layer error classes tied to HTTP statuses. Orthogonal to domain CoreException — these are for shaping client responses.
| Class | Status | Default message |
|-------|--------|----------------|
| BadRequestApiError | 400 | bad request |
| UnauthorizedApiError | 401 | unauthorized |
| ForbiddenApiError | 403 | forbidden |
| NotFoundApiError | 404 | not found |
| ConflictApiError | 409 | conflict |
| InternalServerErrorApiError | 500 | internal server error |
| ServiceUnavailableApiError | 503 | service unavailable |
All extend BaseApiError and implement toResponse() returning { code, message }.
Filters
HttpCoreExceptionFilter
Catches CoreException, maps errorCode → HTTP status → BaseApiError response. The errorCode → status mapping is injected via DI, so each project provides its own:
import {
CORE_EXCEPTION_STATUS_MAPPER,
CoreExceptionStatusMapper,
HttpCoreExceptionFilter,
} from '@verkut/common';
@Injectable()
class MyStatusMapper implements CoreExceptionStatusMapper {
mapToStatus(errorCode: string): number {
if (errorCode === 'UNAUTHORIZED') return 401;
if (errorCode.endsWith('_NOT_FOUND')) return 404;
if (errorCode.endsWith('_ALREADY_EXISTS')) return 409;
if (errorCode === 'VALIDATION_ERROR') return 400;
return 500;
}
}
@Module({
providers: [
{ provide: CORE_EXCEPTION_STATUS_MAPPER, useClass: MyStatusMapper },
{ provide: APP_FILTER, useClass: HttpCoreExceptionFilter },
],
})
export class AppModule {}HttpExceptionFilter
Catch-all fallback filter. Wraps any unhandled exception as InternalServerErrorApiError (500) and logs it as InternalException.
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from '@verkut/common';
@Module({
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }],
})
export class AppModule {}Interceptors
HttpLogRequestInterceptor
Logs incoming HTTP requests and responses (including duration and error details).
Middlewares
HttpLogRequestMiddleware
Logs HTTP request/response with method, URL, query, params, body, status code, and duration.
Utils
| Export | Description |
|--------|------------|
| AssertUtils | Runtime assertions (assertDefined, etc.) |
| CoreAssert | Domain-level assertions that throw CoreException |
| PaginationUtils | Pagination helpers (clampLimit, etc.) |
| PerformanceTimer | performance.now()-based timer |
| capitalize(str) | Capitalize first letter |
| chunkArray(arr, size) | Split array into chunks |
| flattenErrors(errors) | Flatten nested validation errors |
| inspectFlat(obj) | Flat util.inspect for logging |
| isNot, isEmpty, isNotEmpty, isFalsy | Predicate helpers |
| isValidSortOrder, validateSortOrder | Sort order validation |
Types
| Export | Description |
|--------|------------|
| Nullable<T> | T \| null |
| Optional<T> | T \| undefined |
Package quality checks
Run the public-package checks through Nx:
pnpm nx run @verkut/common:lint
pnpm nx run @verkut/common:typecheck
pnpm nx run @verkut/common:test-coverage
pnpm nx run @verkut/common:buildtest-coverage is the required coverage-gated target. The ordinary test
target remains the fast local test run. Packaging invokes a clean TypeScript
build automatically, so deleted source files cannot survive in dist through
incremental compilation.
License
MIT
