npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

aggregate-kit

v1.0.0

Published

Zero-dependency DDD building blocks for TypeScript: Result, Option, Entity, Aggregate, ValueObject, domain events and repository ports.

Downloads

24

Readme

aggregate-kit

Zero-dependency Domain-Driven Design building blocks for TypeScript.

NPM Version NPM Downloads TypeScript License: MIT Zero Dependencies

📦 npm: npmjs.com/package/aggregate-kit

The reusable tactical patterns for a DDD / Clean Architecture codebase — Result, Option, Entity, Aggregate, ValueObject, domain events and repository ports — extracted into one ESM-only package with no runtime dependencies.

Why

  • Errors are values. Domain and application code returns Result<T, E> and Option<T> instead of throwing or returning null. Failure paths are visible in the type signature.
  • Pure domain. Aggregates, value objects and events have no framework or I/O coupling. The repository and unit-of-work ports keep infrastructure at arm's length.
  • Ownership-safe by construction. ScopedRepository constrains every query to a RepoScope, so a wrong owner reads as absent (no existence leak), never forbidden.

Install

pnpm add aggregate-kit  # or npm i aggregate-kit / bun add aggregate-kit

Node >= 24, ESM only. Everything is imported from the package root:

import { Result, Option, Entity, Aggregate, ValueObject } from 'aggregate-kit';

Quick start

import { Aggregate, BaseDomainEvent, Result, UUID, ValueObject } from 'aggregate-kit';

// 1. A validated value object — never throws, returns a Result.
class Email extends ValueObject<string> {
  protected validate(value: string): Result<string> {
    return value.includes('@')
      ? Result.ok(value.toLowerCase())
      : Result.fail('EMAIL_INVALID');
  }
}

// 2. A domain event.
class UserRegistered extends BaseDomainEvent<{ email: string }> {
  readonly eventType = 'user.registered';
  readonly aggregateId: string;
  readonly payload: { email: string };
  constructor(aggregateId: string, email: string) {
    super();
    this.aggregateId = aggregateId;
    this.payload = { email };
  }
}

// 3. An aggregate root that records events on creation.
interface UserProps {
  email: Email;
}

class User extends Aggregate<UserProps> {
  static register(email: Email): User {
    const user = new User({ email }, new UUID());
    user.addEvent(new UserRegistered(user._id.value.toString(), email.value));
    return user;
  }
}

const email = Email.create('[email protected]');
if (email.isSuccess) {
  const user = User.register(email.getValue());
  user.toObject(); // { id, email: '[email protected]' }
  user.pullDomainEvents(); // [UserRegistered] — drained for the outbox
}

API overview

Primitives

| Export | What it is | | --- | --- | | Result<T, E> | Success/failure container. ok, fail, combine, getValue, getError, isSuccess, isFailure. | | Option<T> / Some / None | Explicit optional. some, none, fromNullable, map, flatMap, filter, unwrapOr, match(...). | | UUID<T> | Typed identifier wrapper; defaults to a fresh UUID v7. Subclass for nominal ids. | | uuidv7() / randomUuidV4() | Standalone id generators (time-ordered v7 / random v4). |

Domain

| Export | What it is | | --- | --- | | Entity<T> | Identity-based equality, get(key), toObject(), clone(). | | Aggregate<T> | Entity + domain-event recording (addEvent, pullDomainEvents, clearEvents). | | ValueObject<T> | Immutable, structurally-compared value; static create() validates via validate(). | | BaseDomainEvent<T> / IDomainEvent<T> | Event base stamping dateOccurred, and its interface. | | WatchedList<T> | Tracks added/removed items against an initial snapshot for delta persistence. |

Application

| Export | What it is | | --- | --- | | BaseRepository<T> | Repository port; reads return Option, all methods return Result. | | ScopedRepository<T, TScope> | Ownership-scoped repository port. | | RepoScope / ScopeOf<K> | user | org | user-in-org scopes, with factories and guards. | | PaginationParams / PaginatedResult<T> / createPaginatedResult / DEFAULT_PAGINATION | Pagination helpers. | | IUnitOfWork<TTx> | Transaction / unit-of-work port (startTransaction, run). | | ErrorCode / AppError / AppErrorException / httpStatusFromCode | Error taxonomy whose suffix maps to an HTTP status. | | DatabaseOperationError / InputParseError | Infrastructure error types preserving cause. |

Events

| Export | What it is | | --- | --- | | EventCollector | AsyncLocalStorage-backed, request-scoped event buffer (runWithContext, add, drain). | | IEventDispatcher / InMemoryEventDispatcher | Dispatcher port and an in-process implementation that never lets one handler abort the others. | | IEventHandler / EventHandlerFn | Handler shapes. | | onEvent / isEventHandler / EVENT_HANDLER_SYMBOL | DI-friendly handler factory and brand guard. | | domainEventToOutboxRow / OutboxRow | Map a domain event to a CloudEvents-shaped transactional-outbox row. |

Patterns

Validate at the boundary with value objects

import { Result, ValueObject } from 'aggregate-kit';

class Quantity extends ValueObject<number> {
  protected validate(value: number): Result<number> {
    if (!Number.isInteger(value) || value <= 0) return Result.fail('QUANTITY_INVALID');
    return Result.ok(value);
  }
}

Need schema validation? validate() is a plain method — plug in any validator (this keeps aggregate-kit dependency-free):

import { z } from 'zod';
import { Result, ValueObject } from 'aggregate-kit';

const schema = z.string().uuid();

class UserId extends ValueObject<string> {
  protected validate(value: string): Result<string> {
    const parsed = schema.safeParse(value);
    return parsed.success ? Result.ok(parsed.data) : Result.fail('USER_ID_INVALID');
  }
}

Scoped repositories never leak existence

import { RepoScope, type ScopedRepository, type ScopeOf, UUID } from 'aggregate-kit';

declare const notes: ScopedRepository<Note, ScopeOf<'user'>>;

const scope = RepoScope.user(new UUID<string>(currentUserId));
const result = await notes.findById(noteId, scope);
// Another user's note → Option.none(), never a 403.

Drain events through a unit of work

import { EventCollector } from 'aggregate-kit';

await EventCollector.runWithContext(async () => {
  // ... mutate aggregates; EventCollector.add(aggregate.pullDomainEvents()) ...
  const events = EventCollector.drain(); // flush to the outbox before commit
});

Scripts

| Command | What it does | | -------------------- | ------------------------------------- | | pnpm build | Bundle to dist/ (+ publint + attw) | | pnpm test | Run tests | | pnpm test:coverage | Tests with coverage report (90% min) | | pnpm typecheck | tsc --noEmit | | pnpm lint | Biome check | | pnpm lint:fix | Biome check + autofix | | pnpm check | Lint + typecheck |

Architecture

src/
  primitives/    Result · Option · UUID
  domain/        Entity · Aggregate · ValueObject · DomainEvent · WatchedList
  application/   Repository ports · Scope · Pagination · UnitOfWork · errors
  events/        EventCollector · dispatchers · onEvent · outbox mapping
  index.ts       public barrel — the only entry point

Toolchain

  • TypeScript 6strict, noUncheckedIndexedAccess, isolatedDeclarations, verbatimModuleSyntax, erasableSyntaxOnly
  • tsdown (Rolldown) — ESM bundle + .d.ts + minify + treeshake, validated with publint and are-the-types-wrong
  • Vitest 4 + V8 coverage (90% thresholds, 326 tests)
  • Biome 2 — lint + format
  • semantic-release — versioning & npm publish with provenance

Publishing

This repo ships with "private": true as a safety gate. To publish:

  1. Remove "private": true from package.json.
  2. Add the NPM_TOKEN and GH_TOKEN secrets in GitHub.
  3. Push Conventional Commits to main.github/workflows/release.yml runs semantic-release.

LLM / AI integration

Ships llms.txt files (llms.txt, llms-full.txt) for AI-assisted development. Compatible with Context7.

Compatibility

  • Node.js >= 24 (uses node:crypto and node:async_hooks)
  • TypeScript >= 6.0
  • ESM only

License

MIT