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

thinple

v0.1.0

Published

Type-safe dependency injection container for TypeScript - works in both Browser and Node.js

Readme

Thinple

A type-safe Dependency Injection (DI) container for TypeScript inspired by Pimple (PHP). Works seamlessly in both Browser and Node.js environments.

Why Thinple?

Stop runtime surprises, start with confidence. Traditional DI containers fail at runtime when services are missing or misconfigured. Thinple leverages TypeScript's type system to catch these errors at compile time, ensuring your application works before it even runs.

// ❌ Other containers: Runtime error
const missing = container.get('nonexistent'); // Throws at runtime

// ✅ Thinple: Compile-time error  
const missing = container.get('nonexistent'); // TypeScript error - won't compile

No decorators, no magic. Unlike many DI frameworks that rely on decorators and reflection, Thinple uses a simple, explicit approach. No experimental features, no metadata, just pure TypeScript.

// ❌ Other frameworks: Decorator magic
@Injectable()
class UserService {
  constructor(@Inject('DATABASE') private db: Database) {}
}

// ✅ Thinple: Simple and explicit
const container = new Container()
  .set<UserService>((c) => new UserService(c.get('database')))('userService');

Surprisingly simple API. Learn the entire API in minutes, not hours. Just a handful of methods that do exactly what you expect.

Installation

# npm
npm install thinple

# yarn  
yarn add thinple

# pnpm
pnpm add thinple

Requirements: Node.js 18+ or modern browsers with ES2020+ support

Quick Start

Get up and running in 30 seconds:

import { Container } from 'thinple';

// 1. Create a container
const container = new Container();

// 2. Register a service
const appContainer = container
  .set(() => 'Hello, World!')('greeting');

// 3. Use the service
const greeting = appContainer.get('greeting');
console.log(greeting); // "Hello, World!"

Core Concepts

Services and Factories

Services are registered using factory functions that describe how to create them:

interface ApiClient {
  baseUrl: string;
  fetch(path: string): Promise<any>;
}

const container = new Container()
  .set<ApiClient>(() => ({
    baseUrl: 'https://api.example.com',
    fetch: async (path) => { /* implementation */ }
  }))('apiClient');

Dependency Injection

Services can depend on other services by accessing the container:

interface Logger {
  log(message: string): void;
}

interface UserService {
  getUser(id: string): Promise<User>;
}

const container = new Container()
  .set<Logger>(() => ({
    log: (msg) => console.log(`[LOG] ${msg}`)
  }))('logger')
  .set<UserService>((c) => ({
    getUser: async (id) => {
      const logger = c.get('logger');
      logger.log(`Fetching user ${id}`);
      // ... implementation
    }
  }))('userService');

Type Safety

Everything is type-checked. No more guessing what services are available:

const container = new Container()
  .set(() => ({ message: 'Hello' }))('greeting');

// ✅ Valid - TypeScript knows 'greeting' exists
const greeting = container.get('greeting');

// ❌ Compile error - TypeScript knows 'missing' doesn't exist  
const missing = container.get('missing');

Service Types

Thinple supports different service lifecycles:

Regular Services (set)

Create a new instance every time:

const container = new Container()
  .set(() => new Date())('timestamp'); // New date each time

Shared Services (share) - Singleton

Create once, reuse everywhere. Perfect for expensive resources that should be shared across your application:

const container = new Container()
  .share(() => new DatabaseConnection())('db'); // Same instance always

When to use share:

  • Database connections - Connection pools are expensive to create
  • External API clients - Maintain authentication, rate limiting, connection reuse
  • Cache instances - Memory efficiency and data consistency
  • Heavy initialization - Config parsing, cryptographic key generation
  • Resource pools - Thread pools, worker pools
const container = new Container()
  // ✅ Expensive resources - create once, share everywhere
  .share(() => new PostgresDatabase({
    connectionPool: { max: 20, min: 5 }
  }))('database')
  
  .share(() => new RedisClient({
    host: 'redis.example.com',
    retryDelayOnFailover: 1000
  }))('cache')
  
  .share(() => new ExternalApiClient({
    apiKey: process.env.API_KEY,
    rateLimit: 1000 // requests per minute
  }))('apiClient')
  
  // ✅ Regular services - new instances are fine
  .set((c) => new UserRepository(c.get('database')))('userRepository')
  .set((c) => new UserService(c.get('userRepository'), c.get('cache')))('userService');

// All UserService instances share the same database connection
const userService1 = container.get('userService');
const userService2 = container.get('userService');
// userService1 and userService2 use the same database connection instance

Raw Values (raw)

Store pre-computed values:

const container = new Container()
  .raw({ apiUrl: 'https://api.example.com' })('config');

Protected Services (protect)

Prevent accidental overrides:

const container = new Container()
  .protect(() => new CriticalService())('critical'); // Cannot be overridden

Advanced Features

Service Reservation

Reserve service names before implementation for better dependency management:

interface Logger {
  log(message: string): void;
}

// Reserve 'logger' service without implementing it yet
const container = new Container()
  .reserve<Logger, 'logger'>()
  .set((c) => ({
    doSomething: () => {
      c.get('logger').log('Doing something'); // TypeScript knows logger exists
    }
  }))('app');

// Provide implementation later
const app = container.get('app', {
  logger: () => ({ log: console.log })
});

Container Merging

Combine multiple containers:

const databaseContainer = new Container()
  .share(() => new Database())('db');

const loggingContainer = new Container()
  .set(() => new FileLogger())('logger');

const appContainer = new Container()
  .merge(databaseContainer)
  .merge(loggingContainer)
  .set((c) => new AppService(c.get('db'), c.get('logger')))('app');

Service Extension

Wrap existing services to add functionality:

const container = new Container()
  .set(() => new BasicLogger())('logger')
  .extend('logger', (originalLogger) => 
    new TimestampLogger(originalLogger)
  );

Real-World Example

Here's how you might structure a typical web application:

// types.ts
interface Database {
  query<T>(sql: string, params?: any[]): Promise<T[]>;
}

interface Logger {
  info(message: string): void;
  error(message: string, error?: Error): void;
}

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

interface UserService {
  getUser(id: string): Promise<User>;
  createUser(data: CreateUserData): Promise<User>;
}

// container.ts
export const createContainer = () => new Container()
  // Infrastructure
  .share<Database>(() => new PostgresDatabase({
    host: process.env.DB_HOST,
    database: process.env.DB_NAME,
  }))('database')
  
  .share<Logger>(() => new WinstonLogger({
    level: process.env.LOG_LEVEL || 'info',
  }))('logger')
  
  // Repositories  
  .set<UserRepository>((c) => new UserRepositoryImpl(
    c.get('database'),
    c.get('logger')
  ))('userRepository')
  
  // Services
  .set<UserService>((c) => new UserServiceImpl(
    c.get('userRepository'),
    c.get('logger')
  ))('userService');

// app.ts
const container = createContainer();
const userService = container.get('userService');

// Everything is type-safe and ready to use
const user = await userService.getUser('123');

API Reference

Container Methods

| Method | Description | Returns | |--------|-------------|---------| | set<T>(factory) | Register a service factory | (key: string) => Container | | share<T>(factory) | Register a shared service (singleton) | (key: string) => Container | | raw<T>(value) | Register a raw value | (key: string) => Container | | protect<T>(factory) | Register a protected service | (key: string) => Container | | get<K>(key, implementations?) | Retrieve a service | Service<K> | | has(key) | Check if service exists | boolean | | extend<K>(key, extender) | Extend existing service | Container | | merge<T>(container) | Merge another container | Container | | reserve<T, K>() | Reserve a service slot | Container |

Type Definitions

type ServiceFactory<T, TServices> = (container: Container<TServices>) => T;
type ServiceExtender<T, TServices> = (service: T, container: Container<TServices>) => T;

Testing with Thinple

Perfect Test Isolation

Thinple's killer feature for testing: Every .set() call creates a new Container instance, leaving the original unchanged. This means you can safely derive test containers from your production setup without any interference between tests.

// production.ts
export const container = new Container()
  .share(() => new PostgresDatabase())('database')
  .share(() => new RedisCache())('cache')
  .set((c) => new UserRepository(c.get('database')))('userRepository')
  .set((c) => new UserService(c.get('userRepository'), c.get('cache')))('userService');

// test.ts
import { container as prodContainer } from './production';

describe('UserService', () => {
  it('should handle repository errors', () => {
    // ✅ Production setup + MockRepository only
    // Original prodContainer is completely unchanged
    const testContainer = prodContainer
      .set(() => new MockUserRepositoryWithError())('userRepository');
    
    const userService = testContainer.get('userService');
    expect(() => userService.getUser('invalid')).toThrow('User not found');
  });

  it('should work with different mocks', () => {
    // ✅ Different test, different container - zero interference
    const testContainer = prodContainer
      .set(() => new MockUserRepositoryWithDelay())('userRepository');
    
    // This test is completely independent from the previous one
    const userService = testContainer.get('userService');
    // ... test logic
  });
});

Why This Matters

No test interference: Each test gets its own container instance. Run tests in parallel, in any order, with complete confidence.

// All of these run independently with zero conflicts
const testA = prodContainer.set(() => new MockA())('serviceA');
const testB = prodContainer.set(() => new MockB())('serviceA');  
const testC = prodContainer.set(() => new MockC())('serviceA');

// prodContainer remains unchanged, each test has isolated behavior

Testing Strategies

Strategy 1: Production-based (Recommended)

import { container } from '../src/container';

// Replace only what you need to mock
const testContainer = container
  .set(() => new MockDatabase())('database')
  .set(() => new TestLogger())('logger');

Strategy 2: Full Mocks

// Build test container from scratch
const testContainer = new Container()
  .set(() => new MockDatabase())('database')
  .set(() => new MockLogger())('logger')
  .set((c) => new UserService(c.get('database'), c.get('logger')))('userService');

Strategy 3: Shared Test Setup

// For integration tests where you want shared expensive resources
describe('UserService Integration', () => {
  let baseContainer: Container;
  
  beforeAll(() => {
    baseContainer = new Container()
      .share(() => new TestDatabase())('database'); // Expensive setup once
  });
  
  it('should persist user', () => {
    const testContainer = baseContainer
      .set((c) => new UserRepository(c.get('database')))('userRepository');
    // Test with real database, fresh repository per test
  });
});

For Contributors

🚀 Thinple is in active development! We're building something awesome and would love your help. Whether you're fixing bugs, adding features, improving documentation, or just sharing ideas - every contribution matters. Don't hesitate to jump in, we're a friendly community!

Development Setup

  1. Clone and install dependencies:

    git clone https://github.com/yourusername/thinple.git
    cd thinple
    npm install
  2. Available scripts:

    npm run dev       # Watch mode development
    npm test          # Run tests in watch mode
    npm run test:run  # Run tests once
    npm run build     # Build for production
    npm run typecheck # Type checking
    npm run lint      # Lint code
    npm run format    # Format code

Project Structure

src/
├── container.ts    # Main Container implementation
├── types.ts        # Type definitions
├── index.ts        # Public API exports
└── __tests__/      # Test suites
    ├── container.test.ts
    ├── container.merge.test.ts
    └── container.reserve.test.ts

Running Tests

# Watch mode (recommended for development)
npm test

# Run once
npm run test:run

# With coverage
npm run test:coverage

Building

# Build for all targets (ESM, CJS, types)
npm run build

# Check types only
npm run typecheck

Code Standards

  • TypeScript: Strict mode enabled
  • Formatting: Biome (auto-format on save recommended)
  • Testing: Vitest with comprehensive coverage
  • Commits: Conventional commit format preferred

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature-name
  3. Make changes with tests
  4. Ensure all tests pass: npm run test:run
  5. Submit a pull request

For bugs and feature requests, please create an issue.


License

MIT © ytetsuro