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

archstone

v1.5.4

Published

TypeScript architecture foundation for backend services based on Domain-Driven Design and Clean Architecture

Readme

Archstone

The TypeScript foundation for serious backend services.

Build on Domain-Driven Design and Clean Architecture — without writing the same boilerplate on every project.

failcraft

npm version license typescript bun

Quick Start · Why Archstone · Usage · Agent Skills · Architecture · Contributing


Why Archstone?

Every backend project in DDD needs the same structural pieces — and most teams rewrite them from scratch each time. Archstone gives you a battle-tested, minimal set of base classes and contracts so you can skip the boilerplate and go straight to modeling your domain.

// ❌ Before — scattered, inconsistent, no error contract
class User { id: string }
function createUser() { throw new Error('not found') }

// ✅ After — structured, predictable, type-safe
class User extends AggregateRoot<UserProps> { ... }
async function createUser(): Promise<Either<NotFoundError, User>> { ... }

Features

| | | |---|---| | Either | Functional error handling — use cases never throw | | Maybe | Nullable value handling without null checks — just, nothing, maybe | | Entity / AggregateRoot | Identity-based domain objects with built-in event support | | ValueObject | Equality by value, not reference | | UniqueEntityId | UUID v7 identity, consistent across your entire domain | | WatchedList | Track additions and removals in collections without overwriting persistence | | UseCase | Typed contract for application logic that always returns Either | | Repository contracts | Define your interface in the domain — implement anywhere in infrastructure | | Agent Skills | Built-in AI skill so your coding agent knows every DDD convention |


Install

bun add archstone
# or
npm install archstone

Minimal dependencies — only failcraft for Either and Maybe. Pure TypeScript.


Usage

Either — stop throwing, start returning

import { Either, left, right } from 'archstone/core'

type FindUserResult = Either<UserNotFoundError, User>

async function findUser(id: string): Promise<FindUserResult> {
  const user = await repo.findById(id)

  if (!user) return left(new UserNotFoundError(id))
  return right(user)
}

// The caller always handles both cases — no surprises
const result = await findUser('123')

if (result.isLeft()) {
  console.error(result.value) // UserNotFoundError
} else {
  console.log(result.value)   // User ✓
}

Maybe — nullable values without null checks

import { Maybe, just, nothing, maybe } from 'archstone/core'

type FindUserResult = Maybe<User>

async function findUser(id: string): Promise<FindUserResult> {
  const user = await repo.findById(id)
  return maybe(user) // wraps null/undefined as nothing(), anything else as just()
}

const result = await findUser('123')

if (result.isNothing()) {
  console.log('not found')
} else {
  console.log(result.value) // User ✓
}

Entity & AggregateRoot — model your domain

import { AggregateRoot } from 'archstone/domain/enterprise'
import { UniqueEntityId, Optional } from 'archstone/core'

interface OrderProps {
  customerId: UniqueEntityId
  total: number
  createdAt: Date
}

class Order extends AggregateRoot<OrderProps> {
  get customerId() { return this.props.customerId }
  get total()      { return this.props.total }

  static create(props: Optional<OrderProps, 'createdAt'>): Order {
    const order = new Order({
      ...props,
      createdAt: props.createdAt ?? new Date(),
    })

    // Raise domain events from inside the aggregate
    order.addDomainEvent(new OrderCreatedEvent(order))
    return order
  }
}

ValueObject — equality that makes sense

import { ValueObject } from 'archstone/core'

interface EmailProps { value: string }

class Email extends ValueObject<EmailProps> {
  get value() { return this.props.value }

  static create(raw: string): Email {
    if (!raw.includes('@')) throw new Error('Invalid email')
    return new Email({ value: raw.toLowerCase() })
  }
}

const a = Email.create('[email protected]')
const b = Email.create('[email protected]')

a.equals(b) // ✅ true — compared by value, not reference

WatchedList — persist only what changed

import { WatchedList } from 'archstone/core'

class TagList extends WatchedList<Tag> {
  compareItems(a: Tag, b: Tag) { return a.id.equals(b.id) }
}

const tags = new TagList([existingTag])
tags.add(newTag)
tags.remove(existingTag)

// Send only the diff to your repository — not the whole list
tags.getNewItems()     // → [newTag]
tags.getRemovedItems() // → [existingTag]

Domain Events — decouple side effects

import type { EventHandler } from 'archstone/core'
import { DomainEvents } from 'archstone/core'

class OnUserCreated implements EventHandler<UserCreatedEvent> {
  constructor(private readonly mailer: Mailer) {
    this.setupSubscriptions()
  }

  setupSubscriptions(): void {
    DomainEvents.register(this.handle.bind(this), UserCreatedEvent.name)
  }

  async handle(event: UserCreatedEvent): Promise<void> {
    await this.mailer.send(event.user.email.value)
  }
}

// Instantiate in infrastructure — handler self-registers via constructor
new OnUserCreated(mailer)

// Dispatch after persistence — events stay inside the aggregate until then
await userRepository.create(user)
DomainEvents.dispatchEventsForAggregate(user.id)

Repository Contracts — keep infrastructure out of your domain

import { Repository, Creatable } from 'archstone/domain/application'

// Define your contract in the application layer
export interface UserRepository extends Repository<User> {
  findByEmail(email: string): Promise<User | null>
}

// Compose only what you need
export interface AuditRepository extends Creatable<AuditLog> {}

// Implement anywhere in infrastructure — domain stays clean

Package Exports

| Import | Contents | |---|---| | archstone | Everything | | archstone/core | Either, Maybe, left, right, just, nothing, maybe, ValueObject, UniqueEntityId, WatchedList, Optional, DomainEvent, DomainEvents, EventHandler | | archstone/domain | All domain exports | | archstone/domain/enterprise | Entity, AggregateRoot | | archstone/domain/application | UseCase, UseCaseError, repository contracts |

All sub-paths share type declarations via a common chunk — mixing imports from multiple sub-paths is fully type-safe with no duplicate declaration conflicts.


Architecture

src/
├── core/                    # Zero domain knowledge — pure language utilities
│   ├── value-object.ts      # Value equality base class
│   ├── unique-entity-id.ts  # UUID v7 identity wrapper
│   ├── watched-list.ts      # Change-tracked collection
│   ├── events/
│   │   ├── domain-event.ts  # Marker interface for all domain events
│   │   ├── domain-events.ts # Central registry and dispatcher (singleton)
│   │   └── event-handler.ts # Generic handler interface EventHandler<T>
│   └── types/
│       └── optional.ts      # Optional<T, K> helper type
│
└── domain/
    ├── enterprise/          # Pure domain model — zero framework dependencies
    │   └── entities/
    │       ├── entity.ts
    │       └── aggregate-root.ts
    │
    └── application/         # Orchestration — use cases & repository contracts
        ├── use-cases/
        │   ├── use-case.ts
        │   └── use-case.error.ts
        └── repositories/
            ├── repository.ts
            ├── findable.ts
            ├── creatable.ts
            ├── saveable.ts
            └── deletable.ts

Technology Stack

| | | |---|---| | Language | TypeScript 5+ | | Runtime / Package Manager | Bun (required) | | Test Framework | bun:test (built-in) | | Build Tool | bunup | | Linter / Formatter | Biome via Ultracite | | Dependencies | failcraftEither and Maybe types |


Development Workflow

Requirements: Bun >= 1.0

bun install          # install dev dependencies
bun test             # run tests
bun run build        # compile to dist/
bun x ultracite fix  # lint + format

Branch naming: feat/<name>, fix/<name>, docs/<name>, chore/<name>

Every commit must pass bun test and bun x ultracite fix before pushing.


Coding Standards

  • Error handling: never throw inside use cases — always return left(error) with Either
  • Imports: always import from a layer's index (archstone/core, archstone/domain/enterprise), never from deep paths
  • Layer boundaries: inner layers never import outer ones — core has zero domain knowledge, enterprise never imports application
  • Factories: always provide a static create() factory on entities and value objects — never expose constructors directly
  • Style: no semicolons, 2-space indent, double quotes (enforced by Biome via Ultracite)
  • Commits: Conventional Commitsfeat, fix, chore, docs, refactor, test

Testing

Framework: bun:test. Test files are *.spec.ts co-located with the source they test.

import { test, expect } from "bun:test"

test("example", () => {
  expect(1).toBe(1)
})

Use in-memory repository implementations for use case tests — never couple tests to a real database. Isolate domain event state between tests:

import { DomainEvents } from "archstone/core"
import { beforeEach } from "bun:test"

beforeEach(() => {
  DomainEvents.clearHandlers()
  DomainEvents.clearMarkedAggregates()
})

Agent Skills — new in v1.1.0

Archstone ships with a built-in skill for AI coding agents. Once installed, your agent understands every DDD convention, layer boundary, and usage pattern — without you ever having to explain them.

The skill covers:

  • Entities, aggregates, and value objects
  • Use cases with Either error handling
  • Repository contracts and in-memory implementations
  • Domain events — raising, dispatching, and handling
  • Testing patterns with bun:test and in-memory repos
  • Common mistakes and how to avoid them

Install with Claude Code:

bun x skills add joao-coimbra/archstone

Or copy from the installed package:

cp -r node_modules/archstone/skills/use-archstone .claude/skills/

Built with ❤️ for the TypeScript community.

Contributing · Code of Conduct · MIT License