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 🙏

© 2025 – Pkg Stats / Ryan Hefner

chista

v2.1.1

Published

A minimal, framework-agnostic base class for building clean service layers with LIVR validation

Downloads

2,463

Readme

chista

npm version npm downloads TypeScript Bundle Size Known Vulnerabilities

A minimal, framework-agnostic base class for building clean service layers with LIVR validation.

"Chista" means "clean" in Ukrainian.

Installation

npm install chista

Features

  • LIVR validation
  • Generic types for type-safe input/output
  • Hook-based architecture for lifecycle events
  • Validator caching for performance
  • ServiceError for consistent error handling
  • TypeScript support

Building REST APIs

For building REST API backends, chista works best with chista-express - a lightweight Express.js helper library that:

  • Provides a builder pattern for proper middleware ordering
  • Maps chista's ServiceError to REST API responses
  • Includes dependency injection for service instantiation
  • Supports session management and WebSocket integration

Together, they provide a complete structure for building clean, validated REST API backends.

Design

Why Chista?

Chista provides a consistent structure for service layer operations, separating concerns into distinct phases:

run(input) → validate → checkPermissions → aroundExecute → execute → onSuccess/onError

This design is:

  • Framework-agnostic: Works with any database, HTTP framework, or messaging system
  • Type-safe: Full TypeScript generics for input/output types
  • Extensible: Override any phase or add cross-cutting concerns via inheritance

Design Patterns

Template Method Pattern: The run() method defines the algorithm skeleton. Subclasses implement abstract methods (execute, checkPermissions) and optionally override hooks (onSuccess, onError).

Layered Inheritance: The recommended pattern uses three layers:

  1. ServiceBase (library) - Core template with validation and lifecycle
  2. ProjectBase (your code) - Adds project-specific concerns (transactions, logging)
  3. ConcreteService (your code) - Implements business logic

Validator Caching

LIVR validators are cached per service class as a static property. This avoids re-parsing validation rules on every run() call.

// Cached: validator created once, reused for all calls
class MyService extends ServiceBase {
  static validation = { email: ['required', 'email'] };
  // ...
}

Note: Caching only applies when using the validation property. When you override validate() and use validateWithRules(), validators are created fresh each call (necessary for dynamic rules).

Examples

See the examples/ directory for runnable code demonstrating key patterns:

| Example | Description | |---------|-------------| | simple/ | Basic 3-layer pattern with transactions and event publishing | | dynamic-validation/ | Multi-step validation with conditional rules using validateWithRules() | | permissions/ | Role-based access control and resource ownership checks | | hooks/ | Lifecycle hooks (onSuccess/onError) for logging, metrics, and cleanup |

Run any example with: npx tsx examples/<name>/main.ts

Basic Usage

1. Create a project-specific base class

The recommended pattern is to create an intermediate base class that:

  • Overrides aroundExecute to add transaction support (or other cross-cutting concerns)
import { ServiceBase, RunContext } from 'chista/ServiceBase.js';
import { ServiceError } from 'chista/ServiceError.js';

export abstract class Base<TInput = unknown, TOutput = unknown> extends ServiceBase<TInput, TOutput> {
  constructor(protected db: Database, private pubSub: PubSub) {
    super();
  }

  // Override aroundExecute to wrap execution in a transaction
  protected override async aroundExecute(
    data: TInput,
    proceed: (data: TInput) => Promise<TOutput>
  ): Promise<TOutput> {
    return this.db.withTransaction(() => super.aroundExecute(data, proceed));
  }

  protected override async onSuccess(result: TOutput, context: RunContext<TInput>): Promise<void> {
    await this.pubSub.processPublishedEvents();
  }
}

2. Implement a service

import type { InferFromSchema } from 'livr/types';

// Define validation schema with `as const` for type inference
const usersCreateValidation = {
  email: ['required', 'email'],
  password: ['required', { min_length: 8 }]
} as const;

// Infer input type from validation schema (no duplicate interface needed!)
type CreateUserInput = InferFromSchema<typeof usersCreateValidation>;

interface CreateUserOutput {
  userId: string;
}

class UsersCreate extends Base<CreateUserInput, CreateUserOutput> {
  static validation = usersCreateValidation;

  async checkPermissions(data: CreateUserInput): Promise<boolean> {
    return true; // Public endpoint
  }

  async execute(data: CreateUserInput): Promise<CreateUserOutput> {
    const user = await this.db.users.create(data);
    return { userId: user.id };
  }
}

3. Run the service

const service = new UsersCreate(db, pubSub);
const result = await service.run({ email: '[email protected]', password: 'secret123' });
// result is typed as CreateUserOutput

API

ServiceBase<TInput, TOutput>

Abstract base class with the following methods:

| Method | Description | |--------|-------------| | run(inputData) | Main entry point - validates, checks permissions, calls execute | | validate(data) | LIVR validation (override validation property) | | validateWithRules<T>(data, rules) | Helper for dynamic validation with custom rules | | execute(data) | Abstract - implement business logic | | aroundExecute(data, proceed) | Hook for wrapping execute (transactions, retries, etc.) | | checkPermissions(data) | Abstract - implement authorization |

Hook methods (override in subclass):

| Hook | Description | |------|-------------| | onSuccess(result, context) | Called after successful execution (async) | | onError(error, context) | Called on error (async) |

ServiceError

new ServiceError({ fields: { email: 'INVALID' }, code: 'VALIDATION_ERROR' })

Properties: code, fields, toObject()

RunContext

The context object passed to hook methods:

interface RunContext<TInput> {
  inputData: unknown;        // Original input data
  cleanData: TInput | null;  // Validated/cleaned data
  startTime: Date;           // When run() was called
  endTime: Date | null;      // When execution completed
  executionTimeMs: number | null; // Execution duration in ms
}

Dynamic Validation

Use validateWithRules() for dynamic or multi-step validation:

import type { InferFromSchema } from 'livr/types';

// For dynamic validation, define schemas for each branch
const typeCheckSchema = {
  type: ['required', 'string']
} as const;

const premiumSchema = {
  type: ['required', 'string'],
  features: ['required', { list_of: 'string' }]
} as const;

const standardSchema = {
  type: ['required', 'string'],
  name: ['required', 'string']
} as const;

type PremiumInput = InferFromSchema<typeof premiumSchema>;
type StandardInput = InferFromSchema<typeof standardSchema>;
type UpdateInput = PremiumInput | StandardInput;

class ItemsUpdate extends Base<UpdateInput, UpdateOutput> {
  async validate(data: unknown): Promise<UpdateInput> {
    // First pass - get the type
    const { type } = this.validateWithRules<InferFromSchema<typeof typeCheckSchema>>(
      data,
      typeCheckSchema
    );

    // Dynamic rules based on type
    if (type === 'premium') {
      return this.validateWithRules<PremiumInput>(data, premiumSchema);
    }
    return this.validateWithRules<StandardInput>(data, standardSchema);
  }
}

LIVR Validation

The library uses LIVR (Language Independent Validation Rules).

LIVR supports automatic TypeScript type inference from validation schemas. Use as const and InferFromSchema to derive types:

import type { InferFromSchema } from 'livr/types';

const myServiceValidation = {
  email: ['required', 'email'],
  age: ['required', 'positive_integer'],
  role: ['required', { one_of: ['admin', 'user'] as const }],
  tags: { list_of: 'string' }
} as const;

// Inferred type: { email: string; age: number; role: 'admin' | 'user'; tags?: string[] }
type MyServiceInput = InferFromSchema<typeof myServiceValidation>;

class MyService extends ServiceBase<MyServiceInput, void> {
  static validation = myServiceValidation;
  // ...
}

Error Handling

Services throw ServiceError for validation and business logic errors:

// Validation error (automatic)
throw new ServiceError({
  fields: { email: 'REQUIRED' }
  // code defaults to 'VALIDATION_ERROR'
});

// Business logic error
throw new ServiceError({
  code: 'NOT_FOUND',
  fields: { id: 'WRONG_ID' }
});

// Permission error
throw new ServiceError({
  code: 'PERMISSION_DENIED',
  fields: { targetId: 'PERMISSION_DENIED' }
});

License

MIT