A lightweight, powerful dependency injection container for Node.js and browsers. Jimple is a JavaScript port of the popular Pimple DI container from PHP, bringing clean dependency management to your JavaScript projects.
- Features
- Why Dependency Injection?
- Quick Start
- Installation
- Core Concepts
- Advanced Features
- ES6 Proxy Mode
- TypeScript Support
- Modular Configuration with Providers
- API Reference
- Real-World Example
- More Examples
- Extending Jimple
- Performance Tips
- Migration from Other DI Containers
- Documentation
β
Lightweight - ~1KB minified and gzipped
β
Zero dependencies - No external dependencies in Node.js
β
Universal - Works in Node.js and browsers
β
TypeScript - Fully typed with excellent IDE support
β
ES6 Proxy support - Modern syntax with property access
β
Extensible - Easy to extend and customize
β
Well tested - 100% code coverage
β
Stable API - Mature, stable API you can depend on
Dependency injection helps you write more maintainable, testable code by:
- Decoupling components - Services don't need to know how their dependencies are created
- Improving testability - Easy to swap dependencies with mocks during testing
- Managing complexity - Centralized configuration of how objects are wired together
- Lazy loading - Services are only created when needed
- Singleton by default: Same instance returned on subsequent calls
- Dependency management: Services can depend on other services
npm install jimple
import Jimple from 'jimple';
// Create container
const container = new Jimple();
// Define a simple service
container.set('logger', (c) => {
return {
log: (msg) => console.log(`[${new Date().toISOString()}] ${msg}`)
};
});
// Define a service that depends on another
container.set('userService', (c) => {
const logger = c.get('logger');
return {
createUser: (name) => {
logger.log(`Creating user: ${name}`);
return { id: Math.random(), name };
}
};
});
// Use your services
const userService = container.get('userService');
const user = userService.createUser('Alice');
npm install jimple
<script src="https://cdn.jsdelivr.net/npm/jimple@latest/src/Jimple.js"></script>
latest
with a specific version for production use.
ES6 Modules
import Jimple from "jimple";
CommonJS
const Jimple = require("jimple");
AMD
define(["jimple"], function(Jimple) {
// Your code here
});
Services are objects that perform tasks in your application. They're defined as functions that return the service instance:
// Database connection service
container.set('database', (c) => {
const config = c.get('dbConfig');
return new Database(config.host, config.port);
});
// Email service that depends on database
container.set('emailService', (c) => {
const db = c.get('database');
return new EmailService(db);
});
Parameters store configuration values, strings, numbers, or any non-function data:
// Configuration parameters
container.set('dbConfig', {
host: 'localhost',
port: 5432,
database: 'myapp'
});
container.set('apiKey', 'abc123');
container.set('isProduction', process.env.NODE_ENV === 'production');
When you need a new instance every time instead of a singleton:
container.set('httpRequest', container.factory((c) => {
const config = c.get('httpConfig');
return new HttpRequest(config);
}));
// Each call returns a new instance
const req1 = container.get('httpRequest');
const req2 = container.get('httpRequest'); // Different instance
To store an actual function (not a service factory) as a parameter:
container.set('utility', container.protect(() => {
return Math.random() * 100;
}));
const utilityFn = container.get('utility'); // Returns the function itself
const result = utilityFn(); // Call the function
Add behavior to existing services:
container.set('logger', (c) => new Logger());
// Extend the logger to add file output
container.extend('logger', (logger, c) => {
logger.addFileHandler('/var/log/app.log');
return logger;
});
Handle optional services with fallbacks:
container.set('cache', (c) => {
if (c.has('redisConfig')) {
return new RedisCache(c.get('redisConfig'));
}
return new MemoryCache(); // Fallback
});
Get the service definition function instead of the service itself:
container.set('database', (c) => new Database());
const dbFactory = container.raw('database');
const db1 = dbFactory(container);
const db2 = dbFactory(container); // Create another instance manually
Use modern JavaScript syntax for a more natural API:
const container = new Jimple();
// Set services using property syntax
container['logger'] = (c) => new Logger();
container['userService'] = (c) => new UserService(c['logger']);
// Access services as properties
const userService = container.userService;
Limitations:
- Can't overwrite built-in methods (
set
,get
, etc.) - Accessing non-existent properties throws an error
- TypeScript requires special handling (see below)
Jimple provides full TypeScript support with interface definitions:
interface Services {
logger: Logger;
database: Database;
userService: UserService;
apiKey: string;
}
const container = new Jimple<Services>();
container.set('apiKey', 'secret-key');
container.set('logger', (c) => new Logger());
container.set('database', (c) => new Database());
container.set('userService', (c) =>
new UserService(c.get('logger'), c.get('database'))
);
// Type-safe access
const userService: UserService = container.get('userService'); // β
const wrong: Database = container.get('userService'); // β Compile error
interface Services {
logger: Logger;
userService: UserService;
}
const container = Jimple.create<Services>({
logger: (c) => new Logger(),
userService: (c) => new UserService(c.logger)
});
const userService: UserService = container.userService; // β
Type-safe
Note: Due to TypeScript limitations with proxies, you can't set properties directly. Use the set
method instead:
container.set('newService', (c) => new Service()); // β
Works
container.newService = (c) => new Service(); // β TypeScript error
Organize your container configuration into reusable modules:
const databaseProvider = {
register(container) {
container.set('dbConfig', {
host: process.env.DB_HOST ?? 'localhost',
port: process.env.DB_PORT ?? 5432
});
container.set('database', (c) => {
const config = c.get('dbConfig');
return new Database(config);
});
}
};
container.register(databaseProvider);
// providers/database.js
module.exports.register = function(container) {
container.set('database', (c) => new Database(c.get('dbConfig')));
};
// main.js
container.register(require('./providers/database'));
const { provider } = require("jimple");
module.exports = provider((container) => {
container.set('apiService', (c) => new ApiService(c.get('apiConfig')));
});
module.exports = {
database: provider((c) => {
c.set('database', () => new Database());
}),
cache: provider((c) => {
c.set('cache', () => new Cache());
})
};
Method | Description | Returns |
---|---|---|
set(id, value) |
Define a service or parameter | void |
get(id) |
Retrieve a service or parameter | any |
has(id) |
Check if service/parameter exists | boolean |
factory(fn) |
Create a factory service | fn |
protect(fn) |
Protect a function from being treated as service | fn |
extend(id, fn) |
Extend an existing service | void |
raw(id) |
Get the raw service definition | Function |
register(provider) |
Register a service provider | void |
const provider = {
register(container) {
// Define services and parameters
}
};
π For complete API documentation with detailed examples, see the full API reference
π For complete API documentation with detailed examples, see the full API reference
Here's a more comprehensive example showing how to structure a web application:
import Jimple from 'jimple';
const container = new Jimple();
// Configuration
container.set('config', {
database: {
host: process.env.DB_HOST ?? 'localhost',
port: process.env.DB_PORT ?? 5432
},
server: {
port: process.env.PORT ?? 3000
}
});
// Infrastructure services
container.set('database', (c) => {
const config = c.get('config').database;
return new Database(config.host, config.port);
});
container.set('logger', (c) => {
return new Logger(c.get('config').logLevel);
});
// Business services
container.set('userRepository', (c) => {
return new UserRepository(c.get('database'));
});
container.set('userService', (c) => {
return new UserService(
c.get('userRepository'),
c.get('logger')
);
});
// HTTP services
container.set('userController', (c) => {
return new UserController(c.get('userService'));
});
container.set('server', (c) => {
const config = c.get('config').server;
const app = new ExpressApp();
app.use('/users', c.get('userController').routes());
return app;
});
// Start the application
const server = container.get('server');
server.listen(container.get('config').server.port);
import express from 'express';
import Jimple from 'jimple';
const container = new Jimple();
// Configuration
container.set('port', process.env.PORT ?? 3000);
container.set('corsOrigins', process.env.CORS_ORIGINS?.split(',') ?? ['http://localhost:3000']);
// Services
container.set('app', (c) => {
const app = express();
app.use(express.json());
return app;
});
container.set('cors', (c) => {
return (req, res, next) => {
const origin = req.headers.origin;
if (c.get('corsOrigins').includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
next();
};
});
container.set('userController', (c) => {
return {
getUsers: (req, res) => res.json([{ id: 1, name: 'Alice' }]),
createUser: (req, res) => res.json({ id: 2, ...req.body })
};
});
// Setup routes
container.set('server', (c) => {
const app = c.get('app');
const cors = c.get('cors');
const userController = c.get('userController');
app.use(cors);
app.get('/users', userController.getUsers);
app.post('/users', userController.createUser);
return app;
});
// Start server
const server = container.get('server');
server.listen(container.get('port'), () => {
console.log(`Server running on port ${container.get('port')}`);
});
// Production container
const container = new Jimple();
container.set('emailService', (c) => new RealEmailService(c.get('apiKey')));
container.set('userService', (c) => new UserService(c.get('emailService')));
// Test container with mocks
const testContainer = new Jimple();
testContainer.set('emailService', () => ({
send: jest.fn().mockResolvedValue({ success: true })
}));
testContainer.set('userService', (c) => new UserService(c.get('emailService')));
// Your tests use the mock
const userService = testContainer.get('userService');
await userService.registerUser('test@example.com');
const container = new Jimple();
// Core services
container.set('eventBus', () => new EventEmitter());
container.set('pluginManager', (c) => new PluginManager(c.get('eventBus')));
// Plugin provider
const analyticsPlugin = {
register(container) {
container.set('analytics', (c) => {
const analytics = new Analytics();
const eventBus = c.get('eventBus');
eventBus.on('user.created', (user) => analytics.track('user_signup', user));
eventBus.on('user.login', (user) => analytics.track('user_login', user));
return analytics;
});
}
};
container.register(analyticsPlugin);
const container = new Jimple();
container.set('env', process.env.NODE_ENV ?? 'development');
// Base configuration
container.set('baseConfig', {
database: { poolSize: 10 },
cache: { ttl: 3600 }
});
container.set('database', (c) => {
if (c.get('env') === 'production') {
const config = { ...c.get('baseConfig').database, poolSize: 50 };
return new PostgresDatabase(config);
}
return new SQLiteDatabase(':memory:');
});
container.set('cache', (c) => {
if (c.get('env') === 'production') {
return new RedisCache(process.env.REDIS_URL);
}
return new MemoryCache();
});
You can create custom container classes:
class MyContainer extends Jimple {
constructor() {
super();
this.loadDefaultServices();
}
loadDefaultServices() {
this.set('logger', () => new DefaultLogger());
}
// Add custom methods
getLogger() {
return this.get('logger');
}
}
const container = new MyContainer();
- Use factories sparingly - Only when you truly need new instances
- Lazy load expensive services - Services are created only when needed
- Organize with providers - Split configuration into logical modules
- Avoid circular dependencies - Design services to avoid circular references
Before:
const logger = new Logger();
const database = new Database(config);
const userService = new UserService(logger, database);
After:
container.set('logger', () => new Logger());
container.set('database', (c) => new Database(c.get('config')));
container.set('userService', (c) =>
new UserService(c.get('logger'), c.get('database'))
);
- π Interactive Guide - Learn with live examples and tutorials
- π Complete API Reference - Detailed JSDoc documentation with full method signatures and examples
MIT License - see LICENSE file for details.
Happy coding! π