@worktif/utils
v0.2.27
Published
TypeScript utilities library providing dependency injection, logging, exception handling, and Zod validation tools with AWS Lambda integration
Maintainers
Readme
@worktif/utils
Overview
@worktif/utils is a TypeScript-first utility toolkit for enterprise Node.js, CLI and AWS Lambda applications. It provides composable primitives for logging, dependency injection, decorators, exception handling, process/CLI ergonomics, and schema-friendly I/O–engineered for low-latency paths, high observability, and production safety.
The library emphasizes:
- Deterministic behavior under concurrency
- Explicit error semantics and structured logs
- Composability via a minimal DI core
- Zero-cost abstractions in hot paths
Key Features
- Production logger
- Built on @aws-lambda-powertools/logger with custom formatter and serializers
- Sync/async payload serialization, structured fields, level-aware emission
- Stage-aware defaults and service name namespacing
- Lightweight DI container (PureContainer)
- Factory and constant bindings, explicit dependency graphs
- Deterministic construction order and optional guarded arguments
- Decorators for orchestration
- Pre/post injectors, safe error interception hooks
- Request-scoped context injection (before-instance pattern)
- Exception utilities
- Custom exceptions and typed error surfaces for predictable handling
- Common utilities
- ANSI-safe CLI logs, identity helpers, safe object access, small functional helpers
- Cloud-native ergonomics
- First-class Lambda readiness, zero-dependency bootstraps, environment-driven configuration
Installation
npm install @worktif/utilsyarn add @worktif/utilsPeer requirements
- Node.js >= 20
- TypeScript 5.8.x recommended
- reflect-metadata must be imported once at app entry
- If using DI/decorators, enable
"emitDecoratorMetadata": trueand"experimentalDecorators": trueintsconfig.json
Usage
- Logger: structured, serializer-aware logs
import 'reflect-metadata';
import { logger, initLog, LoggerLevel } from '@worktif/utils';
// Configure a namespaced logger
const appLogger = logger({ serviceName: 'inventory/worker' });
async function main() {
const log = await initLog(appLogger, 'ReconcileInventory', LoggerLevel.Info);
// Log immediate payloads
log.now({ sku: 'A-123', delta: 7 }, { tag: 'inventoryChange' });
// Log future/async results with an async serializer
const result = await log.future(
Promise.resolve({ requestId: 'req-1', status: 'ok', items: 120 }),
{
serializer: async () => (payload: any) => ({
...payload,
isoTime: new Date().toISOString(),
}),
},
);
// Typed log message with level override
log.now(result, { level: LoggerLevel.Debug, tag: 'debugSnapshot' });
}
main().catch((e) => {
appLogger.error('Unhandled exception', { error: e instanceof Error ? e.message : String(e) });
});- Dependency Injection: composable factories and constants
import 'reflect-metadata';
import { PureContainer } from '@worktif/utils';
// Example services
class ConfigService {
constructor(public readonly env: 'local' | 'prod' = 'local') {}
}
class MetricsService {
constructor(public readonly cfg: ConfigService) {}
}
const container = new PureContainer<string>();
// Constants
container.tieConst({
Env: { instance: null, args: [{ value: process.env.STAGE ?? 'local' }], dependencies: [] },
});
// Factories (note the explicit dependency graph)
container.tie({
ConfigService: {
instance: ConfigService,
args: [], // optional args first
dependencies: ['Env'], // then dependencies by key
},
MetricsService: {
instance: MetricsService,
args: [],
dependencies: ['MetricsService'],
},
AnalysisService: {
instance: AnalysisService,
args: [{
value: 'John Doe',
condition: (v: string) => v === 'John Doe', // optional condition
}],
dependencies: ['ConfigService', 'MetricsService'],
},
});
// Resolve instances
const cfg = container.run<ConfigService>('ConfigService');
const metrics = container.run<MetricsService>('MetricsService');
console.log({ env: cfg.env, hasMetrics: !!metrics });- Decorators: run logic before a method with safe error interception
import 'reflect-metadata';
import { injectBefore, logger, initLog, LoggerLevel } from '@worktif/utils';
type BeforeInstance = { typeDef: 'before_instance'; log: Awaited<ReturnType<typeof initLog>> };
const appLogger = logger({ serviceName: 'orders/api' });
async function beforeStep(...args: any[]): Promise<BeforeInstance> {
const log = await initLog(appLogger, 'CreateOrder', LoggerLevel.Info);
log.now(args, { tag: 'methodArgs' });
return { typeDef: 'before_instance', log };
}
function onCatch(e: any, beforeInstance?: BeforeInstance) {
const err = e instanceof Error ? e : new Error(String(e));
beforeInstance?.log.now({
message: err.message,
level: LoggerLevel.Error,
tag: 'exception',
});
}
class OrderService {
@injectBefore(beforeStep, onCatch)
async create(input: { id: string; amount: number }, beforeInstance?: BeforeInstance): Promise<{ ok: boolean }> {
beforeInstance?.log.now({ step: 'validating' }, { level: LoggerLevel.Debug });
if (!input.id) throw new Error('Missing id');
// do work...
beforeInstance?.log.now({ step: 'persisted' }, { tag: 'audit' });
return { ok: true };
}
}
(async () => {
const svc = new OrderService();
await svc.create({ id: 'o-1', amount: 42 });
})();API Reference
Logger
logger(config?): Loggerconfig.serviceName?: string– appended to base service name to namespace logsconfig.logFormatter?: LogFormatter– custom powertools-compatible formatter- Returns a configured Logger instance with stage-aware defaults:
sampleRateValue: 1in non-prod, 0.1 in prodlogLevel: DEBUGin non-prod, INFO in prod
initLog(loggerInstance, actionName, logLevel?): Promise<LoggerInstance>loggerInstance: LoggeractionName: string– logical operation name for correlationlogLevel?: LoggerLevel– default Info- Returns
{ now, future }now(payload, options?): payload | LogItemMessageoptions.level?: LoggerLeveloptions.tag?: string– places payload under a structured fieldoptions.params?.serializer?: <T>(message: LogItemMessage) => T– transforms final messageoptions.serializer?: EntitySerializer | Promise<EntitySerializer>EntitySerializer: (payload: any) => any– runs before logging
future(promise, options?): Promise<payload | LogItemMessage>options.serializercan be async-producing –EntityLoggerSerializer
LoggerLevelDebug|Info|Warn|Error|Critical
DI – PureContainer
tie(options, ...args): void- Registers factories under names. Each factory:
instance: new-able constructorargs: optional argument descriptors:{ value, condition? }[]- resolved as the first constructor arguments
dependencies: string[]- resolved instances are appended as subsequent constructor arguments
- Deterministic factory resolution with explicit errors for invalid graphs
- Registers factories under names. Each factory:
run(name, ...args): T- Resolves and instantiates a factory each time (non-singleton by design)
runConstant(name): T- Retrieves constant value
tieConst(options, ...args): void- Binds immutable constants under names. Each option accepts:
args: [{ value: any, condition?: (v) => any }]dependencies: string[](not used for constants; for structure compatibility)
- Example:
Env => 'prod',FeatureFlags => { a: true }
- Binds immutable constants under names. Each option accepts:
Decorators
injectBefore(injectFn, injectCatchFn?): MethodDecoratorinjectFn: (...args) => Promise<BeforeInstance>- If
BeforeInstanceis found among args it is merged; otherwise appended
- If
injectCatchFn: (error, ...args) => any- Ensures method receives a before-instance context; catches and reports errors
Exceptions
CustomException(subset)CustomException.InternalError(message, meta?)- Throw typed errors for DI wiring or runtime faults with consistent messages
Common types
Maybe<T> = T | undefined- Other default informatics types
Serialization helpers
loggerSerializers: EntityLoggerSerializerMapaxios: extracts response.data for convenient logging
Types
EntitySerializer: (payload: any) => anyEntityLoggerSerializer: (logger: Logger | Console) => Promise<EntitySerializer>LoggerInstance: { now: (...), future: (...) }
LoggerInstanceOptions: {
level?: LoggerLevel;
tag?: string;
params?: { serializer?: <T>(message: LogItemMessage) => T };
serializer?: EntitySerializer | Promise<EntitySerializer>;
}Use Cases
- High-volume Lambda handlers
- Leverage sampling and level-aware logs to reduce noise in prod while preserving debug fidelity in lower stages
- Async serializers to shape third-party responses (e.g., axios, GraphQL) before logging
- Regulated workloads (finance/health)
- Explicit DI wiring yields predictable dependency graphs and repeatable construction paths
- Structured logs with service/action names aid traceability and audit trails
- CLI and batch processors
- ANSI-aware messages, stderr vs stdout separation, and deterministic exit handling
- Microservices with shared modules
- Consistent exception and decorator patterns across services enable uniform observability and error surfaces
Design Principles
- Functional core, imperative shell
- Serializers and formatters are pure; side effects contained in logger emission
- Composability over inheritance
- DI factories and constants as first-class primitives
- Observability-first
- Structured events, stage-driven defaults, minimal branching per hot path
- Zero/low overhead in hot paths
- Level checks and identity fallbacks keep logging overhead negligible when disabled
- Explicit wiring
- Dependency graphs must be declared, preventing hidden coupling
Technical Notes & Conventions
- All terminal output supports ANSI coloring and explicit no-color scenarios for portable CLI integration.
- Compatible with both ES module and CommonJS (
mainpoints to bundled dist file; types to declaration). - Designed for cross-platform shell and cloud (primary focus: Unix-like environments).
Contributing
This section is intended for external publishers responsible for releasing the package to npm. Follow the sequence precisely to ensure auditability, semantic versioning integrity, and a clean release trail.
- Authenticate to the scoped registry
npm login --scope=@worktif- If you encounter a TLS/registry error, set the registry explicitly:
npm config set registry https://registry.npmjs.org/
- Complete your enhancement
- Implement and locally validate your changes (types, build, docs as applicable).
- Open a Pull Request (PR)
- Submit your changes for review.
- Await approval before proceeding.
- Merge the PR
- After approval, merge into main using your standard merge policy.
- Synchronize your local main
git checkout maingit pullto ensure you’re up to date.
- Prepare a release branch
- Create a branch using the release template:
releases/v[your.semantic.version-[pre+[meta]]]-next-release-description
- Create a branch using the release template:
- Bump the version
- Update the package version according to SemVer (major/minor/patch).
- Commit the version bump to the release branch
- Commit only the version change (and any generated artifacts if required by your policy).
- Push the release branch
- Push the branch to the remote to trigger any CI gates.
- Open a Release PR
- Create a PR from the release branch to main.
- Await approval and required checks.
- Merge the Release PR
- Merge into main after approvals and passing checks.
- Final synchronization
- Pull the latest changes from main locally.
- Validate the version in package.json
- Ensure the version reflects the intended release.
- Publish
- If the version was not increased (npm will reject):
- Bump the version, commit, and then run yarn run publish:npm.
- If the version has been increased and publishing fails unexpectedly:
- Contact the maintainer at [email protected] with context (command output, Node/npm versions, CI logs).
- If the version was not increased (npm will reject):
Successful publish output resembles:
+ @worktif/utils@[your.semantic.version-[pre+[meta]]]
✨ Done in 28.81s.Security and responsible disclosure
- Do not include secrets in tests or examples
- Report vulnerabilities privately to the maintainers contact below
License
This project is licensed under the Elastic License 2.0.
- See LICENSE for the full license text.
- See NOTICE for attribution and relicensing details (re-licensed from BUSL-1.1 on 2025-09-15).
- See
THIRD_PARTY_LICENSES.txtfor third-party attributions and license texts.
Maintainers / Contact
- Maintainer: Raman Marozau, [email protected]
- Documentation and support:
docs/generated via TypeDoc
If you have production questions, provide:
- Package version, Node version, runtime (local/Lambda/container)
- Minimal reproduction (if applicable)
- Redacted logs with service name and action name for correlation
