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

@ceryn/vault

v2.4.1

Published

Zero-reflection dependency injection container for TypeScript

Readme

@ceryn/vault

A zero-reflection dependency injection container for TypeScript that prioritizes performance, type safety, and explicit design over magic.

Why Ceryn Vault?

  • Zero Reflection: No runtime reflection overhead - all metadata captured at decorator evaluation time
  • Low Overhead: Resolution paths use precomputed metadata, bit flags, and singleton caching
  • Type-Safe: Full TypeScript support with compile-time type checking via phantom types
  • Explicit Over Implicit: Every dependency must be explicitly declared with @Inject()
  • Modular Architecture: Compose modules with imports for clean separation of concerns
  • Modern: Built for ES modules, Node.js 18+, and contemporary TypeScript

Installation

npm install @ceryn/vault

Package Format

@ceryn/vault is published as an ESM package. Use import syntax in Node.js ESM and TypeScript projects. CommonJS require() is not part of the supported package contract.

Quick Start

import { Container, Injectable, Inject, Module, token } from '@ceryn/vault';

// 1. Create type-safe tokens
const DatabaseT = token<Database>('Database');
const UserServiceT = token<UserService>('UserService');

// 2. Define injectable providers with explicit dependencies
@Injectable({ provide: DatabaseT })
class Database {
  query(sql: string) {
    return `Result: ${sql}`;
  }
}

@Injectable({ provide: UserServiceT })
class UserService {
  constructor(@Inject(DatabaseT) private db: Database) {}

  getUser(id: number) {
    return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
  }
}

// 3. Create a module to compose your dependencies
@Module({
  providers: [Database, UserService],
  exports: [UserServiceT],
})
class AppModule {}

// 4. Bootstrap and resolve
const container = Container.from(AppModule);
const userService = container.resolve(UserServiceT);

console.log(userService.getUser(1));
// Output: Result: SELECT * FROM users WHERE id = 1

Core Concepts

Tokens

Tokens are type-safe identifiers for your dependencies. They carry compile-time type information and provide runtime identity.

import { token } from '@ceryn/vault';

// Create tokens with type information
const LoggerT = token<Logger>('Logger');
const ConfigT = token<AppConfig>('AppConfig');
const CacheT = token<Cache>('Cache');

Providers

Providers are injectable classes registered with the DI container. Use the @Injectable() decorator to mark classes as injectable.

import { Injectable, Inject, Lifecycle } from '@ceryn/vault';

@Injectable({ provide: LoggerT })
class Logger {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

// With explicit lifecycle
@Injectable({
  provide: RequestHandlerT,
  lifecycle: Lifecycle.Transient,
})
class RequestHandler {
  constructor(
    @Inject(LoggerT) private logger: Logger,
    @Inject(ConfigT) private config: AppConfig
  ) {}
}

Lifecycles

Ceryn Vault supports three lifecycle strategies:

  • Singleton (default): One instance per module, shared across all resolutions
  • Scoped: One instance per logical scope (e.g., per HTTP request)
  • Transient: Fresh instance for every resolution
import { Lifecycle } from '@ceryn/vault';

@Injectable({ provide: ConfigT, lifecycle: Lifecycle.Singleton })
class Config {}

@Injectable({ provide: RequestContextT, lifecycle: Lifecycle.Scoped })
class RequestContext {}

@Injectable({ provide: FactoryT, lifecycle: Lifecycle.Transient })
class Factory {}

Modules

Modules are containers that organize and compose your dependencies. They support modular architecture through module composition.

import { Module } from '@ceryn/vault';

@Module({
  providers: [Logger, Config], // Classes to register
  exports: [LoggerT, ConfigT], // Tokens to expose
  name: 'CoreModule', // Optional name for debugging
})
class CoreModule {}

Module Composition

Compose modules together to create modular, maintainable architectures. Only exported tokens are accessible to importing modules.

// Core module with shared services
@Module({
  providers: [Logger, Config],
  exports: [LoggerT, ConfigT],
  global: true, // Transitive accessibility
})
class CoreModule {}

// Database module that uses core services
@Module({
  providers: [Database, DatabaseConfig],
  exports: [DatabaseT],
  imports: [CoreModule], // Import core services
})
class DatabaseModule {}

// Application module composing everything
@Module({
  providers: [UserService, UserRepository],
  exports: [UserServiceT],
  imports: [CoreModule, DatabaseModule],
})
class AppModule {}

Container

Container is the entry point for bootstrapping module instances with lazy instantiation and caching.

import { Container } from '@ceryn/vault';

// Create module instance (cached)
const container = Container.from(AppModule);

// Resolve singleton dependencies
const userService = container.resolve(UserServiceT);

// Create scopes for request-level dependencies
const scope = container.createScope();
const handler = scope.resolve(HandlerT);
await scope.dispose();

// Clear cache for testing
Container.clearCache();

Advanced Features

Scoped Dependencies

Create isolated scopes for request-level dependencies:

import { Lifecycle } from '@ceryn/vault';

@Injectable({ provide: RequestContextT, lifecycle: Lifecycle.Scoped })
class RequestContext {
  constructor(@Inject(ConfigT) private config: Config) {}
}

@Injectable({ provide: HandlerT, lifecycle: Lifecycle.Scoped })
class RequestHandler {
  constructor(@Inject(RequestContextT) private ctx: RequestContext) {}

  handle() {
    // ... handle request
  }
}

// Create a scope for each request
async function handleRequest(req: Request) {
  const scope = container.createScope();

  try {
    // Scoped instances are automatically created and isolated per scope
    const handler = scope.resolve(HandlerT);
    await handler.handle();
  } finally {
    await scope.dispose(); // Clean up scoped resources
  }
}

Dynamic Scope Registration

Dynamically provide values to scopes at runtime. Scope-local registrations override module registrations:

// Define tokens for runtime values
const HttpRequestT = token<Request>('HttpRequest');
const HttpResponseT = token<Response>('HttpResponse');
const RequestIdT = token<string>('RequestId');

// Create a handler that depends on runtime values
@Injectable({ provide: HandlerT })
class RequestHandler {
  constructor(
    @Inject(HttpRequestT) private req: Request,
    @Inject(HttpResponseT) private res: Response,
    @Inject(RequestIdT) private requestId: string
  ) {}

  handle() {
    this.res.setHeader('X-Request-ID', this.requestId);
    // ... process request
  }
}

// In your HTTP server
app.use(async (req, res) => {
  const scope = container.createScope();

  try {
    // Provide runtime values to the scope
    scope.provide(HttpRequestT, req);
    scope.provide(HttpResponseT, res);
    scope.provide(RequestIdT, crypto.randomUUID());

    // Dependencies are automatically injected
    const handler = scope.resolve(HandlerT);
    await handler.handle();
  } finally {
    await scope.dispose();
  }
});

Scope Methods:

  • provide<T>(token: Token<T>, value: T): Register a scope-local value
  • has<T>(token: Token<T>): boolean: Check if token exists in scope or module
  • tryResolve<T>(token: Token<T>): T | undefined: Safe resolution with fallback
  • override<T>(token: Token<T>, value: T): Replace existing registration
// Check token availability
if (scope.has(OptionalServiceT)) {
  const service = scope.resolve(OptionalServiceT);
  service.doWork();
}

// Safe resolution with fallback
const logger = scope.tryResolve(LoggerT) ?? console;
logger.log('Using fallback logger if needed');

// Override for testing
const mockDb = createMockDatabase();
scope.override(DatabaseT, mockDb);

Key Features:

  • Scope-local registrations take highest priority (even over singleton cache)
  • Automatic cleanup for disposable instances (dispose() or close() methods)
  • Multiple scopes are completely isolated from each other
  • Type-safe API with full IntelliSense support

Factory Providers

Register dependencies using factory functions:

import { Module } from '@ceryn/vault';

@Module({
  providers: [
    {
      provide: LoggerT,
      useFactory: (config: AppConfig) => new Logger(config.logLevel),
      deps: [ConfigT],
      lifecycle: Lifecycle.Singleton,
    },
  ],
})
class AppModule {}

Value Providers

Register pre-created values or configuration objects:

@Module({
  providers: [
    {
      provide: ConfigT,
      useValue: { apiKey: 'secret', logLevel: 'info' },
    },
  ],
})
class AppModule {}

Global Mode

Enable transitive accessibility for shared modules:

@Module({
  providers: [Logger, Config, Cache],
  exports: [LoggerT, ConfigT, CacheT],
  global: true, // All descendants can access these services
})
class InfrastructureModule {}

Custom Lazy Resolvers

Provide custom resolution logic for advanced scenarios:

const customResolver = (moduleClass: Constructor) => {
  // Custom module instantiation logic
  return new Module(/* ... */);
};

@Module({
  providers: [
    /* ... */
  ],
  lazyResolve: customResolver,
})
class CustomModule {}

Telemetry Hooks

Monitor instantiation performance:

@Module({
  providers: [
    /* ... */
  ],
  onInstantiate: (token: string, durationNs: number) => {
    console.log(`${token} instantiated in ${durationNs}ns`);
  },
})
class ObservableModule {}

Performance

Ceryn Vault is designed for low-overhead resolution without runtime reflection. Benchmark results vary by runtime, dependency graph shape, dependency versions, and hardware. See benchmarks/README.md for the reproducible methodology before comparing results.

Benchmark Results

The benchmark suite compares request-like dependency graphs across Ceryn Vault and selected TypeScript DI containers. Treat results as local measurements only unless the raw output includes environment details, dependency versions, iteration counts, and variance.

Why it's fast

  • Zero reflection — all metadata captured at decorator evaluation time, no reflect-metadata scanning
  • Bit-flag lifecycles — integer bitwise checks instead of string comparisons on hot path
  • Singleton instance cache — O(1) Map lookup for repeated resolutions
  • Frozen metadata — V8 optimization-friendly immutable objects
  • Lazy module instantiation — imported modules materialized on first cross-module resolve

Run benchmarks yourself:

npm run bench -w packages/vault

Architecture Patterns

Layered Architecture

// Infrastructure Layer
@Module({
  providers: [Database, Cache, Logger],
  exports: [DatabaseT, CacheT, LoggerT],
  global: true,
})
class InfraModule {}

// Repository Layer
@Module({
  providers: [UserRepository, OrderRepository],
  exports: [UserRepoT, OrderRepoT],
  imports: [InfraModule],
})
class DataModule {}

// Service Layer
@Module({
  providers: [UserService, OrderService],
  exports: [UserServiceT, OrderServiceT],
  imports: [DataModule],
})
class ServiceModule {}

// Presentation Layer
@Module({
  providers: [UserController, OrderController],
  exports: [UserControllerT, OrderControllerT],
  imports: [ServiceModule],
})
class AppModule {}

Request-Scoped HTTP Handler

import { Lifecycle } from '@ceryn/vault';

// Scoped services are instantiated once per scope
@Injectable({ provide: RequestContextT, lifecycle: Lifecycle.Scoped })
class RequestContext {
  public readonly requestId = crypto.randomUUID();

  constructor(@Inject(LoggerT) private logger: Logger) {
    this.logger.log(`Request ${this.requestId} started`);
  }
}

@Injectable({ provide: RequestHandlerT, lifecycle: Lifecycle.Scoped })
class RequestHandler {
  constructor(
    @Inject(RequestContextT) private ctx: RequestContext,
    @Inject(UserServiceT) private userService: UserService
  ) {}

  async handle(userId: string) {
    const user = await this.userService.getUser(userId);
    return { requestId: this.ctx.requestId, user };
  }
}

// In your HTTP server
app.get('/api/user/:id', async (req, res) => {
  // Create scope - binds resolve methods to this module
  const scope = container.createScope();

  try {
    // All Lifecycle.Scoped providers are automatically isolated to this scope
    const handler = scope.resolve(RequestHandlerT);
    const result = await handler.handle(req.params.id);
    res.json(result);
  } finally {
    await scope.dispose(); // Cleanup scoped resources
  }
});

Error Handling

Ceryn Vault provides detailed error messages for common issues:

import {
  AggregateDisposalError,
  CircularDependencyError,
  LazyFusionResolverMissingError,
  LifecycleViolationError,
  MissingInjectDecoratorError,
  ProviderNotFoundError,
  ScopedWithoutScopeError,
} from '@ceryn/vault';

try {
  const service = container.resolve(ServiceT);
} catch (error) {
  if (error instanceof ProviderNotFoundError) {
    console.error('Service not registered');
  } else if (error instanceof CircularDependencyError) {
    console.error('Circular dependency detected');
  }
}

Diagnostic Error Examples

Ceryn errors are designed to be read directly. In development, messages include the dependency chain, nearby context, why the configuration is invalid, and the usual fixes. In production (NODE_ENV=production), messages are shortened while the error class and structured properties remain available.

Missing provider

If UserService depends on CacheT, but no provider is registered for that token, the error points at both the missing token and the provider that asked for it:

Cannot resolve provider 'Cache [tok_2]'.

Dependency chain:
  UserService [tok_1] -> Cache [tok_2]

Available providers:
  - UserService [tok_1]
  - Logger [tok_3]

To fix this:
  1. Add @Injectable() decorator to Cache [tok_2]
  2. Include it in the 'providers' array when constructing the module
  3. Check for typos in @Inject('Cache [tok_2]') or provider tokens

Missing injection decorator

Because Vault does not use reflection, every constructor parameter must declare its token explicitly. The error names the class and parameter index, then shows the expected pattern:

Missing @Inject decorator

Parameter 0 of UserService is missing a @Inject decorator.

Fix:
  - Add @Inject(SomeService) to the constructor parameter at index 0

Example:
  @Injectable()
  class UserService {
    constructor(
      @Inject(SomeService) private service: SomeService
    ) {}
  }

Lifecycle violation

Lifecycle errors explain the rule, the dependency chain, and the consequence of ignoring it:

Lifecycle violation: singleton provider 'UserService [tok_1]'
cannot depend on scoped provider 'RequestContext [tok_2]'.

Dependency chain:
  UserService [tok_1] -> RequestContext [tok_2]

Why this is an error:

  Singleton providers live for the entire application lifetime.
  Scoped providers are isolated per scope (e.g., per HTTP request).
  If a singleton depends on a scoped provider, it would capture the
  first scope's instance, defeating the purpose of scoping.

To fix this:
  1. Change 'UserService [tok_1]' to scoped lifecycle
  2. Change 'RequestContext [tok_2]' to singleton lifecycle
  3. Restructure your dependencies to follow lifecycle rules

Scoped provider without a scope

Resolving a scoped provider from the root container tells you exactly how to create and pass a scope:

Cannot resolve scoped provider 'RequestContext [tok_2]' without a scope.

Provider 'RequestContext [tok_2]' is registered with Lifecycle.Scoped but no
scope was provided.

To fix this:
  1. Pass a scope when resolving:
     const scope = container.createScope();
     const instance = scope.resolve(Token);
     await scope.dispose();

  2. Or change the lifecycle to Singleton or Transient if scoping is not needed.

Lazy import resolver missing

If you construct CoreVault directly with module classes in imports, the message points you back to the higher-level bootstrap API:

Lazy import resolver missing

Lazy import resolver is unavailable. Import 'Container' before constructing
modules that import classes.

Disposal errors

When several resources fail during shutdown, Vault aggregates them instead of dropping later failures:

Multiple disposal errors occurred

2 error(s) occurred during container disposal:
  1. database connection already closed
  2. cache flush timed out

Check the `errors` property for detailed information about each failure.

Testing

Ceryn Vault is designed with testing in mind:

import { Container } from '@ceryn/vault';
import { MetadataRegistry } from '@ceryn/vault';

describe('UserService', () => {
  beforeEach(() => {
    // Reset registries between tests
    MetadataRegistry.resetForTests();
    Container.clearCache();
  });

  it('should get user', () => {
    // Create test module with mocks
    @Module({
      providers: [{ provide: DatabaseT, useValue: mockDatabase }, UserService],
      exports: [UserServiceT],
    })
    class TestModule {}

    const container = Container.from(TestModule);
    const service = container.resolve(UserServiceT);

    expect(service.getUser(1)).toBeDefined();
  });
});

API Reference

Core Exports

  • token<T>(label?: string): Token<T> - Create a type-safe injection token
  • @Injectable(options: InjectableOptions) - Mark a class as injectable
  • @Inject(token: Token<T>) - Inject a dependency in constructor
  • @Module(config: ModuleConfig) - Define a dependency container
  • Container.from(moduleClass: Constructor): Vault - Bootstrap a module
  • Container.create(moduleClass: Constructor): Vault - Compatibility alias for Container.from()

Types

  • Token<T> - Type-safe injection token
  • Lifecycle - Lifecycle enum (Singleton, Scoped, Transient)
  • ModuleConfig - Module configuration options
  • Provider - Union of ClassProvider, ValueProvider, FactoryProvider
  • Constructor<T> - Generic constructor type

Utilities

  • MetadataRegistry - Global registry for provider metadata
  • Scope - Scoped resolution context
  • ModuleRegistry - Module metadata lookup utilities

Design Philosophy

Ceryn Vault is built on these principles:

  1. Explicit Over Implicit: Every dependency must be explicitly declared. No magic.
  2. Type Safety First: Leverage TypeScript's type system for compile-time guarantees.
  3. Performance Matters: Zero-reflection architecture for minimal runtime overhead.
  4. Modular by Default: Module composition enables clean separation of concerns.
  5. Developer Experience: Clear error messages and intuitive APIs.

Requirements

  • Node.js >= 18.0.0
  • TypeScript >= 5.3.3
  • experimentalDecorators enabled in tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": false
  }
}

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

License

MIT

Acknowledgments

Built with inspiration from the TypeScript DI ecosystem, with a focus on performance and explicitness.


Made with TypeScript | GitHub | Issues