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

modject

v0.1.0

Published

A lightweight framework focusing on modularity and dependency injection

Readme

Modject

A lightweight, zero-dependency framework for building modular applications with dependency injection and inversion of control.

Why Modject?

Modject helps you organize projects with a modular structure where components depend only on interfaces, not concrete implementations. This approach promotes clean architecture, maintainability, and testability across any project type.

Inspired by repluggable, Modject offers a streamlined, framework-agnostic approach to modularity. While Repluggable excels at React/Redux applications, Modject focuses purely on modularity and dependency injection, making it suitable for any JavaScript/TypeScript project.

Key Features

Zero Dependencies - No external dependencies, minimal footprint
🔒 Strongly Typed - Full TypeScript support with complete type safety
🧩 Modular Architecture - Build applications from independent, reusable modules
💉 Dependency Injection - Clean IoC container for managing dependencies
🎯 Interface-Based - Depend on contracts, not implementations
🪶 Lightweight - Small bundle size, maximum flexibility
🌐 Universal - Works for frontend, backend, CLI, or any Node.js project
📦 SOLID Principles - Built-in support for clean architecture patterns
Well Tested - 98%+ test coverage with comprehensive test suite

Installation

npm install modject
# or
yarn add modject
# or
bun add modject

Quick Start

Here's a simple example demonstrating the core concepts:

import { createOrchestrator, SlotKey, EntryPoint } from 'modject';

// 1. Define your API interface
interface LoggerAPI {
  log(message: string): void;
}

// 2. Create a SlotKey (typed contract)
const LoggerAPI: SlotKey<LoggerAPI> = {
  name: 'Logger API',
};

// 3. Create an entry point that provides the implementation
const LoggerEntryPoint: EntryPoint = {
  name: 'Logger Entry Point',
  contributes: [LoggerAPI],
  
  contribute(shell) {
    shell.contribute(LoggerAPI, () => ({
      log: (message) => console.log(`[LOG] ${message}`),
    }));
  },
};

// 4. Create an entry point that uses the API
const AppEntryPoint: EntryPoint = {
  name: 'App Entry Point',
  dependsOn: [LoggerAPI],
  
  run(shell) {
    const logger = shell.get(LoggerAPI);
    logger.log('Application started!');
  },
};

// 5. Orchestrate everything
const orchestrator = createOrchestrator();
orchestrator.addEntryPoints([LoggerEntryPoint, AppEntryPoint]);
orchestrator.startEntryPoints('App Entry Point');
// Output: [LOG] Application started!

Core Concepts

SlotKey - Typed API Contract

A SlotKey defines a typed contract that modules can provide or consume:

export interface DatabaseAPI {
  query(sql: string): Promise<any[]>;
  execute(sql: string): Promise<void>;
}

export const DatabaseAPI: SlotKey<DatabaseAPI> = {
  name: 'Database API',
};

Entry Points - Modular Components

Entry points are self-contained modules that can:

  • Contribute implementations to APIs
  • Depend on other APIs
  • Run logic when started
const DatabaseEntryPoint: EntryPoint = {
  name: 'Database Entry Point',
  contributes: [DatabaseAPI],
  
  contribute(shell) {
    shell.contribute(DatabaseAPI, () => ({
      query: async (sql) => { /* implementation */ },
      execute: async (sql) => { /* implementation */ },
    }));
  },
};

Orchestrator - Lifecycle Manager

The orchestrator manages entry point registration, dependency resolution, and lifecycle:

const orchestrator = createOrchestrator();

// Add entry points
orchestrator.addEntryPoints([DatabaseEntryPoint, ApiEntryPoint]);

// Start specific entry points (dependencies auto-resolved)
orchestrator.startEntryPoints(['API Entry Point']);

// Stop entry points when needed
orchestrator.stopEntryPoints(['API Entry Point']);

API Reference

createOrchestrator()

Creates a new orchestrator instance for managing entry points.

const orchestrator = createOrchestrator();

Returns: EntryPointOrchestrator

EntryPointOrchestrator

Interface for managing entry point lifecycle.

addEntryPoints(entryPoints: EntryPoint[]): void

Registers one or more entry points with the orchestrator.

orchestrator.addEntryPoints([LoggerEntryPoint, DatabaseEntryPoint]);

removeEntryPoints(names: string[]): void

Removes entry points by name.

orchestrator.removeEntryPoints(['Logger Entry Point']);

startEntryPoints(names: string | string[], onStarted?: (shell: RunShell) => void): void

Starts one or more entry points. Dependencies are automatically resolved and started.

// Start single entry point
orchestrator.startEntryPoints('App Entry Point');

// Start multiple entry points
orchestrator.startEntryPoints(['API Entry Point', 'Worker Entry Point']);

// With callback after startup
orchestrator.startEntryPoints('App Entry Point', (shell) => {
  console.log('Application started!');
});

stopEntryPoints(names: string[], onStopped?: () => void): void

Stops specific entry points.

orchestrator.stopEntryPoints(['Worker Entry Point'], () => {
  console.log('Worker stopped');
});

stopAllEntryPoints(onStopped?: () => void): void

Stops all running entry points.

orchestrator.stopAllEntryPoints(() => {
  console.log('All entry points stopped');
});

getStartedEntryPoints(): string[]

Returns names of all currently running entry points.

const running = orchestrator.getStartedEntryPoints();
console.log('Running:', running);

defineLayers(layers: string[]): void

Defines architectural layers for organizing entry points. Must be called before adding entry points.

orchestrator.defineLayers(['data', 'business', 'presentation']);

SlotKey<T>

Defines a typed API contract.

interface SlotKey<T> {
  readonly name: string;        // Unique identifier
  readonly public?: boolean;    // Whether this API is public
  readonly multi?: boolean;     // Whether to create new instances per access
  readonly layer?: string;      // Architectural layer (if using layers)
}

Properties:

  • name - Unique identifier for the slot
  • public (optional) - Marks the API as public (documentation purposes)
  • multi (optional) - When true, factory is called each time shell.get() is invoked
  • layer (optional) - Specifies which architectural layer this API belongs to

EntryPoint

Defines a modular component.

interface EntryPoint {
  readonly name: string;                          // Unique identifier
  readonly layer?: string;                        // Architectural layer
  readonly contributes?: SlotKey<unknown>[];      // APIs this provides
  readonly dependsOn?: SlotKey<unknown>[];        // APIs this requires
  contribute?(shell: ContributeShell): void;      // Called during startup
  run?(shell: RunShell): void;                    // Called during startup
  withdraw?(shell: WithdrawShell): void;          // Called during shutdown
}

Lifecycle hooks:

  • contribute(shell) - Called first; use to provide API implementations
  • run(shell) - Called second; use to execute startup logic
  • withdraw(shell) - Called during shutdown; use to clean up resources

RunShell

Provides access to dependency APIs.

interface RunShell {
  get<T>(key: SlotKey<T>): T;
}

Methods:

  • get<T>(key) - Retrieves an API implementation (must be declared in dependsOn)

ContributeShell

Used to contribute API implementations.

interface ContributeShell {
  contribute<T>(key: SlotKey<T>, factory: (shell: RunShell) => T): void;
}

Methods:

  • contribute<T>(key, factory) - Registers an implementation for an API

WithdrawShell

Used to withdraw API implementations during shutdown.

interface WithdrawShell {
  withdraw<T>(key: SlotKey<T>): void;
}

Methods:

  • withdraw<T>(key) - Removes an API implementation

Examples

The repository includes two comprehensive examples:

Simple Example

A banking application demonstrating core Modject concepts:

  • Basic API definition with SlotKey
  • Entry points with dependencies
  • Orchestration and dependency injection
  • Clean separation between interfaces and implementations

Perfect for: Learning Modject basics, understanding the core patterns

Fullstack Example

A full-stack book catalog application with:

  • React frontend with dynamic routing
  • Express backend with REST API
  • Shared types between frontend and backend
  • Monorepo structure with yarn workspaces
  • Multiple entry points for pages and controllers

Perfect for: Understanding how Modject scales to real-world applications

See the individual README files in each example directory for detailed documentation.

SOLID Principles Support

Modject is designed with SOLID principles at its core:

Single Responsibility Principle (SRP)

Each entry point has a single, well-defined responsibility. APIs are focused interfaces.

// Each entry point does one thing well
const LoggerEntryPoint: EntryPoint = {
  name: 'Logger Entry Point',
  contributes: [LoggerAPI],
  contribute(shell) {
    // Only responsible for logging
  },
};

Open/Closed Principle (OCP)

Applications are open for extension (add new entry points) but closed for modification (existing entry points remain unchanged).

// Add new functionality without modifying existing code
orchestrator.addEntryPoints([
  ExistingEntryPoint1,
  ExistingEntryPoint2,
  NewFeatureEntryPoint,  // Extend without modification
]);

Liskov Substitution Principle (LSP)

Entry points depend on interfaces (SlotKey), so implementations can be substituted without breaking consumers.

// Both implementations satisfy the same interface
const ConsoleLoggerEntryPoint: EntryPoint = {
  contributes: [LoggerAPI],
  contribute(shell) {
    shell.contribute(LoggerAPI, () => ({
      log: (msg) => console.log(msg),
    }));
  },
};

const FileLoggerEntryPoint: EntryPoint = {
  contributes: [LoggerAPI],
  contribute(shell) {
    shell.contribute(LoggerAPI, () => ({
      log: (msg) => fs.appendFileSync('log.txt', msg + '\n'),
    }));
  },
};

// Consumers work with either implementation

Interface Segregation Principle (ISP)

SlotKey definitions encourage small, focused interfaces that clients depend on.

// Small, focused interfaces instead of large ones
interface ReadAPI {
  read(id: string): Promise<Data>;
}

interface WriteAPI {
  write(data: Data): Promise<void>;
}

// Clients depend only on what they need
const ReaderEntryPoint: EntryPoint = {
  dependsOn: [ReadAPI],  // Only depends on read operations
};

Dependency Inversion Principle (DIP)

High-level modules depend on abstractions (SlotKey interfaces), not concrete implementations.

// High-level module depends on abstraction
const ApplicationEntryPoint: EntryPoint = {
  name: 'Application Entry Point',
  dependsOn: [DatabaseAPI, LoggerAPI],  // Abstractions, not implementations
  
  run(shell) {
    const database = shell.get(DatabaseAPI);
    const logger = shell.get(LoggerAPI);
    // Use interfaces, not concrete classes
  },
};

Use Cases

Modject is suitable for any JavaScript/TypeScript project:

  • Backend APIs - Organize Express/Fastify apps into modular controllers
  • Frontend Apps - Structure React/Vue/Angular apps with pluggable features
  • CLI Tools - Build extensible command-line applications
  • Libraries - Create plugin systems for your libraries
  • Microservices - Share common patterns across services
  • Full-stack Apps - Maintain consistency between frontend and backend

Development

# Install dependencies
bun install

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Build
npm run build

# Lint
npm run lint

# Format
npm run format

Test Coverage

Modject maintains high test coverage to ensure reliability:

  • 98.78% statement coverage
  • 95.83% branch coverage
  • 100% function coverage
  • 77 comprehensive tests covering core functionality, edge cases, and error paths

Run npm run test:coverage to generate a detailed coverage report.

License

MIT