A lightweight framework focusing on modularity and dependency injection
npm install modjectA lightweight, zero-dependency framework for building modular applications with dependency injection and inversion of control.
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.
⨠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
``bash`
npm install modjector
yarn add modjector
bun add modject
Here's a simple example demonstrating the core concepts:
`typescript
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
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!
`
A SlotKey defines a typed contract that modules can provide or consume:
`typescript
export interface DatabaseAPI {
query(sql: string): Promise
execute(sql: string): Promise
}
export const DatabaseAPI: SlotKey
name: 'Database API',
};
`
Entry points are self-contained modules that can:
- Contribute implementations to APIs
- Depend on other APIs
- Run logic when started
`typescript`
const DatabaseEntryPoint: EntryPoint = {
name: 'Database Entry Point',
contributes: [DatabaseAPI],
contribute(shell) {
shell.contribute(DatabaseAPI, () => ({
query: async (sql) => { / implementation / },
execute: async (sql) => { / implementation / },
}));
},
};
The orchestrator manages entry point registration, dependency resolution, and lifecycle:
`typescript
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']);
`
Creates a new orchestrator instance for managing entry points.
`typescript`
const orchestrator = createOrchestrator();
Returns: EntryPointOrchestrator
Interface for managing entry point lifecycle.
#### addEntryPoints(entryPoints: EntryPoint[]): void
Registers one or more entry points with the orchestrator.
`typescript`
orchestrator.addEntryPoints([LoggerEntryPoint, DatabaseEntryPoint]);
#### removeEntryPoints(names: string[]): void
Removes entry points by name.
`typescript`
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.
`typescript
// 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.
`typescript`
orchestrator.stopEntryPoints(['Worker Entry Point'], () => {
console.log('Worker stopped');
});
#### stopAllEntryPoints(onStopped?: () => void): void
Stops all running entry points.
`typescript`
orchestrator.stopAllEntryPoints(() => {
console.log('All entry points stopped');
});
#### getStartedEntryPoints(): string[]
Returns names of all currently running entry points.
`typescript`
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.
`typescript`
orchestrator.defineLayers(['data', 'business', 'presentation']);
Defines a typed API contract.
`typescript`
interface SlotKey
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 slotpublic
- (optional) - Marks the API as public (documentation purposes)multi
- (optional) - When true, factory is called each time shell.get() is invokedlayer
- (optional) - Specifies which architectural layer this API belongs to
Defines a modular component.
`typescript`
interface EntryPoint {
readonly name: string; // Unique identifier
readonly layer?: string; // Architectural layer
readonly contributes?: SlotKey
readonly dependsOn?: SlotKey
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 implementationsrun(shell)
- - Called second; use to execute startup logicwithdraw(shell)
- - Called during shutdown; use to clean up resources
Provides access to dependency APIs.
`typescript`
interface RunShell {
get
}
Methods:
- get - Retrieves an API implementation (must be declared in dependsOn)
Used to contribute API implementations.
`typescript`
interface ContributeShell {
contribute
}
Methods:
- contribute - Registers an implementation for an API
Used to withdraw API implementations during shutdown.
`typescript`
interface WithdrawShell {
withdraw
}
Methods:
- withdraw - Removes an API implementation
The repository includes two comprehensive examples:
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
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.
Modject is designed with SOLID principles at its core:
Each entry point has a single, well-defined responsibility. APIs are focused interfaces.
`typescript`
// Each entry point does one thing well
const LoggerEntryPoint: EntryPoint = {
name: 'Logger Entry Point',
contributes: [LoggerAPI],
contribute(shell) {
// Only responsible for logging
},
};
Applications are open for extension (add new entry points) but closed for modification (existing entry points remain unchanged).
`typescript`
// Add new functionality without modifying existing code
orchestrator.addEntryPoints([
ExistingEntryPoint1,
ExistingEntryPoint2,
NewFeatureEntryPoint, // Extend without modification
]);
Entry points depend on interfaces (SlotKey), so implementations can be substituted without breaking consumers.
`typescript
// 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
`
SlotKey definitions encourage small, focused interfaces that clients depend on.
`typescript
// Small, focused interfaces instead of large ones
interface ReadAPI {
read(id: string): Promise;
}
interface WriteAPI {
write(data: Data): Promise
}
// Clients depend only on what they need
const ReaderEntryPoint: EntryPoint = {
dependsOn: [ReadAPI], // Only depends on read operations
};
`
High-level modules depend on abstractions (SlotKey interfaces), not concrete implementations.
`typescript`
// 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
},
};
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
`bashInstall dependencies
bun install
$3
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.MIT