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

@archtx/core

v6.2.0

Published

Core types and utils for @archtx

Downloads

34

Readme

@archtx/core

Core building blocks for clean architecture TypeScript applications. Provides foundational abstractions for domain entities, use cases, scheduled processes, and a structured error hierarchy.

Installation

npm install @archtx/core

Entity

The Entity class is the core abstraction for domain entities with immutable props and built-in validation.

Basic Usage

import { Entity } from '@archtx/core'

interface UserSchema {
  id: string
  name: string
  email: string
  age?: number
}

class User extends Entity<UserSchema> {
  // Use static factory methods - constructor is protected
  static create(props: UserSchema) {
    const user = new User(props, { newEntity: true })
    user.validate(props)
    return user
  }

  static fromPersistence(props: UserSchema) {
    return new User(props, { newEntity: false })
  }

  validate(props: UserSchema) {
    if (!props.name || props.name.length === 0) {
      throw new ValidationError('Name is required')
    }
    if (!props.email.includes('@')) {
      throw new ValidationError('Invalid email format')
    }
  }
}

const user = User.create({ id: '123', name: 'John', email: '[email protected]' })
console.log(user.props.name) // 'John'

Key Features

Immutable Props

Entity props are deeply frozen and cloned on construction. Direct modification attempts throw errors:

const user = User.create({ id: '123', name: 'John', email: '[email protected]' })

// This throws - props are frozen
user.props.name = 'Jane' // TypeError: Cannot assign to read only property

// Props are deeply frozen
user.props.metadata.nested = 'value' // TypeError

The set() Method

Update entity props safely through the set() method. It validates before applying changes and preserves immutability:

// Update a single property with a value
user.set('name', 'Jane')

// Update a single property with a function
user.set('age', (current) => (current ?? 0) + 1)

// Update multiple properties at once
user.set((draft) => {
  draft.name = 'Jane Doe'
  draft.age = 30
})

The set() method:

  • Clones props before modification
  • Validates the new props via the validate() method
  • Rejects invalid updates (original props are preserved)
  • Deep freezes the result
  • Prevents changing the id property
// Validation errors prevent updates
user.set('name', '') // throws ValidationError, props unchanged

// Cannot change entity ID
user.set('id', 'new-id') // throws EntityError

Private Setters

Exclude properties from the public set() method using the second generic parameter:

interface PostSchema {
  id: string
  title: string
  createdAt: Date
  updatedAt: Date
}

// createdAt and updatedAt cannot be set via the public set() method
class Post extends Entity<PostSchema, 'createdAt' | 'updatedAt'> {
  static create(props: Omit<PostSchema, 'createdAt' | 'updatedAt'>) {
    const now = new Date()
    return new Post(
      { ...props, createdAt: now, updatedAt: now },
      { newEntity: true }
    )
  }

  // Internal method to update timestamp
  touch() {
    // Use the draft updater to modify protected props
    this.set((draft) => {
      draft.updatedAt = new Date()
    })
  }

  validate() {}
}

const post = Post.create({ id: '1', title: 'Hello' })
post.set('title', 'Updated') // OK
post.set('createdAt', new Date()) // TypeScript error - not allowed

Change Tracking

Track modifications to entities using originalProps and hasChanges:

// New entities always have changes (they need to be persisted)
const newUser = User.create({ id: '123', name: 'John', email: '[email protected]' })
console.log(newUser.hasChanges) // true - new entity needs INSERT

// Existing entities start with no changes
const user = User.fromPersistence({ id: '123', name: 'John', email: '[email protected]' })
console.log(user.originalProps) // null - no changes yet
console.log(user.hasChanges) // false

user.set('name', 'Jane')

console.log(user.originalProps) // { id: '123', name: 'John', email: '[email protected]' }
console.log(user.hasChanges) // true
console.log(user.props.name) // 'Jane'

Use hasChanges in repositories to determine persistence action:

  • newEntity && hasChanges → INSERT
  • !newEntity && hasChanges → UPDATE
  • !hasChanges → skip (no persistence needed)

New Entity Flag

The metadata.newEntity flag distinguishes newly created entities from those loaded from persistence:

const newUser = User.create({ id: '123', name: 'John', email: '[email protected]' })
// newUser.metadata.newEntity === true

const existingUser = User.fromPersistence({ id: '123', name: 'John', email: '[email protected]' })
// existingUser.metadata.newEntity === false

Use this flag in repositories to determine whether to INSERT or UPDATE.

Convert to Plain Object

Use toPOJO() to get a mutable clone of the props:

const data = user.toPOJO()
data.name = 'Modified' // OK - it's a clone
console.log(user.props.name) // Still 'John'

UseCase

Abstract base class for application use cases:

import { UseCase, ILogger } from '@archtx/core'

interface UserRepository {
  findById(id: string): Promise<User | null>
}

class GetUserUseCase extends UseCase<{ userRepo: UserRepository }> {
  async execute(userId: string): Promise<User> {
    this.log.info('Fetching user', { userId })

    const user = await this.services.userRepo.findById(userId)
    if (!user) {
      throw new NotFoundError('User not found')
    }

    return user
  }
}

// Usage
const useCase = new GetUserUseCase({
  log: console, // or your logger implementation
  userRepo: myUserRepository
})

const user = await useCase.execute('123')

ScheduledProcess

Abstract base class for CRON-scheduled background jobs:

import { ScheduledProcess } from '@archtx/core'

class CleanupExpiredTokens extends ScheduledProcess<{ tokenRepo: TokenRepository }> {
  CronSchedule = '0 0 * * *' // Daily at midnight

  async execute() {
    const deleted = await this.services.tokenRepo.deleteExpired()
    return { deletedCount: deleted }
  }
}

Error Hierarchy

All errors extend RootError which supports internal messages and user-facing messages:

import {
  RootError,
  ValidationError,
  NotFoundError,
  AuthenticationError,
  AuthorizationError,
  EntityError,
  ExpiredError,
  ParamError,
  RepoError,
  ServiceError,
  UseCaseError
} from '@archtx/core'

// Internal message for logs, public message for users
throw new ValidationError('Email validation failed: missing @ symbol', {
  publicMsg: 'Please enter a valid email address',
  field: 'email'
})

// Access error metadata
try {
  // ...
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(error.message) // Internal: 'Email validation failed: missing @ symbol'
    console.log(error.meta?.publicMsg) // User-facing: 'Please enter a valid email address'
    console.log(error.meta?.field) // 'email'
  }
}

Error Types

| Error | Purpose | |-------|---------| | ValidationError | User input validation failures | | NotFoundError | Resource not found | | AuthenticationError | Authentication failures | | AuthorizationError | Permission/access denied | | EntityError | Domain entity invariant violations | | ExpiredError | Token/session expiration | | ParamError | Invalid parameters | | RepoError | Repository/data access errors | | ServiceError | External service failures | | UseCaseError | Use case execution errors |

Utility Types

import { WithID, DeepReadonly, ISODateString, UUID } from '@archtx/core'

// WithID<T> - Adds id: string to any object type
type UserWithID = WithID<{ name: string }> // { name: string; id: string }

// DeepReadonly<T> - Recursive readonly wrapper
type ReadonlyUser = DeepReadonly<{ profile: { name: string } }>
// { readonly profile: { readonly name: string } }

// Semantic type aliases
const timestamp: ISODateString = '2024-01-15T10:30:00.000Z'
const userId: UUID = '550e8400-e29b-41d4-a716-446655440000'

Logger Interface

Implement the ILogger interface for your logging solution:

import { ILogger } from '@archtx/core'

const logger: ILogger = {
  debug: (msg, ctx) => console.debug(msg, ctx),
  info: (msg, ctx) => console.info(msg, ctx),
  warn: (msg, ctx) => console.warn(msg, ctx),
  error: (msg, ctx) => console.error(msg, ctx)
}

License

MIT